rhymix/modules/editor/skins/xquared/javascripts/Editor.js
haneul 7c3b336e41 17223554 : xquared upgrade to 0.7
git-svn-id: http://xe-core.googlecode.com/svn/sandbox@4968 201d5d3c-b55e-5fd7-737f-ddc643e51545
2008-11-24 08:52:19 +00:00

2551 lines
70 KiB
JavaScript

/**
* @requires Xquared.js
* @requires Browser.js
* @requires Timer.js
* @requires rdom/Factory.js
* @requires validator/Factory.js
* @requires EditHistory.js
* @requires plugin/Base.js
* @requires RichTable.js
* @requires ui/Control.js
* @requires ui/Toolbar.js
* @requires ui/_templates.js
* @requires Shortcut.js
*/
xq.Editor = xq.Class(/** @lends xq.Editor.prototype */{
/**
* Initialize editor but it doesn't automatically start designMode. setEditMode should be called after initialization.
*
* @constructs
* @param {Object} contentElement TEXTAREA to be replaced with editable area, or DOM ID string for TEXTAREA.
* @param {Object} toolbarContainer HTML element which contains toolbar icons, or DOM ID string.
*/
initialize: function(contentElement, toolbarContainer) {
xq.addToFinalizeQueue(this);
if(typeof contentElement === 'string') {
contentElement = xq.$(contentElement);
}
if(!contentElement) {
throw "[contentElement] is null";
}
if(contentElement.nodeName !== 'TEXTAREA') {
throw "[contentElement] is not a TEXTAREA";
}
xq.asEventSource(this, "Editor", ["StartInitialization", "Initialized", "ElementChanged", "BeforeEvent", "AfterEvent", "CurrentContentChanged", "StaticContentChanged", "CurrentEditModeChanged"]);
/**
* Editor's configuration.
* @type object
*/
this.config = {};
/**
* Automatically gives initial focus.
* @type boolean
*/
this.config.autoFocusOnInit = false;
/**
* Makes links clickable.
* @type boolean
*/
this.config.enableLinkClick = false;
/**
* Changes mouse cursor to pointer when the cursor is on a link.
* @type boolean
*/
this.config.changeCursorOnLink = false;
/**
* Generates default toolbar if there's no toolbar provided.
* @type boolean
*/
this.config.generateDefaultToolbar = true;
this.config.defaultToolbarButtonGroups = {
"color": [
{className:"foregroundColor", title:"Foreground color", handler:"xed.handleForegroundColor()"},
{className:"backgroundColor", title:"Background color", handler:"xed.handleBackgroundColor()"}
],
"font": [
{className:"fontFace", title:"Font face", list:[
{title:"Arial", handler:"xed.handleFontFace('Arial')"},
{title:"Helvetica", handler:"xed.handleFontFace('Helvetica')"},
{title:"Serif", handler:"xed.handleFontFace('Serif')"},
{title:"Tahoma", handler:"xed.handleFontFace('Tahoma')"},
{title:"Verdana", handler:"xed.handleFontFace('Verdana')"}
]},
{className:"fontSize", title:"Font size", list:[
{title:"1", handler:"xed.handleFontSize('1')"},
{title:"2", handler:"xed.handleFontSize('2')"},
{title:"3", handler:"xed.handleFontSize('3')"},
{title:"4", handler:"xed.handleFontSize('4')"},
{title:"5", handler:"xed.handleFontSize('5')"},
{title:"6", handler:"xed.handleFontSize('6')"}
]}
],
"link": [
{className:"link", title:"Link", handler:"xed.handleLink()"},
{className:"removeLink", title:"Remove link", handler:"xed.handleRemoveLink()"}
],
"style": [
{className:"strongEmphasis", title:"Strong emphasis", handler:"xed.handleStrongEmphasis()"},
{className:"emphasis", title:"Emphasis", handler:"xed.handleEmphasis()"},
{className:"underline", title:"Underline", handler:"xed.handleUnderline()"},
{className:"strike", title:"Strike", handler:"xed.handleStrike()"},
{className:"superscription", title:"Superscription", handler:"xed.handleSuperscription()"},
{className:"subscription", title:"Subscription", handler:"xed.handleSubscription()"},
{className:"removeFormat", title:"Remove format", handler:"xed.handleRemoveFormat()"}
],
"justification": [
{className:"justifyLeft", title:"Justify left", handler:"xed.handleJustify('left')"},
{className:"justifyCenter", title:"Justify center", handler:"xed.handleJustify('center')"},
{className:"justifyRight", title:"Justify right", handler:"xed.handleJustify('right')"},
{className:"justifyBoth", title:"Justify both", handler:"xed.handleJustify('both')"}
],
"indentation": [
{className:"indent", title:"Indent", handler:"xed.handleIndent()"},
{className:"outdent", title:"Outdent", handler:"xed.handleOutdent()"}
],
"block": [
{className:"paragraph", title:"Paragraph", handler:"xed.handleApplyBlock('P')"},
{className:"heading1", title:"Heading 1", handler:"xed.handleApplyBlock('H1')"},
{className:"blockquote", title:"Blockquote", handler:"xed.handleApplyBlock('BLOCKQUOTE')"},
{className:"code", title:"Code", handler:"xed.handleList('OL', 'code')"},
{className:"division", title:"Division", handler:"xed.handleApplyBlock('DIV')"},
{className:"unorderedList", title:"Unordered list", handler:"xed.handleList('UL')"},
{className:"orderedList", title:"Ordered list", handler:"xed.handleList('OL')"}
],
"insert": [
{className:"table", title:"Table", handler:"xed.handleTable(4, 4,'tl')"},
{className:"separator", title:"Separator", handler:"xed.handleSeparator()"}
]
};
/**
* Button map for default toolbar
* @type Object
*/
this.config.defaultToolbarButtonMap = [
this.config.defaultToolbarButtonGroups.color,
this.config.defaultToolbarButtonGroups.font,
this.config.defaultToolbarButtonGroups.link,
this.config.defaultToolbarButtonGroups.style,
this.config.defaultToolbarButtonGroups.justification,
this.config.defaultToolbarButtonGroups.indentation,
this.config.defaultToolbarButtonGroups.block,
this.config.defaultToolbarButtonGroups.insert,
[
{className:"html", title:"Edit source", handler:"xed.toggleSourceAndWysiwygMode()"}
],
[
{className:"undo", title:"Undo", handler:"xed.handleUndo()"},
{className:"redo", title:"Redo", handler:"xed.handleRedo()"}
]
];
/**
* Image path for default toolbar.
* @type String
*/
this.config.imagePathForDefaultToolbar = '../images/toolbar/';
/**
* Image path for content.
* @type String
*/
this.config.imagePathForContent = '../images/content/';
/**
* Widget Container path.
* @type String
*/
this.config.widgetContainerPath = 'widget_container.html';
/**
* Array of URL containig CSS for WYSIWYG area.
* @type Array
*/
this.config.contentCssList = ['../stylesheets/xq_contents.css'];
/**
* URL Validation mode. One or "relative", "host_relative", "absolute", "browser_default"
* @type String
*/
this.config.urlValidationMode = 'absolute';
/**
* Turns off validation in source editor.<br />
* Note that the validation will be performed regardless of this value when you switching edit mode.
* @type boolean
*/
this.config.noValidationInSourceEditMode = false;
/**
* Automatically hooks onsubmit event.
* @type boolean
*/
this.config.automaticallyHookSubmitEvent = true;
/**
* Set of whitelist(tag name and attributes) for use in validator
* @type Object
*/
this.config.whitelist = xq.predefinedWhitelist;
/**
* Specifies a value of ID attribute for WYSIWYG document's body
* @type String
*/
this.config.bodyId = "";
/**
* Specifies a value of CLASS attribute for WYSIWYG document's body
* @type String
*/
this.config.bodyClass = "xed";
/**
* Plugins
* @type Object
*/
this.config.plugins = {};
/**
* Shortcuts
* @type Object
*/
this.config.shortcuts = {};
/**
* Autocorrections
* @type Object
*/
this.config.autocorrections = {};
/**
* Autocompletions
* @type Object
*/
this.config.autocompletions = {};
/**
* Template processors
* @type Object
*/
this.config.templateProcessors = {};
/**
* Context menu handlers
* @type Object
*/
this.config.contextMenuHandlers = {};
/**
* Original content element
* @type Element
*/
this.contentElement = contentElement;
/**
* Owner document of content element
* @type Document
*/
this.doc = this.contentElement.ownerDocument;
/**
* Body of content element
* @type Element
*/
this.body = this.doc.body;
/**
* False or 'source' means source editing mode, true or 'wysiwyg' means WYSIWYG editing mode.
* @type Object
*/
this.currentEditMode = '';
/**
* Timer
* @type xq.Timer
*/
this.timer = new xq.Timer(100);
/**
* Base instance
* @type xq.rdom.Base
*/
this.rdom = xq.rdom.Base.createInstance();
/**
* Base instance
* @type xq.validator.Base
*/
this.validator = null;
/**
* Outmost wrapper div
* @type Element
*/
this.outmostWrapper = null;
/**
* Source editor container
* @type Element
*/
this.sourceEditorDiv = null;
/**
* Source editor textarea
* @type Element
*/
this.sourceEditorTextarea = null;
/**
* WYSIWYG editor container
* @type Element
*/
this.wysiwygEditorDiv = null;
/**
* Outer frame
* @type IFrame
*/
this.outerFrame = null;
/**
* Design mode iframe
* @type IFrame
*/
this.editorFrame = null;
this.toolbarContainer = toolbarContainer;
/**
* Toolbar container
* @type Element
*/
this.toolbar = null;
/**
* Undo/redo manager
* @type xq.EditHistory
*/
this.editHistory = null;
/**
* Context menu container
* @type Element
*/
this.contextMenuContainer = null;
/**
* Context menu items
* @type Array
*/
this.contextMenuItems = null;
/**
* Platform dependent key event type
* @type String
*/
this.platformDepedentKeyEventType = (xq.Browser.isMac && xq.Browser.isGecko ? "keypress" : "keydown");
this.addShortcuts(this.getDefaultShortcuts());
this.addListener({
onEditorCurrentContentChanged: function(xed) {
var curFocusElement = xed.rdom.getCurrentElement();
if(!curFocusElement || curFocusElement.ownerDocument !== xed.rdom.getDoc()) {
return;
}
if(xed.lastFocusElement !== curFocusElement) {
if(!xed.rdom.tree.isBlockOnlyContainer(xed.lastFocusElement) && xed.rdom.tree.isBlock(xed.lastFocusElement)) {
xed.rdom.removeTrailingWhitespace(xed.lastFocusElement);
}
xed._fireOnElementChanged(xed, xed.lastFocusElement, curFocusElement);
xed.lastFocusElement = curFocusElement;
}
xed.toolbar.triggerUpdate();
}
});
},
finalize: function() {
for(var key in this.config.plugins) this.config.plugins[key].unload();
},
/////////////////////////////////////////////
// Configuration Management
getDefaultShortcuts: function() {
if(xq.Browser.isMac) {
// Mac FF & Safari
return [
{event:"Ctrl+Shift+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
{event:"SPACE", handler:"this.handleSpace()"},
{event:"ENTER", handler:"this.handleEnter(false, false)"},
{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
{event:"TAB", handler:"this.handleTab()"},
{event:"Shift+TAB", handler:"this.handleShiftTab()"},
{event:"DELETE", handler:"this.handleDelete()"},
{event:"BACKSPACE", handler:"this.handleBackspace()"},
{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
{event:"Meta+B", handler:"this.handleStrongEmphasis()"},
{event:"Ctrl+I", handler:"this.handleEmphasis()"},
{event:"Meta+I", handler:"this.handleEmphasis()"},
{event:"Ctrl+U", handler:"this.handleUnderline()"},
{event:"Meta+U", handler:"this.handleUnderline()"},
{event:"Ctrl+K", handler:"this.handleStrike()"},
{event:"Meta+K", handler:"this.handleStrike()"},
{event:"Meta+Z", handler:"this.handleUndo()"},
{event:"Meta+Shift+Z", handler:"this.handleRedo()"},
{event:"Meta+Y", handler:"this.handleRedo()"}
];
} else if(xq.Browser.isUbuntu) {
// Ubunto FF
return [
{event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
{event:"SPACE", handler:"this.handleSpace()"},
{event:"ENTER", handler:"this.handleEnter(false, false)"},
{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
{event:"TAB", handler:"this.handleTab()"},
{event:"Shift+TAB", handler:"this.handleShiftTab()"},
{event:"DELETE", handler:"this.handleDelete()"},
{event:"BACKSPACE", handler:"this.handleBackspace()"},
{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
{event:"Ctrl+I", handler:"this.handleEmphasis()"},
{event:"Ctrl+U", handler:"this.handleUnderline()"},
{event:"Ctrl+K", handler:"this.handleStrike()"},
{event:"Ctrl+Z", handler:"this.handleUndo()"},
{event:"Ctrl+Shift+Z", handler:"this.handleRedo()"},
{event:"Ctrl+Y", handler:"this.handleRedo()"}
];
} else {
// Win IE & FF
return [
{event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
{event:"SPACE", handler:"this.handleSpace()"},
{event:"ENTER", handler:"this.handleEnter(false, false)"},
{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
{event:"TAB", handler:"this.handleTab()"},
{event:"Shift+TAB", handler:"this.handleShiftTab()"},
{event:"DELETE", handler:"this.handleDelete()"},
{event:"BACKSPACE", handler:"this.handleBackspace()"},
{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
{event:"Ctrl+I", handler:"this.handleEmphasis()"},
{event:"Ctrl+U", handler:"this.handleUnderline()"},
{event:"Ctrl+K", handler:"this.handleStrike()"},
{event:"Ctrl+Z", handler:"this.handleUndo()"},
{event:"Ctrl+Shift+Z", handler:"this.handleRedo()"},
{event:"Ctrl+Y", handler:"this.handleRedo()"}
];
}
},
/**
* Adds or replaces plugin.
*
* @param {String} id unique identifier
*/
addPlugin: function(id) {
// already added?
if(this.config.plugins[id]) return;
// else
var clazz = xq.plugin[id + "Plugin"];
if(!clazz) throw "Unknown plugin id: [" + id + "]";
var plugin = new clazz();
this.config.plugins[id] = plugin;
plugin.load(this);
},
/**
* Adds several plugins at once.
*
* @param {Array} list of plugin ids.
*/
addPlugins: function(list) {
for(var i = 0; i < list.length; i++) {
this.addPlugin(list[i]);
}
},
/**
* Returns plugin matches with given identifier.
*
* @param {String} id unique identifier
*/
getPlugin: function(id) {return this.config.plugins[id];},
/**
* Returns entire plugins
*/
getPlugins: function() {return this.config.plugins;},
/**
* Remove plugin matches with given identifier.
*
* @param {String} id unique identifier
*/
removePlugin: function(id) {
var plugin = this.config.shortcuts[id];
if(plugin) {
plugin.unload();
}
delete this.config.shortcuts[id];
},
/**
* Adds or replaces keyboard shortcut.
*
* @param {String} shortcut keymap expression like "CTRL+Space"
* @param {Object} handler string or function to be evaluated or called
*/
addShortcut: function(shortcut, handler) {
this.config.shortcuts[shortcut] = {"event":new xq.Shortcut(shortcut), "handler":handler};
},
/**
* Adds several keyboard shortcuts at once.
*
* @param {Array} list of shortcuts. each element should have following structure: {event:"keymap expression", handler:handler}
*/
addShortcuts: function(list) {
for(var i = 0; i < list.length; i++) {
this.addShortcut(list[i].event, list[i].handler);
}
},
/**
* Returns keyboard shortcut matches with given keymap expression.
*
* @param {String} shortcut keymap expression like "CTRL+Space"
*/
getShortcut: function(shortcut) {return this.config.shortcuts[shortcut];},
/**
* Returns entire keyboard shortcuts' map
*/
getShortcuts: function() {return this.config.shortcuts;},
/**
* Remove keyboard shortcut matches with given keymap expression.
*
* @param {String} shortcut keymap expression like "CTRL+Space"
*/
removeShortcut: function(shortcut) {delete this.config.shortcuts[shortcut];},
/**
* Adds or replaces autocorrection handler.
*
* @param {String} id unique identifier
* @param {Object} criteria regex pattern or function to be used as a criterion for match
* @param {Object} handler string or function to be evaluated or called when criteria met
*/
addAutocorrection: function(id, criteria, handler) {
if(criteria.exec) {
var pattern = criteria;
criteria = function(text) {return text.match(pattern)};
}
this.config.autocorrections[id] = {"criteria":criteria, "handler":handler};
},
/**
* Adds several autocorrection handlers at once.
*
* @param {Array} list of autocorrection. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
*/
addAutocorrections: function(list) {
for(var i = 0; i < list.length; i++) {
this.addAutocorrection(list[i].id, list[i].criteria, list[i].handler);
}
},
/**
* Returns autocorrection handler matches with given id
*
* @param {String} id unique identifier
*/
getAutocorrection: function(id) {return this.config.autocorrection[id];},
/**
* Returns entire autocorrections' map
*/
getAutocorrections: function() {return this.config.autocorrections;},
/**
* Removes autocorrection handler matches with given id
*
* @param {String} id unique identifier
*/
removeAutocorrection: function(id) {delete this.config.autocorrections[id];},
/**
* Adds or replaces autocompletion handler.
*
* @param {String} id unique identifier
* @param {Object} criteria regex pattern or function to be used as a criterion for match
* @param {Object} handler string or function to be evaluated or called when criteria met
*/
addAutocompletion: function(id, criteria, handler) {
if(criteria.exec) {
var pattern = criteria;
criteria = function(text) {
var m = pattern.exec(text);
return m ? m.index : -1;
};
}
this.config.autocompletions[id] = {"criteria":criteria, "handler":handler};
},
/**
* Adds several autocompletion handlers at once.
*
* @param {Array} list of autocompletion. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
*/
addAutocompletions: function(list) {
for(var i = 0; i < list.length; i++) {
this.addAutocompletion(list[i].id, list[i].criteria, list[i].handler);
}
},
/**
* Returns autocompletion handler matches with given id
*
* @param {String} id unique identifier
*/
getAutocompletion: function(id) {return this.config.autocompletions[id];},
/**
* Returns entire autocompletions' map
*/
getAutocompletions: function() {return this.config.autocompletions;},
/**
* Removes autocompletion handler matches with given id
*
* @param {String} id unique identifier
*/
removeAutocompletion: function(id) {delete this.config.autocompletions[id];},
/**
* Adds or replaces template processor.
*
* @param {String} id unique identifier
* @param {Object} handler string or function to be evaluated or called when template inserted
*/
addTemplateProcessor: function(id, handler) {
this.config.templateProcessors[id] = {"handler":handler};
},
/**
* Adds several template processors at once.
*
* @param {Array} list of template processors. Each element should have following structure: {id:"identifier", handler:handler}
*/
addTemplateProcessors: function(list) {
for(var i = 0; i < list.length; i++) {
this.addTemplateProcessor(list[i].id, list[i].handler);
}
},
/**
* Returns template processor matches with given id
*
* @param {String} id unique identifier
*/
getTemplateProcessor: function(id) {return this.config.templateProcessors[id];},
/**
* Returns entire template processors' map
*/
getTemplateProcessors: function() {return this.config.templateProcessors;},
/**
* Removes template processor matches with given id
*
* @param {String} id unique identifier
*/
removeTemplateProcessor: function(id) {delete this.config.templateProcessors[id];},
/**
* Adds or replaces context menu handler.
*
* @param {String} id unique identifier
* @param {Object} handler string or function to be evaluated or called when onContextMenu occured
*/
addContextMenuHandler: function(id, handler) {
this.config.contextMenuHandlers[id] = {"handler":handler};
},
/**
* Adds several context menu handlers at once.
*
* @param {Array} list of handlers. Each element should have following structure: {id:"identifier", handler:handler}
*/
addContextMenuHandlers: function(list) {
for(var i = 0; i < list.length; i++) {
this.addContextMenuHandler(list[i].id, list[i].handler);
}
},
/**
* Returns context menu handler matches with given id
*
* @param {String} id unique identifier
*/
getContextMenuHandler: function(id) {return this.config.contextMenuHandlers[id];},
/**
* Returns entire context menu handlers' map
*/
getContextMenuHandlers: function() {return this.config.contextMenuHandlers;},
/**
* Removes context menu handler matches with given id
*
* @param {String} id unique identifier
*/
removeContextMenuHandler: function(id) {delete this.config.contextMenuHandlers[id];},
/**
* Sets width of editor.
*
* @param {String} w Valid CSS value for style.width. For example, "100%", "200px".
*/
setWidth: function(w) {
this.outmostWrapper.style.width = w;
},
/**
* Sets height of editor.
*
* @param {String} h Valid CSS value for style.height. For example, "100%", "200px".
*/
setHeight: function(h) {
this.wysiwygEditorDiv.style.height = h;
this.sourceEditorDiv.style.height = h;
},
/////////////////////////////////////////////
// Edit mode management
/**
* Returns current edit mode - wysiwyg, source
*/
getCurrentEditMode: function() {
return this.currentEditMode;
},
/**
* Toggle edit mode between source and wysiwyg
*/
toggleSourceAndWysiwygMode: function() {
var mode = this.getCurrentEditMode();
this.setEditMode(mode === 'wysiwyg' ? 'source' : 'wysiwyg');
},
/**
* Switches between WYSIWYG/Source mode.
*
* @param {String} mode 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
*/
setEditMode: function(mode) {
if(typeof mode !== 'string') throw "[mode] is not a string."
if(['wysiwyg', 'source'].indexOf(mode) === -1) throw "Illegal [mode] value: '" + mode + "'. Use 'wysiwyg' or 'source'";
if(this.currentEditMode === mode) return;
// create editor frame if there's no editor frame.
var editorCreated = !!this.outmostWrapper;
if(!editorCreated) {
// create validator
this.validator = xq.validator.Base.createInstance(
this.doc.location.href,
this.config.urlValidationMode,
this.config.whitelist
);
this._fireOnStartInitialization(this);
this._createEditorFrame(mode);
var temp = window.setInterval(function() {
// wait for loading
if(this.getBody()) {
window.clearInterval(temp);
// @WORKAROUND: it is needed to fix IE6 horizontal scrollbar problem
if(xq.Browser.isIE6) {
this.rdom.getDoc().documentElement.style.overflowY='auto';
this.rdom.getDoc().documentElement.style.overflowX='hidden';
}
this.setEditMode(mode);
if(this.config.autoFocusOnInit) this.focus();
this.timer.start();
this._fireOnInitialized(this);
}
}.bind(this), 10);
return;
}
// switch mode
if(mode === 'wysiwyg') {
this._setEditModeToWysiwyg();
} else { // mode === 'source'
this._setEditModeToSource();
}
// fire event
var oldEditMode = this.currentEditMode;
this.currentEditMode = mode;
this._fireOnCurrentEditModeChanged(this, oldEditMode, this.currentEditMode);
},
_setEditModeToWysiwyg: function() {
// Turn off static content and source editor
this.contentElement.style.display = "none";
this.sourceEditorDiv.style.display = "none";
// Update contents
if(this.currentEditMode === 'source') {
// get html from source editor
var html = this.getSourceContent(true);
// invalidate it and load it into wysiwyg editor
var invalidHtml = this.validator.invalidate(html);
invalidHtml = this.removeUnnecessarySpaces(invalidHtml);
if(invalidHtml.isBlank()) {
this.rdom.clearRoot();
} else {
this.rdom.getRoot().innerHTML = invalidHtml;
this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
}
} else {
// invalidate static html and load it into wysiwyg editor
var invalidHtml = this.validator.invalidate(this.getStaticContent());
invalidHtml = this.removeUnnecessarySpaces(invalidHtml);
if(invalidHtml.isBlank()) {
this.rdom.clearRoot();
} else {
this.rdom.getRoot().innerHTML = invalidHtml;
this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
}
}
// Turn on wysiwyg editor
this.wysiwygEditorDiv.style.display = "block";
this.outmostWrapper.style.display = "block";
// Without this, xq.rdom.Base.focus() doesn't work correctly.
if(xq.Browser.isGecko) this.rdom.placeCaretAtStartOf(this.rdom.getRoot());
if(this.toolbar) this.toolbar.enableButtons();
},
_setEditModeToSource: function() {
// Update contents
var validHtml = null;
if(this.currentEditMode === 'wysiwyg') {
validHtml = this.getWysiwygContent();
} else {
validHtml = this.getStaticContent();
}
this.sourceEditorTextarea.value = validHtml
// Turn off static content and wysiwyg editor
this.contentElement.style.display = "none";
this.wysiwygEditorDiv.style.display = "none";
// Turn on source editor
this.sourceEditorDiv.style.display = "block";
this.outmostWrapper.style.display = "block";
if(this.toolbar) this.toolbar.disableButtons(['html']);
},
/**
* Load CSS into WYSIWYG mode document
*
* @param {string} path URL
*/
loadStylesheet: function(path) {
var head = this.getDoc().getElementsByTagName("HEAD")[0];
var link = this.getDoc().createElement("LINK");
link.rel = "Stylesheet";
link.type = "text/css";
link.href = path;
head.appendChild(link);
},
/**
* Sets editor's dynamic content from static content
*/
loadCurrentContentFromStaticContent: function() {
if(this.getCurrentEditMode() == 'wysiwyg') {
// update WYSIWYG editor
var html = this.validator.invalidate(this.getStaticContent());
html = this.removeUnnecessarySpaces(html);
if(html.isBlank()) {
this.rdom.clearRoot();
} else {
this.rdom.getRoot().innerHTML = html;
this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
}
} else { // 'source'
this.sourceEditorTextarea.value = this.getStaticContent();
}
this._fireOnCurrentContentChanged(this);
},
/**
* Removes unnecessary spaces, tabs and new lines.
*
* @param {String} html HTML string.
* @returns {String} Modified HTML string.
*/
removeUnnecessarySpaces: function(html) {
var blocks = this.rdom.tree.getBlockTags().join("|");
var regex = new RegExp("\\s*<(/?)(" + blocks + ")>\\s*", "img");
return html.replace(regex, '<$1$2>');
},
/**
* Gets editor's dynamic content from current editor(source or WYSIWYG)
*
* @return {Object} HTML String
*/
getCurrentContent: function() {
if(this.getCurrentEditMode() === 'source') {
return this.getSourceContent(this.config.noValidationInSourceEditMode);
} else {
return this.getWysiwygContent();
}
},
/**
* Gets editor's dynamic content from WYSIWYG editor
*
* @return {Object} HTML String
*/
getWysiwygContent: function() {
return this.validator.validate(this.rdom.getRoot());
},
/**
* Gets editor's dynamic content from source editor
*
* @return {Object} HTML String
*/
getSourceContent: function(noValidation) {
var raw = this.sourceEditorTextarea.value;
if(noValidation) return raw;
var tempDiv = document.createElement('div');
tempDiv.innerHTML = this.removeUnnecessarySpaces(raw);
var rdom = xq.rdom.Base.createInstance();
rdom.wrapAllInlineOrTextNodesAs("P", tempDiv, true);
return this.validator.validate(tempDiv, true);
},
/**
* Sets editor's original content
*
* @param {Object} content HTML String
*/
setStaticContent: function(content) {
this.contentElement.value = content;
this._fireOnStaticContentChanged(this, content);
},
/**
* Gets editor's original content
*
* @return {Object} HTML String
*/
getStaticContent: function() {
return this.contentElement.value;
},
/**
* Gets editor's original content as (newely created) DOM node
*
* @return {Element} DIV element
*/
getStaticContentAsDOM: function() {
var div = this.doc.createElement('DIV');
div.innerHTML = this.contentElement.value;
return div;
},
/**
* Gives focus to editor
*/
focus: function() {
if(this.getCurrentEditMode() === 'wysiwyg') {
this.rdom.focus();
if(this.toolbar) this.toolbar.triggerUpdate();
} else if(this.getCurrentEditMode() === 'source') {
this.sourceEditorTextarea.focus();
}
},
getWysiwygEditorDiv: function() {
return this.wysiwygEditorDiv;
},
getSourceEditorDiv: function() {
return this.sourceEditorDiv;
},
/**
* Returns outer iframe object
*/
getOuterFrame: function() {
return this.outerFrame;
},
/**
* Returns outer iframe document
*/
getOuterDoc: function() {
return this.outerFrame.contentWindow.document;
},
/**
* Returns designmode iframe object
*/
getFrame: function() {
return this.editorFrame;
},
/**
* Returns designmode window object
*/
getWin: function() {
return this.rdom.getWin();
},
/**
* Returns designmode document object
*/
getDoc: function() {
return this.rdom.getDoc();
},
/**
* Returns designmode body object
*/
getBody: function() {
return this.rdom.getRoot();
},
/**
* Returns outmost wrapper element
*/
getOutmostWrapper: function() {
return this.outmostWrapper;
},
_createIFrame: function(doc, width, height) {
var frame = doc.createElement("iframe");
// IE displays warning when a protocol is HTTPS, because IE6 treats IFRAME
// without SRC attribute as insecure.
if(xq.Browser.isIE) frame.src = 'javascript:""';
frame.style.width = width || "100%";
frame.style.height = height || "100%";
frame.setAttribute("frameBorder", "0");
frame.setAttribute("marginWidth", "0");
frame.setAttribute("marginHeight", "0");
frame.setAttribute("allowTransparency", "auto");
return frame;
},
_createDoc: function(frame, head, cssList, bodyId, bodyClass, body) {
var sb = [];
if(!xq.Browser.isTrident) {
// @WORKAROUND: IE6/7 has caret movement and scrolling problem if I include following DTD.
sb.push('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">');
}
sb.push('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">');
sb.push('<head>');
sb.push('<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />');
if(head) sb.push(head);
if(cssList) for(var i = 0; i < cssList.length; i++) {
sb.push('<link rel="Stylesheet" type="text/css" href="' + cssList[i] + '" />');
}
sb.push('</head>');
sb.push('<body ' + (bodyClass ? 'class="' + bodyClass + '"' : '') + ' ' + (bodyId ? 'id="' + bodyId + '"' : '') + '>');
if(body) sb.push(body);
sb.push('</body>');
sb.push('</html>');
var doc = frame.contentWindow.document;
doc.open();
doc.write(sb.join(""));
doc.close();
return doc;
},
_createEditorFrame: function(mode) {
// turn off static content
this.contentElement.style.display = "none";
// create outer DIV
this.outmostWrapper = this.doc.createElement('div');
this.outmostWrapper.className = "xquared";
this.contentElement.parentNode.insertBefore(this.outmostWrapper, this.contentElement);
// create toolbar
if(this.toolbarContainer || this.config.generateDefaultToolbar) {
this.toolbar = new xq.ui.Toolbar(
this,
this.toolbarContainer,
this.outmostWrapper,
this.config.defaultToolbarButtonMap,
this.config.imagePathForDefaultToolbar,
function() {
var element = this.getCurrentEditMode() === 'wysiwyg' ? this.lastFocusElement : null;
return element && element.nodeName != "BODY" ? this.rdom.collectStructureAndStyle(element) : null;
}.bind(this)
);
}
// create source editor div
this.sourceEditorDiv = this.doc.createElement('div');
this.sourceEditorDiv.className = "editor source_editor"; //TODO: remove editor
this.sourceEditorDiv.style.display = "none";
this.outmostWrapper.appendChild(this.sourceEditorDiv);
// create TEXTAREA for source editor
this.sourceEditorTextarea = this.doc.createElement('textarea');
this.sourceEditorDiv.appendChild(this.sourceEditorTextarea);
// create WYSIWYG editor div
this.wysiwygEditorDiv = this.doc.createElement('div');
this.wysiwygEditorDiv.className = "editor wysiwyg_editor"; //TODO: remove editor
this.outmostWrapper.appendChild(this.wysiwygEditorDiv);
// create outer iframe for WYSIWYG editor
this.outerFrame = this._createIFrame(document);
this.wysiwygEditorDiv.appendChild(this.outerFrame);
var outerDoc = this._createDoc(
this.outerFrame,
'<style type="text/css">html, body {margin:0px; padding:0px; background-color: transparent; width: 100%; height: 100%; overflow: hidden;}</style>'
);
// create designmode iframe for WYSIWYG editor
this.editorFrame = this._createIFrame(outerDoc);
outerDoc.body.appendChild(this.editorFrame);
var editorDoc = this._createDoc(
this.editorFrame,
'<style type="text/css">html, body {margin:0px; padding:0px; background-color: transparent;}</style>' +
(!xq.Browser.isTrident ? '<base href="./" />' : '') + // @WORKAROUND: it is needed to force href of pasted content to be an absolute url
(this.config.changeCursorOnLink ? '<style>.xed a {cursor: pointer !important;}</style>' : ''),
this.config.contentCssList,
this.config.bodyId,
this.config.bodyClass,
''
);
this.rdom.setWin(this.editorFrame.contentWindow);
this.editHistory = new xq.EditHistory(this.rdom);
// turn on designmode
this.rdom.getDoc().designMode = "On";
// turn off Firefox's table editing feature
if(xq.Browser.isGecko) {
try {this.rdom.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {}
}
// register event handlers
this._registerEventHandlers();
// hook onsubmit of form
if(this.config.automaticallyHookSubmitEvent && this.contentElement.form) {
var original = this.contentElement.form.onsubmit;
this.contentElement.form.onsubmit = function() {
this.contentElement.value = this.getCurrentContent();
return original ? original.bind(this.contentElement.form)() : true;
}.bind(this);
}
},
/////////////////////////////////////////////
// Event Management
_registerEventHandlers: function() {
var events = [this.platformDepedentKeyEventType, 'click', 'keyup', 'mouseup', 'contextmenu'];
if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove');
var handler = this._handleEvent.bindAsEventListener(this);
for(var i = 0; i < events.length; i++) {
xq.observe(this.getDoc(), events[i], handler);
}
if(xq.Browser.isGecko) {
xq.observe(this.getDoc(), "focus", handler);
xq.observe(this.getDoc(), "blur", handler);
xq.observe(this.getDoc(), "scroll", handler);
xq.observe(this.getDoc(), "dragdrop", handler);
} else {
xq.observe(this.getWin(), "focus", handler);
xq.observe(this.getWin(), "blur", handler);
xq.observe(this.getWin(), "scroll", handler);
}
},
_handleEvent: function(e) {
this._fireOnBeforeEvent(this, e);
if(e.stopProcess) {
xq.stopEvent(e);
return false;
}
// Trident only
if(e.type === 'mousemove') {
if(!this.config.changeCursorOnLink) return true;
var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]);
var editable = this.getBody().contentEditable;
editable = editable === 'inherit' ? false : editable;
if(editable !== link && !this.rdom.hasSelection()) this.getBody().contentEditable = !link;
return true;
}
var stop = false;
var modifiedByCorrection = false;
if(e.type === this.platformDepedentKeyEventType) {
var undoPerformed = false;
modifiedByCorrection = this.rdom.correctParagraph();
for(var key in this.config.shortcuts) {
if(!this.config.shortcuts[key].event.matches(e)) continue;
var handler = this.config.shortcuts[key].handler;
var xed = this;
stop = (typeof handler === "function") ? handler(this) : eval(handler);
if(key === "undo") undoPerformed = true;
}
} else if(e.type === 'click' && e.button === 0 && this.config.enableLinkClick) {
var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]);
if(a) stop = this.handleClick(e, a);
} else if(["keyup", "mouseup"].indexOf(e.type) !== -1) {
modifiedByCorrection = this.rdom.correctParagraph();
} else if(["contextmenu"].indexOf(e.type) !== -1) {
this._handleContextMenu(e);
} else if("focus" == e.type) {
this.rdom.focused = true;
} else if("blur" == e.type) {
this.rdom.focused = false;
}
if(stop) xq.stopEvent(e);
this._fireOnCurrentContentChanged(this);
this._fireOnAfterEvent(this, e);
if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e);
return !stop;
},
/**
* TODO: remove dup with handleAutocompletion
*/
handleAutocorrection: function() {
var block = this.rdom.getCurrentBlockElement();
// TODO: use complete unescape algorithm
var text = this.rdom.getInnerText(block).replace(/&nbsp;/gi, " ");
var acs = this.config.autocorrections;
var performed = false;
var stop = false;
for(var key in acs) {
var ac = acs[key];
if(ac.criteria(text)) {
try {
this.editHistory.onCommand();
this.editHistory.disable();
if(typeof ac.handler === "String") {
var xed = this;
var rdom = this.rdom;
eval(ac.handler);
} else {
stop = ac.handler(this, this.rdom, block, text);
}
this.editHistory.enable();
} catch(ignored) {}
block = this.rdom.getCurrentBlockElement();
text = this.rdom.getInnerText(block);
performed = true;
if(stop) break;
}
}
return stop;
},
/**
* TODO: remove dup with handleAutocorrection
*/
handleAutocompletion: function() {
var acs = this.config.autocompletions;
if(xq.isEmptyHash(acs)) return;
if(this.rdom.hasSelection()) {
var text = this.rdom.getSelectionAsText();
this.rdom.deleteSelection();
var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN"));
wrapper.innerHTML = text;
var marker = this.rdom.pushMarker();
var filtered = [];
for(var key in acs) {
filtered.push([key, acs[key].criteria(text)]);
}
filtered = filtered.findAll(function(elem) {
return elem[1] !== -1;
});
if(filtered.length === 0) {
this.rdom.popMarker(true);
return;
}
var minIndex = 0;
var min = filtered[0][1];
for(var i = 0; i < filtered.length; i++) {
if(filtered[i][1] < min) {
minIndex = i;
min = filtered[i][1];
}
}
var ac = acs[filtered[minIndex][0]];
this.editHistory.disable();
this.rdom.selectElement(wrapper);
} else {
var marker = this.rdom.pushMarker();
var filtered = [];
for(var key in acs) {
filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]);
}
filtered = filtered.findAll(function(elem) {
return elem[1] !== -1;
});
if(filtered.length === 0) {
this.rdom.popMarker(true);
return;
}
var minIndex = 0;
var min = filtered[0][1];
for(var i = 0; i < filtered.length; i++) {
if(filtered[i][1] < min) {
minIndex = i;
min = filtered[i][1];
}
}
var ac = acs[filtered[minIndex][0]];
this.editHistory.disable();
var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria);
}
var block = this.rdom.getCurrentBlockElement();
// TODO: use complete unescape algorithm
var text = this.rdom.getInnerText(wrapper).replace(/&nbsp;/gi, " ");
try {
// call handler
if(typeof ac.handler === "String") {
var xed = this;
var rdom = this.rdom;
eval(ac.handler);
} else {
ac.handler(this, this.rdom, block, wrapper, text);
}
} catch(ignored) {}
try {
this.rdom.unwrapElement(wrapper);
} catch(ignored) {}
if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block);
this.editHistory.enable();
this.editHistory.onCommand();
this.rdom.popMarker(true);
},
/**
* Handles click event
*
* @param {Event} e click event
* @param {Element} target target element(usually has A tag)
*/
handleClick: function(e, target) {
var href = decodeURI(target.href);
if(!xq.Browser.isTrident) {
if(!e.ctrlKey && !e.shiftKey && e.button !== 1) {
window.location.href = href;
return true;
}
} else {
if(e.shiftKey) {
window.open(href, "_blank");
} else {
window.location.href = href;
}
return true;
}
return false;
},
/**
* Show link dialog
*
* TODO: should support modify/unlink
* TODO: Add selenium test
*/
handleLink: function() {
var text = this.rdom.getSelectionAsText() || '';
var dialog = new xq.ui.FormDialog(
this,
xq.ui_templates.basicLinkDialog,
function(dialog) {
if(text) {
dialog.form.text.value = text;
dialog.form.url.focus();
dialog.form.url.select();
}
},
function(data) {
this.focus();
if(xq.Browser.isTrident) {
var rng = this.rdom.rng();
rng.moveToBookmark(bm);
rng.select();
}
if(!data) return;
this.handleInsertLink(false, data.url, data.text, data.text);
}.bind(this)
);
if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
dialog.show({position: 'centerOfEditor'});
return true;
},
/**
* Inserts link or apply link into selected area
* @TODO Add selenium test
*
* @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible)
* @param {String} url url
* @param {String} title title of link
* @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text
*
* @returns {Element} created element
*/
handleInsertLink: function(autoSelection, url, title, text) {
if(autoSelection && !this.rdom.hasSelection()) {
var marker = this.rdom.pushMarker();
var a = this.rdom.smartWrap(marker, "A", function(text) {
var index = text.lastIndexOf(" ");
return index === -1 ? index : index + 1;
});
a.href = url;
a.title = title;
if(text) {
a.innerHTML = ""
a.appendChild(this.rdom.createTextNode(text));
} else if(!a.hasChildNodes()) {
this.rdom.deleteNode(a);
}
this.rdom.popMarker(true);
} else {
text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null);
if(!text) return;
this.rdom.deleteSelection();
var a = this.rdom.createElement('A');
a.href = url;
a.title = title;
a.appendChild(this.rdom.createTextNode(text));
this.rdom.insertNode(a);
}
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* @TODO Add selenium test
*/
handleSpace: function() {
// If it has selection, perform default action.
if(this.rdom.hasSelection()) return false;
// Trident performs URL replacing automatically
if(!xq.Browser.isTrident) {
this.replaceUrlToLink();
}
return false;
},
/**
* Called when enter key pressed.
* @TODO Add selenium test
*
* @param {boolean} skipAutocorrection if set true, skips autocorrection
* @param {boolean} forceInsertParagraph if set true, inserts paragraph
*/
handleEnter: function(skipAutocorrection, forceInsertParagraph) {
// If it has selection, perform default action.
if(this.rdom.hasSelection()) return false;
// @WORKAROUND:
// If caret is in HR, default action should be performed and
// this._handleEvent() will correct broken HTML
if(xq.Browser.isTrident && this.rdom.tree.isBlockOnlyContainer(this.rdom.getCurrentElement()) && this.rdom.recentHR) {
this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.recentHR, "before");
this.rdom.recentHR = null;
return true;
}
// Perform autocorrection
if(!skipAutocorrection && this.handleAutocorrection()) return true;
var block = this.rdom.getCurrentBlockElement();
var info = this.rdom.collectStructureAndStyle(block);
// Perform URL replacing. Trident performs URL replacing automatically
if(!xq.Browser.isTrident) {
this.replaceUrlToLink();
}
var atEmptyBlock = this.rdom.isCaretAtEmptyBlock();
var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart();
var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd());
var atEdge = atEmptyBlock || atStart || atEnd;
if(!atEdge) {
var marker = this.rdom.pushMarker();
if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) {
var parent = block.parentNode;
this.rdom.unwrapElement(block);
block = parent;
} else if(block.nodeName !== "LI" && this.rdom.tree.isBlockContainer(block)) {
block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first();
}
this.rdom.splitElementUpto(marker, block);
this.rdom.popMarker(true);
} else if(atEmptyBlock) {
this._handleEnterAtEmptyBlock();
if(!xq.Browser.isWebkit) {
if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize);
if(info.fontName) this.handleFontFace(info.fontName);
}
} else {
this._handleEnterAtEdge(atStart, forceInsertParagraph);
if(!xq.Browser.isWebkit) {
if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize);
if(info.fontName) this.handleFontFace(info.fontName);
}
}
return true;
},
/**
* Moves current block upward or downward
*
* @param {boolean} up moves current block upward
*/
handleMoveBlock: function(up) {
var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up);
if(block) {
this.rdom.selectElement(block, false);
if(this.rdom.isEmptyBlock(block)) this.rdom.collapseSelection(true);
block.scrollIntoView(false);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
}
return true;
},
/**
* Called when tab key pressed
* @TODO: Add selenium test
*/
handleTab: function() {
var hasSelection = this.rdom.hasSelection();
var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
if(hasSelection) {
this.handleIndent();
} else if (table && table.className === "datatable") {
this.handleMoveToNextCell();
} else if (this.rdom.isCaretAtBlockStart()) {
this.handleIndent();
} else {
this.handleInsertTab();
}
return true;
},
/**
* Called when shift+tab key pressed
* @TODO: Add selenium test
*/
handleShiftTab: function() {
var hasSelection = this.rdom.hasSelection();
var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
if(hasSelection) {
this.handleOutdent();
} else if (table && table.className === "datatable") {
this.handleMoveToPreviousCell();
} else {
this.handleOutdent();
}
return true;
},
/**
* Inserts three non-breaking spaces
* @TODO: Add selenium test
*/
handleInsertTab: function() {
this.rdom.insertHtml('&nbsp;');
this.rdom.insertHtml('&nbsp;');
this.rdom.insertHtml('&nbsp;');
return true;
},
/**
* Called when delete key pressed
* @TODO: Add selenium test
*/
handleDelete: function() {
if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false;
return this._handleMerge(true);
},
/**
* Called when backspace key pressed
* @TODO: Add selenium test
*/
handleBackspace: function() {
if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false;
return this._handleMerge(false);
},
_handleMerge: function(withNext) {
var block = this.rdom.getCurrentBlockElement();
if(this.rdom.isEmptyBlock(block) && !this.rdom.tree.isBlockContainer(block.nextSibling) && withNext) {
var blockToMove = this.rdom.removeBlock(block);
this.rdom.placeCaretAtStartOf(blockToMove);
blockToMove.scrollIntoView(false);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
} else {
// save caret position;
var marker = this.rdom.pushMarker();
// perform merge
var merged = this.rdom.mergeElement(block, withNext, withNext);
if(!merged && !withNext) this.rdom.extractOutElementFromParent(block);
// restore caret position
this.rdom.popMarker(true);
if(merged) this.rdom.correctEmptyElement(merged);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return !!merged;
}
},
/**
* (in table) Moves caret to the next cell
* @TODO: Add selenium test
*/
handleMoveToNextCell: function() {
this._handleMoveToCell("next");
},
/**
* (in table) Moves caret to the previous cell
* @TODO: Add selenium test
*/
handleMoveToPreviousCell: function() {
this._handleMoveToCell("prev");
},
/**
* (in table) Moves caret to the above cell
* @TODO: Add selenium test
*/
handleMoveToAboveCell: function() {
this._handleMoveToCell("above");
},
/**
* (in table) Moves caret to the below cell
* @TODO: Add selenium test
*/
handleMoveToBelowCell: function() {
this._handleMoveToCell("below");
},
_handleMoveToCell: function(dir) {
var block = this.rdom.getCurrentBlockElement();
var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]);
var table = this.rdom.getParentElementOf(cell, ["TABLE"]);
var rtable = new xq.RichTable(this.rdom, table);
var target = null;
if(["next", "prev"].indexOf(dir) !== -1) {
var toNext = dir === "next";
target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell);
} else {
var toBelow = dir === "below";
target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell);
}
if(!target) {
var finder = function(node) {return ['TD', 'TH'].indexOf(node.nodeName) === -1 && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom);
var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom);
target = (toNext || toBelow) ?
this.rdom.tree.findForward(cell, finder, exitCondition) :
this.rdom.tree.findBackward(table, finder, exitCondition);
}
if(target) this.rdom.placeCaretAtStartOf(target);
},
/**
* Applies STRONG tag
* @TODO: Add selenium test
*/
handleStrongEmphasis: function() {
this.rdom.applyStrongEmphasis();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Applies EM tag
* @TODO: Add selenium test
*/
handleEmphasis: function() {
this.rdom.applyEmphasis();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Applies EM.underline tag
* @TODO: Add selenium test
*/
handleUnderline: function() {
this.rdom.applyUnderline();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Applies SPAN.strike tag
* @TODO: Add selenium test
*/
handleStrike: function() {
this.rdom.applyStrike();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Removes all style
* @TODO: Add selenium test
*/
handleRemoveFormat: function() {
this.rdom.applyRemoveFormat();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Remove link
* @TODO: Add selenium test
*/
handleRemoveLink: function() {
this.rdom.applyRemoveLink();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Inserts table
* @TODO: Add selenium test
*
* @param {Number} cols number of columns
* @param {Number} rows number of rows
* @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left.
*/
handleTable: function(cols, rows, headerPositions) {
var cur = this.rdom.getCurrentBlockElement();
if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true;
var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions);
if(this.rdom.tree.isBlockContainer(cur)) {
var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true);
cur = wrappers.last();
}
var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after");
this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0));
if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
handleInsertNewRowAt: function(where) {
var cur = this.rdom.getCurrentBlockElement();
var tr = this.rdom.getParentElementOf(cur, ["TR"]);
if(!tr) return true;
var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
var rtable = new xq.RichTable(this.rdom, table);
var row = rtable.insertNewRowAt(tr, where);
this.rdom.placeCaretAtStartOf(row.cells[0]);
return true;
},
/**
* @TODO: Add selenium test
*/
handleInsertNewColumnAt: function(where) {
var cur = this.rdom.getCurrentBlockElement();
var td = this.rdom.getParentElementOf(cur, ["TD"], true);
if(!td) return true;
var table = this.rdom.getParentElementOf(td, ["TABLE"]);
var rtable = new xq.RichTable(this.rdom, table);
rtable.insertNewCellAt(td, where);
this.rdom.placeCaretAtStartOf(cur);
return true;
},
/**
* @TODO: Add selenium test
*/
handleDeleteRow: function() {
var cur = this.rdom.getCurrentBlockElement();
var tr = this.rdom.getParentElementOf(cur, ["TR"]);
if(!tr) return true;
var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
var rtable = new xq.RichTable(this.rdom, table);
var blockToMove = rtable.deleteRow(tr);
this.rdom.placeCaretAtStartOf(blockToMove);
return true;
},
/**
* @TODO: Add selenium test
*/
handleDeleteColumn: function() {
var cur = this.rdom.getCurrentBlockElement();
var td = this.rdom.getParentElementOf(cur, ["TD"], true);
if(!td) return true;
var table = this.rdom.getParentElementOf(td, ["TABLE"]);
var rtable = new xq.RichTable(this.rdom, table);
rtable.deleteCell(td);
//this.rdom.placeCaretAtStartOf(table);
return true;
},
/**
* Performs block indentation
* @TODO: Add selenium test
*/
handleIndent: function() {
if(this.rdom.hasSelection()) {
var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
if(blocks.first() !== blocks.last()) {
var affected = this.rdom.indentElements(blocks.first(), blocks.last());
this.rdom.selectBlocksBetween(affected.first(), affected.last());
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
}
}
var block = this.rdom.getCurrentBlockElement();
var affected = this.rdom.indentElement(block);
if(affected) {
this.rdom.placeCaretAtStartOf(affected);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
}
return true;
},
/**
* Performs block outdentation
* @TODO: Add selenium test
*/
handleOutdent: function() {
if(this.rdom.hasSelection()) {
var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
if(blocks.first() !== blocks.last()) {
var affected = this.rdom.outdentElements(blocks.first(), blocks.last());
this.rdom.selectBlocksBetween(affected.first(), affected.last());
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
}
}
var block = this.rdom.getCurrentBlockElement();
var affected = this.rdom.outdentElement(block);
if(affected) {
this.rdom.placeCaretAtStartOf(affected);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
}
return true;
},
/**
* Applies list.
* @TODO: Add selenium test
*
* @param {String} type "UL" or "OL"
* @param {String} CSS class name
*/
handleList: function(type, className) {
if(this.rdom.hasSelection()) {
var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
if(blocks.first() !== blocks.last()) {
blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type, className);
} else {
blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type, className);
}
this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
} else {
var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type, className);
this.rdom.placeCaretAtStartOf(block);
}
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Applies justification
* @TODO: Add selenium test
*
* @param {String} dir "left", "center", "right" or "both"
*/
handleJustify: function(dir) {
if(this.rdom.hasSelection()) {
var blocks = this.rdom.getSelectedBlockElements();
var dir = (dir === "left" || dir === "both") && (blocks[0].style.textAlign === "left" || blocks[0].style.textAlign === "") ? "both" : dir;
this.rdom.justifyBlocks(blocks, dir);
this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
} else {
var block = this.rdom.getCurrentBlockElement();
var dir = (dir === "left" || dir === "both") && (block.style.textAlign === "left" || block.style.textAlign === "") ? "both" : dir;
this.rdom.justifyBlock(block, dir);
}
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Removes current block element
* @TODO: Add selenium test
*/
handleRemoveBlock: function() {
var block = this.rdom.getCurrentBlockElement();
var blockToMove = this.rdom.removeBlock(block);
this.rdom.placeCaretAtStartOf(blockToMove);
blockToMove.scrollIntoView(false);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Applies background color
* @TODO: Add selenium test
*
* @param {String} color CSS color string
*/
handleBackgroundColor: function(color) {
if(color) {
this.rdom.applyBackgroundColor(color);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
} else {
var dialog = new xq.ui.FormDialog(
this,
xq.ui_templates.basicColorPickerDialog,
function(dialog) {},
function(data) {
this.focus();
if(xq.Browser.isTrident) {
var rng = this.rdom.rng();
rng.moveToBookmark(bm);
rng.select();
}
if(!data) return;
this.handleBackgroundColor(data.color);
}.bind(this)
);
if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
dialog.show({position: 'centerOfEditor'});
}
return true;
},
/**
* Applies foreground color
* @TODO: Add selenium test
*
* @param {String} color CSS color string
*/
handleForegroundColor: function(color) {
if(color) {
this.rdom.applyForegroundColor(color);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
} else {
var dialog = new xq.ui.FormDialog(
this,
xq.ui_templates.basicColorPickerDialog,
function(dialog) {},
function(data) {
this.focus();
if(xq.Browser.isTrident) {
var rng = this.rdom.rng();
rng.moveToBookmark(bm);
rng.select();
}
if(!data) return;
this.handleForegroundColor(data.color);
}.bind(this)
);
if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
dialog.show({position: 'centerOfEditor'});
}
return true;
},
/**
* Applies font face
* @TODO: Add selenium test
*
* @param {String} face font face
*/
handleFontFace: function(face) {
if(face) {
this.rdom.applyFontFace(face);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
} else {
//TODO: popup font dialog
}
return true;
},
/**
* Applies font size
*
* @param {Number} font size (1 to 6)
*/
handleFontSize: function(size) {
if(size) {
this.rdom.applyFontSize(size);
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
} else {
//TODO: popup font dialog
}
return true;
},
/**
* Applies superscription
* @TODO: Add selenium test
*/
handleSuperscription: function() {
this.rdom.applySuperscription();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Applies subscription
* @TODO: Add selenium test
*/
handleSubscription: function() {
this.rdom.applySubscription();
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Change or wrap current block(or selected blocks)'s tag
* @TODO: Add selenium test
*
* @param {String} [tagName] Name of tag. If not provided, it does not modify current tag name
* @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed.
*/
handleApplyBlock: function(tagName, className) {
if(!tagName && !className) return true;
// if current selection contains multi-blocks
if(this.rdom.hasSelection()) {
var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
if(blocks.first() !== blocks.last()) {
var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last(), className);
this.rdom.selectBlocksBetween(applied.first(), applied.last());
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
}
}
// else
var block = this.rdom.getCurrentBlockElement();
this.rdom.pushMarker();
var applied =
this.rdom.applyTagIntoElement(tagName, block, className) ||
block;
this.rdom.popMarker(true);
if(this.rdom.isEmptyBlock(applied)) {
this.rdom.correctEmptyElement(applied);
this.rdom.placeCaretAtStartOf(applied);
}
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Inserts seperator (HR)
* @TODO: Add selenium test
*/
handleSeparator: function() {
this.rdom.collapseSelection();
var curBlock = this.rdom.getCurrentBlockElement();
var atStart = this.rdom.isCaretAtBlockStart();
if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0];
this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after");
this.rdom.placeCaretAtStartOf(curBlock);
// add undo history
var historyAdded = this.editHistory.onCommand();
this._fireOnCurrentContentChanged(this);
return true;
},
/**
* Performs UNDO
* @TODO: Add selenium test
*/
handleUndo: function() {
var performed = this.editHistory.undo();
this._fireOnCurrentContentChanged(this);
var curBlock = this.rdom.getCurrentBlockElement();
if(!xq.Browser.isTrident && curBlock) {
curBlock.scrollIntoView(false);
}
return true;
},
/**
* Performs REDO
* @TODO: Add selenium test
*/
handleRedo: function() {
var performed = this.editHistory.redo();
this._fireOnCurrentContentChanged(this);
var curBlock = this.rdom.getCurrentBlockElement();
if(!xq.Browser.isTrident && curBlock) {
curBlock.scrollIntoView(false);
}
return true;
},
_handleContextMenu: function(e) {
if (xq.Browser.isWebkit) {
if (e.metaKey || xq.isLeftClick(e)) return false;
} else if (e.shiftKey || e.ctrlKey || e.altKey) {
return false;
}
var point = xq.getEventPoint(e);
var x = point.x;
var y = point.y;
var pos = xq.getCumulativeOffset(this.wysiwygEditorDiv);
x += pos.left;
y += pos.top;
this._contextMenuTargetElement = e.target || e.srcElement;
if (!xq.Browser.isTrident) {
var doc = this.getDoc();
var body = this.getBody();
x -= doc.documentElement.scrollLeft;
y -= doc.documentElement.scrollTop;
x -= body.scrollLeft;
y -= body.scrollTop;
}
for(var cmh in this.config.contextMenuHandlers) {
var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y);
if(stop) {
xq.stopEvent(e);
return true;
}
}
return false;
},
showContextMenu: function(menuItems, x, y) {
if (!menuItems || menuItems.length <= 0) return;
if (!this.contextMenuContainer) {
this.contextMenuContainer = this.doc.createElement('UL');
this.contextMenuContainer.className = 'xqContextMenu';
this.contextMenuContainer.style.display='none';
xq.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this));
xq.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this));
this.body.appendChild(this.contextMenuContainer);
} else {
while (this.contextMenuContainer.childNodes.length > 0)
this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]);
}
for (var i=0; i < menuItems.length; i++) {
menuItems[i]._node = this._addContextMenuItem(menuItems[i]);
}
this.contextMenuContainer.style.display='block';
this.contextMenuContainer.style.left = Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth) - this.contextMenuContainer.offsetWidth, x) + 'px';
this.contextMenuContainer.style.top = Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight) - this.contextMenuContainer.offsetHeight, y) + 'px';
this.contextMenuItems = menuItems;
},
hideContextMenu: function() {
if (this.contextMenuContainer)
this.contextMenuContainer.style.display='none';
},
_addContextMenuItem: function(item) {
if (!this.contextMenuContainer) throw "No conext menu container exists";
var node = this.doc.createElement('LI');
if (item.disabled) node.className += ' disabled';
if (item.title === '----') {
node.innerHTML = '&nbsp;';
node.className = 'separator';
} else {
if(item.handler) {
node.innerHTML = '<a href="#" onclick="return false;">'+(item.title.toString().escapeHTML())+'</a>';
} else {
node.innerHTML = (item.title.toString().escapeHTML());
}
}
if(item.className) node.className = item.className;
this.contextMenuContainer.appendChild(node);
return node;
},
_contextMenuClicked: function(e) {
this.hideContextMenu();
if (!this.contextMenuContainer) return;
var node = e.srcElement || e.target;
while(node && node.nodeName !== "LI") {
node = node.parentNode;
}
if (!node || !this.rdom.tree.isDescendantOf(this.contextMenuContainer, node)) return;
for (var i=0; i < this.contextMenuItems.length; i++) {
if (this.contextMenuItems[i]._node === node) {
var handler = this.contextMenuItems[i].handler;
if (!this.contextMenuItems[i].disabled && handler) {
var xed = this;
var element = this._contextMenuTargetElement;
if(typeof handler === "function") {
handler(xed, element);
} else {
eval(handler);
}
}
break;
}
}
},
/**
* Inserts HTML template
* @TODO: Add selenium test
*
* @param {String} html Template string. It should have single root element
* @returns {Element} inserted element
*/
insertTemplate: function(html) {
return this.rdom.insertHtml(this._processTemplate(html));
},
/**
* Places given HTML template nearby target.
* @TODO: Add selenium test
*
* @param {String} html Template string. It should have single root element
* @param {Node} target Target node.
* @param {String} where Possible values: "before", "start", "end", "after"
*
* @returns {Element} Inserted element.
*/
insertTemplateAt: function(html, target, where) {
return this.rdom.insertHtmlAt(this._processTemplate(html), target, where);
},
_processTemplate: function(html) {
// apply template processors
var tps = this.getTemplateProcessors();
for(var key in tps) {
var value = tps[key];
html = value.handler(html);
}
// remove all whitespace characters between block tags
return this.removeUnnecessarySpaces(html);
},
/** @private */
_handleEnterAtEmptyBlock: function() {
var block = this.rdom.getCurrentBlockElement();
if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) {
block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
} else {
block =
this.rdom.outdentElement(block) ||
this.rdom.extractOutElementFromParent(block) ||
this.rdom.replaceTag("P", block) ||
this.rdom.insertNewBlockAround(block);
}
this.rdom.placeCaretAtStartOf(block);
if(!xq.Browser.isTrident) block.scrollIntoView(false);
},
/** @private */
_handleEnterAtEdge: function(atStart, forceInsertParagraph) {
var block = this.rdom.getCurrentBlockElement();
var blockToPlaceCaret;
if(atStart && this.rdom.isFirstBlockOfBody(block)) {
blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
} else {
if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true;
var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null);
blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling;
}
this.rdom.placeCaretAtStartOf(blockToPlaceCaret);
if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false);
},
/**
* Replace URL text nearby caret into a link
* @TODO: Add selenium test
*/
replaceUrlToLink: function() {
// If there's link nearby caret, nothing happens
if(this.rdom.getParentElementOf(this.rdom.getCurrentElement(), ["A"])) return;
var marker = this.rdom.pushMarker();
var criteria = function(text) {
var m = /(http|https|ftp|mailto)\:\/\/[^\s]+$/.exec(text);
return m ? m.index : -1;
};
var test = this.rdom.testSmartWrap(marker, criteria);
if(test.textIndex !== -1) {
var a = this.rdom.smartWrap(marker, "A", criteria);
a.href = encodeURI(test.text);
}
this.rdom.popMarker(true);
}
});