diff --git a/public/build.json b/public/build.json index 8fb5c5156d..c2e468345c 100644 --- a/public/build.json +++ b/public/build.json @@ -155,6 +155,7 @@ "lib/public/js/legacy/widgets/form/form_comments.js", "lib/public/js/legacy/wn/widgets/form/sidebar.js", "lib/public/js/legacy/wn/widgets/form/comments.js", + "lib/public/js/wn/form/editors.js", "lib/public/js/wn/form/attachments.js", "lib/public/js/wn/form/linked_with.js", "lib/public/js/wn/form/states.js", diff --git a/public/css/ui/common.css b/public/css/ui/common.css index f7919b5713..743ea50ede 100644 --- a/public/css/ui/common.css +++ b/public/css/ui/common.css @@ -113,3 +113,18 @@ a { font-size: 32px; color: #888; } + +.wysiwyg-editor { + min-height: 400px; + background-color: white; + border-collapse: separate; + border: 1px solid rgb(204, 204, 204); + padding: 4px; + box-sizing: content-box; + -webkit-box-shadow: rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset; + box-shadow: rgba(0, 0, 0, 0.0745098) 0px 1px 1px 0px inset; + border-top-right-radius: 3px; border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; border-top-left-radius: 3px; + overflow: scroll; + outline: none; +} diff --git a/public/js/legacy/widgets/form/form_fields.js b/public/js/legacy/widgets/form/form_fields.js index 450bd84754..6186850f92 100644 --- a/public/js/legacy/widgets/form/form_fields.js +++ b/public/js/legacy/widgets/form/form_fields.js @@ -218,143 +218,33 @@ _f.CodeField.prototype.make_input = function() { this.label_span.innerHTML = this.df.label; + $(this.input_area).css({"min-height":"360px"}); + if(this.df.fieldtype=='Text Editor') { - $(this.input_area).css({"min-height":"360px"}); - this.input = $a(this.input_area, 'text_area', '', {fontSize:'12px'}); - this.myid = wn.dom.set_unique_id(this.input); - - // setup tiny mce - $(me.input).tinymce({ - // Location of TinyMCE script - script_url : 'lib/js/lib/tiny_mce_3.5.7/tiny_mce.js', - - // General options - theme : "advanced", - plugins : "style,inlinepopups,table,advimage", - extended_valid_elements: "script|embed", - - // w/h - width: '100%', - height: '360px', - - // buttons - theme_advanced_buttons1 : "bold,italic,underline,hr,|,justifyleft,justifycenter,|,formatselect,fontsizeselect,|,bullist,numlist,|,image,|,outdent,indent,|,link,|,forecolor,backcolor,|,code", - theme_advanced_buttons2 : "", - theme_advanced_buttons3 : "", - - theme_advanced_toolbar_location : "top", - theme_advanced_toolbar_align : "left", - theme_advanced_statusbar_location: "none", - theme_advanced_path: false, - - valid_elements : "*[*]", - - content_css: "lib/js/lib/tiny_mce_3.5.7/custom_content.css?q=1", - - oninit: function() { me.init_editor(); }, - setup: function(ed) { - ed.onChange.add(function(ed, l) { - me.set(l.content); - me.run_trigger(); - }); - } + this.input = new wn.editors.BootstrapWYSIWYG({ + parent: this.input_area, + change: function(value) { + me.set_value_and_run_trigger(value); + }, + field: this }); - - this.input.set_input = function(v) { - if(me.editor) { - me.editor.setContent(v==null ? "" : v); - } else { - $(me.input).val(v); - } - } - this.get_value = function() { - return me.editor && me.editor.getContent(); // tinyMCE - } - } else { - // setup ace - wn.require('lib/js/lib/ace/ace.js'); - - $(this.input_area).css('border','1px solid #aaa'); - this.pre = $("
").appendTo(this.input_area).get(0);
-
- this.input = {};
- this.myid = wn.dom.set_unique_id(this.pre);
- this.editor = ace.edit(this.myid);
-
- if(me.df.options=='Markdown' || me.df.options=='HTML') {
- wn.require('lib/js/lib/ace/mode-html.js');
- var HTMLMode = require("ace/mode/html").Mode;
- me.editor.getSession().setMode(new HTMLMode());
- }
-
- else if(me.df.options=='Javascript') {
- wn.require('lib/js/lib/ace/mode-javascript.js');
- var JavascriptMode = require("ace/mode/javascript").Mode;
- me.editor.getSession().setMode(new JavascriptMode());
- }
-
- else if(me.df.options=='Python') {
- wn.require('lib/js/lib/ace/mode-python.js');
- var PythonMode = require("ace/mode/python").Mode;
- me.editor.getSession().setMode(new PythonMode());
- }
-
- this.input.set_input = function(v) {
- // during field refresh in run trigger, set_input is called
- // if called during on_change, setting doesn't make sense
- // and causes cursor to shift back to first position
- if(me.changing_value) return;
-
- me.setting_value = true;
- me.editor.getSession().setValue(v==null ? "" : v);
- me.setting_value = false;
- }
-
- this.get_value = function() {
- return me.editor.getSession().getValue(); // tinyMCE
- }
- $(cur_frm.wrapper).bind('render_complete', function() {
- me.editor.resize();
- me.editor.getSession().on('change', function() {
- if(me.setting_value) return;
- var val = me.get_value();
- if(locals[cur_frm.doctype][cur_frm.docname][me.df.fieldname] != val) {
- me.set(me.get_value());
-
- me.changing_value = true;
- me.run_trigger();
- me.changing_value = false;
- }
- })
+ this.input = new wn.editors.ACE({
+ parent: this.input_area,
+ change: function(value) {
+ me.set_value_and_run_trigger(value);
+ },
+ field: this
});
- this.onrefresh = function() {
- me.editor && me.editor.resize();
- }
}
-
}
-_f.CodeField.prototype.init_editor = function() {
- // attach onchange methods
- var me = this;
- this.editor = tinymce.get(this.myid);
- this.editor.onKeyUp.add(function(ed, e) {
- me.set(ed.getContent());
- });
- this.editor.onPaste.add(function(ed, e) {
- me.set(ed.getContent());
- });
- this.editor.onSetContent.add(function(ed, e) {
- me.set(ed.getContent());
- });
-
- // reset content
- var c = locals[cur_frm.doctype][cur_frm.docname][this.df.fieldname];
- if(cur_frm && c) {
- this.editor.setContent(c);
+_f.CodeField.prototype.set_value_and_run_trigger = function(value) {
+ if(locals[cur_frm.doctype][cur_frm.docname][this.df.fieldname] != value) {
+ this.set(value);
+ this.changing_value = true;
+ this.run_trigger();
+ this.changing_value = false;
}
}
@@ -363,7 +253,8 @@ _f.CodeField.prototype.set_disp = function(val) {
if(this.df.fieldtype=='Text Editor') {
this.disp_area.innerHTML = val;
} else {
- this.disp_area.innerHTML = '';
+ this.disp_area.innerHTML = '';
}
}
diff --git a/public/js/lib/beautify-html.js b/public/js/lib/beautify-html.js
new file mode 100644
index 0000000000..005a4fb48a
--- /dev/null
+++ b/public/js/lib/beautify-html.js
@@ -0,0 +1,637 @@
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+ The MIT License (MIT)
+
+ Copyright (c) 2007-2013 Einar Lielmanis and contributors.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+ Style HTML
+---------------
+
+ Written by Nochum Sossonko, (nsossonko@hotmail.com)
+
+ Based on code initially developed by: Einar Lielmanis,
+ http://jsbeautifier.org/
+
+ Usage:
+ style_html(html_source);
+
+ style_html(html_source, options);
+
+ The options are:
+ indent_size (default 4) — indentation size,
+ indent_char (default space) — character to indent with,
+ max_char (default 250) - maximum amount of characters per line (0 = disable)
+ brace_style (default "collapse") - "collapse" | "expand" | "end-expand"
+ put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line.
+ unformatted (defaults to inline tags) - list of tags, that shouldn't be reformatted
+ indent_scripts (default normal) - "keep"|"separate"|"normal"
+
+ e.g.
+
+ style_html(html_source, {
+ 'indent_size': 2,
+ 'indent_char': ' ',
+ 'max_char': 78,
+ 'brace_style': 'expand',
+ 'unformatted': ['a', 'sub', 'sup', 'b', 'i', 'u']
+ });
+*/
+
+(function() {
+
+ function style_html(html_source, options, js_beautify, css_beautify) {
+ //Wrapper function to invoke all the necessary constructors and deal with the output.
+
+ var multi_parser,
+ indent_size,
+ indent_character,
+ max_char,
+ brace_style,
+ unformatted;
+
+ options = options || {};
+ indent_size = options.indent_size || 4;
+ indent_character = options.indent_char || ' ';
+ brace_style = options.brace_style || 'collapse';
+ max_char = options.max_char === 0 ? Infinity : options.max_char || 250;
+ unformatted = options.unformatted || ['a', 'span', 'bdo', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'q', 'sub', 'sup', 'tt', 'i', 'b', 'big', 'small', 'u', 's', 'strike', 'font', 'ins', 'del', 'pre', 'address', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+
+ function Parser() {
+
+ this.pos = 0; //Parser position
+ this.token = '';
+ this.current_mode = 'CONTENT'; //reflects the current Parser mode: TAG/CONTENT
+ this.tags = { //An object to hold tags, their position, and their parent-tags, initiated with default values
+ parent: 'parent1',
+ parentcount: 1,
+ parent1: ''
+ };
+ this.tag_type = '';
+ this.token_text = this.last_token = this.last_text = this.token_type = '';
+
+ this.Utils = { //Uilities made available to the various functions
+ whitespace: "\n\r\t ".split(''),
+ single_token: 'br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed,?php,?,?='.split(','), //all the single tags for HTML
+ extra_liners: 'head,body,/html'.split(','), //for tags that need a line of whitespace before them
+ in_array: function (what, arr) {
+ for (var i=0; i= this.input.length) {
+ return content.length?content.join(''):['', 'TK_EOF'];
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+ this.line_char_count++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ if (content.length) {
+ space = true;
+ }
+ this.line_char_count--;
+ continue; //don't want to insert unnecessary space
+ }
+ else if (space) {
+ if (this.line_char_count >= this.max_char) { //insert a line when the max_char is reached
+ content.push('\n');
+ for (var i=0; i', 'igm');
+ reg_match.lastIndex = this.pos;
+ var reg_array = reg_match.exec(this.input);
+ var end_script = reg_array?reg_array.index:this.input.length; //absolute end of script
+ if(this.pos < end_script) { //get everything in between the script tags
+ content = this.input.substring(this.pos, end_script);
+ this.pos = end_script;
+ }
+ return content;
+ };
+
+ this.record_tag = function (tag){ //function to record a tag and its parent in this.tags Object
+ if (this.tags[tag + 'count']) { //check for the existence of this tag type
+ this.tags[tag + 'count']++;
+ this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+ }
+ else { //otherwise initialize this tag type
+ this.tags[tag + 'count'] = 1;
+ this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+ }
+ this.tags[tag + this.tags[tag + 'count'] + 'parent'] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
+ this.tags.parent = tag + this.tags[tag + 'count']; //and make this the current parent (i.e. in the case of a div 'div1')
+ };
+
+ this.retrieve_tag = function (tag) { //function to retrieve the opening tag to the corresponding closer
+ if (this.tags[tag + 'count']) { //if the openener is not in the Object we ignore it
+ var temp_parent = this.tags.parent; //check to see if it's a closable tag.
+ while (temp_parent) { //till we reach '' (the initial value);
+ if (tag + this.tags[tag + 'count'] === temp_parent) { //if this is it use it
+ break;
+ }
+ temp_parent = this.tags[temp_parent + 'parent']; //otherwise keep on climbing up the DOM Tree
+ }
+ if (temp_parent) { //if we caught something
+ this.indent_level = this.tags[tag + this.tags[tag + 'count']]; //set the indent_level accordingly
+ this.tags.parent = this.tags[temp_parent + 'parent']; //and set the current parent
+ }
+ delete this.tags[tag + this.tags[tag + 'count'] + 'parent']; //delete the closed tags parent reference...
+ delete this.tags[tag + this.tags[tag + 'count']]; //...and the tag itself
+ if (this.tags[tag + 'count'] === 1) {
+ delete this.tags[tag + 'count'];
+ }
+ else {
+ this.tags[tag + 'count']--;
+ }
+ }
+ };
+
+ this.get_tag = function (peek) { //function to get a full tag and parse its type
+ var input_char = '',
+ content = [],
+ comment = '',
+ space = false,
+ tag_start, tag_end,
+ orig_pos = this.pos,
+ orig_line_char_count = this.line_char_count;
+
+ peek = peek !== undefined ? peek : false;
+
+ do {
+ if (this.pos >= this.input.length) {
+ if (peek) {
+ this.pos = orig_pos;
+ this.line_char_count = orig_line_char_count;
+ }
+ return content.length?content.join(''):['', 'TK_EOF'];
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+ this.line_char_count++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) { //don't want to insert unnecessary space
+ space = true;
+ this.line_char_count--;
+ continue;
+ }
+
+ if (input_char === "'" || input_char === '"') {
+ if (!content[1] || content[1] !== '!') { //if we're in a comment strings don't get treated specially
+ input_char += this.get_unformatted(input_char);
+ space = true;
+ }
+ }
+
+ if (input_char === '=') { //no space before =
+ space = false;
+ }
+
+ if (content.length && content[content.length-1] !== '=' && input_char !== '>' && space) {
+ //no space after = or before >
+ if (this.line_char_count >= this.max_char) {
+ this.print_newline(false, content);
+ this.line_char_count = 0;
+ }
+ else {
+ content.push(' ');
+ this.line_char_count++;
+ }
+ space = false;
+ }
+ if (input_char === '<') {
+ tag_start = this.pos - 1;
+ }
+ content.push(input_char); //inserts character at-a-time (or string)
+ } while (input_char !== '>');
+
+ var tag_complete = content.join('');
+ var tag_index;
+ if (tag_complete.indexOf(' ') !== -1) { //if there's whitespace, thats where the tag name ends
+ tag_index = tag_complete.indexOf(' ');
+ }
+ else { //otherwise go with the tag ending
+ tag_index = tag_complete.indexOf('>');
+ }
+ var tag_check = tag_complete.substring(1, tag_index).toLowerCase();
+ if (tag_complete.charAt(tag_complete.length-2) === '/' ||
+ this.Utils.in_array(tag_check, this.Utils.single_token)) { //if this tag name is a single tag type (either in the list or has a closing /)
+ if ( ! peek) {
+ this.tag_type = 'SINGLE';
+ }
+ }
+ else if (tag_check === 'script') { //for later script handling
+ if ( ! peek) {
+ this.record_tag(tag_check);
+ this.tag_type = 'SCRIPT';
+ }
+ }
+ else if (tag_check === 'style') { //for future style handling (for now it justs uses get_content)
+ if ( ! peek) {
+ this.record_tag(tag_check);
+ this.tag_type = 'STYLE';
+ }
+ }
+ else if (this.is_unformatted(tag_check, unformatted)) { // do not reformat the "unformatted" tags
+ comment = this.get_unformatted(''+tag_check+'>', tag_complete); //...delegate to get_unformatted function
+ content.push(comment);
+ // Preserve collapsed whitespace either before or after this tag.
+ if (tag_start > 0 && this.Utils.in_array(this.input.charAt(tag_start - 1), this.Utils.whitespace)){
+ content.splice(0, 0, this.input.charAt(tag_start - 1));
+ }
+ tag_end = this.pos - 1;
+ if (this.Utils.in_array(this.input.charAt(tag_end + 1), this.Utils.whitespace)){
+ content.push(this.input.charAt(tag_end + 1));
+ }
+ this.tag_type = 'SINGLE';
+ }
+ else if (tag_check.charAt(0) === '!') { //peek for so...
+ comment = this.get_unformatted('-->', tag_complete); //...delegate to get_unformatted
+ content.push(comment);
+ }
+ if ( ! peek) {
+ this.tag_type = 'START';
+ }
+ }
+ else if (tag_check.indexOf('[endif') !== -1) {//peek for ', tag_complete);
+ content.push(comment);
+ this.tag_type = 'SINGLE';
+ }
+ }
+ else if ( ! peek) {
+ if (tag_check.charAt(0) === '/') { //this tag is a double tag so check for tag-ending
+ this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
+ this.tag_type = 'END';
+ }
+ else { //otherwise it's a start-tag
+ this.record_tag(tag_check); //push it on the tag stack
+ this.tag_type = 'START';
+ }
+ if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { //check if this double needs an extra line
+ this.print_newline(true, this.output);
+ }
+ }
+
+ if (peek) {
+ this.pos = orig_pos;
+ this.line_char_count = orig_line_char_count;
+ }
+
+ return content.join(''); //returns fully formatted tag
+ };
+
+ this.get_unformatted = function (delimiter, orig_tag) { //function to return unformatted content in its entirety
+
+ if (orig_tag && orig_tag.toLowerCase().indexOf(delimiter) !== -1) {
+ return '';
+ }
+ var input_char = '';
+ var content = '';
+ var space = true;
+ do {
+
+ if (this.pos >= this.input.length) {
+ return content;
+ }
+
+ input_char = this.input.charAt(this.pos);
+ this.pos++;
+
+ if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+ if (!space) {
+ this.line_char_count--;
+ continue;
+ }
+ if (input_char === '\n' || input_char === '\r') {
+ content += '\n';
+ /* Don't change tab indention for unformatted blocks. If using code for html editing, this will greatly affect tags if they are specified in the 'unformatted array'
+ for (var i=0; i]*>\s*$/);
+
+ // if next_tag comes back but is not an isolated tag, then
+ // let's treat the 'a' tag as having content
+ // and respect the unformatted option
+ if (!tag || this.Utils.in_array(tag, unformatted)){
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ this.printer = function (js_source, indent_character, indent_size, max_char, brace_style) { //handles input/output and some other printing functions
+
+ this.input = js_source || ''; //gets the input for the Parser
+ this.output = [];
+ this.indent_character = indent_character;
+ this.indent_string = '';
+ this.indent_size = indent_size;
+ this.brace_style = brace_style;
+ this.indent_level = 0;
+ this.max_char = max_char;
+ this.line_char_count = 0; //count to see if max_char was exceeded
+
+ for (var i=0; i 0) {
+ this.indent_level--;
+ }
+ };
+ };
+ return this;
+ }
+
+ /*_____________________--------------------_____________________*/
+
+ multi_parser = new Parser(); //wrapping functions Parser
+ multi_parser.printer(html_source, indent_character, indent_size, max_char, brace_style); //initialize starting values
+
+ while (true) {
+ var t = multi_parser.get_token();
+ multi_parser.token_text = t[0];
+ multi_parser.token_type = t[1];
+
+ if (multi_parser.token_type === 'TK_EOF') {
+ break;
+ }
+
+ switch (multi_parser.token_type) {
+ case 'TK_TAG_START':
+ multi_parser.print_newline(false, multi_parser.output);
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.indent();
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_STYLE':
+ case 'TK_TAG_SCRIPT':
+ multi_parser.print_newline(false, multi_parser.output);
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_END':
+ //Print new line only if the tag has no content and has child
+ if (multi_parser.last_token === 'TK_CONTENT' && multi_parser.last_text === '') {
+ var tag_name = multi_parser.token_text.match(/\w+/)[0];
+ var tag_extracted_from_last_output = multi_parser.output[multi_parser.output.length -1].match(/<\s*(\w+)/);
+ if (tag_extracted_from_last_output === null || tag_extracted_from_last_output[1] !== tag_name) {
+ multi_parser.print_newline(true, multi_parser.output);
+ }
+ }
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_TAG_SINGLE':
+ // Don't add a newline before elements that should remain unformatted.
+ var tag_check = multi_parser.token_text.match(/^\s*<([a-z]+)/i);
+ if (!tag_check || !multi_parser.Utils.in_array(tag_check[1], unformatted)){
+ multi_parser.print_newline(false, multi_parser.output);
+ }
+ multi_parser.print_token(multi_parser.token_text);
+ multi_parser.current_mode = 'CONTENT';
+ break;
+ case 'TK_CONTENT':
+ if (multi_parser.token_text !== '') {
+ multi_parser.print_token(multi_parser.token_text);
+ }
+ multi_parser.current_mode = 'TAG';
+ break;
+ case 'TK_STYLE':
+ case 'TK_SCRIPT':
+ if (multi_parser.token_text !== '') {
+ multi_parser.output.push('\n');
+ var text = multi_parser.token_text,
+ _beautifier,
+ script_indent_level = 1;
+ if (multi_parser.token_type === 'TK_SCRIPT') {
+ _beautifier = typeof js_beautify === 'function' && js_beautify;
+ } else if (multi_parser.token_type === 'TK_STYLE') {
+ _beautifier = typeof css_beautify === 'function' && css_beautify;
+ }
+
+ if (options.indent_scripts === "keep") {
+ script_indent_level = 0;
+ } else if (options.indent_scripts === "separate") {
+ script_indent_level = -multi_parser.indent_level;
+ }
+
+ var indentation = multi_parser.get_full_indent(script_indent_level);
+ if (_beautifier) {
+ // call the Beautifier if avaliable
+ text = _beautifier(text.replace(/^\s*/, indentation), options);
+ } else {
+ // simply indent the string otherwise
+ var white = text.match(/^\s*/)[0];
+ var _level = white.match(/[^\n\r]*$/)[0].split(multi_parser.indent_string).length - 1;
+ var reindent = multi_parser.get_full_indent(script_indent_level -_level);
+ text = text.replace(/^\s*/, indentation)
+ .replace(/\r\n|\r|\n/g, '\n' + reindent)
+ .replace(/\s*$/, '');
+ }
+ if (text) {
+ multi_parser.print_token(text);
+ multi_parser.print_newline(true, multi_parser.output);
+ }
+ }
+ multi_parser.current_mode = 'TAG';
+ break;
+ }
+ multi_parser.last_token = multi_parser.token_type;
+ multi_parser.last_text = multi_parser.token_text;
+ }
+ return multi_parser.output.join('');
+ }
+
+ if (typeof define === "function") {
+ // Add support for require.js
+ define(function(require, exports, module) {
+ var js_beautify = require('./beautify.js').js_beautify;
+ var css_beautify = require('./beautify-css.js').css_beautify;
+
+ exports.html_beautify = function(html_source, options) {
+ return style_html(html_source, options, js_beautify, css_beautify);
+ };
+ });
+ } else if (typeof exports !== "undefined") {
+ // Add support for CommonJS. Just put this file somewhere on your require.paths
+ // and you will be able to `var html_beautify = require("beautify").html_beautify`.
+ var js_beautify = require('./beautify.js').js_beautify;
+ var css_beautify = require('./beautify-css.js').css_beautify;
+
+ exports.html_beautify = function(html_source, options) {
+ return style_html(html_source, options, js_beautify, css_beautify);
+ };
+ } else if (typeof window !== "undefined") {
+ // If we're running a web page and don't have either of the above, add our one global
+ window.html_beautify = function(html_source, options) {
+ return style_html(html_source, options, window.js_beautify, window.css_beautify);
+ };;
+ }
+}());
\ No newline at end of file
diff --git a/public/js/lib/bootstrap-wysiwyg.js b/public/js/lib/bootstrap-wysiwyg.js
new file mode 100644
index 0000000000..8cb370480f
--- /dev/null
+++ b/public/js/lib/bootstrap-wysiwyg.js
@@ -0,0 +1,181 @@
+/* http://github.com/mindmup/bootstrap-wysiwyg */
+/*global jQuery, $, FileReader*/
+/*jslint browser:true*/
+jQuery(function ($) {
+ 'use strict';
+ var readFileIntoDataUrl = function (fileInfo) {
+ var loader = $.Deferred(),
+ fReader = new FileReader();
+ fReader.onload = function (e) {
+ loader.resolve(e.target.result);
+ };
+ fReader.onerror = loader.reject;
+ fReader.onprogress = loader.notify;
+ fReader.readAsDataURL(fileInfo);
+ return loader.promise();
+ };
+ $.fn.cleanHtml = function () {
+ var html = $(this).html();
+ return html && html.replace(/(
|\s|
<\/div>| )*$/, '');
+ };
+ $.fn.wysiwyg = function (userOptions) {
+ var editor = this,
+ selectedRange,
+ defaultOptions = {
+ 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'
+ },
+ toolbarSelector: '[data-role=editor-toolbar]',
+ commandRole: 'edit',
+ activeToolbarClass: 'btn-info',
+ selectionMarker: 'edit-focus-marker',
+ selectionColor: 'darkgrey'
+ },
+ options,
+ updateToolbar = function () {
+ if (options.activeToolbarClass) {
+ $(options.toolbarSelector).find('.btn[data-' + options.commandRole + ']').each(function () {
+ var command = $(this).data(options.commandRole);
+ if (document.queryCommandState(command)) {
+ $(this).addClass(options.activeToolbarClass);
+ } else {
+ $(this).removeClass(options.activeToolbarClass);
+ }
+ });
+ }
+ },
+ execCommand = function (commandWithArgs, valueArg) {
+ var commandArr = commandWithArgs.split(' '),
+ command = commandArr.shift(),
+ args = commandArr.join(' ') + (valueArg || '');
+ document.execCommand(command, 0, args);
+ updateToolbar();
+ },
+ bindHotkeys = function (hotKeys) {
+ $.each(hotKeys, function (hotkey, command) {
+ editor.keydown(hotkey, function (e) {
+ if (editor.attr('contenteditable') && editor.is(':visible')) {
+ e.preventDefault();
+ e.stopPropagation();
+ execCommand(command);
+ }
+ }).keyup(hotkey, function (e) {
+ if (editor.attr('contenteditable') && editor.is(':visible')) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+ });
+ },
+ getCurrentRange = function () {
+ var sel = window.getSelection();
+ if (sel.getRangeAt && sel.rangeCount) {
+ return sel.getRangeAt(0);
+ }
+ },
+ saveSelection = function () {
+ selectedRange = getCurrentRange();
+ },
+ restoreSelection = function () {
+ var selection = window.getSelection();
+ if (selectedRange) {
+ selection.removeAllRanges();
+ selection.addRange(selectedRange);
+ }
+ },
+ insertFiles = function (files) {
+ editor.focus();
+ $.each(files, function (idx, fileInfo) {
+ if (/^image\//.test(fileInfo.type)) {
+ $.when(readFileIntoDataUrl(fileInfo)).done(function (dataUrl) {
+ execCommand('insertimage', dataUrl);
+ });
+ }
+ });
+ },
+ markSelection = function (input, color) {
+ restoreSelection();
+ document.execCommand('hiliteColor', 0, color || 'transparent');
+ saveSelection();
+ input.data(options.selectionMarker, color);
+ },
+ bindToolbar = function (toolbar, options) {
+ toolbar.find('a[data-' + options.commandRole + ']').click(function () {
+ restoreSelection();
+ editor.focus();
+ execCommand($(this).data(options.commandRole));
+ saveSelection();
+ });
+ toolbar.find('[data-toggle=dropdown]').click(restoreSelection);
+
+ toolbar.find('input[type=text][data-' + options.commandRole + ']').on('webkitspeechchange change', function () {
+ var newValue = this.value; /* ugly but prevents fake double-calls due to selection restoration */
+ this.value = '';
+ restoreSelection();
+ if (newValue) {
+ editor.focus();
+ execCommand($(this).data(options.commandRole), newValue);
+ }
+ saveSelection();
+ }).on('focus', function () {
+ var input = $(this);
+ if (!input.data(options.selectionMarker)) {
+ markSelection(input, options.selectionColor);
+ input.focus();
+ }
+ }).on('blur', function () {
+ var input = $(this);
+ if (input.data(options.selectionMarker)) {
+ markSelection(input, false);
+ }
+ });
+ toolbar.find('input[type=file][data-' + options.commandRole + ']').change(function () {
+ restoreSelection();
+ if (this.type === 'file' && this.files && this.files.length > 0) {
+ insertFiles(this.files);
+ }
+ saveSelection();
+ this.value = '';
+ });
+ },
+ initFileDrops = function () {
+ 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) {
+ insertFiles(dataTransfer.files);
+ }
+ });
+ };
+ options = $.extend({}, defaultOptions, userOptions);
+ bindHotkeys(options.hotKeys);
+ initFileDrops();
+ bindToolbar($(options.toolbarSelector), options);
+ editor.attr('contenteditable', true)
+ .on('mouseup keyup mouseout', function () {
+ saveSelection();
+ updateToolbar();
+ });
+ $(window).bind('touchend', function (e) {
+ var isInside = (editor.is(e.target) || editor.has(e.target).length > 0),
+ currentRange = getCurrentRange(),
+ clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset);
+ if (!clear || isInside) {
+ saveSelection();
+ updateToolbar();
+ }
+ });
+ return this;
+ };
+});
\ No newline at end of file
diff --git a/public/js/lib/jquery/jquery.hotkeys.js b/public/js/lib/jquery/jquery.hotkeys.js
new file mode 100644
index 0000000000..5905f9db2a
--- /dev/null
+++ b/public/js/lib/jquery/jquery.hotkeys.js
@@ -0,0 +1,100 @@
+/*
+ * jQuery Hotkeys Plugin
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ *
+ * Based upon the plugin by Tzury Bar Yochay:
+ * http://github.com/tzuryby/hotkeys
+ *
+ * Original idea by:
+ * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
+*/
+
+(function(jQuery){
+
+ jQuery.hotkeys = {
+ version: "0.8",
+
+ specialKeys: {
+ 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
+ 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
+ 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
+ 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
+ 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
+ 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
+ 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
+ },
+
+ shiftNums: {
+ "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
+ "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
+ ".": ">", "/": "?", "\\": "|"
+ }
+ };
+
+ function keyHandler( handleObj ) {
+ // Only care when a possible input has been specified
+ if ( typeof handleObj.data !== "string" ) {
+ return;
+ }
+
+ var origHandler = handleObj.handler,
+ keys = handleObj.data.toLowerCase().split(" "),
+ textAcceptingInputTypes = ["text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"];
+
+ handleObj.handler = function( event ) {
+ // Don't fire in text-accepting inputs that we didn't directly bind to
+ if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) ||
+ jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1 ) ) {
+ return;
+ }
+
+ // Keypress represents characters, not special keys
+ var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ],
+ character = String.fromCharCode( event.which ).toLowerCase(),
+ key, modif = "", possible = {};
+
+ // check combinations (alt|ctrl|shift+anything)
+ if ( event.altKey && special !== "alt" ) {
+ modif += "alt+";
+ }
+
+ if ( event.ctrlKey && special !== "ctrl" ) {
+ modif += "ctrl+";
+ }
+
+ // TODO: Need to make sure this works consistently across platforms
+ if ( event.metaKey && !event.ctrlKey && special !== "meta" ) {
+ modif += "meta+";
+ }
+
+ if ( event.shiftKey && special !== "shift" ) {
+ modif += "shift+";
+ }
+
+ if ( special ) {
+ possible[ modif + special ] = true;
+
+ } else {
+ possible[ modif + character ] = true;
+ possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true;
+
+ // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
+ if ( modif === "shift+" ) {
+ possible[ jQuery.hotkeys.shiftNums[ character ] ] = true;
+ }
+ }
+
+ for ( var i = 0, l = keys.length; i < l; i++ ) {
+ if ( possible[ keys[i] ] ) {
+ return origHandler.apply( this, arguments );
+ }
+ }
+ };
+ }
+
+ jQuery.each([ "keydown", "keyup", "keypress" ], function() {
+ jQuery.event.special[ this ] = { add: keyHandler };
+ });
+
+})( jQuery );
\ No newline at end of file
diff --git a/public/js/wn/form/editors.js b/public/js/wn/form/editors.js
new file mode 100644
index 0000000000..4afba1cd82
--- /dev/null
+++ b/public/js/wn/form/editors.js
@@ -0,0 +1,304 @@
+
+// Options
+// parent
+// change (event)
+
+// Properties
+// set_input
+// input
+
+wn.provide("wn.editors");
+
+wn.editors.BootstrapWYSIWYG = Class.extend({
+ init: function(opts) {
+ wn.require("lib/js/lib/jquery/jquery.hotkeys.js");
+ wn.require("lib/js/lib/bootstrap-wysiwyg.js");
+ wn.require("lib/js/lib/beautify-html.js");
+ this.opts = opts;
+ this.make_body();
+ this.make_bindings();
+ },
+ make_body: function() {
+ this.myid = "editor-" + wn.dom.set_unique_id();
+ $('\
+ \
+ \
+ \
+ \
+ \
+ \
+ ').appendTo(this.opts.parent);
+ this.$parent = $(this.opts.parent);
+ this.$editor = $("#" + this.myid)
+ this.$textarea = this.$parent.find(".html-editor");
+ this.input = this.$editor.get(0);
+ },
+ make_bindings: function() {
+ var me = this;
+ var fonts = ['Serif', 'Sans', 'Arial', 'Arial Black', 'Courier',
+ 'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande',
+ 'Lucida Sans', 'Tahoma', 'Times', 'Times New Roman', 'Verdana'],
+ fontTarget = this.$parent.find('[title=Font]').siblings('.dropdown-menu');
+
+ $.each(fonts, function (idx, fontName) {
+ fontTarget.append($(''+
+ fontName + ' '));
+ });
+
+ //this.$parent.find('a[title]').tooltip({container:'body'});
+
+ this.$parent.find('.dropdown-menu input').click(function() {return false;})
+ .change(function () {
+ $(this).parent('.dropdown-menu').siblings('.dropdown-toggle')
+ .dropdown('toggle');
+ })
+ .keydown('esc', function () {
+ this.value='';$(this).change();
+ });
+
+ this.$parent.find('[data-role=magic-overlay]').each(function () {
+ var overlay = $(this), target = $(overlay.data('target'));
+ overlay.css('opacity', 0).css('position', 'absolute')
+ .offset(target.offset())
+ .width(target.outerWidth()).height(target.outerHeight());
+ });
+
+ this.$editor
+ .wysiwyg()
+ .on("mouseup keyup mouseout", function() {
+ var value = $(this).html();
+ if(value==null) value="";
+ me.opts.change(value);
+ })
+ this.$textarea
+ .on("change", function() {
+ var value = $(this).val();
+ if(value==null) value="";
+ me.opts.change(value);
+ });
+
+ this.current_editor = this.$editor;
+ this.$parent.find(".btn-html").click(function() {
+ if($(this).attr("disabled")=="disabled") return;
+ me.$textarea.val(html_beautify(me.$editor.html()));
+ me.$parent.find(".for-rich-text").toggle(false);
+ me.$parent.find(".for-html").toggle(true);
+ me.$parent.find(".btn-html").addClass("btn-info").attr("disabled", "disabled");
+ me.$parent.find(".btn-rich-text").removeClass("btn-info").attr("disabled", false);
+ me.current_editor = me.$textarea;
+ });
+
+ this.$parent.find(".btn-rich-text").click(function() {
+ if($(this).attr("disabled")=="disabled") return;
+ me.$editor.html(me.$textarea.val());
+ me.$parent.find(".for-rich-text").toggle(true);
+ me.$parent.find(".for-html").toggle(false);
+ me.$parent.find(".btn-html").removeClass("btn-info").attr("disabled", false);
+ me.$parent.find(".btn-rich-text").addClass("btn-info").attr("disabled", "disabled");
+ me.current_editor = me.$editor;
+ });
+
+ },
+ set_input: function(value) {
+ if(this.value!=value) {
+ this.value = value==null ? "" : value;
+ this.$editor.html(this.value);
+ this.$textarea.val(this.value);
+ }
+ },
+ get_value: function() {
+ if(this.current_editor==this.$editor)
+ return this.$editor.html();
+ else
+ return this.$textarea.val();
+ }
+})
+
+//// TinyMCE
+
+wn.editors.TinyMCE = Class.extend({
+ init: function(opts) {
+ this.opts = opts;
+ this.make();
+ },
+ make: function() {
+ var me = this;
+ this.input = $("