/**
* @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');
}
}
}
};