").appendTo(this.input_area);
+ this.editor.summernote({
+ minHeight: 400,
+ toolbar: [
+ ['magic', ['style']],
+ ['style', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['para', ['ul', 'ol', 'paragraph']],
+ ['height', ['height']],
+ ['misc', ['fullscreen', 'codeview']]
+ ],
+ callbacks: {
+ onChange: function(value) {
+ me.parse_validate_and_set_in_model(value);
+ },
+ onKeydown: function(e) {
+ var key = frappe.ui.keys.get_key(e);
+ // prevent 'New DocType (Ctrl + B)' shortcut in editor
+ if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) {
+ e.stopPropagation();
+ }
+ if(key.indexOf('escape') !== -1) {
+ if(me.note_editor.hasClass('fullscreen')) {
+ // exit fullscreen on escape key
+ me.note_editor
+ .find('.note-btn.btn-fullscreen')
+ .trigger('click');
+ }
+ }
+ }
+ },
+ icons: {
+ 'align': 'fa fa-align',
+ 'alignCenter': 'fa fa-align-center',
+ 'alignJustify': 'fa fa-align-justify',
+ 'alignLeft': 'fa fa-align-left',
+ 'alignRight': 'fa fa-align-right',
+ 'indent': 'fa fa-indent',
+ 'outdent': 'fa fa-outdent',
+ 'arrowsAlt': 'fa fa-arrows-alt',
+ 'bold': 'fa fa-bold',
+ 'caret': 'caret',
+ 'circle': 'fa fa-circle',
+ 'close': 'fa fa-close',
+ 'code': 'fa fa-code',
+ 'eraser': 'fa fa-eraser',
+ 'font': 'fa fa-font',
+ 'frame': 'fa fa-frame',
+ 'italic': 'fa fa-italic',
+ 'link': 'fa fa-link',
+ 'unlink': 'fa fa-chain-broken',
+ 'magic': 'fa fa-magic',
+ 'menuCheck': 'fa fa-check',
+ 'minus': 'fa fa-minus',
+ 'orderedlist': 'fa fa-list-ol',
+ 'pencil': 'fa fa-pencil',
+ 'picture': 'fa fa-picture',
+ 'question': 'fa fa-question',
+ 'redo': 'fa fa-redo',
+ 'square': 'fa fa-square',
+ 'strikethrough': 'fa fa-strikethrough',
+ 'subscript': 'fa fa-subscript',
+ 'superscript': 'fa fa-superscript',
+ 'table': 'fa fa-table',
+ 'textHeight': 'fa fa-text-height',
+ 'trash': 'fa fa-trash',
+ 'underline': 'fa fa-underline',
+ 'undo': 'fa fa-undo',
+ 'unorderedlist': 'fa fa-list-ul',
+ 'video': 'fa fa-video'
+ }
});
-
- $('
Add <!-- markdown --> \
- to always interpret as markdown
')
- .appendTo(this.md_editor_wrapper);
+ this.note_editor = $(this.input_area).find('.note-editor');
},
- make_switcher: function() {
- var me = this;
- this.current_editor = this.editor;
- this.switcher = $('
\
-
')
- .appendTo(this.input_area)
- .find("a")
- .click(function() {
- me.switch();
- return false;
- });
- this.render_switcher();
- },
- switch: function() {
- if(this.current_editor===this.editor) {
- // switch to md
- var value = this.editor.get_value();
- this.editor_wrapper.addClass("hide");
- this.md_editor_wrapper.removeClass("hide");
- this.current_editor = this.md_editor;
- this.add_type_marker("markdown");
- } else {
- // switch to html
- var value = this.md_editor.val();
- this.md_editor_wrapper.addClass("hide");
- this.editor_wrapper.removeClass("hide");
- this.current_editor = this.editor;
- this.add_type_marker("html");
- }
- this.render_switcher();
- },
- add_type_marker: function(marker) {
- var opp_marker = marker==="html" ? "markdown" : "html";
- if(!this.value) this.value = "";
- if(this.value.indexOf("")!==-1) {
- // replace opposite marker
- this.set_value(this.value.split("").join(""));
- } else if(this.value.indexOf("")===-1) {
- // add marker (marker missing)
- this.set_value(this.value + "\n\n\n");
- }
- },
- render_switcher: function() {
- this.switcher.html(__("Edit as {0}", [this.current_editor == this.editor ?
- __("Markdown") : __("Rich Text")]));
+ hide_elements_on_mobile: function() {
+ this.note_editor.find('.note-btn-underline,\
+ .note-btn-italic, .note-fontsize,\
+ .note-color, .note-height, .btn-codeview')
+ .addClass('hidden-xs');
+ if($('.toggle-sidebar').is(':visible')) {
+ // disable tooltips on mobile
+ this.note_editor.find('.note-btn')
+ .attr('data-original-title', '');
+ }
},
get_value: function() {
- return this.current_editor === this.editor
- ? this.editor.get_value()
- : this.md_editor.val();
+ return this.editor.summernote('code');
},
set_input: function(value) {
- this._set_input(value);
-
- // guess editor type
- var is_markdown = false;
- if(value) {
- if(value.indexOf("") !== -1) {
- var is_markdown = true;
- }
- if((is_markdown && this.current_editor===this.editor)
- || (!is_markdown && this.current_editor===this.md_editor)) {
- this.switch();
- }
- }
- },
- _set_input: function(value) {
if(value == null) value = "";
value = frappe.dom.remove_script_and_style(value);
- this.editor.set_input(value);
- this.md_editor.val(value);
+ if(value !== this.get_value())
+ this.editor.summernote('code', value);
this.last_value = value;
},
set_focus: function() {
- var editor = this.$wrapper.find('.text-editor');
- if(editor) {
- editor.focus();
- return true;
- }
+ return this.editor.summernote('focus');
}
});
diff --git a/frappe/public/js/frappe/ui/editor.html b/frappe/public/js/frappe/ui/editor.html
deleted file mode 100644
index a651279f87..0000000000
--- a/frappe/public/js/frappe/ui/editor.html
+++ /dev/null
@@ -1,84 +0,0 @@
-
diff --git a/frappe/public/js/frappe/ui/editor.js b/frappe/public/js/frappe/ui/editor.js
deleted file mode 100644
index f3ad996779..0000000000
--- a/frappe/public/js/frappe/ui/editor.js
+++ /dev/null
@@ -1,471 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
-
-/* Inspired from: http://github.com/mindmup/bootstrap-wysiwyg */
-
-// todo
-// make it inline friendly
-
-bsEditor = Class.extend({
- init: function(options) {
- this.options = $.extend({}, this.default_options, options || {});
- this.edit_mode = true;
- if(this.options.editor) {
- this.setup_editor(this.options.editor);
- this.setup_fixed_toolbar();
- } else if(this.options.parent) {
- this.wrapper = $("
").appendTo(this.options.parent);
- this.setup_editor($("
").appendTo(this.wrapper));
- this.setup_inline_toolbar();
- this.editor.addClass("text-editor");
- this.set_editing();
- }
- },
- setup_editor: function(editor) {
- var me = this;
- this.editor = $(editor);
- this.editor.on("click", function() {
- if(me.edit_mode && !me.editing) {
- me.set_editing();
- }
- }).on("mouseup keyup mouseout", function() {
- var html = me.clean_html();
- if(me.editing) {
- me.toolbar.save_selection();
- me.toolbar.update();
- if(html != me.last_html) {
- me.options.change && me.options.change(html);
- me.last_html = html;
- }
- }
- }).data("object", this);
-
- this.bind_hotkeys();
- this.init_file_drops();
- },
-
- set_editing: function() {
- this.editor.attr('contenteditable', true);
- this.toolbar.show();
- if(this.options.editor)
- this.toolbar.editor = this.editor.focus();
- this.editing = true;
- },
-
- setup_fixed_toolbar: function() {
- if(!window.bs_editor_toolbar) {
- window.bs_editor_toolbar = new bsEditorToolbar(this.options)
- }
- this.toolbar = window.bs_editor_toolbar;
- },
- setup_inline_toolbar: function() {
- this.toolbar = new bsEditorToolbar(this.options, this.wrapper, this.editor);
- },
- onhide: function() {
- this.editing = false;
- this.options.onsave && this.options.onsave(this);
- this.options.change && this.options.change(this.get_value());
- },
- toggle_edit_mode: function(bool) {
- // switch to enter editing mode
- this.edit_mode = bool;
- if(this.edit_mode) {
- this.editor.trigger("click");
- }
- },
- default_options: {
- hotKeys: {
- 'ctrl+b meta+b': 'bold',
- 'ctrl+i meta+i': 'italic',
- 'ctrl+u meta+u': 'underline',
- 'ctrl+z meta+z': 'undo',
- 'ctrl+y meta+y meta+shift+z': 'redo',
- 'ctrl+l meta+l': 'justifyleft',
- 'ctrl+e meta+e': 'justifycenter',
- 'ctrl+j meta+j': 'justifyfull',
- 'shift+tab': 'outdent',
- 'tab': 'indent'
- },
- toolbar_selector: '[data-role=editor-toolbar]',
- command_role: 'edit',
- selection_marker: 'edit-focus-marker',
- selection_color: 'darkgrey',
- remove_typography: false,
- max_file_size: 5,
- },
-
- bind_hotkeys: function () {
- var me = this;
- $.each(this.options.hotKeys, function (hotkey, command) {
- me.editor.keydown(hotkey, function (e) {
- if (me.editor.attr('contenteditable') && me.editor.is(':visible')) {
- e.preventDefault();
- e.stopPropagation();
- me.toolbar.execCommand(command);
- return false;
- }
- }).keyup(hotkey, function (e) {
- if (me.editor.attr('contenteditable') && me.editor.is(':visible')) {
- e.preventDefault();
- e.stopPropagation();
- return false;
- }
- });
- });
- },
-
- clean_html: function() {
-
- var html = this.editor.html() || "";
-
- if(!$.trim(this.editor.text()) && !(this.editor.find("img"))) html = "";
-
- // remove custom typography (use CSS!)
- if(this.options.remove_typography) {
- var tmp = $("
").html(html);
- // remove style attributes
- tmp.find("*")
- .removeAttr("style")
- .removeAttr("font");
- html = tmp.html();
- }
-
- return html;
- },
-
- init_file_drops: function () {
- var me = this;
- this.editor.on('dragenter dragover', false)
- .on('drop', function (e) {
- var dataTransfer = e.originalEvent.dataTransfer;
- e.stopPropagation();
- e.preventDefault();
- if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
- me.insert_files(dataTransfer.files);
- }
- });
- },
-
- insert_files: function (files) {
- var me = this;
- this.editor.focus();
- $.each(files, function (i, file) {
- if (/^image\//.test(file.type)) {
- me.get_image(file, function(image_url) {
- me.toolbar.execCommand('insertImage', image_url);
- })
- }
- });
- },
-
- get_image: function (fileobj, callback) {
- var freader = new FileReader(),
- me = this;
-
- freader.onload = function() {
- var dataurl = freader.result;
- // add filename to dataurl
- var parts = dataurl.split(",");
- parts[0] += ";filename=" + fileobj.name;
- dataurl = parts[0] + ',' + parts[1];
- if(me.options.max_file_size) {
- if(dataurl.length > (me.options.max_file_size * 1024 * 1024 * 1.4)) {
- bs_get_modal("Upload Error", "Max file size (" + me.options.max_file_size + "M) exceeded.").modal("show");
- throw "file size exceeded";
- }
- }
- callback(dataurl);
- }
- freader.readAsDataURL(fileobj);
- },
-
- get_value: function() {
- return this.clean_html()
- },
-
- set_input: function(value) {
- if(this.options.field && this.options.field.inside_change_event)
- return;
- if(value==null) value = "";
- this.last_html = value;
- this.editor.html(value);
- }
-
-})
-
-bsEditorToolbar = Class.extend({
- init: function(options, parent, editor) {
- this.options = options;
- this.editor = editor;
- this.inline = !!parent;
- this.options.toolbar_style = $.extend((this.inline ? this.inline_style : this.fixed_style),
- this.options.toolbar_style || {});
- this.make(parent);
- this.toolbar.css(this.options.toolbar_style);
- this.setup_image_button();
- this.bind_events();
- //this.bind_touch();
- },
- fixed_style: {
- position: "fixed",
- top: "0px",
- "padding-top": "5px",
- width: "100%",
- height: "45px",
- "background-color": "black",
- display: "none"
- },
- inline_style: {
- "padding-top": "5px",
- },
- make: function(parent) {
- if(!parent)
- parent = $("body");
- if(!parent.find(".frappe-list-toolbar").length) {
- this.toolbar = $(frappe.render_template("editor")).prependTo(parent);
-
- if(this.inline) {
- this.toolbar.find("[data-action]").remove();
- } else {
- this.toolbar.find(".btn-toolbar").addClass("container");
- }
- }
- },
-
- setup_image_button: function() {
- // magic-overlay
- var me = this;
- this.file_input = this.toolbar.find('input[type="file"]')
- .css({
- 'visibility': 'hidden',
- 'width':0,
- 'height':0
- });
- this.toolbar.find(".btn-insert-img").on("click", function() {
- me.file_input.trigger("click");
- })
- },
-
- show: function() {
- var me = this;
- this.toolbar.toggle(true);
- if(!this.inline) {
- $("body").animate({"padding-top": this.toolbar.outerHeight() }, {
- complete: function() { me.toolbar.css("z-index", 1001); }
- });
- }
- },
-
- hide: function() {
- if(!this.editor)
- return;
- var me = this;
- this.toolbar.css("z-index", 0);
- if(!this.inline) {
- $("body").animate({"padding-top": 0 }, {complete: function() {
- me.toolbar.toggle(false);
- }});
- }
-
- this.editor && this.editor.attr('contenteditable', false).data("object").onhide();
- this.editor = null;
- },
-
- bind_events: function () {
- var me = this;
-
- // standard button events
- this.toolbar.find('a[data-' + me.options.command_role + ']').click(function (e) {
- me.restore_selection();
- me.editor.focus();
- me.execCommand($(this).data(me.options.command_role));
- me.save_selection();
- // close dropdown
- if(me.toolbar.find("ul.dropdown-menu:visible").length)
- me.toolbar.find('[data-toggle="dropdown"]').dropdown("toggle");
- e.stopPropagation();
- e.preventDefault();
- return false;
- });
- this.toolbar.find('[data-toggle=dropdown]').click(function() { me.restore_selection() });
-
- // link
- this.toolbar.find(".btn-add-link").on("click", function() {
- if(!me.toolbar.bs_link_editor) {
- if(me.inline) {
- me.toolbar.bs_link_editor = new bsLinkEditor(me);
- } else {
- if(!window.bs_link_editor) {
- window.bs_link_editor = new bsLinkEditor(me);
- }
- me.toolbar.bs_link_editor = window.bs_link_editor;
- }
- }
- me.toolbar.bs_link_editor.show();
- });
-
- // file event
- this.toolbar.find('input[type=file][data-' + me.options.command_role + ']').change(function () {
- me.restore_selection();
- if (this.type === 'file' && this.files && this.files.length > 0) {
- me.editor.data("object").insert_files(this.files);
- }
- me.save_selection();
- this.value = '';
-
- return false;
- });
-
- // save
- this.toolbar.find("[data-action='Save']").on("click", function() { me.hide(); });
-
- // edit html
- this.toolbar.find(".btn-html").on("click", function() {
- new bsHTMLEditor().show(me.editor);
- });
- },
-
- update: function () {
- var me = this;
- if (this.toolbar) {
- $(this.toolbar).find('.btn[data-' + this.options.command_role + ']').each(function () {
- var command = $(this).data(me.options.command_role);
-
- // try catch for buggy firefox!
- try {
- var query_command_state = document.queryCommandState(command);
- } catch(e) {
- var query_command_state = false;
- }
-
- if (query_command_state) {
- $(this).addClass(me.options.active_toolbar_class);
- } else {
- $(this).removeClass(me.options.active_toolbar_class);
- }
- });
- }
- },
-
- execCommand: function (commandWithArgs, valueArg) {
- var commandArr = commandWithArgs.split(' '),
- command = commandArr.shift(),
- args = commandArr.join(' ') + (valueArg || '');
- document.execCommand(command, 0, args);
- this.update();
- },
-
- get_current_range: function () {
- var sel = window.getSelection();
- if (sel.getRangeAt && sel.rangeCount) {
- return sel.getRangeAt(0);
- }
- },
-
- save_selection: function () {
- this.selected_range = this.get_current_range();
- },
-
- restore_selection: function () {
- var selection = window.getSelection();
- if (this.selected_range) {
- selection.removeAllRanges();
- selection.addRange(this.selected_range);
- }
- },
-
- mark_selection: function (input, color) {
- this.restore_selection();
- document.execCommand('hiliteColor', 0, color || 'transparent');
- this.save_selection();
- input.data(this.options.selection_marker, color);
- },
-
- // bind_touch: function() {
- // var me = this;
- // $(window).bind('touchend', function (e) {
- // var isInside = (me.editor.is(e.target) || me.editor.has(e.target).length > 0),
- // current_range = me.get_current_range(),
- // clear = current_range && (current_range.startContainer === current_range.endContainer && current_range.startOffset === current_range.endOffset);
- // if (!clear || isInside) {
- // me.save_selection();
- // me.update();
- // }
- // });
- // }
-});
-
-bsHTMLEditor = Class.extend({
- init: function() {
- var me = this;
- this.modal = bs_get_modal("
Edit HTML", '
');
- this.modal.addClass("frappe-ignore-click");
- this.modal.find(".btn-primary").removeClass("hide").html(__("Update")).on("click", function() {
- me._html = me.modal.find("textarea").val();
-
- $.each(me.editor.dataurls, function(key, val) {
- me._html = replace_all(me._html, key, val);
- });
-
- var editor = me.editor.data("object");
- editor.set_input(me._html);
- editor.options.change && editor.options.change(editor.clean_html());
- me.modal.modal("hide");
- });
- },
- show: function(editor) {
- var me = this;
- this.editor = editor;
- this.modal.modal("show");
- var html = me.editor.html();
- // pack dataurls so that html display is faster
- this.editor.dataurls = {}
- html = html.replace(/

Insert Link", '
\
- \
-
\
-
\
- \
-
\
-
');
-
- this.modal.addClass("frappe-ignore-click");
- this.modal.find(".btn-primary").on("click", function() {
- me.toolbar.restore_selection();
- var url = me.modal.find("input[type=text]").val();
- var selection = me.toolbar.selected_range.toString();
- if(url) {
- if(me.modal.find("input[type=checkbox]:checked").length) {
- var html = "
" + selection + "";
- document.execCommand("insertHTML", false, html);
- } else {
- document.execCommand("CreateLink", false, url);
- }
- }
- me.modal.modal("hide");
- return false;
- });
- },
- show: function() {
- this.modal.find("input[type=text]").val("");
- this.modal.modal("show");
- }
-});
-
-bs_get_modal = frappe.get_modal;
diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js
index a283b40284..5acc54edcd 100644
--- a/frappe/public/js/frappe/ui/keyboard.js
+++ b/frappe/public/js/frappe/ui/keyboard.js
@@ -2,23 +2,7 @@ frappe.provide('frappe.ui.keys.handlers');
frappe.ui.keys.setup = function() {
$(window).on('keydown', function(e) {
- var key = e.key;
- //safari doesn't have key property
- if(!key) {
- key = String.fromCharCode(e.keyCode).toLowerCase();
- }
- if(key.substr(0, 5)==='Arrow') {
- // ArrowDown -> down
- key = key.substr(5).toLowerCase();
- }
- if(e.ctrlKey || e.metaKey) {
- // add ctrl+ the key
- key = 'ctrl+' + key;
- }
- if(e.shiftKey) {
- // add ctrl+ the key
- key = 'shift+' + key;
- }
+ var key = frappe.ui.keys.get_key(e);
if(frappe.ui.keys.handlers[key]) {
var out = null;
for(var i=0, l = frappe.ui.keys.handlers[key].length; i
down
+ key = key.substr(5).toLowerCase();
+ }
+ if(e.ctrlKey || e.metaKey) {
+ // add ctrl+ the key
+ key = 'ctrl+' + key;
+ }
+ if(e.shiftKey) {
+ // add ctrl+ the key
+ key = 'shift+' + key;
+ }
+ return key.toLowerCase();
+}
+
frappe.ui.keys.on = function(key, handler) {
if(!frappe.ui.keys.handlers[key]) {
frappe.ui.keys.handlers[key] = [];
@@ -98,3 +105,14 @@ frappe.ui.keys.on('ctrl+up', function(e) {
frappe.ui.keys.on('shift+ctrl+r', function(e) {
frappe.ui.toolbar.clear_cache();
});
+
+frappe.ui.keys.key_map = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 91: 'meta',
+ 18: 'alt',
+ 27: 'escape'
+}
\ No newline at end of file
diff --git a/frappe/public/js/lib/summernote/summernote.css b/frappe/public/js/lib/summernote/summernote.css
new file mode 100755
index 0000000000..7b23e34a1a
--- /dev/null
+++ b/frappe/public/js/lib/summernote/summernote.css
@@ -0,0 +1 @@
+@font-face{font-family:"summernote";font-style:normal;font-weight:normal;src:url("font/summernote.eot?ad8d7e2d177d2473aecd9b35d16211fb");src:url("font/summernote.eot?#iefix") format("embedded-opentype"),url("font/summernote.woff?ad8d7e2d177d2473aecd9b35d16211fb") format("woff"),url("font/summernote.ttf?ad8d7e2d177d2473aecd9b35d16211fb") format("truetype")}[class^="note-icon-"]:before,[class*=" note-icon-"]:before{display:inline-block;font:normal normal normal 14px summernote;font-size:inherit;-webkit-font-smoothing:antialiased;text-decoration:inherit;text-rendering:auto;text-transform:none;vertical-align:middle;speak:none;-moz-osx-font-smoothing:grayscale}.note-icon-align-center:before{content:"\f101"}.note-icon-align-indent:before{content:"\f102"}.note-icon-align-justify:before{content:"\f103"}.note-icon-align-left:before{content:"\f104"}.note-icon-align-outdent:before{content:"\f105"}.note-icon-align-right:before{content:"\f106"}.note-icon-align:before{content:"\f107"}.note-icon-arrows-alt:before{content:"\f108"}.note-icon-bold:before{content:"\f109"}.note-icon-caret:before{content:"\f10a"}.note-icon-chain-broken:before{content:"\f10b"}.note-icon-circle:before{content:"\f10c"}.note-icon-close:before{content:"\f10d"}.note-icon-code:before{content:"\f10e"}.note-icon-eraser:before{content:"\f10f"}.note-icon-font:before{content:"\f110"}.note-icon-frame:before{content:"\f111"}.note-icon-italic:before{content:"\f112"}.note-icon-link:before{content:"\f113"}.note-icon-magic:before{content:"\f114"}.note-icon-menu-check:before{content:"\f115"}.note-icon-minus:before{content:"\f116"}.note-icon-orderedlist:before{content:"\f117"}.note-icon-pencil:before{content:"\f118"}.note-icon-picture:before{content:"\f119"}.note-icon-question:before{content:"\f11a"}.note-icon-redo:before{content:"\f11b"}.note-icon-special-character:before{content:"\f11c"}.note-icon-square:before{content:"\f11d"}.note-icon-strikethrough:before{content:"\f11e"}.note-icon-subscript:before{content:"\f11f"}.note-icon-summernote:before{content:"\f120"}.note-icon-superscript:before{content:"\f121"}.note-icon-table:before{content:"\f122"}.note-icon-text-height:before{content:"\f123"}.note-icon-trash:before{content:"\f124"}.note-icon-underline:before{content:"\f125"}.note-icon-undo:before{content:"\f126"}.note-icon-unorderedlist:before{content:"\f127"}.note-icon-video:before{content:"\f128"}.note-editor{position:relative}.note-editor .note-dropzone{position:absolute;z-index:100;display:none;color:#87cefa;background-color:white;opacity:.95}.note-editor .note-dropzone .note-dropzone-message{display:table-cell;font-size:28px;font-weight:bold;text-align:center;vertical-align:middle}.note-editor .note-dropzone.hover{color:#098ddf}.note-editor.dragover .note-dropzone{display:table}.note-editor .note-editing-area{position:relative}.note-editor .note-editing-area .note-editable{outline:0}.note-editor .note-editing-area .note-editable sup{vertical-align:super}.note-editor .note-editing-area .note-editable sub{vertical-align:sub}.note-editor.note-frame{border:1px solid #a9a9a9}.note-editor.note-frame.codeview .note-editing-area .note-editable{display:none}.note-editor.note-frame.codeview .note-editing-area .note-codable{display:block}.note-editor.note-frame .note-editing-area{overflow:hidden}.note-editor.note-frame .note-editing-area .note-editable{padding:10px;overflow:auto;color:#000;background-color:#fff}.note-editor.note-frame .note-editing-area .note-editable[contenteditable="false"]{background-color:#e5e5e5}.note-editor.note-frame .note-editing-area .note-codable{display:none;width:100%;padding:10px;margin-bottom:0;font-family:Menlo,Monaco,monospace,sans-serif;font-size:14px;color:#ccc;background-color:#222;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;box-shadow:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;resize:none}.note-editor.note-frame.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important}.note-editor.note-frame.fullscreen .note-editable{background-color:white}.note-editor.note-frame.fullscreen .note-resizebar{display:none}.note-editor.note-frame .note-statusbar{background-color:#f5f5f5;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.note-editor.note-frame .note-statusbar .note-resizebar{width:100%;height:8px;padding-top:1px;cursor:ns-resize}.note-editor.note-frame .note-statusbar .note-resizebar .note-icon-bar{width:20px;margin:1px auto;border-top:1px solid #a9a9a9}.note-editor.note-frame .note-placeholder{padding:10px}.note-popover.popover{max-width:none}.note-popover.popover .popover-content a{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.note-popover.popover .arrow{left:20px!important}.note-popover .popover-content,.panel-heading.note-toolbar{padding:0 0 5px 5px;margin:0}.note-popover .popover-content>.btn-group,.panel-heading.note-toolbar>.btn-group{margin-top:5px;margin-right:5px;margin-left:0}.note-popover .popover-content .btn-group .note-table,.panel-heading.note-toolbar .btn-group .note-table{min-width:0;padding:5px}.note-popover .popover-content .btn-group .note-table .note-dimension-picker,.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker{font-size:18px}.note-popover .popover-content .btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher,.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher{position:absolute!important;z-index:3;width:10em;height:10em;cursor:pointer}.note-popover .popover-content .btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted,.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted{position:relative!important;z-index:1;width:5em;height:5em;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIj4+Pjp6ekKlAqjAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKhmnaJzPAAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat}.note-popover .popover-content .btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted,.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted{position:absolute!important;z-index:2;width:1em;height:1em;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIjd6vvD2f9LKLW+AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKwNDEVT0AAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat}.note-popover .popover-content .note-style h1,.panel-heading.note-toolbar .note-style h1,.note-popover .popover-content .note-style h2,.panel-heading.note-toolbar .note-style h2,.note-popover .popover-content .note-style h3,.panel-heading.note-toolbar .note-style h3,.note-popover .popover-content .note-style h4,.panel-heading.note-toolbar .note-style h4,.note-popover .popover-content .note-style h5,.panel-heading.note-toolbar .note-style h5,.note-popover .popover-content .note-style h6,.panel-heading.note-toolbar .note-style h6,.note-popover .popover-content .note-style blockquote,.panel-heading.note-toolbar .note-style blockquote{margin:0}.note-popover .popover-content .note-color .dropdown-toggle,.panel-heading.note-toolbar .note-color .dropdown-toggle{width:20px;padding-left:5px}.note-popover .popover-content .note-color .dropdown-menu,.panel-heading.note-toolbar .note-color .dropdown-menu{min-width:340px}.note-popover .popover-content .note-color .dropdown-menu .btn-group,.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group{margin:0}.note-popover .popover-content .note-color .dropdown-menu .btn-group:first-child,.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group:first-child{margin:0 5px}.note-popover .popover-content .note-color .dropdown-menu .btn-group .note-palette-title,.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-palette-title{margin:2px 7px;font-size:12px;text-align:center;border-bottom:1px solid #eee}.note-popover .popover-content .note-color .dropdown-menu .btn-group .note-color-reset,.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-color-reset{width:100%;padding:0 3px;margin:3px;font-size:11px;cursor:pointer;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.note-popover .popover-content .note-color .dropdown-menu .btn-group .note-color-row,.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-color-row{height:20px}.note-popover .popover-content .note-color .dropdown-menu .btn-group .note-color-reset:hover,.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-color-reset:hover{background:#eee}.note-popover .popover-content .note-para .dropdown-menu,.panel-heading.note-toolbar .note-para .dropdown-menu{min-width:216px;padding:5px}.note-popover .popover-content .note-para .dropdown-menu>div:first-child,.panel-heading.note-toolbar .note-para .dropdown-menu>div:first-child{margin-right:5px}.note-popover .popover-content .dropdown-menu,.panel-heading.note-toolbar .dropdown-menu{min-width:90px}.note-popover .popover-content .dropdown-menu.right,.panel-heading.note-toolbar .dropdown-menu.right{right:0;left:auto}.note-popover .popover-content .dropdown-menu.right::before,.panel-heading.note-toolbar .dropdown-menu.right::before{right:9px;left:auto!important}.note-popover .popover-content .dropdown-menu.right::after,.panel-heading.note-toolbar .dropdown-menu.right::after{right:10px;left:auto!important}.note-popover .popover-content .dropdown-menu.note-check li a i,.panel-heading.note-toolbar .dropdown-menu.note-check li a i{color:deepskyblue;visibility:hidden}.note-popover .popover-content .dropdown-menu.note-check li a.checked i,.panel-heading.note-toolbar .dropdown-menu.note-check li a.checked i{visibility:visible}.note-popover .popover-content .note-fontsize-10,.panel-heading.note-toolbar .note-fontsize-10{font-size:10px}.note-popover .popover-content .note-color-palette,.panel-heading.note-toolbar .note-color-palette{line-height:1}.note-popover .popover-content .note-color-palette div .note-color-btn,.panel-heading.note-toolbar .note-color-palette div .note-color-btn{width:20px;height:20px;padding:0;margin:0;border:1px solid #fff}.note-popover .popover-content .note-color-palette div .note-color-btn:hover,.panel-heading.note-toolbar .note-color-palette div .note-color-btn:hover{border:1px solid #000}.note-dialog>div{display:none}.note-dialog .form-group{margin-right:0;margin-left:0}.note-dialog .note-modal-form{margin:0}.note-dialog .note-image-dialog .note-dropzone{min-height:100px;margin-bottom:10px;font-size:30px;line-height:4;color:lightgray;text-align:center;border:4px dashed lightgray}@-moz-document url-prefix(){.note-image-input{height:auto}}.note-placeholder{position:absolute;display:none;color:gray}.note-handle .note-control-selection{position:absolute;display:none;border:1px solid black}.note-handle .note-control-selection>div{position:absolute}.note-handle .note-control-selection .note-control-selection-bg{width:100%;height:100%;background-color:black;-webkit-opacity:.3;-khtml-opacity:.3;-moz-opacity:.3;opacity:.3;-ms-filter:alpha(opacity=30);filter:alpha(opacity=30)}.note-handle .note-control-selection .note-control-handle{width:7px;height:7px;border:1px solid black}.note-handle .note-control-selection .note-control-holder{width:7px;height:7px;border:1px solid black}.note-handle .note-control-selection .note-control-sizing{width:7px;height:7px;background-color:white;border:1px solid black}.note-handle .note-control-selection .note-control-nw{top:-5px;left:-5px;border-right:0;border-bottom:0}.note-handle .note-control-selection .note-control-ne{top:-5px;right:-5px;border-bottom:0;border-left:none}.note-handle .note-control-selection .note-control-sw{bottom:-5px;left:-5px;border-top:0;border-right:0}.note-handle .note-control-selection .note-control-se{right:-5px;bottom:-5px;cursor:se-resize}.note-handle .note-control-selection .note-control-se.note-control-holder{cursor:default;border-top:0;border-left:none}.note-handle .note-control-selection .note-control-selection-info{right:0;bottom:0;padding:5px;margin:5px;font-size:12px;color:white;background-color:black;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-opacity:.7;-khtml-opacity:.7;-moz-opacity:.7;opacity:.7;-ms-filter:alpha(opacity=70);filter:alpha(opacity=70)}.note-hint-popover{min-width:100px;padding:2px}.note-hint-popover .popover-content{max-height:150px;padding:3px;overflow:auto}.note-hint-popover .popover-content .note-hint-group .note-hint-item{display:block!important;padding:3px}.note-hint-popover .popover-content .note-hint-group .note-hint-item.active,.note-hint-popover .popover-content .note-hint-group .note-hint-item:hover{display:block;clear:both;font-weight:400;line-height:1.4;color:white;text-decoration:none;white-space:nowrap;cursor:pointer;background-color:#428bca;outline:0}
\ No newline at end of file
diff --git a/frappe/public/js/lib/summernote/summernote.js b/frappe/public/js/lib/summernote/summernote.js
new file mode 100755
index 0000000000..a7cb3a7a0e
--- /dev/null
+++ b/frappe/public/js/lib/summernote/summernote.js
@@ -0,0 +1,7046 @@
+/**
+ * Super simple wysiwyg editor v0.8.2
+ * http://summernote.org/
+ *
+ * summernote.js
+ * Copyright 2013-2016 Alan Hong. and other contributors
+ * summernote may be freely distributed under the MIT license./
+ *
+ * Date: 2016-08-07T05:11Z
+ */
+(function (factory) {
+ /* global define */
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else if (typeof module === 'object' && module.exports) {
+ // Node/CommonJS
+ module.exports = factory(require('jquery'));
+ } else {
+ // Browser globals
+ factory(window.jQuery);
+ }
+}(function ($) {
+ 'use strict';
+
+ /**
+ * @class core.func
+ *
+ * func utils (for high-order func's arg)
+ *
+ * @singleton
+ * @alternateClassName func
+ */
+ var func = (function () {
+ var eq = function (itemA) {
+ return function (itemB) {
+ return itemA === itemB;
+ };
+ };
+
+ var eq2 = function (itemA, itemB) {
+ return itemA === itemB;
+ };
+
+ var peq2 = function (propName) {
+ return function (itemA, itemB) {
+ return itemA[propName] === itemB[propName];
+ };
+ };
+
+ var ok = function () {
+ return true;
+ };
+
+ var fail = function () {
+ return false;
+ };
+
+ var not = function (f) {
+ return function () {
+ return !f.apply(f, arguments);
+ };
+ };
+
+ var and = function (fA, fB) {
+ return function (item) {
+ return fA(item) && fB(item);
+ };
+ };
+
+ var self = function (a) {
+ return a;
+ };
+
+ var invoke = function (obj, method) {
+ return function () {
+ return obj[method].apply(obj, arguments);
+ };
+ };
+
+ var idCounter = 0;
+
+ /**
+ * generate a globally-unique id
+ *
+ * @param {String} [prefix]
+ */
+ var uniqueId = function (prefix) {
+ var id = ++idCounter + '';
+ return prefix ? prefix + id : id;
+ };
+
+ /**
+ * returns bnd (bounds) from rect
+ *
+ * - IE Compatibility Issue: http://goo.gl/sRLOAo
+ * - Scroll Issue: http://goo.gl/sNjUc
+ *
+ * @param {Rect} rect
+ * @return {Object} bounds
+ * @return {Number} bounds.top
+ * @return {Number} bounds.left
+ * @return {Number} bounds.width
+ * @return {Number} bounds.height
+ */
+ var rect2bnd = function (rect) {
+ var $document = $(document);
+ return {
+ top: rect.top + $document.scrollTop(),
+ left: rect.left + $document.scrollLeft(),
+ width: rect.right - rect.left,
+ height: rect.bottom - rect.top
+ };
+ };
+
+ /**
+ * returns a copy of the object where the keys have become the values and the values the keys.
+ * @param {Object} obj
+ * @return {Object}
+ */
+ var invertObject = function (obj) {
+ var inverted = {};
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ inverted[obj[key]] = key;
+ }
+ }
+ return inverted;
+ };
+
+ /**
+ * @param {String} namespace
+ * @param {String} [prefix]
+ * @return {String}
+ */
+ var namespaceToCamel = function (namespace, prefix) {
+ prefix = prefix || '';
+ return prefix + namespace.split('.').map(function (name) {
+ return name.substring(0, 1).toUpperCase() + name.substring(1);
+ }).join('');
+ };
+
+ /**
+ * Returns a function, that, as long as it continues to be invoked, will not
+ * be triggered. The function will be called after it stops being called for
+ * N milliseconds. If `immediate` is passed, trigger the function on the
+ * leading edge, instead of the trailing.
+ * @param {Function} func
+ * @param {Number} wait
+ * @param {Boolean} immediate
+ * @return {Function}
+ */
+ var debounce = function (func, wait, immediate) {
+ var timeout;
+ return function () {
+ var context = this, args = arguments;
+ var later = function () {
+ timeout = null;
+ if (!immediate) {
+ func.apply(context, args);
+ }
+ };
+ var callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ func.apply(context, args);
+ }
+ };
+ };
+
+ return {
+ eq: eq,
+ eq2: eq2,
+ peq2: peq2,
+ ok: ok,
+ fail: fail,
+ self: self,
+ not: not,
+ and: and,
+ invoke: invoke,
+ uniqueId: uniqueId,
+ rect2bnd: rect2bnd,
+ invertObject: invertObject,
+ namespaceToCamel: namespaceToCamel,
+ debounce: debounce
+ };
+ })();
+
+ /**
+ * @class core.list
+ *
+ * list utils
+ *
+ * @singleton
+ * @alternateClassName list
+ */
+ var list = (function () {
+ /**
+ * returns the first item of an array.
+ *
+ * @param {Array} array
+ */
+ var head = function (array) {
+ return array[0];
+ };
+
+ /**
+ * returns the last item of an array.
+ *
+ * @param {Array} array
+ */
+ var last = function (array) {
+ return array[array.length - 1];
+ };
+
+ /**
+ * returns everything but the last entry of the array.
+ *
+ * @param {Array} array
+ */
+ var initial = function (array) {
+ return array.slice(0, array.length - 1);
+ };
+
+ /**
+ * returns the rest of the items in an array.
+ *
+ * @param {Array} array
+ */
+ var tail = function (array) {
+ return array.slice(1);
+ };
+
+ /**
+ * returns item of array
+ */
+ var find = function (array, pred) {
+ for (var idx = 0, len = array.length; idx < len; idx ++) {
+ var item = array[idx];
+ if (pred(item)) {
+ return item;
+ }
+ }
+ };
+
+ /**
+ * returns true if all of the values in the array pass the predicate truth test.
+ */
+ var all = function (array, pred) {
+ for (var idx = 0, len = array.length; idx < len; idx ++) {
+ if (!pred(array[idx])) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ /**
+ * returns index of item
+ */
+ var indexOf = function (array, item) {
+ return $.inArray(item, array);
+ };
+
+ /**
+ * returns true if the value is present in the list.
+ */
+ var contains = function (array, item) {
+ return indexOf(array, item) !== -1;
+ };
+
+ /**
+ * get sum from a list
+ *
+ * @param {Array} array - array
+ * @param {Function} fn - iterator
+ */
+ var sum = function (array, fn) {
+ fn = fn || func.self;
+ return array.reduce(function (memo, v) {
+ return memo + fn(v);
+ }, 0);
+ };
+
+ /**
+ * returns a copy of the collection with array type.
+ * @param {Collection} collection - collection eg) node.childNodes, ...
+ */
+ var from = function (collection) {
+ var result = [], idx = -1, length = collection.length;
+ while (++idx < length) {
+ result[idx] = collection[idx];
+ }
+ return result;
+ };
+
+ /**
+ * returns whether list is empty or not
+ */
+ var isEmpty = function (array) {
+ return !array || !array.length;
+ };
+
+ /**
+ * cluster elements by predicate function.
+ *
+ * @param {Array} array - array
+ * @param {Function} fn - predicate function for cluster rule
+ * @param {Array[]}
+ */
+ var clusterBy = function (array, fn) {
+ if (!array.length) { return []; }
+ var aTail = tail(array);
+ return aTail.reduce(function (memo, v) {
+ var aLast = last(memo);
+ if (fn(last(aLast), v)) {
+ aLast[aLast.length] = v;
+ } else {
+ memo[memo.length] = [v];
+ }
+ return memo;
+ }, [[head(array)]]);
+ };
+
+ /**
+ * returns a copy of the array with all false values removed
+ *
+ * @param {Array} array - array
+ * @param {Function} fn - predicate function for cluster rule
+ */
+ var compact = function (array) {
+ var aResult = [];
+ for (var idx = 0, len = array.length; idx < len; idx ++) {
+ if (array[idx]) { aResult.push(array[idx]); }
+ }
+ return aResult;
+ };
+
+ /**
+ * produces a duplicate-free version of the array
+ *
+ * @param {Array} array
+ */
+ var unique = function (array) {
+ var results = [];
+
+ for (var idx = 0, len = array.length; idx < len; idx ++) {
+ if (!contains(results, array[idx])) {
+ results.push(array[idx]);
+ }
+ }
+
+ return results;
+ };
+
+ /**
+ * returns next item.
+ * @param {Array} array
+ */
+ var next = function (array, item) {
+ var idx = indexOf(array, item);
+ if (idx === -1) { return null; }
+
+ return array[idx + 1];
+ };
+
+ /**
+ * returns prev item.
+ * @param {Array} array
+ */
+ var prev = function (array, item) {
+ var idx = indexOf(array, item);
+ if (idx === -1) { return null; }
+
+ return array[idx - 1];
+ };
+
+ return { head: head, last: last, initial: initial, tail: tail,
+ prev: prev, next: next, find: find, contains: contains,
+ all: all, sum: sum, from: from, isEmpty: isEmpty,
+ clusterBy: clusterBy, compact: compact, unique: unique };
+ })();
+
+ var isSupportAmd = typeof define === 'function' && define.amd;
+
+ /**
+ * returns whether font is installed or not.
+ *
+ * @param {String} fontName
+ * @return {Boolean}
+ */
+ var isFontInstalled = function (fontName) {
+ var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
+ var $tester = $('').css({
+ position: 'absolute',
+ left: '-9999px',
+ top: '-9999px',
+ fontSize: '200px'
+ }).text('mmmmmmmmmwwwwwww').appendTo(document.body);
+
+ var originalWidth = $tester.css('fontFamily', testFontName).width();
+ var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
+
+ $tester.remove();
+
+ return originalWidth !== width;
+ };
+
+ var userAgent = navigator.userAgent;
+ var isMSIE = /MSIE|Trident/i.test(userAgent);
+ var browserVersion;
+ if (isMSIE) {
+ var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
+ if (matches) {
+ browserVersion = parseFloat(matches[1]);
+ }
+ matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
+ if (matches) {
+ browserVersion = parseFloat(matches[1]);
+ }
+ }
+
+ var isEdge = /Edge\/\d+/.test(userAgent);
+
+ var hasCodeMirror = !!window.CodeMirror;
+ if (!hasCodeMirror && isSupportAmd && typeof require !== 'undefined') {
+ if (typeof require.resolve !== 'undefined') {
+ try {
+ // If CodeMirror can't be resolved, `require.resolve` will throw an
+ // exception and `hasCodeMirror` won't be set to `true`.
+ require.resolve('codemirror');
+ hasCodeMirror = true;
+ } catch (e) {
+ // Do nothing.
+ }
+ } else if (typeof eval('require').specified !== 'undefined') {
+ hasCodeMirror = eval('require').specified('codemirror');
+ }
+ }
+
+ /**
+ * @class core.agent
+ *
+ * Object which check platform and agent
+ *
+ * @singleton
+ * @alternateClassName agent
+ */
+ var agent = {
+ isMac: navigator.appVersion.indexOf('Mac') > -1,
+ isMSIE: isMSIE,
+ isEdge: isEdge,
+ isFF: !isEdge && /firefox/i.test(userAgent),
+ isPhantom: /PhantomJS/i.test(userAgent),
+ isWebkit: !isEdge && /webkit/i.test(userAgent),
+ isChrome: !isEdge && /chrome/i.test(userAgent),
+ isSafari: !isEdge && /safari/i.test(userAgent),
+ browserVersion: browserVersion,
+ jqueryVersion: parseFloat($.fn.jquery),
+ isSupportAmd: isSupportAmd,
+ hasCodeMirror: hasCodeMirror,
+ isFontInstalled: isFontInstalled,
+ isW3CRangeSupport: !!document.createRange
+ };
+
+
+ var NBSP_CHAR = String.fromCharCode(160);
+ var ZERO_WIDTH_NBSP_CHAR = '\ufeff';
+
+ /**
+ * @class core.dom
+ *
+ * Dom functions
+ *
+ * @singleton
+ * @alternateClassName dom
+ */
+ var dom = (function () {
+ /**
+ * @method isEditable
+ *
+ * returns whether node is `note-editable` or not.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+ var isEditable = function (node) {
+ return node && $(node).hasClass('note-editable');
+ };
+
+ /**
+ * @method isControlSizing
+ *
+ * returns whether node is `note-control-sizing` or not.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+ var isControlSizing = function (node) {
+ return node && $(node).hasClass('note-control-sizing');
+ };
+
+ /**
+ * @method makePredByNodeName
+ *
+ * returns predicate which judge whether nodeName is same
+ *
+ * @param {String} nodeName
+ * @return {Function}
+ */
+ var makePredByNodeName = function (nodeName) {
+ nodeName = nodeName.toUpperCase();
+ return function (node) {
+ return node && node.nodeName.toUpperCase() === nodeName;
+ };
+ };
+
+ /**
+ * @method isText
+ *
+ *
+ *
+ * @param {Node} node
+ * @return {Boolean} true if node's type is text(3)
+ */
+ var isText = function (node) {
+ return node && node.nodeType === 3;
+ };
+
+ /**
+ * @method isElement
+ *
+ *
+ *
+ * @param {Node} node
+ * @return {Boolean} true if node's type is element(1)
+ */
+ var isElement = function (node) {
+ return node && node.nodeType === 1;
+ };
+
+ /**
+ * ex) br, col, embed, hr, img, input, ...
+ * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
+ */
+ var isVoid = function (node) {
+ return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase());
+ };
+
+ var isPara = function (node) {
+ if (isEditable(node)) {
+ return false;
+ }
+
+ // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
+ return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
+ };
+
+ var isHeading = function (node) {
+ return node && /^H[1-7]/.test(node.nodeName.toUpperCase());
+ };
+
+ var isPre = makePredByNodeName('PRE');
+
+ var isLi = makePredByNodeName('LI');
+
+ var isPurePara = function (node) {
+ return isPara(node) && !isLi(node);
+ };
+
+ var isTable = makePredByNodeName('TABLE');
+
+ var isData = makePredByNodeName('DATA');
+
+ var isInline = function (node) {
+ return !isBodyContainer(node) &&
+ !isList(node) &&
+ !isHr(node) &&
+ !isPara(node) &&
+ !isTable(node) &&
+ !isBlockquote(node) &&
+ !isData(node);
+ };
+
+ var isList = function (node) {
+ return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
+ };
+
+ var isHr = makePredByNodeName('HR');
+
+ var isCell = function (node) {
+ return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
+ };
+
+ var isBlockquote = makePredByNodeName('BLOCKQUOTE');
+
+ var isBodyContainer = function (node) {
+ return isCell(node) || isBlockquote(node) || isEditable(node);
+ };
+
+ var isAnchor = makePredByNodeName('A');
+
+ var isParaInline = function (node) {
+ return isInline(node) && !!ancestor(node, isPara);
+ };
+
+ var isBodyInline = function (node) {
+ return isInline(node) && !ancestor(node, isPara);
+ };
+
+ var isBody = makePredByNodeName('BODY');
+
+ /**
+ * returns whether nodeB is closest sibling of nodeA
+ *
+ * @param {Node} nodeA
+ * @param {Node} nodeB
+ * @return {Boolean}
+ */
+ var isClosestSibling = function (nodeA, nodeB) {
+ return nodeA.nextSibling === nodeB ||
+ nodeA.previousSibling === nodeB;
+ };
+
+ /**
+ * returns array of closest siblings with node
+ *
+ * @param {Node} node
+ * @param {function} [pred] - predicate function
+ * @return {Node[]}
+ */
+ var withClosestSiblings = function (node, pred) {
+ pred = pred || func.ok;
+
+ var siblings = [];
+ if (node.previousSibling && pred(node.previousSibling)) {
+ siblings.push(node.previousSibling);
+ }
+ siblings.push(node);
+ if (node.nextSibling && pred(node.nextSibling)) {
+ siblings.push(node.nextSibling);
+ }
+ return siblings;
+ };
+
+ /**
+ * blank HTML for cursor position
+ * - [workaround] old IE only works with
+ * - [workaround] IE11 and other browser works with bogus br
+ */
+ var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? ' ' : '
';
+
+ /**
+ * @method nodeLength
+ *
+ * returns #text's text size or element's childNodes size
+ *
+ * @param {Node} node
+ */
+ var nodeLength = function (node) {
+ if (isText(node)) {
+ return node.nodeValue.length;
+ }
+
+ if (node) {
+ return node.childNodes.length;
+ }
+
+ return 0;
+
+ };
+
+ /**
+ * returns whether node is empty or not.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+ var isEmpty = function (node) {
+ var len = nodeLength(node);
+
+ if (len === 0) {
+ return true;
+ } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
+ // ex)
,
+ return true;
+ } else if (list.all(node.childNodes, isText) && node.innerHTML === '') {
+ // ex)
,
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * padding blankHTML if node is empty (for cursor position)
+ */
+ var paddingBlankHTML = function (node) {
+ if (!isVoid(node) && !nodeLength(node)) {
+ node.innerHTML = blankHTML;
+ }
+ };
+
+ /**
+ * find nearest ancestor predicate hit
+ *
+ * @param {Node} node
+ * @param {Function} pred - predicate function
+ */
+ var ancestor = function (node, pred) {
+ while (node) {
+ if (pred(node)) { return node; }
+ if (isEditable(node)) { break; }
+
+ node = node.parentNode;
+ }
+ return null;
+ };
+
+ /**
+ * find nearest ancestor only single child blood line and predicate hit
+ *
+ * @param {Node} node
+ * @param {Function} pred - predicate function
+ */
+ var singleChildAncestor = function (node, pred) {
+ node = node.parentNode;
+
+ while (node) {
+ if (nodeLength(node) !== 1) { break; }
+ if (pred(node)) { return node; }
+ if (isEditable(node)) { break; }
+
+ node = node.parentNode;
+ }
+ return null;
+ };
+
+ /**
+ * returns new array of ancestor nodes (until predicate hit).
+ *
+ * @param {Node} node
+ * @param {Function} [optional] pred - predicate function
+ */
+ var listAncestor = function (node, pred) {
+ pred = pred || func.fail;
+
+ var ancestors = [];
+ ancestor(node, function (el) {
+ if (!isEditable(el)) {
+ ancestors.push(el);
+ }
+
+ return pred(el);
+ });
+ return ancestors;
+ };
+
+ /**
+ * find farthest ancestor predicate hit
+ */
+ var lastAncestor = function (node, pred) {
+ var ancestors = listAncestor(node);
+ return list.last(ancestors.filter(pred));
+ };
+
+ /**
+ * returns common ancestor node between two nodes.
+ *
+ * @param {Node} nodeA
+ * @param {Node} nodeB
+ */
+ var commonAncestor = function (nodeA, nodeB) {
+ var ancestors = listAncestor(nodeA);
+ for (var n = nodeB; n; n = n.parentNode) {
+ if ($.inArray(n, ancestors) > -1) { return n; }
+ }
+ return null; // difference document area
+ };
+
+ /**
+ * listing all previous siblings (until predicate hit).
+ *
+ * @param {Node} node
+ * @param {Function} [optional] pred - predicate function
+ */
+ var listPrev = function (node, pred) {
+ pred = pred || func.fail;
+
+ var nodes = [];
+ while (node) {
+ if (pred(node)) { break; }
+ nodes.push(node);
+ node = node.previousSibling;
+ }
+ return nodes;
+ };
+
+ /**
+ * listing next siblings (until predicate hit).
+ *
+ * @param {Node} node
+ * @param {Function} [pred] - predicate function
+ */
+ var listNext = function (node, pred) {
+ pred = pred || func.fail;
+
+ var nodes = [];
+ while (node) {
+ if (pred(node)) { break; }
+ nodes.push(node);
+ node = node.nextSibling;
+ }
+ return nodes;
+ };
+
+ /**
+ * listing descendant nodes
+ *
+ * @param {Node} node
+ * @param {Function} [pred] - predicate function
+ */
+ var listDescendant = function (node, pred) {
+ var descendants = [];
+ pred = pred || func.ok;
+
+ // start DFS(depth first search) with node
+ (function fnWalk(current) {
+ if (node !== current && pred(current)) {
+ descendants.push(current);
+ }
+ for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
+ fnWalk(current.childNodes[idx]);
+ }
+ })(node);
+
+ return descendants;
+ };
+
+ /**
+ * wrap node with new tag.
+ *
+ * @param {Node} node
+ * @param {Node} tagName of wrapper
+ * @return {Node} - wrapper
+ */
+ var wrap = function (node, wrapperName) {
+ var parent = node.parentNode;
+ var wrapper = $('<' + wrapperName + '>')[0];
+
+ parent.insertBefore(wrapper, node);
+ wrapper.appendChild(node);
+
+ return wrapper;
+ };
+
+ /**
+ * insert node after preceding
+ *
+ * @param {Node} node
+ * @param {Node} preceding - predicate function
+ */
+ var insertAfter = function (node, preceding) {
+ var next = preceding.nextSibling, parent = preceding.parentNode;
+ if (next) {
+ parent.insertBefore(node, next);
+ } else {
+ parent.appendChild(node);
+ }
+ return node;
+ };
+
+ /**
+ * append elements.
+ *
+ * @param {Node} node
+ * @param {Collection} aChild
+ */
+ var appendChildNodes = function (node, aChild) {
+ $.each(aChild, function (idx, child) {
+ node.appendChild(child);
+ });
+ return node;
+ };
+
+ /**
+ * returns whether boundaryPoint is left edge or not.
+ *
+ * @param {BoundaryPoint} point
+ * @return {Boolean}
+ */
+ var isLeftEdgePoint = function (point) {
+ return point.offset === 0;
+ };
+
+ /**
+ * returns whether boundaryPoint is right edge or not.
+ *
+ * @param {BoundaryPoint} point
+ * @return {Boolean}
+ */
+ var isRightEdgePoint = function (point) {
+ return point.offset === nodeLength(point.node);
+ };
+
+ /**
+ * returns whether boundaryPoint is edge or not.
+ *
+ * @param {BoundaryPoint} point
+ * @return {Boolean}
+ */
+ var isEdgePoint = function (point) {
+ return isLeftEdgePoint(point) || isRightEdgePoint(point);
+ };
+
+ /**
+ * returns whether node is left edge of ancestor or not.
+ *
+ * @param {Node} node
+ * @param {Node} ancestor
+ * @return {Boolean}
+ */
+ var isLeftEdgeOf = function (node, ancestor) {
+ while (node && node !== ancestor) {
+ if (position(node) !== 0) {
+ return false;
+ }
+ node = node.parentNode;
+ }
+
+ return true;
+ };
+
+ /**
+ * returns whether node is right edge of ancestor or not.
+ *
+ * @param {Node} node
+ * @param {Node} ancestor
+ * @return {Boolean}
+ */
+ var isRightEdgeOf = function (node, ancestor) {
+ if (!ancestor) {
+ return false;
+ }
+ while (node && node !== ancestor) {
+ if (position(node) !== nodeLength(node.parentNode) - 1) {
+ return false;
+ }
+ node = node.parentNode;
+ }
+
+ return true;
+ };
+
+ /**
+ * returns whether point is left edge of ancestor or not.
+ * @param {BoundaryPoint} point
+ * @param {Node} ancestor
+ * @return {Boolean}
+ */
+ var isLeftEdgePointOf = function (point, ancestor) {
+ return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor);
+ };
+
+ /**
+ * returns whether point is right edge of ancestor or not.
+ * @param {BoundaryPoint} point
+ * @param {Node} ancestor
+ * @return {Boolean}
+ */
+ var isRightEdgePointOf = function (point, ancestor) {
+ return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor);
+ };
+
+ /**
+ * returns offset from parent.
+ *
+ * @param {Node} node
+ */
+ var position = function (node) {
+ var offset = 0;
+ while ((node = node.previousSibling)) {
+ offset += 1;
+ }
+ return offset;
+ };
+
+ var hasChildren = function (node) {
+ return !!(node && node.childNodes && node.childNodes.length);
+ };
+
+ /**
+ * returns previous boundaryPoint
+ *
+ * @param {BoundaryPoint} point
+ * @param {Boolean} isSkipInnerOffset
+ * @return {BoundaryPoint}
+ */
+ var prevPoint = function (point, isSkipInnerOffset) {
+ var node, offset;
+
+ if (point.offset === 0) {
+ if (isEditable(point.node)) {
+ return null;
+ }
+
+ node = point.node.parentNode;
+ offset = position(point.node);
+ } else if (hasChildren(point.node)) {
+ node = point.node.childNodes[point.offset - 1];
+ offset = nodeLength(node);
+ } else {
+ node = point.node;
+ offset = isSkipInnerOffset ? 0 : point.offset - 1;
+ }
+
+ return {
+ node: node,
+ offset: offset
+ };
+ };
+
+ /**
+ * returns next boundaryPoint
+ *
+ * @param {BoundaryPoint} point
+ * @param {Boolean} isSkipInnerOffset
+ * @return {BoundaryPoint}
+ */
+ var nextPoint = function (point, isSkipInnerOffset) {
+ var node, offset;
+
+ if (nodeLength(point.node) === point.offset) {
+ if (isEditable(point.node)) {
+ return null;
+ }
+
+ node = point.node.parentNode;
+ offset = position(point.node) + 1;
+ } else if (hasChildren(point.node)) {
+ node = point.node.childNodes[point.offset];
+ offset = 0;
+ } else {
+ node = point.node;
+ offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
+ }
+
+ return {
+ node: node,
+ offset: offset
+ };
+ };
+
+ /**
+ * returns whether pointA and pointB is same or not.
+ *
+ * @param {BoundaryPoint} pointA
+ * @param {BoundaryPoint} pointB
+ * @return {Boolean}
+ */
+ var isSamePoint = function (pointA, pointB) {
+ return pointA.node === pointB.node && pointA.offset === pointB.offset;
+ };
+
+ /**
+ * returns whether point is visible (can set cursor) or not.
+ *
+ * @param {BoundaryPoint} point
+ * @return {Boolean}
+ */
+ var isVisiblePoint = function (point) {
+ if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
+ return true;
+ }
+
+ var leftNode = point.node.childNodes[point.offset - 1];
+ var rightNode = point.node.childNodes[point.offset];
+ if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * @method prevPointUtil
+ *
+ * @param {BoundaryPoint} point
+ * @param {Function} pred
+ * @return {BoundaryPoint}
+ */
+ var prevPointUntil = function (point, pred) {
+ while (point) {
+ if (pred(point)) {
+ return point;
+ }
+
+ point = prevPoint(point);
+ }
+
+ return null;
+ };
+
+ /**
+ * @method nextPointUntil
+ *
+ * @param {BoundaryPoint} point
+ * @param {Function} pred
+ * @return {BoundaryPoint}
+ */
+ var nextPointUntil = function (point, pred) {
+ while (point) {
+ if (pred(point)) {
+ return point;
+ }
+
+ point = nextPoint(point);
+ }
+
+ return null;
+ };
+
+ /**
+ * returns whether point has character or not.
+ *
+ * @param {Point} point
+ * @return {Boolean}
+ */
+ var isCharPoint = function (point) {
+ if (!isText(point.node)) {
+ return false;
+ }
+
+ var ch = point.node.nodeValue.charAt(point.offset - 1);
+ return ch && (ch !== ' ' && ch !== NBSP_CHAR);
+ };
+
+ /**
+ * @method walkPoint
+ *
+ * @param {BoundaryPoint} startPoint
+ * @param {BoundaryPoint} endPoint
+ * @param {Function} handler
+ * @param {Boolean} isSkipInnerOffset
+ */
+ var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
+ var point = startPoint;
+
+ while (point) {
+ handler(point);
+
+ if (isSamePoint(point, endPoint)) {
+ break;
+ }
+
+ var isSkipOffset = isSkipInnerOffset &&
+ startPoint.node !== point.node &&
+ endPoint.node !== point.node;
+ point = nextPoint(point, isSkipOffset);
+ }
+ };
+
+ /**
+ * @method makeOffsetPath
+ *
+ * return offsetPath(array of offset) from ancestor
+ *
+ * @param {Node} ancestor - ancestor node
+ * @param {Node} node
+ */
+ var makeOffsetPath = function (ancestor, node) {
+ var ancestors = listAncestor(node, func.eq(ancestor));
+ return ancestors.map(position).reverse();
+ };
+
+ /**
+ * @method fromOffsetPath
+ *
+ * return element from offsetPath(array of offset)
+ *
+ * @param {Node} ancestor - ancestor node
+ * @param {array} offsets - offsetPath
+ */
+ var fromOffsetPath = function (ancestor, offsets) {
+ var current = ancestor;
+ for (var i = 0, len = offsets.length; i < len; i++) {
+ if (current.childNodes.length <= offsets[i]) {
+ current = current.childNodes[current.childNodes.length - 1];
+ } else {
+ current = current.childNodes[offsets[i]];
+ }
+ }
+ return current;
+ };
+
+ /**
+ * @method splitNode
+ *
+ * split element or #text
+ *
+ * @param {BoundaryPoint} point
+ * @param {Object} [options]
+ * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
+ * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
+ * @return {Node} right node of boundaryPoint
+ */
+ var splitNode = function (point, options) {
+ var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML;
+ var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint;
+
+ // edge case
+ if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) {
+ if (isLeftEdgePoint(point)) {
+ return point.node;
+ } else if (isRightEdgePoint(point)) {
+ return point.node.nextSibling;
+ }
+ }
+
+ // split #text
+ if (isText(point.node)) {
+ return point.node.splitText(point.offset);
+ } else {
+ var childNode = point.node.childNodes[point.offset];
+ var clone = insertAfter(point.node.cloneNode(false), point.node);
+ appendChildNodes(clone, listNext(childNode));
+
+ if (!isSkipPaddingBlankHTML) {
+ paddingBlankHTML(point.node);
+ paddingBlankHTML(clone);
+ }
+
+ return clone;
+ }
+ };
+
+ /**
+ * @method splitTree
+ *
+ * split tree by point
+ *
+ * @param {Node} root - split root
+ * @param {BoundaryPoint} point
+ * @param {Object} [options]
+ * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
+ * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
+ * @return {Node} right node of boundaryPoint
+ */
+ var splitTree = function (root, point, options) {
+ // ex) [#text,
, ]
+ var ancestors = listAncestor(point.node, func.eq(root));
+
+ if (!ancestors.length) {
+ return null;
+ } else if (ancestors.length === 1) {
+ return splitNode(point, options);
+ }
+
+ return ancestors.reduce(function (node, parent) {
+ if (node === point.node) {
+ node = splitNode(point, options);
+ }
+
+ return splitNode({
+ node: parent,
+ offset: node ? dom.position(node) : nodeLength(parent)
+ }, options);
+ });
+ };
+
+ /**
+ * split point
+ *
+ * @param {Point} point
+ * @param {Boolean} isInline
+ * @return {Object}
+ */
+ var splitPoint = function (point, isInline) {
+ // find splitRoot, container
+ // - inline: splitRoot is a child of paragraph
+ // - block: splitRoot is a child of bodyContainer
+ var pred = isInline ? isPara : isBodyContainer;
+ var ancestors = listAncestor(point.node, pred);
+ var topAncestor = list.last(ancestors) || point.node;
+
+ var splitRoot, container;
+ if (pred(topAncestor)) {
+ splitRoot = ancestors[ancestors.length - 2];
+ container = topAncestor;
+ } else {
+ splitRoot = topAncestor;
+ container = splitRoot.parentNode;
+ }
+
+ // if splitRoot is exists, split with splitTree
+ var pivot = splitRoot && splitTree(splitRoot, point, {
+ isSkipPaddingBlankHTML: isInline,
+ isNotSplitEdgePoint: isInline
+ });
+
+ // if container is point.node, find pivot with point.offset
+ if (!pivot && container === point.node) {
+ pivot = point.node.childNodes[point.offset];
+ }
+
+ return {
+ rightNode: pivot,
+ container: container
+ };
+ };
+
+ var create = function (nodeName) {
+ return document.createElement(nodeName);
+ };
+
+ var createText = function (text) {
+ return document.createTextNode(text);
+ };
+
+ /**
+ * @method remove
+ *
+ * remove node, (isRemoveChild: remove child or not)
+ *
+ * @param {Node} node
+ * @param {Boolean} isRemoveChild
+ */
+ var remove = function (node, isRemoveChild) {
+ if (!node || !node.parentNode) { return; }
+ if (node.removeNode) { return node.removeNode(isRemoveChild); }
+
+ var parent = node.parentNode;
+ if (!isRemoveChild) {
+ var nodes = [];
+ var i, len;
+ for (i = 0, len = node.childNodes.length; i < len; i++) {
+ nodes.push(node.childNodes[i]);
+ }
+
+ for (i = 0, len = nodes.length; i < len; i++) {
+ parent.insertBefore(nodes[i], node);
+ }
+ }
+
+ parent.removeChild(node);
+ };
+
+ /**
+ * @method removeWhile
+ *
+ * @param {Node} node
+ * @param {Function} pred
+ */
+ var removeWhile = function (node, pred) {
+ while (node) {
+ if (isEditable(node) || !pred(node)) {
+ break;
+ }
+
+ var parent = node.parentNode;
+ remove(node);
+ node = parent;
+ }
+ };
+
+ /**
+ * @method replace
+ *
+ * replace node with provided nodeName
+ *
+ * @param {Node} node
+ * @param {String} nodeName
+ * @return {Node} - new node
+ */
+ var replace = function (node, nodeName) {
+ if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
+ return node;
+ }
+
+ var newNode = create(nodeName);
+
+ if (node.style.cssText) {
+ newNode.style.cssText = node.style.cssText;
+ }
+
+ appendChildNodes(newNode, list.from(node.childNodes));
+ insertAfter(newNode, node);
+ remove(node);
+
+ return newNode;
+ };
+
+ var isTextarea = makePredByNodeName('TEXTAREA');
+
+ /**
+ * @param {jQuery} $node
+ * @param {Boolean} [stripLinebreaks] - default: false
+ */
+ var value = function ($node, stripLinebreaks) {
+ var val = isTextarea($node[0]) ? $node.val() : $node.html();
+ if (stripLinebreaks) {
+ return val.replace(/[\n\r]/g, '');
+ }
+ return val;
+ };
+
+ /**
+ * @method html
+ *
+ * get the HTML contents of node
+ *
+ * @param {jQuery} $node
+ * @param {Boolean} [isNewlineOnBlock]
+ */
+ var html = function ($node, isNewlineOnBlock) {
+ var markup = value($node);
+
+ if (isNewlineOnBlock) {
+ var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
+ markup = markup.replace(regexTag, function (match, endSlash, name) {
+ name = name.toUpperCase();
+ var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
+ !!endSlash;
+ var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);
+
+ return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
+ });
+ markup = $.trim(markup);
+ }
+
+ return markup;
+ };
+
+ var posFromPlaceholder = function (placeholder) {
+ var $placeholder = $(placeholder);
+ var pos = $placeholder.offset();
+ var height = $placeholder.outerHeight(true); // include margin
+
+ return {
+ left: pos.left,
+ top: pos.top + height
+ };
+ };
+
+ var attachEvents = function ($node, events) {
+ Object.keys(events).forEach(function (key) {
+ $node.on(key, events[key]);
+ });
+ };
+
+ var detachEvents = function ($node, events) {
+ Object.keys(events).forEach(function (key) {
+ $node.off(key, events[key]);
+ });
+ };
+
+ return {
+ /** @property {String} NBSP_CHAR */
+ NBSP_CHAR: NBSP_CHAR,
+ /** @property {String} ZERO_WIDTH_NBSP_CHAR */
+ ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
+ /** @property {String} blank */
+ blank: blankHTML,
+ /** @property {String} emptyPara */
+ emptyPara: '
' + blankHTML + '
',
+ makePredByNodeName: makePredByNodeName,
+ isEditable: isEditable,
+ isControlSizing: isControlSizing,
+ isText: isText,
+ isElement: isElement,
+ isVoid: isVoid,
+ isPara: isPara,
+ isPurePara: isPurePara,
+ isHeading: isHeading,
+ isInline: isInline,
+ isBlock: func.not(isInline),
+ isBodyInline: isBodyInline,
+ isBody: isBody,
+ isParaInline: isParaInline,
+ isPre: isPre,
+ isList: isList,
+ isTable: isTable,
+ isData: isData,
+ isCell: isCell,
+ isBlockquote: isBlockquote,
+ isBodyContainer: isBodyContainer,
+ isAnchor: isAnchor,
+ isDiv: makePredByNodeName('DIV'),
+ isLi: isLi,
+ isBR: makePredByNodeName('BR'),
+ isSpan: makePredByNodeName('SPAN'),
+ isB: makePredByNodeName('B'),
+ isU: makePredByNodeName('U'),
+ isS: makePredByNodeName('S'),
+ isI: makePredByNodeName('I'),
+ isImg: makePredByNodeName('IMG'),
+ isTextarea: isTextarea,
+ isEmpty: isEmpty,
+ isEmptyAnchor: func.and(isAnchor, isEmpty),
+ isClosestSibling: isClosestSibling,
+ withClosestSiblings: withClosestSiblings,
+ nodeLength: nodeLength,
+ isLeftEdgePoint: isLeftEdgePoint,
+ isRightEdgePoint: isRightEdgePoint,
+ isEdgePoint: isEdgePoint,
+ isLeftEdgeOf: isLeftEdgeOf,
+ isRightEdgeOf: isRightEdgeOf,
+ isLeftEdgePointOf: isLeftEdgePointOf,
+ isRightEdgePointOf: isRightEdgePointOf,
+ prevPoint: prevPoint,
+ nextPoint: nextPoint,
+ isSamePoint: isSamePoint,
+ isVisiblePoint: isVisiblePoint,
+ prevPointUntil: prevPointUntil,
+ nextPointUntil: nextPointUntil,
+ isCharPoint: isCharPoint,
+ walkPoint: walkPoint,
+ ancestor: ancestor,
+ singleChildAncestor: singleChildAncestor,
+ listAncestor: listAncestor,
+ lastAncestor: lastAncestor,
+ listNext: listNext,
+ listPrev: listPrev,
+ listDescendant: listDescendant,
+ commonAncestor: commonAncestor,
+ wrap: wrap,
+ insertAfter: insertAfter,
+ appendChildNodes: appendChildNodes,
+ position: position,
+ hasChildren: hasChildren,
+ makeOffsetPath: makeOffsetPath,
+ fromOffsetPath: fromOffsetPath,
+ splitTree: splitTree,
+ splitPoint: splitPoint,
+ create: create,
+ createText: createText,
+ remove: remove,
+ removeWhile: removeWhile,
+ replace: replace,
+ html: html,
+ value: value,
+ posFromPlaceholder: posFromPlaceholder,
+ attachEvents: attachEvents,
+ detachEvents: detachEvents
+ };
+ })();
+
+ /**
+ * @param {jQuery} $note
+ * @param {Object} options
+ * @return {Context}
+ */
+ var Context = function ($note, options) {
+ var self = this;
+
+ var ui = $.summernote.ui;
+ this.memos = {};
+ this.modules = {};
+ this.layoutInfo = {};
+ this.options = options;
+
+ /**
+ * create layout and initialize modules and other resources
+ */
+ this.initialize = function () {
+ this.layoutInfo = ui.createLayout($note, options);
+ this._initialize();
+ $note.hide();
+ return this;
+ };
+
+ /**
+ * destroy modules and other resources and remove layout
+ */
+ this.destroy = function () {
+ this._destroy();
+ $note.removeData('summernote');
+ ui.removeLayout($note, this.layoutInfo);
+ };
+
+ /**
+ * destory modules and other resources and initialize it again
+ */
+ this.reset = function () {
+ var disabled = self.isDisabled();
+ this.code(dom.emptyPara);
+ this._destroy();
+ this._initialize();
+
+ if (disabled) {
+ self.disable();
+ }
+ };
+
+ this._initialize = function () {
+ // add optional buttons
+ var buttons = $.extend({}, this.options.buttons);
+ Object.keys(buttons).forEach(function (key) {
+ self.memo('button.' + key, buttons[key]);
+ });
+
+ var modules = $.extend({}, this.options.modules, $.summernote.plugins || {});
+
+ // add and initialize modules
+ Object.keys(modules).forEach(function (key) {
+ self.module(key, modules[key], true);
+ });
+
+ Object.keys(this.modules).forEach(function (key) {
+ self.initializeModule(key);
+ });
+ };
+
+ this._destroy = function () {
+ // destroy modules with reversed order
+ Object.keys(this.modules).reverse().forEach(function (key) {
+ self.removeModule(key);
+ });
+
+ Object.keys(this.memos).forEach(function (key) {
+ self.removeMemo(key);
+ });
+ };
+
+ this.code = function (html) {
+ var isActivated = this.invoke('codeview.isActivated');
+
+ if (html === undefined) {
+ this.invoke('codeview.sync');
+ return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html();
+ } else {
+ if (isActivated) {
+ this.layoutInfo.codable.val(html);
+ } else {
+ this.layoutInfo.editable.html(html);
+ }
+ $note.val(html);
+ this.triggerEvent('change', html);
+ }
+ };
+
+ this.isDisabled = function () {
+ return this.layoutInfo.editable.attr('contenteditable') === 'false';
+ };
+
+ this.enable = function () {
+ this.layoutInfo.editable.attr('contenteditable', true);
+ this.invoke('toolbar.activate', true);
+ };
+
+ this.disable = function () {
+ // close codeview if codeview is opend
+ if (this.invoke('codeview.isActivated')) {
+ this.invoke('codeview.deactivate');
+ }
+ this.layoutInfo.editable.attr('contenteditable', false);
+ this.invoke('toolbar.deactivate', true);
+ };
+
+ this.triggerEvent = function () {
+ var namespace = list.head(arguments);
+ var args = list.tail(list.from(arguments));
+
+ var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')];
+ if (callback) {
+ callback.apply($note[0], args);
+ }
+ $note.trigger('summernote.' + namespace, args);
+ };
+
+ this.initializeModule = function (key) {
+ var module = this.modules[key];
+ module.shouldInitialize = module.shouldInitialize || func.ok;
+ if (!module.shouldInitialize()) {
+ return;
+ }
+
+ // initialize module
+ if (module.initialize) {
+ module.initialize();
+ }
+
+ // attach events
+ if (module.events) {
+ dom.attachEvents($note, module.events);
+ }
+ };
+
+ this.module = function (key, ModuleClass, withoutIntialize) {
+ if (arguments.length === 1) {
+ return this.modules[key];
+ }
+
+ this.modules[key] = new ModuleClass(this);
+
+ if (!withoutIntialize) {
+ this.initializeModule(key);
+ }
+ };
+
+ this.removeModule = function (key) {
+ var module = this.modules[key];
+ if (module.shouldInitialize()) {
+ if (module.events) {
+ dom.detachEvents($note, module.events);
+ }
+
+ if (module.destroy) {
+ module.destroy();
+ }
+ }
+
+ delete this.modules[key];
+ };
+
+ this.memo = function (key, obj) {
+ if (arguments.length === 1) {
+ return this.memos[key];
+ }
+ this.memos[key] = obj;
+ };
+
+ this.removeMemo = function (key) {
+ if (this.memos[key] && this.memos[key].destroy) {
+ this.memos[key].destroy();
+ }
+
+ delete this.memos[key];
+ };
+
+ this.createInvokeHandler = function (namespace, value) {
+ return function (event) {
+ event.preventDefault();
+ self.invoke(namespace, value || $(event.target).closest('[data-value]').data('value'));
+ };
+ };
+
+ this.invoke = function () {
+ var namespace = list.head(arguments);
+ var args = list.tail(list.from(arguments));
+
+ var splits = namespace.split('.');
+ var hasSeparator = splits.length > 1;
+ var moduleName = hasSeparator && list.head(splits);
+ var methodName = hasSeparator ? list.last(splits) : list.head(splits);
+
+ var module = this.modules[moduleName || 'editor'];
+ if (!moduleName && this[methodName]) {
+ return this[methodName].apply(this, args);
+ } else if (module && module[methodName] && module.shouldInitialize()) {
+ return module[methodName].apply(module, args);
+ }
+ };
+
+ return this.initialize();
+ };
+
+ $.fn.extend({
+ /**
+ * Summernote API
+ *
+ * @param {Object|String}
+ * @return {this}
+ */
+ summernote: function () {
+ var type = $.type(list.head(arguments));
+ var isExternalAPICalled = type === 'string';
+ var hasInitOptions = type === 'object';
+
+ var options = hasInitOptions ? list.head(arguments) : {};
+
+ options = $.extend({}, $.summernote.options, options);
+ options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
+ options.icons = $.extend(true, {}, $.summernote.options.icons, options.icons);
+
+ this.each(function (idx, note) {
+ var $note = $(note);
+ if (!$note.data('summernote')) {
+ var context = new Context($note, options);
+ $note.data('summernote', context);
+ $note.data('summernote').triggerEvent('init', context.layoutInfo);
+ }
+ });
+
+ var $note = this.first();
+ if ($note.length) {
+ var context = $note.data('summernote');
+ if (isExternalAPICalled) {
+ return context.invoke.apply(context, list.from(arguments));
+ } else if (options.focus) {
+ context.invoke('editor.focus');
+ }
+ }
+
+ return this;
+ }
+ });
+
+
+ var Renderer = function (markup, children, options, callback) {
+ this.render = function ($parent) {
+ var $node = $(markup);
+
+ if (options && options.contents) {
+ $node.html(options.contents);
+ }
+
+ if (options && options.className) {
+ $node.addClass(options.className);
+ }
+
+ if (options && options.data) {
+ $.each(options.data, function (k, v) {
+ $node.attr('data-' + k, v);
+ });
+ }
+
+ if (options && options.click) {
+ $node.on('click', options.click);
+ }
+
+ if (children) {
+ var $container = $node.find('.note-children-container');
+ children.forEach(function (child) {
+ child.render($container.length ? $container : $node);
+ });
+ }
+
+ if (callback) {
+ callback($node, options);
+ }
+
+ if (options && options.callback) {
+ options.callback($node);
+ }
+
+ if ($parent) {
+ $parent.append($node);
+ }
+
+ return $node;
+ };
+ };
+
+ var renderer = {
+ create: function (markup, callback) {
+ return function () {
+ var children = $.isArray(arguments[0]) ? arguments[0] : [];
+ var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0];
+ if (options && options.children) {
+ children = options.children;
+ }
+ return new Renderer(markup, children, options, callback);
+ };
+ }
+ };
+
+ var editor = renderer.create('');
+ var toolbar = renderer.create('');
+ var editingArea = renderer.create('');
+ var codable = renderer.create('');
+ var editable = renderer.create('');
+ var statusbar = renderer.create([
+ '',
+ '
',
+ '
',
+ '
',
+ '
',
+ '
',
+ '
'
+ ].join(''));
+
+ var airEditor = renderer.create('');
+ var airEditable = renderer.create('');
+
+ var buttonGroup = renderer.create('');
+ var button = renderer.create('