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_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 = $("