editor-debug.js revision 09c506b10ea0dc1958da33e079eb17b831516336
97N/A * Creates a wrapper around an iframe. It loads the content either from a local 97N/A * file or from script and creates a local YUI instance bound to that new window and document. 97N/A * Creates a wrapper around an iframe. It loads the content either from a local 97N/A * file or from script and creates a local YUI instance bound to that new window and document. 97N/A * @description Internal reference set when the content is ready. 618N/A * @description Internal reference set when render is called. 1119N/A * @description Internal Node reference to the iFrame or the window 97N/A * @description Internal reference to the YUI instance bound to the iFrame or window 99N/A * @description Create the iframe or Window and get references to the Document & Window 151N/A * @return {Object} Hash table containing references to the new Document & Window 3205N/A extra_css = ((
this.
get(
'extracss')) ?
'<style id="extra_css">' +
this.
get(
'extracss') +
'</style>' :
'');
3205N/A Y.
log(
'Creating the document from javascript',
'info',
'frame');
3205N/A Y.
log(
'Adding Doctype to frame',
'info',
'frame');
3643N/A Y.
log(
'DocType skipped because we are in BackCompat Mode.',
'warn',
'frame');
3643N/A Y.
log(
'Injecting content into iframe',
'info',
'frame');
4760N/A Y.
log(
'document.documentElement was not found, running timer',
'warn',
'frame');
4760N/A Y.
log(
'document.documentElement found inside timer',
'info',
'frame');
Y.
log(
'document.documentElement found',
'info',
'frame');
* @description Resolves the document and window from an iframe or window instance * @param {Object} c The YUI Config to add the window and document to * @return {Object} Object hash of window and document references, if a YUI config was passed, it is returned. * @description Generic handler for all DOM events fired by the iframe or window. This handler * takes the current EventFacade and augments it to fire on the Frame host. It adds two new properties * to the EventFacade called frameX and frameY which adds the scroll and xy position of the iframe * to the original pageX and pageY of the event so external nodes can be positioned over the frame. * @param {Event.Facade} e //Y.log('onDOMEvent: ' + e.type, 'info', 'frame'); * @description Simple pass thru handler for the paste event so we can do content cleanup * @param {Event.Facade} e if (
data ===
'') {
// Could be empty, or failed Y.
log(
'Failed to collect clipboard data',
'warn',
'frame');
this.
fire(
'dom:paste', e);
* @description Binds DOM events, sets the iframe to visible and fires the ready event //Y.each(inst.Node.DOM_EVENTS, function(v, k) { if (k !==
'focus' && k !==
'blur' && k !==
'paste') {
//Y.log('Adding DOM event to frame: ' + k, 'info', 'frame'); * @method _onContentReady * @description Called once the content is available in the frame/window and calls the final use call * on the internal instance so that the modules are loaded properly. this.
fire(
'contentready');
Y.
log(
'On available for body of iframe',
'info',
'frame');
//TODO Circle around and deal with CSS loading... Y.
log(
'Callback from final internal use call',
'info',
'frame');
Y.
log(
'Calling use on internal instance: ' +
args,
'info',
'frame');
* @method _resolveBaseHref * @description Resolves the basehref of the page the frame is created on. Only applies to dynamic content. * @param {String} href The new value to use, if empty it will be resolved from the current url. if (
href.
indexOf(
'?') !== -
1) {
//Remove the query string * @description Get the content from the iframe * @param {String} html The raw HTML from the body of the iframe. * @description Set the content of the iframe * @param {String} html The raw HTML to set the body of the iframe to. //This needs to be wrapped in a contentready callback for the !_ready state this.
on(
'contentready', Y.
bind(
function(
html, e) {
* @description Set's the linked CSS on the instance.. str +=
'<link rel="stylesheet" href="' + v +
'" type="text/css">';
* @description Set's the linked CSS on the instance.. * @description Set's the extra CSS on the instance.. * @method _instanceLoaded * @description Called from the first YUI instance that sets up the internal instance. * This loads the content into the window/frame and attaches the contentready event. * @param {YUI} inst The internal YUI instance bound to the frame/window if (
this.
get(
'designMode')) {
//Force other browsers into non CSS styling * @description This is a scoped version of the normal YUI.use method & is bound to this frame/window. * At setup, the inst.use method is mapped to this method. Y.
log(
'Calling augmented use after ready',
'info',
'frame');
Y.
log(
'Internal callback from augmented use',
'info',
'frame');
* @description A delegate method passed to the instance's delegate method * @param {String} type The type of event to listen for * @param {Function} fn The method to attach * @param {String} cont The container to act as a delegate, if no "sel" passed, the body is assumed as the container. * @param {String} sel The selector to match in the event (optional) * @return {EventHandle} The Event handle returned from Y.delegate Y.
log(
'Delegate events can not be attached until after the ready event has fired.',
'error',
'iframe');
* @description Get a reference to the internal YUI instance. * @return {YUI} The internal YUI instance * @description Render the iframe into the container config option or open the window. Y.
log(
'Frame already rendered.',
'warn',
'frame');
Y.
log(
'Internal instance loaded with node-base',
'info',
'frame');
Y.
log(
'New Modules Loaded into main instance',
'info',
'frame');
inst.
log = Y.
log;
//Dump the instance logs to the parent instance. Y.
log(
'Creating new internal instance with node-base only',
'info',
'frame');
Y.
log(
'[TIMER] Internal use call failed, retrying',
'info',
'frame');
Y.
log(
'Internal use call failed, retrying',
'info',
'frame');
Y.
log(
'Adding new modules to main instance: ' +
args,
'info',
'frame');
* @description Does some tricks on focus to set the proper cursor position. * @description Set the focus to the iframe * @param {Function} fn Callback function to execute after focus happens Y.
log(
'Frame focus failed',
'warn',
'frame');
Y.
later(
100,
this,
function() {
Y.
log(
'Frame focus failed',
'warn',
'frame');
* @description Show the iframe instance * @description Hide the iframe instance * @description The DomEvents that the frame automatically attaches and bubbles * @description The default css used when creating the document. //DEFAULT_CSS: 'html { height: 95%; } body { padding: 7px; background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }', DEFAULT_CSS:
'body { background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }',
* @description The template string used to create the iframe //HTML: '<iframe border="0" frameBorder="0" marginWidth="0" marginHeight="0" leftMargin="0" topMargin="0" allowTransparency="true" width="100%" height="99%"></iframe>', HTML:
'<iframe border="0" frameBorder="0" marginWidth="0" marginHeight="0" leftMargin="0" topMargin="0" allowTransparency="true" width="100%" height="99%"></iframe>',
* @description The template used to create the page when created dynamically. PAGE_HTML:
'<html dir="{DIR}" lang="{LANG}"><head><title>{TITLE}</title>{META}<base href="{BASE_HREF}"/>{LINKED_CSS}<style id="editor_css">{DEFAULT_CSS}</style>{EXTRA_CSS}</head><body>{CONTENT}</body></html>',
* @description The DOCTYPE to prepend to the new document when created. Should match the one on the page being served. * @description The meta-tag for Content-Type to add to the dynamic document //META: '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">', META:
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>',
* @description The name of the class (frame) * @description The title to give the blank page. * @description The default text direction for this new frame. Default: ltr * @description The default language. Default: en-US * @description The src of the iframe/window. Defaults to javascript:; //Hackish, IE needs the false in the Javascript URL value:
'javascript' + ((Y.
UA.
ie) ?
':false' :
':') +
';' * @description Should designMode be turned on after creation. * @description The string to inject into the body of the new frame/window. * @description The base href to use in the iframe. * @description Array of modules to include in the scoped YUI instance at render time. Default: ['none', 'selector-css2'] value: [
'substitute',
'node',
'node-style',
'selector-css3']
* @description The container to append the iFrame to on render. * @description Set the id of the new Node. (optional) * @description An array of url's to external linked style sheets * @description A string of CSS to add to the Head of the Editor * @description A reference to the Editor instance * @attribute defaultblock * @description The default tag to use for block level items, defaults to: p },
'@VERSION@' ,{
requires:[
'base',
'node',
'selector-css3',
'substitute'],
skinnable:
false});
YUI.
add(
'selection',
function(Y) {
* Wraps some common Selection/Range functionality into a simple object * Wraps some common Selection/Range functionality into a simple object //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content. //This causes IE to not allow a selection on a doubleclick * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose * the fontFamily when selecting nodes. Y.
log(
'Filtering nodes',
'info',
'selection');
if (n.getStyle(FONT_FAMILY)) { classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY); n.removeAttribute('face'); n.setStyle(FONT_FAMILY, ''); if (n.getAttribute('style') === '') { n.removeAttribute('style'); if (n.getAttribute('style').toLowerCase() === 'font-family: ') { n.removeAttribute('style'); el.
setAttribute(
'style',
'border: 1px solid #ccc; line-height: 0; font-size: 0;margin-top: 5px; margin-bottom: 5px;');
el.
setAttribute(
'contenteditable',
false);
//Keep it from being Edited Y.StyleSheet(cssString, 'editor'); //Not sure about this one? baseNodes.each(function(n, k) { var t = n.get('tagName').toLowerCase(), Y.Selection.prototype._swap(baseNodes.item(k), newTag); //Filter out all the empty UL/OL's Y.Selection.filterBlocks(); var endTime = (new Date()).getTime(); Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'selection'); * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter. Y.Selection.filterBlocks = function() { var startTime = (new Date()).getTime(); Y.log('RAW filter blocks', 'info', 'selection'); var childs = Y.config.doc.body.childNodes, i, node, wrapped = false, doit = true, sel, single, br, divs, spans, c, s; for (i = 0; i < childs.length; i++) { if (!node.test(Y.Selection.BLOCKS)) { if (childs[i].nodeType == 3) { c = childs[i][textContent].match(Y.Selection.REG_CHAR); s = childs[i][textContent].match(Y.Selection.REG_NON); wrapped = Y.Selection._wrapBlock(wrapped); wrapped = Y.Selection._wrapBlock(wrapped); single = Y.all(Y.Selection.DEFAULT_BLOCK_TAG); if (single.size() === 1) { Y.log('Only One default block tag (' + Y.Selection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'selection'); br = single.item(0).all('br'); var html = single.item(0).get('innerHTML'); if (html === '' || html === ' ') { single.set('innerHTML', Y.Selection.CURSOR); sel.focusCursor(true, true); single.each(function(p) { var html = p.get('innerHTML'); Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'selection'); if (d.hasClass('yui-non')) { var html = d.get('innerHTML'); Y.log('Empty DIV/P Tag Found, Removing It', 'info', 'selection'); Y.log('DIVS/PS Count: ' + d.get('childNodes').size(), 'info', 'selection'); if (d.get('childNodes').size() == 1) { Y.log('This Div/P only has one Child Node', 'info', 'selection'); Y.log('This Div/P is a child of a paragraph, remove it..', 'info', 'selection'); d.replace(d.get('firstChild')); /** Removed this, as it was causing Pasting to be funky in Safari spans = Y.all('.Apple-style-span, .apple-style-span'); Y.log('Apple Spans found: ' + spans.size(), 'info', 'selection'); s.setAttribute('style', ''); var endTime = (new Date()).getTime(); Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'selection'); * Regular Expression to determine if a string has a character in it Y.Selection.REG_CHAR = /[a-zA-Z-0-9_]/gi; * Regular Expression to determine if a string has a non-character in it Y.Selection.REG_NON = /[\s\S|\n|\t]/gi; * Regular Expression to remove all HTML from a string Y.Selection.REG_NOHTML = /<\S[^><]*>/g; * Wraps an array of elements in a Block level tag Y.Selection._wrapBlock = function(wrapped) { var newChild = Y.Node.create('<' + Y.Selection.DEFAULT_BLOCK_TAG + '></' + Y.Selection.DEFAULT_BLOCK_TAG + '>'), firstChild = Y.one(wrapped[0]), i; for (i = 1; i < wrapped.length; i++) { newChild.append(wrapped[i]); firstChild.replace(newChild); newChild.prepend(firstChild); * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter. * @return {String} The filtered HTML Y.Selection.unfilter = function() { var nodes = Y.all('body [class]'), Y.log('UnFiltering nodes', 'info', 'selection'); if (n.hasClass(n._yuid)) { n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY)); if (n.getAttribute('class') === '') { n.removeAttribute('class'); nons = Y.all('.yui-non'); if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') { n.removeClass('yui-non').removeClass('yui-skip'); ids = Y.all('body [id]'); if (n.get('id').indexOf('yui_3_') === 0) { n.removeAttribute('_yuid'); html = body.get('innerHTML'); Y.all('.hr').addClass('yui-skip').addClass('yui-non'); n.setStyle(FONT_FAMILY, ''); if (n.getAttribute('style') === '') { n.removeAttribute('style'); * Resolve a node from the selection object and return a Node instance * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode. * @return {Node} The Resolved node Y.Selection.resolve = function(n) { if (n && n.nodeType === 3) { //Adding a try/catch here because in rare occasions IE will //Throw a error accessing the parentNode of a stranded text node. //In the case of Ctrl+Z (Undo) * Returns the innerHTML of a node with all HTML tags removed. * @param {Node} node The Node instance to remove the HTML from * @return {String} The string of text Y.Selection.getText = function(node) { var txt = node.get('innerHTML').replace(Y.Selection.REG_NOHTML, ''); //Clean out the cursor subs to see if the Node is empty txt = txt.replace('<span><br></span>', '').replace('<br>', ''); //Y.Selection.DEFAULT_BLOCK_TAG = 'div'; Y.Selection.DEFAULT_BLOCK_TAG = 'p'; * The selector to use when looking for Nodes to cache the value of: [style],font[face] Y.Selection.ALL = '[style],font[face]'; * The selector to use when looking for block level items. Y.Selection.BLOCKS = 'p,div,ul,ol,table,style'; * The temporary fontname applied to a selection to retrieve their values: yui-tmp Y.Selection.TMP = 'yui-tmp'; * The default tag to use when creating elements: span Y.Selection.DEFAULT_TAG = 'span'; * The id of the outer cursor wrapper Y.Selection.CURID = 'yui-cursor'; * The id used to wrap the inner space of the cursor position Y.Selection.CUR_WRAPID = 'yui-cursor-wrapper'; * The default HTML used to focus the cursor.. Y.Selection.CURSOR = '<span id="' + Y.Selection.CURID + '"><br class="yui-
cursor"></span>'; Y.Selection.hasCursor = function() { var cur = Y.all('#' + Y.Selection.CUR_WRAPID); Y.log('Has Cursor: ' + cur.size(), 'info', 'selection'); * Called from Editor keydown to remove the "extra" space before the cursor. Y.Selection.cleanCursor = function() { var cur, sel = 'br.yui-cursor'; var c = b.get('parentNode.parentNode.childNodes'), html; html = Y.Selection.getText(c.item(0)); var cur = Y.all('#' + Y.Selection.CUR_WRAPID); var html = c.get('innerHTML'); if (html == ' ' || html == '<br>') { if (c.previous() || c.next()) { Y.Selection.prototype = { * Flag to show if the range is collapsed or not * A Node instance of the parentNode of the anchorNode of the range * The offset from the range object * A Node instance of the actual textNode of the range. * @property anchorTextNode * A Node instance of the parentNode of the focusNode of the range * The offset from the range object * A Node instance of the actual textNode of the range. * @property focusTextNode * Wrap an element, with another element * @param {HTMLElement} n The node to wrap * @param {String} tag The tag to use when creating the new element. * @return {HTMLElement} The wrapped node _wrap: function(n, tag) { var tmp = Y.Node.create('<' + tag + '></' + tag + '>'); tmp.set(INNER_HTML, n.get(INNER_HTML)); return Y.Node.getDOMNode(tmp); * Swap an element, with another element * @param {HTMLElement} n The node to swap * @param {String} tag The tag to use when creating the new element. * @return {HTMLElement} The new node _swap: function(n, tag) { var tmp = Y.Node.create('<' + tag + '></' + tag + '>'); tmp.set(INNER_HTML, n.get(INNER_HTML)); return Y.Node.getDOMNode(tmp); * Get all the nodes in the current selection. This method will actually perform a filter first. * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection. * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return. * @return {NodeList} A NodeList of all items in the selection. getSelected: function() { Y.config.doc.execCommand('fontname', null, Y.Selection.TMP); var nodes = Y.all(Y.Selection.ALL), nodes.each(function(n, k) { if (n.getStyle(FONT_FAMILY) == Y.Selection.TMP) { n.setStyle(FONT_FAMILY, ''); n.removeAttribute('face'); if (n.getAttribute('style') === '') { n.removeAttribute('style'); items.push(Y.Node.getDOMNode(nodes.item(k))); * Insert HTML at the current cursor position and return a Node instance of the newly inserted element. * @param {String} html The HTML to insert. * @return {Node} The inserted Node. insertContent: function(html) { return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true); * Insert HTML at the current cursor position, this method gives you control over the text node to insert into and the offset where to put it. * @param {String} html The HTML to insert. * @param {Node} node The text node to break when inserting. * @param {Number} offset The left offset of the text node to break and insert the new content. * @param {Boolean} collapse Should the range be collapsed after insertion. default: false * @return {Node} The inserted Node. insertAtCursor: function(html, node, offset, collapse) { var cur = Y.Node.create('<' + Y.Selection.DEFAULT_TAG + ' class="yui-
non"></' + Y.Selection.DEFAULT_TAG + '>'), inHTML, txt, txt2, newNode, range = this.createRange(), b; if (node && node.test('body')) { b = Y.Node.create('<span></span>'); newNode = Y.Node.create(html); range.pasteHTML('<span id="rte-
insert"></span>'); inHTML = Y.one('#rte-insert'); Y.on('available', function() { //TODO using Y.Node.create here throws warnings & strips first white space character //txt = Y.one(Y.Node.create(inHTML.substr(0, offset))); //txt2 = Y.one(Y.Node.create(inHTML.substr(offset))); inHTML = node.get(textContent); txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset))); txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset))); newNode = Y.Node.create(html); if (newNode.get('nodeType') === 11) { b = Y.Node.create('<span></span>'); txt.insert(newNode, 'after'); //if (txt2 && txt2.get('length')) { newNode.insert(cur, 'after'); cur.insert(txt2, 'after'); this.selectNode(cur, collapse); if (node.get('nodeType') === 3) { node = node.get('parentNode'); newNode = Y.Node.create(html); html = node.get('innerHTML').replace(/\n/gi, ''); if (html === '' || html === '<br>') { node.insert(newNode, 'before'); if (node.get('firstChild').test('br')) { node.get('firstChild').remove(); * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched. * @param {String} tag The tag to wrap all selected items with. * @return {NodeList} A NodeList of all items in the selection. wrapContent: function(tag) { tag = (tag) ? tag : Y.Selection.DEFAULT_TAG; Y.log('Wrapping selection with: ' + tag, 'info', 'selection'); var items = this.getSelected(), changed = [], range, last, first, range2; items.each(function(n, k) { var t = n.get('tagName').toLowerCase(); changed.push(this._swap(items.item(k), tag)); changed.push(this._wrap(items.item(k), tag)); range = this.createRange(); last = changed[changed.length - 1]; if (this._selection.removeAllRanges) { range.setStart(changed[0], 0); range.setEnd(last, last.childNodes.length); this._selection.removeAllRanges(); this._selection.addRange(range); range.moveToElementText(Y.Node.getDOMNode(first)); range2 = this.createRange(); range2.moveToElementText(Y.Node.getDOMNode(last)); range.setEndPoint('EndToEnd', range2); changed = Y.all(changed); Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'selection'); Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'selection'); * Find and replace a string inside a text node and replace it with HTML focusing the node after * to allow you to continue to type. * @param {String} se The string to search for. * @param {String} re The string of HTML to replace it with. * @return {Node} The node inserted. replace: function(se,re) { Y.log('replacing (' + se + ') with (' + re + ')'); var range = this.createRange(), node, txt, index, newNode; index = range.getBookmark(); txt = this.anchorNode.get('innerHTML').replace(se, re); this.anchorNode.set('innerHTML', txt); range.moveToBookmark(index); newNode = Y.one(range.parentElement()); node = this.anchorTextNode; txt = node.get(textContent); txt = txt.replace(se, ''); node.set(textContent, txt); newNode = this.insertAtCursor(re, node, index, true); this._selection.removeAllRanges(); * Wrapper for the different range creation methods. createRange: function() { return Y.config.doc.selection.createRange(); return Y.config.doc.createRange(); * Select a Node (hilighting it). * @param {Node} node The node to select * @param {Boolean} collapse Should the range be collapsed after insertion. default: false selectNode: function(node, collapse, end) { node = Y.Node.getDOMNode(node); var range = this.createRange(); this._selection.removeAllRanges(); this._selection.addRange(range); this._selection.collapse(node, end); this._selection.collapse(node, 0); if (node.nodeType === 3) { range.moveToElementText(node); range.collapse(((end) ? false : true)); * Put a placeholder in the DOM at the current cursor position. this.removeCursor(false); return this.insertContent(Y.Selection.CURSOR); * Get the placeholder in the DOM at the current cursor position. return Y.all('#' + Y.Selection.CURID); * Remove the cursor placeholder from the DOM. * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor. removeCursor: function(keep) { var cur = this.getCursor(); cur.removeAttribute('id'); cur.set('innerHTML', '<br class="yui-
cursor">'); * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor focusCursor: function(collapse, end) { if (collapse !== false) { var cur = this.removeCursor(true); this.selectNode(c, collapse, end); * Generic toString for logging. return 'Selection Object'; }, '@VERSION@' ,{requires:['node'], skinnable:false}); YUI.add('exec-command', function(Y) { * Plugin for the frame module to handle execCommands for Editor * @submodule exec-command * Plugin for the frame module to handle execCommands for Editor * @class Plugin.ExecCommand var ExecCommand = function() { ExecCommand.superclass.constructor.apply(this, arguments); Y.extend(ExecCommand, Y.Base, { * An internal reference to the keyCode of the last key that was pressed. * An internal reference to the instance of the frame plugged into. * Execute a command on the frame's document. * @param {String} action The action to perform (bold, italic, fontname) * @param {String} value The optional value (helvetica) command: function(action, value) { var fn = ExecCommand.COMMANDS[action]; Y.later(0, this, function() { var inst = this.getInstance(); if (inst && inst.Selection) { inst.Selection.cleanCursor(); Y.log('execCommand(' + action + '): "' + value + '"', 'info', 'exec-command'); return fn.call(this, action, value); return this._command(action, value); * The private version of execCommand that doesn't filter for overrides. * @param {String} action The action to perform (bold, italic, fontname) * @param {String} value The optional value (helvetica) _command: function(action, value) { var inst = this.getInstance(); Y.log('Internal execCommand(' + action + '): "' + value + '"', 'info', 'exec-command'); inst.config.doc.execCommand(action, null, value); Y.log(e.message, 'error', 'exec-command'); * Get's the instance of YUI bound to the parent frame * @return {YUI} The YUI instance bound to the parent frame getInstance: function() { this._inst = this.get('host').getInstance(); initializer: function() { Y.mix(this.get('host'), { execCommand: function(action, value) { return this.exec.command(action, value); _execCommand: function(action, value) { return this.exec._command(action, value); this.get('host').on('dom:keypress', Y.bind(function(e) { this._lastKey = e.keyCode; * Static object literal of execCommand overrides * Wraps the content with a new element of type (tag) * @param {String} cmd The command executed: wrap * @param {String} tag The tag to wrap the selection with * @return {NodeList} NodeList of the items touched by this command. wrap: function(cmd, tag) { var inst = this.getInstance(); return (new inst.Selection()).wrapContent(tag); * Inserts the provided HTML at the cursor, should be a single element. * @method COMMANDS.inserthtml * @param {String} cmd The command executed: inserthtml * @param {String} html The html to insert * @return {Node} Node instance of the item touched by this command. inserthtml: function(cmd, html) { var inst = this.getInstance(); if (inst.Selection.hasCursor() || Y.UA.ie) { return (new inst.Selection()).insertContent(html); this._command('inserthtml', html); * Inserts the provided HTML at the cursor, and focuses the cursor afterwards. * @method COMMANDS.insertandfocus * @param {String} cmd The command executed: insertandfocus * @param {String} html The html to insert * @return {Node} Node instance of the item touched by this command. insertandfocus: function(cmd, html) { var inst = this.getInstance(), out, sel; if (inst.Selection.hasCursor()) { html += inst.Selection.CURSOR; out = this.command('inserthtml', html); sel = new inst.Selection(); sel.focusCursor(true, true); this.command('inserthtml', html); * Inserts a BR at the current cursor position * @method COMMANDS.insertbr * @param {String} cmd The command executed: insertbr insertbr: function(cmd) { var inst = this.getInstance(), cur, sel = new inst.Selection(); cur.insert('<br>', 'before'); sel.focusCursor(true, false); return ((cur && cur.previous) ? cur.previous() : null); * Inserts an image at the cursor position * @method COMMANDS.insertimage * @param {String} cmd The command executed: insertimage * @param {String} img The url of the image to be inserted * @return {Node} Node instance of the item touched by this command. insertimage: function(cmd, img) { return this.command('inserthtml', '<img src="' + img + '">'); * Add a class to all of the elements in the selection * @method COMMANDS.addclass * @param {String} cmd The command executed: addclass * @param {String} cls The className to add * @return {NodeList} NodeList of the items touched by this command. addclass: function(cmd, cls) { var inst = this.getInstance(); return (new inst.Selection()).getSelected().addClass(cls); * Remove a class from all of the elements in the selection * @method COMMANDS.removeclass * @param {String} cmd The command executed: removeclass * @param {String} cls The className to remove * @return {NodeList} NodeList of the items touched by this command. removeclass: function(cmd, cls) { var inst = this.getInstance(); return (new inst.Selection()).getSelected().removeClass(cls); * Adds a forecolor to the current selection, or creates a new element and applies it * @method COMMANDS.forecolor * @param {String} cmd The command executed: forecolor * @param {String} val The color value to apply * @return {NodeList} NodeList of the items touched by this command. forecolor: function(cmd, val) { var inst = this.getInstance(), sel = new inst.Selection(), n; this._command('useCSS', false); if (inst.Selection.hasCursor()) { if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) { sel.anchorNode.setStyle('color', val); n = this.command('inserthtml', '<span style="color:
' + val + '">' + inst.Selection.CURSOR + '</span>'); sel.focusCursor(true, true); return this._command(cmd, val); * Adds a background color to the current selection, or creates a new element and applies it * @method COMMANDS.backcolor * @param {String} cmd The command executed: backcolor * @param {String} val The color value to apply * @return {NodeList} NodeList of the items touched by this command. backcolor: function(cmd, val) { var inst = this.getInstance(), sel = new inst.Selection(), n; if (Y.UA.gecko || Y.UA.opera) { this._command('useCSS', false); if (inst.Selection.hasCursor()) { if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) { sel.anchorNode.setStyle('backgroundColor', val); n = this.command('inserthtml', '<span style="background-
color:
' + val + '">' + inst.Selection.CURSOR + '</span>'); sel.focusCursor(true, true); return this._command(cmd, val); * Sugar method, calles backcolor * @method COMMANDS.hilitecolor * @param {String} cmd The command executed: backcolor * @param {String} val The color value to apply * @return {NodeList} NodeList of the items touched by this command. hilitecolor: function() { return ExecCommand.COMMANDS.backcolor.apply(this, arguments); * Adds a font name to the current selection, or creates a new element and applies it * @method COMMANDS.fontname * @param {String} cmd The command executed: fontname * @param {String} val The font name to apply * @return {NodeList} NodeList of the items touched by this command. fontname: function(cmd, val) { this._command('fontname', val); var inst = this.getInstance(), sel = new inst.Selection(); if (sel.isCollapsed && (this._lastKey != 32)) { if (sel.anchorNode.test('font')) { sel.anchorNode.set('face', val); * Adds a fontsize to the current selection, or creates a new element and applies it * @method COMMANDS.fontsize * @param {String} cmd The command executed: fontsize * @param {String} val The font size to apply * @return {NodeList} NodeList of the items touched by this command. fontsize: function(cmd, val) { this._command('fontsize', val); var inst = this.getInstance(), sel = new inst.Selection(); if (sel.isCollapsed && sel.anchorNode && (this._lastKey != 32)) { if (sel.anchorNode.getStyle('lineHeight')) { sel.anchorNode.setStyle('lineHeight', ''); if (sel.anchorNode.test('font')) { sel.anchorNode.set('size', val); var p = sel.anchorNode.ancestor(inst.Selection.DEFAULT_BLOCK_TAG); p.setStyle('fontSize', ''); Y.Plugin.ExecCommand = ExecCommand; }, '@VERSION@' ,{requires:['frame'], skinnable:false}); YUI.add('editor-tab', function(Y) { * @class Plugin.EditorTab var EditorTab = function() { EditorTab.superclass.constructor.apply(this, arguments); Y.extend(EditorTab, Y.Base, { * Listener for host's nodeChange event and captures the tabkey interaction. * @param {Event} e The Event facade passed from the host. _onNodeChange: function(e) { if (e.changedType === 'tab') { if (!e.changedNode.test('li, li *')) { if (e.changedEvent.shiftKey) { Y.log('Overriding TAB to ' + action, 'info', 'editorTab'); this.get(HOST).execCommand(action, ''); initializer: function() { this.get(HOST).on('nodeChange', Y.bind(this._onNodeChange, this)); Y.Plugin.EditorTab = EditorTab; }, '@VERSION@' ,{requires:['editor-base'], skinnable:false}); YUI.add('createlink-base', function(Y) { * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events. * @submodule createlink-base * @class Plugin.CreateLinkBase * Strings used by the plugin CreateLinkBase.STRINGS = { * String used for the Prompt PROMPT: 'Please enter the URL for the link to point to:', * String used as the default value of the Prompt Y.Plugin.CreateLinkBase = CreateLinkBase; Y.mix(Y.Plugin.ExecCommand.COMMANDS, { * @method COMMANDS.createlink * @param {String} cmd The command executed: createlink * @return {Node} Node instance of the item touched by this command. createlink: function(cmd) { var inst = this.get('host').getInstance(), out, a, sel, url = prompt(CreateLinkBase.STRINGS.PROMPT, CreateLinkBase.STRINGS.DEFAULT); Y.log('Adding link: ' + url, 'info', 'createLinkBase'); this.get('host')._execCommand(cmd, url); sel = new inst.Selection(); if (!sel.isCollapsed && out.size()) { a = out.item(0).one('a'); if (a.get('parentNode').test('span')) { if (a.get('parentNode').one('br.yui-cursor')) { a.get('parentNode').insert(a, 'before'); //No selection, insert a new node.. this.get('host').execCommand('inserthtml', '<a href="' + url + '">' + url + '</a>'); }, '@VERSION@' ,{requires:['editor-base'], skinnable:false}); YUI.add('editor-base', function(Y) { * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events. * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events. var EditorBase = function() { EditorBase.superclass.constructor.apply(this, arguments); }, LAST_CHILD = ':last-child', BODY = 'body'; Y.extend(EditorBase, Y.Base, { * Internal reference to the Y.Frame instance initializer: function() { var frame = new Y.Frame({ title: EditorBase.STRINGS.title, extracss: this.get('extracss'), linkedcss: this.get('linkedcss'), defaultblock: this.get('defaultblock'), }).plug(Y.Plugin.ExecCommand); frame.after('ready', Y.bind(this._afterFrameReady, this)); this.publish('nodeChange', { defaultFn: this._defNodeChangeFn //this.plug(Y.Plugin.EditorPara); * Copy certain styles from one node instance to another (used for new paragraph creation mainly) * @param {Node} from The Node instance to copy the styles from * @param {Node} to The Node instance to copy the styles to copyStyles: function(from, to) { //Don't carry the A styles var styles = ['color', 'fontSize', 'fontFamily', 'backgroundColor', 'fontStyle' ], Y.each(styles, function(v) { newStyles[v] = from.getStyle(v); if (from.ancestor('b,strong')) { newStyles.fontWeight = 'bold'; if (from.ancestor('u')) { if (!newStyles.textDecoration) { newStyles.textDecoration = 'underline'; * Holder for the selection bookmark in IE. * @property _lastBookmark * Resolves the e.changedNode in the nodeChange event if it comes from the document. If * the event came from the document, it will get the last child of the last child of the document * and return that instead. * @method _resolveChangedNode * @param {Node} n The node to resolve _resolveChangedNode: function(n) { var inst = this.getInstance(), lc, lc2, found; if (inst && n && n.test('html')) { lc = inst.one(BODY).one(LAST_CHILD); lc2 = lc.one(LAST_CHILD); lc = lc.get('parentNode'); * The default handler for the nodeChange event. * @method _defNodeChangeFn * @param {Event} e The event _defNodeChangeFn: function(e) { var startTime = (new Date()).getTime(); //Y.log('Default nodeChange function: ' + e.changedType, 'info', 'editor'); var inst = this.getInstance(), sel, cur, btag = inst.Selection.DEFAULT_BLOCK_TAG; sel = inst.config.doc.selection.createRange(); this._lastBookmark = sel.getBookmark(); e.changedNode = this._resolveChangedNode(e.changedNode); * This whole method needs to be fixed and made more dynamic. * Maybe static functions for the e.changeType and an object bag * to walk through and filter to pass off the event to before firing.. //inst.later(100, inst, inst.Selection.cleanCursor); inst.Selection.cleanCursor(); if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) { e.changedEvent.frameEvent.preventDefault(); Y.log('Overriding TAB key to insert HTML: HALTING', 'info', 'editor'); this.execCommand('inserttext', '\t'); sel = new inst.Selection(); cur.insert(EditorBase.TABKEY, 'before'); inst.Selection.cleanCursor(); if (Y.UA.webkit && e.commands && (e.commands.indent || e.commands.outdent)) { * When executing execCommand 'indent or 'outdent' Webkit applies * a class to the BLOCKQUOTE that adds left/right margin to it * This strips that style so it is just a normal BLOCKQUOTE var bq = inst.all('.webkit-indent-blockquote'); bq.setStyle('margin', ''); var changed = this.getDomPath(e.changedNode, false), cmds = {}, family, fsize, classes = [], fColor = '', bColor = ''; Y.each(changed, function(el) { var tag = el.tagName.toLowerCase(), cmd = EditorBase.TAG2CMD[tag]; var s = el.currentStyle || el.style; if ((''+s.fontWeight) == 'bold') { //Cast this to a string if (s.fontStyle == 'italic') { if (s.textDecoration == 'underline') { if (s.textDecoration == 'line-through') { if (n.getStyle('fontFamily')) { var family2 = n.getStyle('fontFamily').split(',')[0].toLowerCase(); family = family.replace(/'/g, '').replace(/"/g,
'');
if (v !==
'' && (v.
substr(
0,
4) !==
'yui_')) {
//TODO Dont' like this, not dynamic enough.. * Walk the dom tree from this node up to body, returning a reversed array of parents. * @param {Node} node The Node to start from //return inst.all(domNode); //Check to see if we get el.nodeName and nodeType if (node.test('html') || node.test('doc') || !node.get('tagName')) { //Check to see if we get el.nodeName and nodeType if (node.get('nodeName') && node.get('nodeType') && (node.get('nodeType') == 1)) { domPath.push(inst.Node.getDOMNode(node)); node = node.get('parentNode'); * After frame ready, bind mousedown & keyup listeners * @method _afterFrameReady //this.frame.on('dom:activate', Y.bind(this._onFrameActivate, this)); * Moves the cached selection bookmark back so IE can place the cursor in the right place. * @method _onFrameActivate Y.
log(
'IE Activate handler, resetting cursor position',
'info',
'editor');
Y.
log(
'IE Activate handler, FAILED',
'warn',
'editor');
* @method _onFrameMouseUp * @method _onFrameMouseDown * Caches a copy of the selection for key events. Only creating the selection on keydown * @property _currentSelection * Holds the timer for selection clearing * @property _currentSelectionTimer * Flag to determine if we can clear the selection or not. * @property _currentSelectionClear * @method _onFrameKeyDown * @method _onFrameKeyPress * Fires nodeChange event for keyup on specific keys * Pass through to the frame.execCommand method * @param {String} cmd The command to pass: inserthtml, insertimage, bold * @param {String} val The optional value of the command: Helvetica * @return {Node/NodeList} The Node or Nodelist affected by the command. Only returns on override commands, not browser defined commands. this.
fire(
'nodeChange', e);
* Get the YUI instance of the frame * @return {YUI} The YUI instance bound to the frame. * Renders the Y.Frame to the passed node. * Focus the contentWindow of the iframe * @param {Function} fn Callback function to execute after focus happens * Handles the showing of the Editor instance. Currently only handles the iframe * Handles the hiding of the Editor instance. Currently only handles the iframe * (Un)Filters the content of the Editor, cleaning YUI related code. //TODO better filtering * @return {String} The filtered content of the Editor //Removing the _yuid from the objects in IE * @method NORMALIZE_FONTSIZE * @description Pulls the fontSize from a node, then checks for string values (x-large, x-small) * and converts them to pixel sizes. If the parsed size is different from the original, it calls * node.setStyle to update the node with a pixel size for normalization. case '-webkit-xxx-large':
* @description The HTML markup to use for the tabkey TABKEY:
'<span class="tab"> </span>',
* @param String css The CSS string containing rgb(#,#,#); * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00 var exp =
new RegExp(
"(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)",
"gi");
r = r.
length ==
1 ?
'0' + r : r;
g = g.
length ==
1 ?
'0' + g : g;
b = b.
length ==
1 ?
'0' + b : b;
* @description A hash table of tags to their execcomand's 'ul' :
'insertunorderedlist',
'ol' :
'insertorderedlist' * Hash table of keys to fire a nodeChange event for. * The default modules to use inside the Frame USE: [
'substitute',
'node',
'selector-css3',
'selection',
'stylesheet'],
* The Class Name: editorBase * Title of frame document: Rich Text Editor * @property STRINGS.title title:
'Rich Text Editor' * The content to load into the Editor Frame value:
'<br class="yui-cursor">',
Y.
log(
'Stripping first carriage return from content before injecting',
'warn',
'editor');
str =
'<br class="yui-cursor">';
* The value of the dir attribute on the HTML element of the frame. Default: ltr * @description An array of url's to external linked style sheets * @description A string of CSS to add to the Head of the Editor * @attribute defaultblock * @description The default tag to use for block level items, defaults to: p * @description Fired from mouseup & keyup. * @param {Event.Facade} event An Event Facade object with the following specific properties added: * <dt>changedEvent</dt><dd>The event that caused the nodeChange</dd> * <dt>changedNode</dt><dd>The node that was interacted with</dd> * <dt>changedType</dt><dd>The type of change: mousedown, mouseup, right, left, backspace, tab, enter, etc..</dd> * <dt>commands</dt><dd>The list of execCommands that belong to this change and the dompath that's associated with the changedNode</dd> * <dt>classNames</dt><dd>An array of classNames that are applied to the changedNode and all of it's parents</dd> * <dt>dompath</dt><dd>A sorted array of node instances that make up the DOM path from the changedNode to body.</dd> * <dt>backgroundColor</dt><dd>The cascaded backgroundColor of the changedNode</dd> * <dt>fontColor</dt><dd>The cascaded fontColor of the changedNode</dd> * <dt>fontFamily</dt><dd>The cascaded fontFamily of the changedNode</dd> * <dt>fontSize</dt><dd>The cascaded fontSize of the changedNode</dd> * @description Fired after the frame is ready. * @param {Event.Facade} event An Event Facade object. },
'@VERSION@' ,{
requires:[
'base',
'frame',
'node',
'exec-command',
'selection',
'editor-para'],
skinnable:
false});
YUI.
add(
'editor-lists',
function(Y) {
* Handles list manipulation inside the Editor. Adds keyboard manipulation and execCommand support. Adds overrides for the <a href="Plugin.ExecCommand.html#method_COMMANDS.insertorderedlist">insertorderedlist</a> and <a href="Plugin.ExecCommand.html#method_COMMANDS.insertunorderedlist">insertunorderedlist</a> execCommands. * @submodule editor-lists * Handles list manipulation inside the Editor. Adds keyboard manipulation and execCommand support. Adds overrides for the <a href="Plugin.ExecCommand.html#method_COMMANDS.insertorderedlist">insertorderedlist</a> and <a href="Plugin.ExecCommand.html#method_COMMANDS.insertunorderedlist">insertunorderedlist</a> execCommands. * @class Plugin.EditorLists },
LI =
'li',
OL =
'ol',
UL =
'ul',
HOST =
'host';
* Listener for host's nodeChange event and captures the tabkey interaction only when inside a list node. * @param {Event} e The Event facade passed from the host. Y.
log(
'Overriding the Enter Key',
'info',
'editorLists');
Y.
log(
'Overriding TAB to move lists around',
'info',
'editorLists');
Y.
log(
'ShiftKey: ' +
sTab,
'info',
'editorLists');
Y.
log(
'Shifting list up one level',
'info',
'editorLists');
//li.setStyle('border', '1px solid red'); Y.
log(
'Shifting list down one level',
'info',
'editorLists');
Y.
log(
'Selecting the new node',
'info',
'editorLists');
* The non element placeholder, used for positioning the cursor and filling empty items NON:
'<span class="yui-non"> </span>',
* The selector query to get all non elements * The items to removed from a list when a list item is moved, currently removes BR nodes * @method COMMANDS.insertunorderedlist * @param {String} cmd The command executed: insertunorderedlist * @return {Node} Node instance of the item touched by this command. * @method COMMANDS.insertorderedlist * @param {String} cmd The command executed: insertorderedlist * @return {Node} Node instance of the item touched by this command. YUI.
add(
'editor-bidi',
function(Y) {
* Plugin for Editor to support BiDirectional (bidi) text operations. * Plugin for Editor to support BiDirectional (bidi) text operations. * @class Plugin.EditorBidi * Place holder for the last direction when checking for a switch * @property lastDirection * Tells us that an initial bidi check has already been performed * Method checks to see if the direction of the text has changed based on a nodeChange event. * @method _checkForChange * Checked for a change after a specific nodeChange event has been fired. * @method _afterNodeChange // If this is the first event ever, or an event that can result in a context change * Checks for a direction change after a mouseup occurs. * The events to check for a direction change on * More elements may be needed. BODY *must* be in the list to take care of the special case. * blockParent could be changed to use inst.Selection.BLOCKS * instead, but that would make Y.Plugin.EditorBidi.blockParent * unusable in non-RTE contexts (it being usable is a nice * Template for creating a block element * Returns a block parent for a given element // This shouldn't happen if the RTE handles everything // according to spec: we should get to a P before BODY. But // we don't want to set the direction of BODY even if that // happens, so we wrap everything in a DIV. // The code is based on YUI3's Y.Selection._wrapBlock function. * The data key to store on the node. * @property _NODE_SELECTED * Generates a list of all the block parents of the current NodeList // This works automagically, since new parents added get processed // later themselves. So if there's a node early in the process that // we haven't discovered some of its siblings yet, thus resulting in // its parent not added, the parent will be added later, since those // siblings will be added to the array and then get processed. // Don't add the parent if the parent is the BODY element. // We don't want to change the direction of BODY. Also don't // do it if the parent is already in the list. return true;
// stop more processing * bidi execCommand override for setting the text direction of a node. * @for Plugin.ExecCommand * @property COMMANDS.bidi }
else {
// some text is selectedYUI.
add(
'editor-para',
function(Y) {
* Plugin for Editor to paragraph auto wrapping and correction. * Plugin for Editor to paragraph auto wrapping and correction. * @class Plugin.EditorPara * Utility method to create an empty paragraph when the document is empty. * nodeChange handler to handle fixing an empty document. //Webkit doesn't support shift+enter as a BR, this fixes that. //TODO Move this to a GECKO MODULE - Can't for the moment, requires no change to metadata (YMAIL) Y.
log(
'Stopping the backspace event',
'warn',
'editor-para');
* This forced FF to redraw the content on backspace. * On some occasions FF will leave a cursor residue after content has been deleted. * Dropping in the empty textnode and then removing it causes FF to redraw and * remove the "ghost cursors" * Performs a block element filter when the Editor is first ready * @method _afterEditorReady * Performs a block element filter when the Editor after an content change * @method _afterContentChange Y.
error(
'Can not plug EditorPara and EditorBR at the same time.');
YUI.
add(
'editor-br',
function(Y) {
* Plugin for Editor to normalize BR's. * Plugin for Editor to normalize BR's. * Frame keyDown handler that normalizes BR's when pressing ENTER. * Adds listeners for keydown in IE and Webkit. Also fires insertbeonreturn for supporting browsers. * @method _afterEditorReady Y.
error(
'Can not plug EditorBR and EditorPara at the same time.');
YUI.
add(
'editor',
function(Y){},
'@VERSION@' ,{
skinnable:
false,
use:[
'frame',
'selection',
'exec-command',
'editor-base',
'editor-para',
'editor-br']});