Source: MarkupEditor.js

/**
 * @constructor
 * @param options {Object} The options
 * @param options.debug {boolean}
 * @param options.value {String} The HTML to edit
 * @param options.css {String}
 * @param options.autoHideToolbar {boolean}
 * @param options.replace {Element | String}
 */
hui.ui.MarkupEditor = function(options) {
  this.name = options.name;
  this.options = options = hui.override({debug:false,value:'',autoHideToolbar:true},options);
  if (options.replace) {
    options.replace = hui.get(options.replace);
    options.element = hui.build('div',{'class':'hui_markupeditor '+options.replace.className});
    options.replace.parentNode.insertBefore(options.element,options.replace);
    options.replace.style.display='none';
    options.value = this.options.replace.innerHTML;
  }
  this.ready = false;
  this.pending = [];
  this.element = hui.get(options.element);
  if (hui.browser.msie) {
    this.impl = hui.ui.MarkupEditor.MSIE;
  } else {
    this.impl = hui.ui.MarkupEditor.webkit;
  }
  this.impl.initialize({
    element : this.element,
    controller : this,
    $ready : this._ready.bind(this)
  });
  if (options.value) {
    this.setValue(options.value);
  }
  hui.ui.extend(this);
};

hui.ui.MarkupEditor.create = function(options) {
  options = options || {};
  options.element = hui.build('div',{className:'hui_markupeditor'});
  return new hui.ui.MarkupEditor(options);
};

hui.ui.MarkupEditor.prototype = {

  _ready : function() {
    this.ready = true;
    for (var i=0; i < this.pending.length; i++) {
      this.pending[i]();
    }
  },

  /** @private */
  implFocused : function() {
    this._showBar();
  },

  /** @private */
  implBlurred : function() {
    this.bar.hide();
    this.fire('blur');
  },

  /** @private */
  implValueChanged : function() {
    this._valueChanged();
  },

  implSelectionChanged : function() {
    if (this.options.linkDelegate) {
      this.options.linkDelegate.$cancel();
    }
    this._highlightNode(null);
    this.temporaryLink = null;
    this._valueChanged();
    this._refreshInfoWindow();
    if (this.bar) this.bar.setBlock(this._getFirstBlock());
  },
  _getFirstBlock : function() {
    var path = this.impl.getPath();
    var blocks = ['P','DIV','H1','H2','H3','H4','H5','H6','BLOCKQUOTE'];
    for (var i = path.length - 1; i >= 0; i--) {
      var tag = path[i].tagName;
      if (blocks.indexOf(tag)!==-1) {
        return path[i];
      }
    }
    return null;
  },

  /** Remove the widget from the DOM */
  detach : function() {
    if (this.options.replace) {
      this.options.replace.style.display='';
    }
    var dest = ['_infoWindow','impl'];
    for (var i = dest.length - 1; i >= 0; i--) {
      if (this[dest[i]]) {
        this[dest[i]].destroy();
      }
    }
  },
  getAccessories : function() {
    return [this.colorPicker,this.bar].filter(function(e) {!!e});
  },

  /** Get the HTML value */
  getValue : function() {
    return this.impl.getHTML();
  },

  /** Set the HTML value */
  setValue : function(value) {
    this._whenReady(function() {
      this.impl.setHTML(value);
    }.bind(this));
  },
  /** Focus the editor */
  focus : function() {
    this._whenReady(this.impl.focus.bind(this.impl));
  },

  _whenReady : function(func) {
    if (this.ready) {
      func();
    } else {
      this.pending.push(func);
    }
  },
  _showBar : function() {
    if (!this.bar) {
      this.bar = new hui.ui.MarkupEditor.Bar({
        $clickButton : this._buttonClicked.bind(this),
        $changeBlock : this._changeBlock.bind(this)
      });
    }
    this.bar.show(this);
  },
  _buttonClicked : function(info) {
    this.impl.saveSelection();
    if (info.key=='color') {
      this._showColorPicker();
    } else if (info.key=='addLink') {
      this._showLinkEditor();
    } else if (info.key=='align') {
      this.impl.align(info.value);
    } else if (info.key=='clear') {
      this.impl.removeFormat();
    } else if (info.key=='info') {
      this._toggleInfoWindow();
    } else {
      this.impl.format(info);
    }
    this._valueChanged();
    this._refreshInfoWindow();
    this.impl.restoreSelection();
    this.impl._selectionChanged();
  },
  _changeBlock : function(tag) {
    var block = this._getFirstBlock();
    if (block) {
      block = hui.dom.changeTag(block,tag);
      this.impl.selectNode(block);
    }
  },
  _showColorPicker : function() {
    if (!this.colorPicker) {
      this.colorPicker = hui.ui.Window.create({title:{en:'Color',da:'Farve'}});
      var picker = hui.ui.ColorPicker.create();
      picker.listen(this);
      this.colorPicker.add(picker);
      this.colorPicker.listen({
        $userClosedWindow : function() {
          this.impl.restoreSelection();
        }.bind(this)
      });
    }
    this.colorPicker.show({avoid:this.element});
  },
  _highlightNode : function(node) {
    if (this._highlightedNode) {
      hui.cls.remove(this._highlightedNode,'hui_markupeditor_highlighted');
    }
    this._highlightedNode = node;
    if (node) {
      hui.cls.add(node,'hui_markupeditor_highlighted');
    }
  },
  _showLinkEditor : function() {
    this.temporaryLink = this.impl.getOrCreateLink();
    this._highlightNode(this.temporaryLink);
    if (this.options.linkDelegate ) {
      var delegate = this.options.linkDelegate;
      delegate.$editLink({
        node : this.temporaryLink,
        $changed : function() {
          this._highlightNode(null);
          this.temporaryLink = null;
          this._valueChanged();
        }.bind(this),
        $cancel : function() {
          this._highlightNode(null);
          this.temporaryLink = null;
          this._valueChanged();
        }.bind(this),
        $remove : function() {
          // TODO: Standardise this
          this.impl._unWrap(this.temporaryLink);
          this.impl._selectionChanged();
        }.bind(this)
      });
    } else if (!this.linkEditor) {
      this.linkEditor = hui.ui.Window.create({padding:5,width:300});
      this.linkForm = hui.ui.Form.create();
      this.linkEditor.add(this.linkForm);
      this.linkForm.buildGroup({},[
        {type : 'TextInput', options:{key:'url',label:'Address:'}}
      ]);
      var buttons = this.linkForm.createButtons();
      var ok = hui.ui.Button.create({text:'OK',submit:true});
      this.linkForm.listen({$submit:this._updateLink.bind(this)});
      buttons.add(ok);
    }
    if (this.linkEditor) {
      this.linkForm.setValues({url:this.temporaryLink.href});
      this.linkEditor.show({avoid:this.element});
      this.linkForm.focus();
    }
  },
  _updateLink : function() {
    var values = this.linkForm.getValues();
    this.temporaryLink.href = values.url;
    this.linkForm.reset();
    this.temporaryLink = null;
    this.linkEditor.hide();
    this._valueChanged();
  },
  _valueChanged : function() {
    this.fire('valueChanged',this.impl.getHTML());
    this._refreshInfoWindow();
  },

  // Info window

  _toggleInfoWindow : function() {
    if (!this._infoWindow) {
      this._infoWindow = new hui.ui.MarkupEditor.Info({editor:this});
    }
    this._infoWindow.toggle();
    this._refreshInfoWindow();
  },

  _refreshInfoWindow : function() {
    if (!this._infoWindow) {return;}
    this._infoWindow.updatePath(this.impl.getPath());
  },

  /** @private */
  $colorWasSelected : function(color) {
    this.impl.restoreSelection(function() {
      this.impl.colorize(color);
      this._valueChanged();
    }.bind(this));
  },

  /** @private */
  $$parentMoved : function() {
    if (this.bar) {
      this.bar.place(this);
    }
  }
};








hui.ui.MarkupEditor.Bar = function(options) {
  this.options = options;
  this._initialize();
  hui.ui.extend(this);
};

hui.ui.MarkupEditor.Bar.prototype = {
  _initialize : function() {

    var things = [
      {key:'bold',icon:'edit/text_bold'},
      {key:'italic',icon:'edit/text_italic'},
      {divider:true},
      {key:'color',icon:'common/color'},
      {key:'addLink',icon:'monochrome/link'},
      {divider:true},
      {key:'align',value:'left',icon:'edit/text_align_left'},
      {key:'align',value:'center',icon:'edit/text_align_center'},
      {key:'align',value:'right',icon:'edit/text_align_right'},
      {key:'align',value:'justify',icon:'edit/text_align_justify'},
      {divider:true},
      {key:'clear',icon:'edit/clear'},
      {key:'info',icon:'monochrome/info'}
    ];

    this.bar = hui.ui.Bar.create({absolute:true,variant:'mini',small:true});
    var drop = this.blockSelector = hui.ui.DropDown.create({focus:false,variant:'bar_mini',items:[
      {value:'h1',text:'Header 1'},
      {value:'h2',text:'Header 2'},
      {value:'h3',text:'Header 3'},
      {value:'h4',text:'Header 4'},
      {value:'h5',text:'Header 5'},
      {value:'h6',text:'Header 6'},
      {value:'p',text:'Paragraph'},
      {value:'div',text:'Division'},
      {value:'blockquote',text:'Blockquote'}
    ]});
    this.bar.add(drop);

    drop.listen({
      $valueChanged : function(value) {
        this.options.$changeBlock(value);
      }.bind(this)
    });

    hui.each(things,function(info) {
      if (info.divider) {
        this.bar.addDivider();
        return;
      }
      var button = new hui.ui.Bar.Button.create({icon:info.icon,stopEvents:true});
      button.listen({
        $mousedown : function() { this.options.$clickButton(info); }.bind(this)
      });
      this.bar.add(button);
    }.bind(this));
    this.bar.addToDocument();
  },

  show : function(widget) {
    this.bar.placeAbove(widget);
    this.bar.show();
  },
  place : function(widget) {
    this.bar.placeAbove(widget);
  },
  hide : function() {
    this.bar.hide();
  },
  setBlock : function(value) {
    if (value) {
      this.blockSelector.setValue(value.tagName.toLowerCase());
    }
  },
  getAccessories : function() {
    return [this.bar];
  }
};





hui.ui.MarkupEditor.Info = function(options) {
  this.options = options;
  this._initialize();
};

hui.ui.MarkupEditor.Info.prototype = {
  _initialize : function() {
    this._window = hui.ui.Window.create({title:'Info',width:400});
    this._css = hui.ui.CodeInput.create();
    this._css.listen({
      $valueChanged : function(value) {
        if (!this.tag) {return;}
        this.tag.setAttribute('style',value);
      }.bind(this)
    });
    this._window.add(this._css);
    this._path = hui.build('div',{'class':'hui_markupeditor_path'});
    this._window.add(this._path);
  },
  toggle : function() {
   this._window.toggle({avoid:this.options.editor.element});
  },
  updatePath : function(path) {
    var html = '';
    for (var i = path.length - 1; i >= 0; i--) {
      html+='<a data-index="' + i + '" href="javascript://">' + path[i].tagName + '</a> ';
    }
    this._path.innerHTML = html;
    this.tag = path[0];
    this._css.setValue(this.tag ? this.tag.getAttribute('style') : '');
  },
  destroy : function() {
    this._window.destroy();
  }
};




/** @namespace */
hui.ui.MarkupEditor.webkit = {

  path : [],

  initialize : function(options) {
    this.element = options.element;
    hui.style.set(this.element,options.controller.options.style);
    this.element.style.overflow='auto';
    this.element.contentEditable = true;
    var ctrl = this.controller = options.controller;
    hui.listen(this.element,'focus',function() {
      ctrl.implFocused();
    });
    hui.listen(this.element,'blur',function() {
      ctrl.implBlurred();
    });
    hui.listen(this.element,'keyup',this._change.bind(this));
    hui.listen(this.element,'mouseup',this._change.bind(this));
    options.$ready();
  },
  saveSelection : function() {

  },
  restoreSelection : function(callback) {
    if (callback) {callback();}
  },
  focus : function() {
    this.element.focus();
    this._selectionChanged();
    this.controller.implFocused();
  },
  format : function(info) {
    if (info.key=='strong' || info.key=='em') {
      this._wrapInTag(info.key);
    } else if (info.key=='insert-table') {
      this._insertHTML('<table><tbody><tr><td>Lorem ipsum dolor</td><td>Lorem ipsum dolor</td></tr></tbody></table>');
    } else {
      document.execCommand(info.key,null,info.value);
      var node = this._getSelectedNode();
      if (node.tagName=='B') {
        node = hui.dom.changeTag(node,'strong');
        this.selectNode(node);
      } else if (node.tagName=='I') {
        node = hui.dom.changeTag(node,'em');
        this.selectNode(node);
      }
      this.controller._valueChanged();
    }
    this._selectionChanged();
  },
  selectNode : function(node) {
    window.getSelection().selectAllChildren(node);
    this._selectionChanged();
  },
  getOrCreateLink : function() {
    var node = this._getSelectedNode();
    if (node && node.tagName.toLowerCase()=='a') {
      return node;
    }
    document.execCommand('createLink',null,'#');
    this._selectionChanged();
    return this._getSelectedNode();
  },
  _getSelectedNode : function() {
    var selection = window.getSelection();
    var range = selection.getRangeAt(0);
    var ancestor = range.commonAncestorContainer;
    if (!hui.dom.isElement(ancestor)) {
      ancestor = ancestor.parentNode;
    }
    return ancestor;
  },
  colorize : function(color) {
    document.execCommand('forecolor',null,color);
    var node = this._getSelectedNode();
    if (node.tagName=='FONT') {
      node = hui.dom.changeTag(node,'span');
      node.style.color = color;
      node.removeAttribute('color');
      var selection = window.getSelection();
      selection.selectAllChildren(node);
    }
    this._selectionChanged();
  },
  align : function(value) {
    var x = {center:'justifycenter',justify:'justifyfull',left:'justifyleft',right:'justifyright'};
    document.execCommand(x[value],null,null);
    this._updateInlinePanel();
  },
  _change : function() {
    this.controller.implValueChanged();
    this._selectionChanged();
  },
  _wrapInTag : function(tag) {
    var selection = window.getSelection();
    if (selection.rangeCount<1) {return;}
    var range = selection.getRangeAt(0);
    var ancestor = range.commonAncestorContainer;
    if (!hui.dom.isElement(ancestor)) {
      ancestor = ancestor.parentNode;
    }
    if (ancestor.tagName.toLowerCase()==tag) {
      this._unWrap(ancestor);
    } else {
      var node = document.createElement(tag);
      range.surroundContents(node);
      selection.selectAllChildren(node);
    }
    this._selectionChanged();
  },
  _getInlineTag : function() {
    var selection = window.getSelection();
    if (selection.rangeCount<1) {return;}

  },
  removeLink : function(node) {
    this._unWrap(node);
    this._selectionChanged();
  },
  _unWrap : function(node) {
    var c = node.childNodes;
    for (var i=0; i < c.length; i++) {
      node.parentNode.insertBefore(c[i],node);
    }
    node.parentNode.removeChild(node);
  },
  _insertHTML : function(html) {
    document.execCommand('inserthtml',null,html);
  },
  _getAncestor : function() {
    var selection = window.getSelection();
    if (selection.rangeCount<1) {
      return null;
    }
    var range = selection.getRangeAt(0);
    var ancestor = range.commonAncestorContainer;
    if (!hui.dom.isElement(ancestor)) {
      ancestor = ancestor.parentNode;
    }
    return ancestor;
  },
  _buildInlinePanel : function() {
    this._inlinePanel = hui.ui.BoundPanel.create({variant:'light'});
    var content = hui.build('div',{
      'class' : 'hui_markupeditor_inlinepanel',
      html : '<a href="javascript://" data="bold"><strong>Bold</strong></a><a href="javascript://" data="italic"><em>Italic</em></a>'
    });
    hui.listen(content,'mousedown',function(e) {
      e = hui.event(e);
      e.stop();
      var a = e.findByTag('a');
      if (a) {
        this.saveSelection();
        this.format({key:a.getAttribute('data')});
        this.restoreSelection();
        this._selectionChanged();
      }
    }.bind(this));
    this._inlinePanel.add(content);
  },
  _updateInlinePanel : function() {
    var selection = window.getSelection();
    if (!this._inlinePanel) this._buildInlinePanel();
    if (selection.rangeCount < 1) {
      this._inlinePanel.hide();
      return;
    }
    var range = selection.getRangeAt(0);
    if (range.startOffset==range.endOffset) {
      this._inlinePanel.hide();
      return;
    }
    var rects = range.getClientRects();
    if (rects.length > 0) {
      var rect = rects[0];
      this._inlinePanel.position({rect:rect,position:'vertical'});
      this._inlinePanel.show();
    }

  },
  _selectionChanged : function() {
    var sel = window.getSelection();
    var hash = this._hash(sel);
    var node = sel.anchorNode ? sel.anchorNode.parentNode : null;
    var latest = this._latestSelection;
    if (latest) {
      if (node == latest.node && latest.hash == hash) {
        return;
      }
    }
    this._latestSelection = {node:node,hash:hash};
    var path = [],
      tag = this._getAncestor();
    while (tag && tag !== this.element) {
      path.push(tag);
      tag = tag.parentNode;
    }
    this.path = path;
    this.controller.implSelectionChanged();
    this._updateInlinePanel();
  },
  _storeSelection : function(selection) {

  },
  _hash : function(sel) {
    return sel.anchorOffset+':'+sel.baseOffset+':'+sel.extentOffset;
  },
  removeFormat : function() {
    document.execCommand('removeFormat',null,null);
    this._selectionChanged();
  },
  setHTML : function(html) {
    this.element.innerHTML = html;
  },
  getHTML : function() {
    var cleaned = hui.ui.MarkupEditor.util.clean(this.element);
    return cleaned.innerHTML;
  },
  getPath : function() {
    return this.path;
  },
  destroy : function() {
    if (this._inlinePanel) {
      this._inlinePanel.destroy();
    }
  }
};






















/** @namespace */
hui.ui.MarkupEditor.MSIE = {
  initialize : function(options) {
    this.element = options.element;
    this.iframe = hui.build('iframe',{style:'display:block; width: 100%; border: 0;',parent:this.element});
    hui.listen(this.iframe,'load',function() {
      this._load();
      options.$ready();
    }.bind(this));
    this.controller = options.controller;
  },
  saveSelection : function() {
    this.savedRange = this.document.selection.createRange();
    //this.savedSelection = this.document.selection.createRange().getBookmark();
  },
  restoreSelection : function(callback) {
    window.setTimeout(function() {
      this.body.focus();
      this.savedRange.select();
      if (callback) {callback();}
    }.bind(this));
  },
  _load : function() {
    this.document = hui.frame.getDocument(this.iframe);
    this.body = this.document.body;
    this.body.contentEditable = true;
    hui.listen(this.body,'keyup',this._keyUp.bind(this));
    hui.listen(this.body,'mouseup',this._mouseUp.bind(this));
  },
  _keyUp : function() {
    this.controller.implValueChanged();
    this.saveSelection();
  },
  _mouseUp : function() {
    this.saveSelection();
  },
  focus : function() {
    this.body.focus();
    this.controller.implFocused();
  },
  align : function(value) {
    var x = {center:'justifycenter',justify:'justifyfull',left:'justifyleft',right:'justifyright'};
    this.document.execCommand(x[value],null,null);
  },
  format : function(info) {
    if (info.key=='strong' || info.key=='em') {
      this._wrapInTag(info.key);
    } else if (info.key=='insert-table') {
      this._insertHTML('<table><tbody><tr><td>Lorem ipsum dolor</td><td>Lorem ipsum dolor</td></tr></tbody></table>');
    } else {
      this.document.execCommand(info.key,null,null);
    }
  },
  removeFormat : function() {
    this.document.execCommand('removeFormat',null,null);
  },
  colorize : function(color) {
    this.document.execCommand('forecolor',null,color);
    this.restoreSelection();
  },
  _wrapInTag : function(tag) {
    document.execCommand('inserthtml',null,'<'+tag+'>'+hui.string.escape(hui.selection.getText())+'</'+tag+'>');
  },
  _insertHTML : function(html) {
    document.execCommand('inserthtml',null,html);
  },
  setHTML : function(html) {
    this.body.innerHTML = html;
  },
  getHTML : function() {
    var cleaned = hui.ui.MarkupEditor.util.clean(this.body);
    return cleaned.innerHTML;
  },
  getPath : function() {
    return [];
  },
  destroy : function() {
  }
};

/** @namespace */
hui.ui.MarkupEditor.util = {
  clean : function(node) {
    var copy = node.cloneNode(true);
    this.replaceNodes(copy,{b:'strong',i:'em',font:'span'});

    var apples = hui.get.byClass(copy,'Apple-style-span');
    for (var i = apples.length - 1; i >= 0; i--){
      apples[i].removeAttribute('class');
    }
    this.convertAttributesToStyle(copy);
    return copy;
  },
  replaceNodes : function(node,recipe) {
    for (var key in recipe) {
      var bs = node.getElementsByTagName(key);
      for (var i = bs.length - 1; i >= 0; i--) {
        var x = bs[i];
        var replacement = document.createElement(recipe[key]);
        var color = bs[i].getAttribute('color');
        if (color) {
          replacement.style.color=color;
        }
        hui.dom.replaceNode(x,replacement);
        var children = x.childNodes;
        for (var j=0; j < children.length; j++) {
          var removed = x.removeChild(children[j]);
          replacement.appendChild(removed);
        }
      }
    }
  },
  convertAttributesToStyle : function(node) {
    var all = node.getElementsByTagName('*');
    for (var i=0; i < all.length; i++) {
      var n = all[i];
      var align = n.getAttribute('align');
      if (align) {
        n.style.textAlign = align;
        n.removeAttribute('align');
      }
    }
  }
};