/* --- name: MooEditable description: Class for creating a WYSIWYG editor, for contentEditable-capable browsers. license: MIT-style license authors: - Lim Chee Aun - Radovan Lozej - Ryan Mitchell - Olivier Refalo - T.J. Leahy requires: - Core/Class.Extras - Core/Element.Event - Core/Element.Dimensions inspiration: - Code inspired by Stefan's work [Safari Supports Content Editing!](http://www.xs4all.nl/~hhijdra/stefan/ContentEditable.html) from [safari gets contentEditable](http://walkah.net/blog/walkah/safari-gets-contenteditable) - Main reference from Peter-Paul Koch's [execCommand compatibility](http://www.quirksmode.org/dom/execCommand.html) - Some ideas and code inspired by [TinyMCE](http://tinymce.moxiecode.com/) - Some functions inspired by Inviz's [Most tiny wysiwyg you ever seen](http://forum.mootools.net/viewtopic.php?id=746), [mooWyg (Most tiny WYSIWYG 2.0)](http://forum.mootools.net/viewtopic.php?id=5740) - Some regex from Cameron Adams's [widgEditor](http://widgeditor.googlecode.com/) - Some code from Juan M Martinez's [jwysiwyg](http://jwysiwyg.googlecode.com/) - Some reference from MoxieForge's [PunyMCE](http://punymce.googlecode.com/) - IE support referring Robert Bredlau's [Rich Text Editing](http://www.rbredlau.com/drupal/node/6) provides: [MooEditable, MooEditable.Selection, MooEditable.UI, MooEditable.Actions] ... */ (function(){ var blockEls = /^(H[1-6]|HR|P|DIV|ADDRESS|PRE|FORM|TABLE|LI|OL|UL|TD|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|SCRIPT|NOSCRIPT|STYLE)$/i; var urlRegex = /^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i; var protectRegex = /<(script|noscript|style)[\u0000-\uFFFF]*?<\/(script|noscript|style)>/g; this.MooEditable = new Class({ Implements: [Events, Options], options: { toolbar: true, cleanup: true, paragraphise: true, xhtml : true, semantics : true, actions: 'bold italic underline strikethrough | insertunorderedlist insertorderedlist indent outdent | undo redo | createlink unlink | urlimage | toggleview', handleSubmit: true, handleLabel: true, disabled: false, baseCSS: 'html{ height: 100%; cursor: text; } body{ font-family: sans-serif; }', extraCSS: '', externalCSS: '', html: '
{BASEHREF}{EXTERNALCSS}', rootElement: 'p', baseURL: '', dimensions: null }, initialize: function(el, options){ this.setOptions(options); this.textarea = document.id(el); this.textarea.store('MooEditable', this); this.actions = this.options.actions.clean().split(' '); this.keys = {}; this.dialogs = {}; this.protectedElements = []; this.actions.each(function(action){ var act = MooEditable.Actions[action]; if (!act) return; if (act.options){ var key = act.options.shortcut; if (key) this.keys[key] = action; } if (act.dialogs){ Object.each(act.dialogs, function(dialog, name){ dialog = dialog.attempt(this); dialog.name = action + ':' + name; if (typeOf(this.dialogs[action]) != 'object') this.dialogs[action] = {}; this.dialogs[action][name] = dialog; }, this); } if (act.events){ Object.each(act.events, function(fn, event){ this.addEvent(event, fn); }, this); } }.bind(this)); this.render(); }, toElement: function(){ return this.textarea; }, render: function(){ var self = this; // Dimensions var dimensions = this.options.dimensions || this.textarea.getSize(); // Build the container this.container = new Element('div', { id: (this.textarea.id) ? this.textarea.id + '-mooeditable-container' : null, 'class': 'mooeditable-container', styles: { width: dimensions.x } }); // Override all textarea styles this.textarea.addClass('mooeditable-textarea').setStyle('height', dimensions.y); // Build the iframe this.iframe = new IFrame({ 'class': 'mooeditable-iframe', frameBorder: 0, src: 'javascript:""', // Workaround for HTTPs warning in IE6/7 styles: { height: dimensions.y } }); this.toolbar = new MooEditable.UI.Toolbar({ onItemAction: function(){ var args = Array.from(arguments); var item = args[0]; self.action(item.name, args); } }); this.attach.delay(1, this); // Update the event for textarea's corresponding labels if (this.options.handleLabel && this.textarea.id) $$('label[for="'+this.textarea.id+'"]').addEvent('click', function(e){ if (self.mode != 'iframe') return; e.preventDefault(); self.focus(); }); // Update & cleanup content before submit if (this.options.handleSubmit){ this.form = this.textarea.getParent('form'); if (!this.form) return; this.form.addEvent('submit', function(){ if (self.mode == 'iframe') self.saveContent(); }); } this.fireEvent('render', this); }, attach: function(){ var self = this; // Assign view mode this.mode = 'iframe'; // Editor iframe state this.editorDisabled = false; // Put textarea inside container this.container.wraps(this.textarea); this.textarea.setStyle('display', 'none'); this.iframe.setStyle('display', '').inject(this.textarea, 'before'); Object.each(this.dialogs, function(action, name){ Object.each(action, function(dialog){ document.id(dialog).inject(self.iframe, 'before'); var range; dialog.addEvents({ open: function(){ range = self.selection.getRange(); self.editorDisabled = true; self.toolbar.disable(name); self.fireEvent('dialogOpen', this); }, close: function(){ self.toolbar.enable(); self.editorDisabled = false; self.focus(); if (range) self.selection.setRange(range); self.fireEvent('dialogClose', this); } }); }); }); // contentWindow and document references this.win = this.iframe.contentWindow; this.doc = this.win.document; // Deal with weird quirks on Gecko if (Browser.firefox) this.doc.designMode = 'On'; // Build the content of iframe var docHTML = this.options.html.substitute({ BASECSS: this.options.baseCSS, EXTRACSS: this.options.extraCSS, EXTERNALCSS: (this.options.externalCSS) ? '': '', BASEHREF: (this.options.baseURL) ? '\s*
\s*<\/p>/gi, '
\u00a0
'); source = source.replace(/( |\s)*<\/p>/gi, '
\u00a0
'); if (!this.options.semantics){ source = source.replace(/\s*$1
$1
'); } //tags around a list will get moved to after the list if (!Browser.ie){ //not working properly in safari? source = source.replace(/
[\s\n]*(<(?:ul|ol)>.*?<\/(?:ul|ol)>)(.*?)<\/p>/ig, '$1
$2
'); source = source.replace(/<\/(ol|ul)>\s*(?!<(?:p|ol|ul|img).*?>)((?:<[^>]*>)?\w.*)$/g, '$1>$2
'); } source = source.replace(/\s*(]+>)\s*<\/p>/ig, '$1\n'); // if a
only contains , remove the
tags //format the source source = source.replace(/
]*)>(.*?)<\/p>(?!\n)/g, '
$2
\n'); // break after paragraphs source = source.replace(/<\/(ul|ol|p)>(?!\n)/g, '$1>\n'); // break after tags source = source.replace(/>tags and empty
tags source = source.replace(/
(?:\s*)
/g, '
'); source = source.replace(/<\/p>\s*<\/p>/g, '
'); // Replaceautomatically added by some browsers source = source.replace(/]*>.*?<\/pre>/gi, function(match){ return match.replace(/
/gi, '\n'); }); // Final trim source = source.trim(); } while (source != oSource); return source; } }); MooEditable.Selection = new Class({ initialize: function(win){ this.win = win; }, getSelection: function(){ this.win.focus(); return (this.win.getSelection) ? this.win.getSelection() : this.win.document.selection; }, getRange: function(){ var s = this.getSelection(); if (!s) return null; try { return s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : null); } catch(e) { // IE bug when used in frameset return this.doc.body.createTextRange(); } }, setRange: function(range){ if (range.select){ Function.attempt(function(){ range.select(); }); } else { var s = this.getSelection(); if (s.addRange){ s.removeAllRanges(); s.addRange(range); } } }, selectNode: function(node, collapse){ var r = this.getRange(); var s = this.getSelection(); if (r.moveToElementText){ Function.attempt(function(){ r.moveToElementText(node); r.select(); }); } else if (s.addRange){ collapse ? r.selectNodeContents(node) : r.selectNode(node); s.removeAllRanges(); s.addRange(r); } else { s.setBaseAndExtent(node, 0, node, 1); } return node; }, isCollapsed: function(){ var r = this.getRange(); if (r.item) return false; return r.boundingWidth == 0 || this.getSelection().isCollapsed; }, collapse: function(toStart){ var r = this.getRange(); var s = this.getSelection(); if (r.select){ r.collapse(toStart); r.select(); } else { toStart ? s.collapseToStart() : s.collapseToEnd(); } }, getContent: function(){ var r = this.getRange(); var body = new Element('body'); if (this.isCollapsed()) return ''; if (r.cloneContents){ body.appendChild(r.cloneContents()); } else if (r.item != undefined || r.htmlText != undefined){ body.set('html', r.item ? r.item(0).outerHTML : r.htmlText); } else { body.set('html', r.toString()); } var content = body.get('html'); return content; }, getText : function(){ var r = this.getRange(); var s = this.getSelection(); return this.isCollapsed() ? '' : r.text || (s.toString ? s.toString() : ''); }, getNode: function(){ var r = this.getRange(); if (!Browser.ie || Browser.version >= 9){ var el = null; if (r){ el = r.commonAncestorContainer; // Handle selection a image or other control like element such as anchors if (!r.collapsed) if (r.startContainer == r.endContainer) if (r.startOffset - r.endOffset < 2) if (r.startContainer.hasChildNodes()) el = r.startContainer.childNodes[r.startOffset]; while (typeOf(el) != 'element') el = el.parentNode; } return document.id(el); } return document.id(r.item ? r.item(0) : r.parentElement()); }, insertContent: function(content){ if (Browser.ie){ var r = this.getRange(); if (r.pasteHTML){ r.pasteHTML(content); r.collapse(false); r.select(); } else if (r.insertNode){ r.deleteContents(); if (r.createContextualFragment){ r.insertNode(r.createContextualFragment(content)); } else { var doc = this.win.document; var fragment = doc.createDocumentFragment(); var temp = doc.createElement('div'); fragment.appendChild(temp); temp.outerHTML = content; r.insertNode(fragment); } } } else { this.win.document.execCommand('insertHTML', false, content); } } }); // Avoiding Locale dependency // Wrapper functions to be used internally and for plugins, defaults to en-US var phrases = {}; MooEditable.Locale = { define: function(key, value){ if (typeOf(window.Locale) != 'null') return Locale.define('en-US', 'MooEditable', key, value); if (typeOf(key) == 'object') Object.merge(phrases, key); else phrases[key] = value; }, get: function(key){ if (typeOf(window.Locale) != 'null') return Locale.get('MooEditable.' + key); return key ? phrases[key] : ''; } }; MooEditable.Locale.define({ ok: 'OK', cancel: 'Cancel', bold: 'Bold', italic: 'Italic', underline: 'Underline', strikethrough: 'Strikethrough', unorderedList: 'Unordered List', orderedList: 'Ordered List', indent: 'Indent', outdent: 'Outdent', undo: 'Undo', redo: 'Redo', removeHyperlink: 'Remove Hyperlink', addHyperlink: 'Add Hyperlink', selectTextHyperlink: 'Please select the text you wish to hyperlink.', enterURL: 'Enter URL', enterImageURL: 'Enter image URL', addImage: 'Add Image', toggleView: 'Toggle View' }); MooEditable.UI = {}; MooEditable.UI.Toolbar= new Class({ Implements: [Events, Options], options: { /* onItemAction: function(){}, */ 'class': '' }, initialize: function(options){ this.setOptions(options); this.el = new Element('div', {'class': 'mooeditable-ui-toolbar ' + this.options['class']}); this.items = {}; this.content = null; }, toElement: function(){ return this.el; }, render: function(actions){ if (this.content){ this.el.adopt(this.content); } else { this.content = actions.map(function(action){ return (action == '|') ? this.addSeparator() : this.addItem(action); }.bind(this)); } return this; }, addItem: function(action){ var self = this; var act = MooEditable.Actions[action]; if (!act) return; var type = act.type || 'button'; var options = act.options || {}; var item = new MooEditable.UI[type.camelCase().capitalize()](Object.append(options, { name: action, 'class': action + '-item toolbar-item', title: act.title, onAction: self.itemAction.bind(self) })); this.items[action] = item; document.id(item).inject(this.el); return item; }, getItem: function(action){ return this.items[action]; }, addSeparator: function(){ return new Element('span', {'class': 'toolbar-separator'}).inject(this.el); }, itemAction: function(){ this.fireEvent('itemAction', arguments); }, disable: function(except){ Object.each(this.items, function(item){ (item.name == except) ? item.activate() : item.deactivate().disable(); }); return this; }, enable: function(){ Object.each(this.items, function(item){ item.enable(); }); return this; }, show: function(){ this.el.setStyle('display', ''); return this; }, hide: function(){ this.el.setStyle('display', 'none'); return this; } }); MooEditable.UI.Button = new Class({ Implements: [Events, Options], options: { /* onAction: function(){}, */ title: '', name: '', text: 'Button', 'class': '', shortcut: '', mode: 'icon' }, initialize: function(options){ this.setOptions(options); this.name = this.options.name; this.render(); }, toElement: function(){ return this.el; }, render: function(){ var self = this; var key = (Browser.Platform.mac) ? 'Cmd' : 'Ctrl'; var shortcut = (this.options.shortcut) ? ' ( ' + key + '+' + this.options.shortcut.toUpperCase() + ' )' : ''; var text = this.options.title || name; var title = text + shortcut; this.el = new Element('button', { 'class': 'mooeditable-ui-button ' + self.options['class'], title: title, html: ' ', events: { click: self.click.bind(self), mousedown: function(e){ e.preventDefault(); } } }); if (this.options.mode != 'icon') this.el.addClass('mooeditable-ui-button-' + this.options.mode); this.active = false; this.disabled = false; // add hover effect for IE if (Browser.ie) this.el.addEvents({ mouseenter: function(e){ this.addClass('hover'); }, mouseleave: function(e){ this.removeClass('hover'); } }); return this; }, click: function(e){ e.preventDefault(); if (this.disabled) return; this.action(e); }, action: function(){ this.fireEvent('action', [this].concat(Array.from(arguments))); }, enable: function(){ if (this.active) this.el.removeClass('onActive'); if (!this.disabled) return; this.disabled = false; this.el.removeClass('disabled').set({ disabled: false, opacity: 1 }); return this; }, disable: function(){ if (this.disabled) return; this.disabled = true; this.el.addClass('disabled').set({ disabled: true, opacity: 0.4 }); return this; }, activate: function(){ if (this.disabled) return; this.active = true; this.el.addClass('onActive'); return this; }, deactivate: function(){ this.active = false; this.el.removeClass('onActive'); return this; } }); MooEditable.UI.Dialog = new Class({ Implements: [Events, Options], options:{ /* onOpen: function(){}, onClose: function(){}, */ 'class': '', contentClass: '' }, initialize: function(html, options){ this.setOptions(options); this.html = html; var self = this; this.el = new Element('div', { 'class': 'mooeditable-ui-dialog ' + self.options['class'], html: ' ', styles: { 'display': 'none' }, events: { click: self.click.bind(self) } }); }, toElement: function(){ return this.el; }, click: function(){ this.fireEvent('click', arguments); return this; }, open: function(){ this.el.setStyle('display', ''); this.fireEvent('open', this); return this; }, close: function(){ this.el.setStyle('display', 'none'); this.fireEvent('close', this); return this; } }); MooEditable.UI.AlertDialog = function(alertText){ if (!alertText) return; var html = alertText + ' '; return new MooEditable.UI.Dialog(html, { 'class': 'mooeditable-alert-dialog', onOpen: function(){ var button = this.el.getElement('.dialog-ok-button'); (function(){ button.focus(); }).delay(10); }, onClick: function(e){ e.preventDefault(); if (e.target.tagName.toLowerCase() != 'button') return; if (document.id(e.target).hasClass('dialog-ok-button')) this.close(); } }); }; MooEditable.UI.PromptDialog = function(questionText, answerText, fn){ if (!questionText) return; var html = ' ' + ''; return new MooEditable.UI.Dialog(html, { 'class': 'mooeditable-prompt-dialog', onOpen: function(){ var input = this.el.getElement('.dialog-input'); (function(){ input.focus(); input.select(); }).delay(10); }, onClick: function(e){ e.preventDefault(); if (e.target.tagName.toLowerCase() != 'button') return; var button = document.id(e.target); var input = this.el.getElement('.dialog-input'); if (button.hasClass('dialog-cancel-button')){ input.set('value', answerText); this.close(); } else if (button.hasClass('dialog-ok-button')){ var answer = input.get('value'); input.set('value', answerText); this.close(); if (fn) fn.attempt(answer, this); } } }); }; MooEditable.Actions = { bold: { title: MooEditable.Locale.get('bold'), options: { shortcut: 'b' }, states: { tags: ['b', 'strong'], css: {'font-weight': 'bold'} }, events: { beforeToggleView: function(){ if(Browser.firefox){ var value = this.textarea.get('value'); var newValue = value.replace(/]*)>/gi, '').replace(/<\/strong>/gi, ''); if (value != newValue) this.textarea.set('value', newValue); } }, attach: function(){ if(Browser.firefox){ var value = this.textarea.get('value'); var newValue = value.replace(/]*)>/gi, '').replace(/<\/strong>/gi, ''); if (value != newValue){ this.textarea.set('value', newValue); this.setContent(newValue); } } } } }, italic: { title: MooEditable.Locale.get('italic'), options: { shortcut: 'i' }, states: { tags: ['i', 'em'], css: {'font-style': 'italic'} }, events: { beforeToggleView: function(){ if (Browser.firefox){ var value = this.textarea.get('value'); var newValue = value.replace(/