/** * Editor * WYSIWYG Editor * --- * Written by José Carlos Nieto Jarquín * * 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', ''+html+''); 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 : ' '; } } 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 += ''; } 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 : ' '; } 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 : ' '; } }, 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); } } } );