All pastes #927188 Raw Edit

Mine

public text v1 · immutable
·published 2008-03-04 02:01 UTC
rendered paste body
/**
 * Editor
 * WYSIWYG Editor
 * ---
 *  Written by José Carlos Nieto Jarquín <xiam@astrata.com.mx>
 *
 * Copyright (c) 2007 Astrata Software S.A. de C.V.
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @author          José Carlos Nieto Jarquín
 * @copyright       Copyright (c) 2007, Astrata Software S.A. de C.V.
 * @link            http://astrata.com.mx Astrata Open Source Projects
 * @version         $Revision: $
 * @modifiedby      $LastChangedBy: $
 * @lastmodified    $Date: $
 * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
 *
 */

// http://www.mozilla.org/editor/midas-spec.html

var Editor = Control.extend(
{
  __imageExtension: '.png',
  __colors: [
  '#000', '#333', '#666', '#888', '#999', '#aaa',
  '#ddd', '#008', '#00f', '#080', '#088', '#08f',
  '#0f0', '#0f8', '#0ff', '#f00', '#f08', '#f0f',
  '#f80', '#f88', '#f8f', '#fff'
  ],

  '__selectNode': function(el) {
    this.focusVisual();

    var sel = this.__getSelection();
    var range = this.__createRange(sel);

    if (window.ie) {
      range.moveToElementText(el);
      range.select();
    } else {
      sel.removeAllRanges();
      
      sel.selectAllChildren(range);
      sel.addRange(range);
    }
  },

  'focusVisual': function() {
    this.components.iframe.contentWindow.focus();
  },

  setBookmark: function() {
    this.focusVisual(); 
    var range = this.__createRange();
    if (window.ie) {
      this.__bm = range.getBookmark();
    } else {
      this.__bm = {
        'startContainer': range.startContainer,
        'startOffset': range.startOffset,
        'endContainer': range.endContainer,
        'endOffset': range.endOffset
      }
    }
  },

  loadBookmark: function() {
    if (this.__bm) {
      this.focusVisual(); 
      var range = this.__createRange();
      if (window.ie) {
        range.moveToBookmark(this.__bm);
        range.select();
      } else {
        var sel = this.__getSelection();
        range.setStart(this.__bm.startContainer, this.__bm.startOffset); 
        range.setEnd(this.__bm.endContainer, this.__bm.endOffset); 
        sel.removeAllRanges();
        sel.addRange(range);
      }
    }
  },

  getIcon: function(icon) {
    return $mediaDir+'/editor/'+icon+this.__imageExtension;
    return Widget.img({src: $mediaDir+'/editor/'+icon+this.__imageExtension});
  },

  focus: function() {
    this.iframeDocument().focus();
  },

  buttons: {},

  reset: function() {
    this.components.textarea.value = '';
    this._updateVisual();
  },

  updateSource: function() {
    if (this.__updateSourceTimer) {
      window.clearTimeout(this.__updateSourceTimer);
    }
    this.__updateSourceTimer = window.setTimeout(function() { this._updateSource() }.bind(this), 500);
  },

  updateVisual: function() {
    if (this.__updateVisualTimer) {
      window.clearTimeout(this.__updateVisualTimer);
    }
    this.__updateVisualTimer = window.setTimeout(function() { this._updateVisual() }.bind(this), 500);
  },

  findParent: function(callback) {
    this.focusVisual();

    var range = this.__createRange();
    var parent = this.getRangeParent(range);

    while (parent) {
      if (window.ie) {
        if (typeof callback == 'function') {
          if (callback(parent)) {
            return parent;
          }
        } else if (parent.nodeName.toLowerCase().match(callback)) {
          return parent;
        }
      } else {
        if ($type(callback) == 'regexp') {
          if (parent.nodeName.toLowerCase().match(callback)) {
            return parent;
          }
        } else if (callback(parent)) {
          return parent;
        }
      }
      parent = parent.parentNode;
    }
    return null;
  },

  'removeAttribute': function (node, style) {
    if ($type(node) == 'element') {
      node.style[style] = '';
      for (var i = 0; i < node.childNodes.length; i++) {
        this.removeAttribute(node.childNodes[i], style);
      } 
    }
  },

  'getRangeParent': function(range) {

    if (window.ie) {
      var html = range.htmlText;

      this.execCommand('delete');
      this.execCommand('inserthtml', '<span id="__temp_node__">'+html+'</span>');
      var node = this.iframeDocument().getElementById('__temp_node__');
      node.removeAttribute('id');
      range.moveToElementText(node);
      range.select();
      
      return node;

    } else {

      // removing selected contents
      var contents = range.extractContents();
      
      // opera hack
      if (window.opera) {
        var hackNode = range.commonAncestorContainer.childNodes[range.startOffset-1];
        this.trimTextNodes(hackNode);
        /*
        for (var i in hackNode) {
          log(i+': '+hackNode[i]);
        }
        */
        if (!hackNode.textContent) {
          range.commonAncestorContainer.removeChild(hackNode);
        }
      }

      this.trimTextNodes(contents);

      if (contents.childNodes.length == 1 && $type(contents.childNodes[0]) == 'element') {
        // the user selected only one node
        var el = contents.childNodes[0];
      } else {
        // a wrapper is needed
        var el = this.iframeDocument().createElement('span');
        el.appendChild(contents);
      }

      // reselecting
      range.insertNode(el);
      range.selectNode(el);
    }

    this.__selectNode(el);

    return el;
  },

  'trimTextNodes': function(contents) {
    // garbage hack to delete childNodes
    var garbage = document.createDocumentFragment();

    // emptying nodes from the end
    var node = contents.childNodes[contents.childNodes.length - 1];
    while (node && node.nodeType == 3 && node.nodeValue == '') {
      garbage.appendChild(node);
      node = node.previousSibling;
    }

    // emptying nodes from the start
    var node = contents.childNodes[0];
    while (node && node.nodeType == 3 && node.nodeValue == '') {
      garbage.appendChild(node);
      node = node.nextSibling;
    }
  },

  'execAction': function(params) {
    // This is the tricky part so it needs to be documented

    this.focusVisual();

    var sel = this.__getSelection();
    var range = this.__createRange(sel);

    var parent = this.getRangeParent(range);

    var formatted = false;

    log('while');

    while
    (
      !formatted && parent
      && !parent.nodeName.toLowerCase().match(/^(body|html|head)$/)
    ) {
      var match = false;
      var name = parent.nodeName.toLowerCase();
      log(name);
      if
        (
          params.replaceTag
          && name.match(params.replaceTag)
        ) {
        log('replaceTag');
        if (window.ie) {
          range.moveToElementText(parent);
          var html = new String(parent.innerHTML);
          range.select();
          var bm = range.getBookmark();
          this.execCommand('delete');
          range.collapse();
          range.select();
          range.pasteHTML(html);
          range.moveToBookmark(bm);
          range.select();
          log('replace');
        } else {
          var sel = this.__getSelection();
          sel.removeAllRanges();
          var range = this.__createRange();
          var start = false;
          while (parent.firstChild) {
            var node = parent.firstChild;
            parent.parentNode.insertBefore(node, parent);
            if (!start) {
              range.setStart(node, 0);
            }
          }
          range.setEndBefore(node.nextSibling);
          sel.addRange(range);
        }
        break;
      } else if
        (
          params.matchTag
          && name.match(params.matchTag)
        ) {
        // Parent tag matches, replacing whole tag with its
        // contents.
        log('matchTag');

        if (window.ie) {
          range.moveToElementText(parent);
          var html = new String(parent.innerHTML);
          range.select();
          var bm = range.getBookmark();
          this.execCommand('delete');
          range.collapse();
          range.select();
          range.pasteHTML(html);
          range.moveToBookmark(bm);
          range.select();
        } else {
          var sel = this.__getSelection();
          sel.removeAllRanges();
          var range = this.__createRange();
          var start = false;
          while (parent.firstChild) {
            var node = parent.firstChild;
            parent.parentNode.insertBefore(node, parent);
            if (!start) {
              range.setStart(node, 0);
            }
          }
          range.setEndBefore(node.nextSibling);
          sel.addRange(range);
          $(parent).dump();
        }

        formatted = true;
      } else if
        (
          params.matchStyle
          && (parent.style && typeof parent.style[params.matchStyle[0]] != 'undefined' && parent.style[params.matchStyle[0]])
        ) {
        log('matchStyle');
        if (parent.style[params.matchStyle[0]].match(params.matchStyle[1])) {
          log('style match, removing....');
          // Style matches, removing style
          if (window.ie) {
            range.moveToElementText(parent);
            parent.style[params.matchStyle[0]] = parent.style[params.matchStyle[0]].replace(params.matchStyle[1], '');
            range.select();
          } else {
            parent.style[params.matchStyle[0]] = parent.style[params.matchStyle[0]].replace(params.matchStyle[1], '');
          }
        } else {
          log('style does not match');
          // Style does not match, changing
          if (!params.replaceStyle && typeof parent.style[params.matchStyle[0]] != 'undefined') {
            try {
              parent.style[params.matchStyle[0]] = parent.style[params.matchStyle[0]]+' '+params.matchStyle[1];
            } catch(e) {
              parent.style[params.matchStyle[0]] = params.matchStyle[1];
            }
          } else {
            parent.style[params.matchStyle[0]] = params.matchStyle[1];
          }
        }
        this.__selectNode(parent);
        formatted = true;
      } else if
        (
          params.matchStyleTag
          && name.match(params.matchStyleTag)
        ) {
        // Special case when a whole with matching style is selected
        log('matchStyleTag');

        if
          (
            (typeof parent.style[params.matchStyle[0]] != 'undefined')
            && parent.style[params.matchStyle[0]].match(params.matchStyle[1])
          ) {
          log('style matches');
          // Style matches, we need to remove it
          parent.style[params.matchStyle[0]] = parent.style[params.matchStyle[0]].replace(params.matchStyle[1], '');
        } else {
          // Style does not match
          if (parent.style[params.matchStyle[0]]) {
            // Appeding style
            log('appending style');
            parent.style[params.matchStyle[0]] += ' '+params.matchStyle[1];
          } else {
            // Creating style
            log('inserting style');
            log(parent.style[params.matchStyle[0]]);
            parent.style[params.matchStyle[0]] = params.matchStyle[1];
          }
          for (var i = 0; i < parent.childNodes.length; i++) {
            this.removeAttribute(parent.childNodes[i], params.matchStyle[0]);
          }
        }
        this.__selectNode(parent);
        formatted = true;
      }

      parent = parent.parentNode;
    }

    if (formatted == false && !params.matchForRemove) {
      
      log('non formatted');

      // Patterns were not found, so the text needs to
      // be formatted.
      if (window.ie) {
        var selected = this.getSelectedHTML(); 
      } else {
        var selected = this.__domToHTML(range.cloneContents());
      }

      var node = this.iframeDocument().createElement(params.wrapWithTag ? params.wrapWithTag : 'span');
      node.setAttribute('id', '__temp_node__');
      if (params.matchStyle && (params.wrapWithStyle || !params.wrapWithTag)) {
        node.style[params.matchStyle[0]] = params.matchStyle[1];
      }
      if (params.wrapWithAttr) {
        for (var i in params.wrapWithAttr) {
          node.setAttribute(i, params.wrapWithAttr[i]);
        }
      }

      if (params.stripTags) {
        selected = selected.replace(new RegExp('<\\/?('+params.stripTags+')(\\s[^>]*)?>', 'ig'));
      }

      try {
        if (params.insertValue) {
          node.innerHTML = params.insertValue;
        } else {
          node.innerHTML = selected ? selected : '&nbsp;';
        }
      } catch(e) {
        log('possible bug? do you need to strip some tags?');
        log(node.nodeName);
        log(selected);
      }

      if (window.ie) {
        this.execCommand('inserthtml', node.outerHTML);
        var node = this.iframeDocument().getElementById('__temp_node__');
        node.removeAttribute('id');

        range.moveToElementText(node);
        range.select();

        if (!selected && params.collapseIfNull) {
          range.collapse(true);
          range.select();
        }
      } else {
        range.deleteContents();
        range.insertNode(node);
        var sel = this.__getSelection();
        sel.removeAllRanges();
        range.selectNodeContents(node);
        sel.addRange(range);
        
        node.removeAttribute('id');
        if (params.collapseIfNull) {
          range.collapse(true);
        }
      }
    }
    this.focusVisual();
  },

  __createButtons: function() {
    
    this.__buttons = $extend({
      bold: {
        tooltip: 'Bold',
        onAction: function() {

          this.execAction({
            'matchTag': /^(b|strong)$/,
            'matchStyleTag': /^(span)$/,
            'matchStyle': [ 'fontWeight', 'bold' ],
            'wrapWithTag': 'b'
          });

        },
        nodeStyle: 'fontWeight',
        nodeStyleValue: 'bold',
        changeElementPattern: /^(b|strong)$/
      },
      italic: {
        tooltip: 'Italics',
        onAction: function() {
          this.execAction({
            'matchTag': /^(i)$/,
            'matchStyleTag': /^(span)$/,
            'matchStyle': [ 'fontStyle', 'italic' ],
            'wrapWithTag': 'i'
          });
        },
        nodeStyle: 'fontStyle',
        nodeStyleValue: 'italic',
        changeElementPattern: /^i$/
      },
      underline: {
        tooltip: 'Underline',
        onAction: function() {
          this.execAction({
            'matchTag': /^(u)$/,
            'matchStyleTag': /^(span)$/,
            'matchStyle': [ 'textDecoration', 'underline' ]
          });
        },
        nodeStyle: 'textDecoration',
        nodeStyleValue: 'underline',
        changeElementPattern: /^u$/
      },
      strikethrough: {
        tooltip: 'Strikethrough',
        onAction: function() {
          this.execAction({
            'matchTag': /^(s)$/,
            'matchStyleTag': /^(span)$/,
            'matchStyle': [ 'textDecoration', 'line-through' ]
          });
        },
        nodeStyle: 'textDecoration',
        nodeStyleValue: 'line-through',
        changeElementPattern: /^s$/
      },
      subscript: {
        tooltip: 'Subscript',
        onAction: function() {
          this.unselectButton('superscript');
          this.execAction({
            'matchTag': /^(sub)$/,
            'replaceTag': /^(sup)$/,
            'wrapWithTag': 'sub'
          });
        },
        changeElementPattern: /^sub$/
      },
      superscript: {
        tooltip: 'Superscript',
        onAction: function() {
          this.unselectButton('subscript');
          this.execAction({
            'matchTag': /^(sup)$/,
            'replaceTag': /^(sub)$/,
            'wrapWithTag': 'sup'
          });
        },
        changeElementPattern: /^sup$/
      },
      justifyleft: {
        tooltip: 'Align left',
        onAction: function() {
          this.unselectButton('justifycenter');
          this.unselectButton('justifyfull');
          this.unselectButton('justifyright');
          this.execAction({
            'matchStyleTag': /^(div|p)$/,
            'matchStyle': ['textAlign', 'left'],
            'collapseIfNull': true,
            'wrapWithTag': 'p',
            'replaceStyle': true,
            'stripTags': 'p',
            'wrapWithStyle': true
          });
        },
        nodeStyle: 'textAlign',
        nodeStyleValue: 'left'
      },
      justifycenter: {
        tooltip: 'Align center',
        onAction: function() {
          this.unselectButton('justifyleft');
          this.unselectButton('justifyfull');
          this.unselectButton('justifyright');
          
          this.execAction({
            'matchStyleTag': /^(div|p)$/,
            'matchStyle': ['textAlign', 'center'],
            'collapseIfNull': true,
            'wrapWithTag': 'p',
            'replaceStyle': true,
            'stripTags': 'p',
            'wrapWithStyle': true
          });
        },
        nodeStyle: 'textAlign',
        nodeStyleValue: 'center'
      },
      justifyfull: {        
        tooltip: 'Justify',
        onAction: function() {
          this.unselectButton('justifycenter');
          this.unselectButton('justifyleft');
          this.unselectButton('justifyright');
          this.execAction({
            'matchStyleTag': /^(div|p)$/,
            'matchStyle': ['textAlign', 'justify'],
            'collapseIfNull': true,
            'wrapWithTag': 'p',
            'replaceStyle': true,
            'stripTags': 'p',
            'wrapWithStyle': true
          });
        },
        nodeStyle: 'textAlign',
        nodeStyleValue: 'justify'
      },
      justifyright: {
        tooltip: 'Align right',
        onAction: function() {
          this.unselectButton('justifycenter');
          this.unselectButton('justifyfull');
          this.unselectButton('justifyleft');

          this.execAction({
            'matchStyleTag': /^(div|p)$/,
            'matchStyle': ['textAlign', 'right'],
            'collapseIfNull': true,
            'wrapWithTag': 'p',
            'replaceStyle': true,
            'stripTags': 'p',
            'wrapWithStyle': true
          });

        },
        nodeStyle: 'textAlign',
        nodeStyleValue: 'right'
      },
      indent: {
        tooltip: 'Indent',
        onAction: function() {
          this.execCommand('indent', null);
        }
      },
      outdent: {
        tooltip: 'Outdent',
        onAction: function() {
          this.execCommand('outdent', null);
        }
      },
      insertorderedlist: {
        tooltip: 'Ordered List',
        onAction: function() {
          this.execCommand('insertorderedlist');
        }
      },
      insertunorderedlist: {
        tooltip: 'Unordered List',
        onAction: function() {
          this.execCommand('insertunorderedlist');
        }
      },
      inserthorizontalrule: {
        tooltip: 'Insert Horizontal Rule',
        onAction: function() {
          this.execCommand('inserthorizontalrule');
        }
      },
      removeformat: {
        tooltip: 'Remove format',
        onAction: function() {
          this.execCommand('removeformat');
        }
      },
      insertimage: {
        tooltip: 'Insert image',
        onAction: function() {
          var value = Dialog.prompt('Image URL', {
            'value': 'http://',
            'onOk': function (dialog) {
              var value = dialog.input.value;
              if (value && value != 'http://') {
                this.execAction({
                  'matchTag': /^(img)$/,
                  'wrapWithTag': 'img',
                  'wrapWithAttr': {
                    'src': value
                  }
                });
              }
              dialog.close();
            }.bind(this)
          });
        }
      },
      createlink: {
        tooltip: 'Create a link',
        onAction: function() {

          var def = 'http://';

          if (this.isPressed('createlink')) {
            var a = this.findParent(/^a$/);
            if (a) {
              def = a.href;
              this.selectNode(a);
            }
          } 

          this.setBookmark();

          Dialog.prompt(__('Web address'),
          {
            'value': def,
            'onOk': function(dialog) {

              this.loadBookmark();
              var a = this.findParent(/^a$/);
              
              if (dialog.input.value && dialog.input.value != 'http://') {
                if (a) {
                  // updating
                  a.href = dialog.input.value;
                } else {
                  log('new link');
                  // new link
                  this.execAction({
                    'matchTag': /^(a)$/,
                    'wrapWithTag': 'a',
                    'stripTags': 'a',
                    'wrapWithAttr': {
                      'href': dialog.input.value
                    }
                  });
                }
              } else {
                if (a) {
                  this.execAction({
                    'matchTag': /^(a)$/,
                    'matchForRemove': true
                  });
                }
              }
              
              dialog.close();

              this._updateSource();

            }.bind(this)
          });
        },
        changeElementPattern: /^a$/
      },
      fullscreen: {
        tooltip: 'Fullscreen editor',
        onAction: function() {

          if (this.isPressed('fullscreen')) {

            this.components.iframe.style.height = this.components.iframe.origHeight+'px';
            this.unselectButton('fullscreen');

            if (this.__notebook) {
              this.__notebook.toggleControlFullScreen();
            } else {
              this.toggleControlFullScreen();
            }

            this.components.iframe.setStyle({
              'width':   this.__iframeDimensions.width+'px',
              'height':  this.__iframeDimensions.height+'px'
            });
            
            window.removeEvent(
              'resize',
              this.__fullScreenResizeIframe
            );
            
            this.__isFullScreen = false;
          } else {

            this.selectButton('fullscreen');
            
            this.__iframeDimensions = $(this.components.iframe).getDimensions();
            this.__iframeOffset     = {
              'x': this.element.offsetWidth - this.components.iframe.offsetWidth,
              'y': this.element.offsetHeight - this.components.iframe.offsetHeight
            }

            if (this.__notebook) {
              this.__notebook.toggleControlFullScreen();
            } else {
              this.toggleControlFullScreen();
            }

            this.__fullScreenResizeIframe = function() {
              this.components.iframe.style.height = (Browser.clientHeight() - this.__iframeOffset.y) + 'px';
              this.components.iframe.style.width = (Browser.clientWidth() - this.__iframeOffset.x) + 'px';
            }.bindAsEventListener(this)

            this.__fullScreenResizeIframe();

            window.addEvent(
              'resize',
              this.__fullScreenResizeIframe
            );

            this.__isFullScreen = true;

          }
        }.bindAsEventListener(this)
      },

      // HEADING
      heading: {
        tooltip: 'Paragraph type',
        element: Widget.span({'class': 'm-select'}, 
          [
            Widget.img({'src': this.getIcon('heading')}),
            Widget.select({
              'class': 'm-form-input',
              'onchange': function(e) {
                this.__buttons['heading'].onAction.bind(this)(e);
                this.updateSource();
              }.bind(this) 
            }, 
            [
              Widget.option({value: ''}, 'Text format'),
              Widget.option({value: 'h2'}, 'Main title'),
              Widget.option({value: 'h3'}, 'Subtitle 1'),
              Widget.option({value: 'h4'}, 'Subtitle 2'),
              Widget.option({value: 'code'}, 'Code'),
              Widget.option({value: 'blockquote'}, 'Quote')
            ])
          ]),
        onAction: function(e) {
          e = new Event(e);
          if (e.target.value) {
            this.execAction({
              'matchTag': new RegExp('/^('+e.target.value+')$/', 'i'),
              'replaceTag': /^(h\d+|code|pre)$/,
              'wrapWithTag': e.target.value
            });
          } else {
            this.execAction({
              'matchTag': /^(h\d+|code|pre)$/,
              'matchForRemove': true
            });
          }
        },
        onNodeChange: function(node) {
          if (!node.__status.heading) {
            var select = this.element.getElementsByTagName('select')[0];
            select.value = node.nodeName.toLowerCase();
            node.__status.heading = true;
          }
        },
        listenEvent: 'auto',
        changeElementPattern: /^h\d$/
      },


      // FONT FACE
      fontname: {
        tooltip: 'Font face',
        element: Widget.span({'class': 'm-select'}, [
          Widget.img({'src': this.getIcon('fontface')}),
          Widget.select({
            'class': 'm-form-input',
            'onchange': function(e) {
              this.__buttons['fontname'].onAction.bind(this)(e);
              this.updateSource();
            }.bind(this)
          }, Widget.option({'value': ''}, 'Font name'))
        ]),
        onStart: function(parent, el) {
          var fonts = {
            'Verdana':          'Verdana',
            'Arial':            'Arial',
            'Trebuchet MS':     'Trebuchet MS',
            'Times New Roman':  'Times New Roman',
            'monotype':         'Monotype',
            'sans-serif':       'Sans Serif'
          };
          var select = el.getElementsByTagName('select')[0];
          for (var font in fonts) {
            select.appendChild(Widget.option({'value': font, 'style': {'fontFamily': font}}, fonts[font]));
          }
        },
        onNodeChange: function(node) {
          if (!node.__status.fontname) {
            var select = this.element.getElementsByTagName('select')[0];
            select.value = node.style.fontFamily;
            node.__status.fontname = true;
          }
        },
        onAction: function(e) {
          var e = new Event(e);
          this.execAction({
            'matchTag': /^(font)$/,
            'matchStyleTag': /^(span)$/,
            'wrapWithTag': 'span',
            'wrapWithStyle': true,
            'replaceStyle': true,
            'matchStyle': [ 'fontFamily', e.target.value ]
          });
        },
        nodeStyle: 'fontFamily',
        nodeStyleValue: /.*/,
        listenEvent: 'auto'
      },
      
      // FONT SIZE
      fontsize: {
        tooltip: 'Font size',
        element: Widget.span({'class': 'm-select'}, [
          Widget.img({'src': this.getIcon('fontsize')}),
          Widget.select({
            'class': 'm-form-input',
            'onchange': function(e) {
              this.__buttons['fontsize'].onAction.bind(this)(e);
              this.updateSource();
            }.bind(this)
          }, Widget.option({'value': ''}, 'Font size'))
        ]),
        onStart: function(parent, el) {
          var sizes = {
            'xx-small': 'Micro',
            'x-small': 'Extra small',
            'small': 'Small',
            'medium': 'Medium',
            'large': 'Large',
            'x-large': 'Extra large',
            'xx-large': 'Huge'
          };
          var select = el.getElementsByTagName('select')[0];
          for (var i in sizes) {
            select.appendChild(Widget.option({'value': i}, sizes[i]));
          }
        },
        onNodeChange: function(node) {
          if (!node.__status.fontsize) {
            var select = this.element.getElementsByTagName('select')[0];
            select.value = node.style.fontSize;
            node.__status.fontsize = true;
          }
        },
        onAction: function(e) {
          var e = new Event(e);
          this.execAction({
            'matchTag': /^(font)$/,
            'matchStyleTag': /^(span)$/,
            'wrapWithTag': 'span',
            'wrapWithStyle': true,
            'replaceStyle': true,
            'matchStyle': [ 'fontSize', e.target.value ]
          });
        },
        nodeStyle: 'fontSize',
        nodeStyleValue: /.*/,
        listenEvent: 'auto'
      },


      // FORECOLOR
      forecolor: {
        tooltip: 'Font color',
        element: Widget.span({'class': 'm-select'}, [
          Widget.img({'src': this.getIcon('forecolor') }),
          Widget.select({
            'class': 'm-form-input',
            'onchange': function(e) {
              this.__buttons['forecolor'].onAction.bind(this)(e);
              this.updateSource();
            }.bind(this)
          }, Widget.option({'value': ''}, 'Font color'))
        ]),
        onStart: function(parent, el) {
          for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if ($type(node) == 'element') {
              if (node.nodeName.toLowerCase() == 'select') {
                var colors = parent.__colors;
                for (var i = 0; i < colors.length; i++) {
                  node.appendChild(Widget.option({'value': colors[i], 'style': { 'backgroundColor': colors[i] } }));
                }
                return;
              }  
            }
          }
        },
        onNodeChange: function(node) {
          if (!node.__status.forecolor) {
            var select = this.element.getElementsByTagName('select')[0];
            select.value = node.style.color;
            select.style.backgroundColor = node.style.color;
            node.__status.forecolor = true;
          }
        },
        onAction: function(e) {
          var e = new Event(e);
          var color = e.target.value;
          e.target.style.backgroundColor = color;
          if (color.length == '4') {
            color = '#'+color.charAt(1)+color.charAt(1)+color.charAt(2)+color.charAt(2)+color.charAt(3)+color.charAt(3);
          }
          this.execAction({
            'matchTag': /^(font)$/,
            'matchStyleTag': /^(span)$/,
            'wrapWithTag': 'span',
            'wrapWithStyle': true,
            'replaceStyle': true,
            'matchStyle': [ 'color', color ]
          });
        },
        nodeStyle: 'color',
        nodeStyleValue: /.*/,
        listenEvent: 'auto'
      },

      // BACKGROUND COLOR
      hilitecolor: {
        tooltip: 'Background color',
        element: Widget.span({'class': 'm-select'}, [
          Widget.img({'src': this.getIcon('hilitecolor') }),
          Widget.select({
            'class': 'm-form-input',
            'onchange': function(e) {
              this.__buttons['hilitecolor'].onAction.bind(this)(e);
              this.updateSource();
            }.bind(this)
          }, Widget.option({value: ''}, 'Background'))
        ]),
        onStart: function(parent, el) {
          
          for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if ($type(node) == 'element') {
              if (node.nodeName.toLowerCase() == 'select') {
                var colors = parent.__colors;
                for (var i = 0; i < colors.length; i++) {
                  node.appendChild(Widget.option({'value': colors[i], 'style': {'backgroundColor': colors[i]}}));
                }
                return;
              }
            }
          }
        },
        onAction: function(e) {
          var e = new Event(e);
          var color = e.target.value;
          e.target.style.backgroundColor = color;
          if (color.length == '4') {
            color = '#'+color.charAt(1)+color.charAt(1)+color.charAt(2)+color.charAt(2)+color.charAt(3)+color.charAt(3);
          }
          this.execAction({
            'matchTag': /^(font)$/,
            'matchStyleTag': /^(span)$/,
            'wrapWithTag': 'span',
            'wrapWithStyle': true,
            'replaceStyle': true,
            'matchStyle': [ 'backgroundColor', color ]
          });
        },
        listenEvent: 'auto',
        nodeStyle: 'backgroundColor',
        nodeStyleValue: /.*/,
        onNodeChange: function(node) {
          if (!node.__status.hilite) {
            var select = this.element.getElementsByTagName('select')[0];
            select.value = node.style.backgroundColor;
            select.style.backgroundColor = node.style.backgroundColor;
            node.__status.hilitecolor = true;
          }
        }
      }
    }, this.buttons);

    if (!this.options.buttons) {
      this.options.buttons = [];
      for (var i in this.__buttons) {
        this.options.buttons.push(i);
      }
    }

    for (var i = 0; i < this.options.buttons.length; i++) {
      
      var name = this.options.buttons[i];
      if ($type(this.__buttons[name])) {
        var button = this.__buttons[name];

        button.name = name;
        button.parent = this;
        
        // creating an element if the button doesn't has one
        if (!button.element) {
          button.element = Widget.img({ 'class': 'm-button', 'src': this.getIcon(name) });
        }

        if (button.onStart) {
          button.onStart(this, button.element);
        }
        
        if (!button.listenEvent) {
          button.listenEvent = 'onclick';
        }

        button.element[button.listenEvent] = function () {
          
          // calling the button's action
          this.onAction.bind(this.parent).call();

          this.parent.updateSource();
          
          // shading and unshading the button
          if (this.element.nodeName.toLowerCase() == 'img' && (this.changeElementPattern || this.nodeStyle)) {
            this.parent.toggleStatus(this.element);
          }

        }.bind(button);
        
        if (button.tooltip) {
          button.element.title = button.tooltip;
        }
        if (button.element.nodeName.toLowerCase() == 'img') {
          this.components.buttonsUpper.appendChild(button.element);
        } else {
          this.components.buttonsLower.appendChild(button.element);
        }

      } else {
        log('The button '+name+' does not exist.');
      }
    }
  },
  toggleStatus: function(obj) {
    if (obj.src.match(/\/active-([^\/]+)/)) {
      this.__unselectButton(obj);    
    } else {
      this.__selectButton(obj);
    }
  },
  __execCommand: function(command, value) {
    try {
      this.iframeDocument().execCommand(command, false, value);      
    } catch(e) {
      log('Unsupported command: '+command);
    }
  },
  selectNode: function(el) {
    if (window.ie) {
      var range = this.__createRange();
      range.moveToElementText(el);
      range.select();
    }
  },
  execCommand: function(command, value) {
    switch (command) {
      case 'inserthtml':
        if (window.ie) {
          var sel = this.__getSelection();
          var range = this.__createRange(sel);
          range.pasteHTML(value);
        } else {
          var range = this.__createRange();
          range.deleteContents();
          this.__execCommand('inserthtml', value);
        }
      break;
      default:
        this.__execCommand(command, value);
      break;
    }
    if (this.__started) {
      this.updateSource();
    }
  },
  resizeTo: function(width, height) {
    this.element.style.width = width+'px';
    this.components.iframe.style.height = height+'px';
  },
  __buildComponents: function() {
    
    this.element = Widget.div({'class': 'm-editor'});
    
    this.components = {
      buttonsUpper: Widget.div(),
      buttonsLower: Widget.div(),
      buttons: Widget.div({'class': 'm-editor-buttons'}),
      visualarea: Widget.div({'class': 'm-visual-area'}),
      iframe: Widget.iframe({
        'src': this.options.blank,
        'border': '0',
        'frameborder': '0',
        'style': {
          'display': 'block'
        }
      })
    }

    this.components.buttons.appendChildren([
      this.components.buttonsUpper,
      this.components.buttonsLower
    ]);

    this.components.textarea = this.__textarea;
    //this.components.textarea.setAttribute('wrap', 'off');

    if (this.components.textarea.offsetWidth && this.components.textarea.offsetHeight) {
      this.resizeTo(this.components.textarea.offsetWidth, this.components.textarea.offsetHeight);
    } else {
      if (this.components.textarea.style.width && this.components.textarea.style.height) {
        if (this.components.textarea.style.width.indexOf('%') == -1 && this.components.textarea.style.height.indexOf('%') == -1) {
          this.resizeTo(parseInt(this.components.textarea.style.width), parseInt(this.components.textarea.style.height));
        }
      } else {
        this.element.style.width = '100%';
      }
    }

    this.__createButtons();

    if (!this.options.buttons.length) {
      this.components.buttons.hide();
    }

    this.components.textarea.parentNode.insertBefore(this.element, this.components.textarea);
    switch (this.options.variant) {
      case 1:
        this.components.visualarea.appendChildren([
          this.components.iframe,
          this.components.buttons
        ]);
      break;
      default:
        this.components.visualarea.appendChildren([
          this.components.buttons,
          this.components.iframe
        ]);
      break;
    }

    if (this.options.showSource) {
      nb = new Notebook(this.element, { allowBookmark: false } );
      nb.addPage({id: 'visual', title: 'Visual mode'}, this.components.visualarea);
      nb.addPage({id: 'source', title: 'Source mode'}, this.components.textarea);
      this.__notebook = nb;
    } else {
      this.element.appendChild(this.components.visualarea);
      this.components.textarea.parentNode.insertBefore(this.element, this.components.textarea);
      this.components.textarea.hide();
    }

    var form = this.components.textarea;
    while (form) {
      if (form.nodeName.toLowerCase() == 'form') {
        $(form).addEvent(
          'reset', function() {
            this.reset();
          }.bind(this)
        );
        $(form).addEvent(
          'submit', function(e) {
            if (this.options.autoSave) {
              this.__autoSave(true);
            }
          }.bind(this)
        );
        break;
      }
      form = form.parentNode;
    }
  },

  __bindEvents: function() { 

    $(this.components.iframe).addEvent(
      'load',
      function() {
        this.__started = false;
        this.__start();
      }.bind(this)
    );
   
  },
  
  __createRange: function(sel) {
    if (!sel) {
      sel = this.__getSelection();
    }
    if (window.ie) {
      return sel.createRange();
    } else {
      try {
        return sel.getRangeAt(0);
      } catch(e) {
        return this.iframeDocument().createRange();
      }
    }
  },
  
  __domToHTML: function(content) {
    
    var buff = new String("");
    
    var isTag = (content.nodeName.substr(0, 1) != '#');
    
    if (!isTag) {
      if (content.nodeName == '#text') {
        buff += content.nodeValue;
      }
    }
    
    if (isTag) {
      buff += '<'+content.nodeName.toLowerCase();
      
      if (content.attributes.length) {
        var attr = new Array();
        for (var i = 0; i < content.attributes.length; i++) {
          var name = content.attributes[i].nodeName;
          var value = content.attributes[i].nodeValue;
          value = value.replace(/-moz-.*;/, '');
          
          if (name.substr(0, 1) != '-')
            attr.push(name+'="'+value+'"');
        }
        buff += ' '+attr.join(' ');
      }
      
      if (this.__isSingleTag(content.nodeName))
        buff += ' /';
      
      buff += '>';
    }
    
    for (var i = 0; i < content.childNodes.length; i++) {
      buff += this.__domToHTML(content.childNodes[i]);
    }
    
    if (isTag && !this.__isSingleTag(content.nodeName)) {
      buff += '</'+content.nodeName.toLowerCase()+'>';
    }
    
    return buff;
  },
  
  __isSingleTag: function (tag) {
    tag = tag.toLowerCase();
    if (tag == 'br' || tag == 'img' || tag == 'input' || tag == 'hr') {
      return true;
    }
    return false;
  },

  'getSelectedHTML': function(sel) {
    if (!sel) {
      var sel = this.__getSelection();
    }
    var range = this.__createRange(sel);
    
    if ($type(range.htmlText)) {
      return range.htmlText;
    } else {
      var content = range.cloneContents();
      return this.__domToHTML(content);
    }
  },
  
  __unselectButtons: function() {
    for (i in this.__buttons) {
      this.__unselectButton(this.__buttons[i].element);
    }
  },

  isPressed: function(buttonName) {
    return (this.__buttons[buttonName].element.src.match(/.*active-[\.a-z0-9]+\.png$/i));
  },
  
  selectButton: function(buttonName) {
    if (this.__buttons[buttonName]) {
      this.__selectButton(this.__buttons[buttonName].element);
    }
  },
  unselectButton: function(buttonName) {
    if (this.__buttons[buttonName]) {
      this.__unselectButton(this.__buttons[buttonName].element);
    }
  },
  __selectButton: function(button) {
    if (button) {
      this.__unselectButton(button);
      var match = button.src.match(/(.+)\/([^\/]+)$/);
      button.src = match[1]+'/'+'active-'+match[2];
    }
  },
  
  __unselectButton: function(button) {
    if (button) {
      button.src = button.src.replace(/active-/,  '');
    }
  },
  
  '__handleNodeChange': function() {

    var sel = this.__getSelection();
    var range = this.__createRange(sel);

    if (window.ie) {
      
      try {
      var parent = range.parentElement();
      } catch(e) {
        return;
      }

    } else {

      var parent = range.commonAncestorContainer;
      // read comments from getRangeParent
      var contents = range.cloneContents();
      var garbage = document.createDocumentFragment();

      // emptying nodes from the end
      var node = contents.childNodes[contents.childNodes.length - 1];
      while (node && node.nodeType == 3 && node.nodeValue == '') {
        garbage.appendChild(node);
        node = node.previousSibling;
      }

      // emptying nodes from the start
      var node = contents.childNodes[0];
      while (node && node.nodeType == 3 && node.nodeValue == '') {
        garbage.appendChild(node);
        node = node.nextSibling;
      }
      
      if (contents.childNodes.length == 1 && $type(contents.childNodes[0]) == 'element') {
        // the user selected only one node
        var parent = contents.childNodes[0];
      } else {
        // a wrapper is needed
        while ($type(parent) != 'element') {
          parent = parent.parentNode;
        }
      }
    }

    // special cases
    if (parent.nodeName.toLowerCase() == 'a') {
      this.__selectNode(parent);
    }

    if (parent) {

      parent.__status = {};
      
      var button = this.components.buttons.getElementsByTagName('img');
      for (var i = 0; i < button.length; i++) {
        this.__unselectButton(button[i]);
      }

      if (this.__isFullScreen) {
        this.selectButton('fullscreen');
      }

      var select = this.components.buttons.getElementsByTagName('select');
      for (var i = 0; i < select.length; i++) {
        select[i].value = '';
        select[i].style.backgroundColor = '';
      }

      while (parent) {
        var name = parent.nodeName.toLowerCase();
        if (name == 'body') {
          break;
        }
        for (var i in this.__buttons) {
          var button = this.__buttons[i];
          if (button.changeElementPattern) {
            if (name.match(button.changeElementPattern)) {
              if (typeof button.onNodeChange != 'undefined') {
                button.onNodeChange(parent);
              } else {
                this.selectButton(button.name);
              }
            }
          }
          if (parent.style && button.nodeStyle && button.nodeStyleValue) {
            if (parent.style[button.nodeStyle]) {
              if (parent.style[button.nodeStyle].match(button.nodeStyleValue)) {
                if (button.onNodeChange) {
                  button.onNodeChange(parent);
                } else {
                  this.selectButton(button.name);
                }
              } 
            }
          }
        }
        if (parent.parentNode) {
          parent.parentNode.__status = parent.__status;
        }
        parent = parent.parentNode;
      }
    }
    
  },

  addEvent: function(name, callback) {
    if (window.ie) {
      $(this.components.iframe.contentWindow.document).addEvent(name, callback);
    } else {
      $(this.components.iframe.contentWindow).addEvent(name, callback);
    }
  },
  
  removeEvent: function(name, callback) {
    if (window.ie) {
      $(this.components.iframe.contentWindow.document).removeEvent(name, callback);
    } else {
      $(this.components.iframe.contentWindow).removeEvent(name, callback);
    }
  },
  
  __getSelection: function() {
    if (window.ie) {
      var sel = this.iframeDocument().selection;  
    } else {
      var sel = this.components.iframe.contentWindow.getSelection();  
    }
    return sel;
  },
  
  __start: function() {

    if (!this.__started) {

      var parent = this.components.iframe;

      try {
        
        if (this.components.iframe.contentWindow) {
          this.components.iframe.contentWindow.document.body.innerHTML = this.components.textarea.value ? this.components.textarea.value : '&nbsp;';
        }
        
        this.designMode(true);

        if (this.__startAgain) {
          window.clearTimeout(this.__startAgain);
        }

        $(this.iframeDocument()).addEvent(
          'mouseup',
          function() {
            this.__handleNodeChange();
          }.bind(this)
        );
        
        $(this.components.textarea).addEvent(
          'keydown',
          function() {
            this.updateVisual();
          }.bind(this)
        );
       

        if (this.options.autoSave) {
          this._autoSave();
        }

        var events = ['keydown', 'mouseout', 'mouseover'];

        for (var i = 0; i < events.length; i++) {
          this.addEvent(
            events[i],
            function(e) {
              e = new Event(e);
              if (e.type == 'keydown' && !this.options.tabIndents) {
                if (e.key == 'tab') {
                  var form = this.components.textarea;
                  var change = false;
                  while (form) {
                    if (form.nodeName.toLowerCase() == 'form') {
                      for (var i = 0; i < form.elements.length; i++) {
                        var el = form.elements[i];
                        if (change == true) {
                          el.focus();
                          return e.stop();
                        }
                        if (el == this.components.textarea) {
                          var change = true;
                        }
                      }
                    }
                    form = form.parentNode;
                  }
                }
              }
              this.updateSource();
            }.bind(this)
          );
        }

        this.__started = true;
      } catch(e) {
        if (!this.__startAgain) {
          this.__startAgain = window.setInterval(this.__start.bind(this), 500);
        }
      }
      
    }

  },

  __autoSave: function(clear) {
    if (!this.__autoSaveForm) {
      this.__autoSaveForm = Widget.form({
        'action': this.options.autoSave,
        'method': 'post',
        'style': {'display': 'none'}
      });
      this.__autoSaveForm.data = Widget.textarea({'name': 'data'});
      this.__autoSaveForm.ident = Widget.input({
        'name': 'ident',
        'value': this.__ident
      });
      this.__autoSaveForm.appendChild(this.__autoSaveForm.data);
      this.__autoSaveForm.appendChild(this.__autoSaveForm.ident);
      document.body.appendChild(this.__autoSaveForm);
    }

    if (clear == true) {
      window.clearTimeout(this.__autoSaveTimer);
      this.__autoSaveForm.data.value = '';
    } else {
      this.__autoSaveForm.data.value = this.components.textarea.value;
    }

    if (!this.components.textarea.parentNode) {
      return window.clearTimeout(this.__autoSaveTimer);
    }

    // saving...
    return formSubmit(this.__autoSaveForm);
  }, 

  _autoSave: function() {

    var parent = this.components.textarea;
    
    while (parent && parent.nodeName.toLowerCase() != 'form') {
      parent = parent.parentNode;
    }

    this.__ident = location.href+'|'+this.components.textarea.name+(parent ? '|'+parent.action+'|'+parent.method : null);
    
    if (parent) {
      parent.appendChild(Widget.input({
        'name': '__ident',
        'type': 'hidden',
        'value': this.__ident
      }));
    }
    
    this.__autoSaveForm = null;
   
    new Ajax(
      this.options.autoSave,
      {
        'method': 'get',
        'data': 'ident='+encodeURIComponent(this.__ident),
        'onComplete': function(t) {
          var json = eval('('+t+')');
          if (json) {
            new JsonRpc(json);
            this.components.textarea.value = json.data;
            this._updateVisual();
          }
        }.bind(this)
      }
    ).request();
    
    this.__autoSaveTimer = window.setInterval(this.__autoSave.bind(this), parseInt(this.options.autoSaveInterval) * 1000);

  },
  
  _updateSource: function() {
    if (this.__started && this.components.iframe.contentWindow) {
      log('source up');
      this.components.textarea.value = this.components.iframe.contentWindow.document.body.innerHTML;
    }
  },
  
  _updateVisual: function() {
    if (this.components.iframe.contentWindow) {
      log('visual up');
      this.components.iframe.contentWindow.document.body.innerHTML = this.components.textarea.value ? this.components.textarea.value : '&nbsp;';
    }
  },

  iframeDocument: function() {
    if (window.ie) {
      return this.components.iframe.contentWindow.document;
    } else {    
      return this.components.iframe.contentDocument;
    }   
  },
  
  designMode: function(status) {
    this.components.iframe.style.display = 'block';
    if (window.ie) {
      this.iframeDocument().body.contentEditable = true;
    } else { 
      this.iframeDocument().designMode = status ? 'On' : 'Off';
      this.execCommand('styleWithCSS', true);
    } 
  },
  
  initialize: function(textarea, options) {

    // default options
    this.options = {
      'buttons':          null,
      'mode':             'complete',
      'blank':            $mediaDir+'/blank.html',
      'showSource':       true,
      'tabIndents':       true,
      'autoSave':         false,
      'autoSaveInterval': 30,
      'variant':          0
    }

    this.setOptions(options);

    if (!this.options.buttons) {
      switch (this.options.mode) {
        case 'nano':
          this.options.showSource = false; 
          this.options.buttons = [
            'bold',
            'italic',
            'underline',
            'strikethrough',
            'createlink',
            'removeformat'
          ];
        break;
        case 'mini':
          this.options.showSource = false; 
          this.options.buttons = [
            'bold',
            'italic',
            'underline',
            'strikethrough',
            'createlink',
            'insertorderedlist',
            'insertunorderedlist',
            'removeformat',
            'fullscreen'
          ];
        break;
        case 'basic':
          this.options.showSource = false; 
          this.options.buttons = [
            'bold',
            'italic',
            'underline',
            'strikethrough',
            'createlink',
            'insertorderedlist',
            'insertunorderedlist',
            'justifyleft',
            'justifyright',
            'justifyfull',
            'justifycenter',
            'indent',
            'outdent',
            'removeformat',
            'fullscreen'
          ];
        break;
        case 'advanced':
          this.options.showSource = true; 
          this.options.buttons = [
            'bold',
            'italic',
            'underline',
            'strikethrough',
            'createlink',
            'insertorderedlist',
            'insertunorderedlist',
            'justifyleft',
            'justifyright',
            'justifyfull',
            'justifycenter',
            'indent',
            'outdent',
            'inserthorizontalrule',
            'insertimage',
            'removeformat',
            'fullscreen'
          ];
        break;
        case 'complete':
          // all buttons
          this.options.showSource = true;
        break;
      }
    }

    // explicit rewrite
    if ($type(options.showSource)) {
      this.options.showSource = options.showSource;
    }

    this.__textarea = $(textarea);
    this.__buildComponents();
    this.__bindEvents();
    //this.components.iframe.contentWindow.focus();
  }
}
);

Meteora.onStart(
  function() {
    var textareas = document.getElementsByTagName('textarea');
    for (var i = 0; i < textareas.length; i++) {
      var t = textareas[i];
      if (t.className == 'meteoraEditor') {
        new Editor(t);
      }
    }
  }
);