editor.js revision d0bccce76452becc96b65acaaa684aa6fabaf386
* Creates a wrapper around an iframe. It loads the content either from a local * file or from script and creates a local YUI instance bound to that new window and document. * @description Internal reference set when the content is ready. * @description Internal reference set when render is called. * @description Internal Node reference to the iFrame or the window * @description Internal reference to the YUI instance bound to the iFrame or window * @description Create the iframe or Window and get references to the Document & Window * @return {Object} Hash table containing references to the new Document & Window extra_css = ((
this.
get(
'extracss')) ?
'<style id="extra_css">' +
this.
get(
'extracss') +
'</style>' :
'');
if (
this.
get(
'designMode')) {
* @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 * @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 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') {
* @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');
//TODO Circle around and deal with CSS loading... * @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 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. * @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 * @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. * @description Set the focus to the iframe * @param {Function} fn Callback function to execute after focus happens Y.
later(
100,
this,
function() {
* @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: 'html { } body { margin: -15px 0 0 -15px; padding: 7px 0 0 15px; display: block; background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; }', //DEFAULT_CSS: 'html { height: 95%; } body { height: 100%; padding: 7px; margin: 0 0 0 -7px; postion: relative; 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: 'html { margin: 0; padding: 0; border: none; border-size: 0; } body { height: 97%; margin: 0; padding: 0; display: block; background-color: gray; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; }', * @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" 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}"/><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 A string of CSS to add to the Head of the Editor * @description A reference to the Editor instance },
'@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. 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(); * 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(); 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); if (single.size() === 1) { 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'); if (d.hasClass('yui-non')) { var html = d.get('innerHTML'); if (d.get('childNodes').size() == 1) { 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'); s.setAttribute('style', ''); var endTime = (new Date()).getTime(); * 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; * Wraps an array of elements in a Block level tag Y.Selection._wrapBlock = function(wrapped) { var newChild = Y.Node.create('<p></p>'), 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]'), 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.get('innerHTML') === '') { n.removeClass('yui-non'); ids = Y.all('body [id]'); if (n.get('id').indexOf('yui_3_') === 0) { n.removeAttribute('_yuid'); html = Y.one('body').get('innerHTML'); 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) { * 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 t = node.get('innerHTML').replace(Y.Selection.STRIP_HTML, ''), c = t.match(Y.Selection.REG_CHAR), s = t.match(Y.Selection.REG_NON); * The selector to use when looking for Nodes to cache the value of: [style],font[face] Y.Selection.ALL = '[style],font[face]'; * RegExp used to strip HTML tags from a string Y.Selection.STRIP_HTML = /<\S[^><]*>/g; * 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 + '"><span id="' + Y.Selection.CUR_WRAPID + '"> </span></span>'; Y.Selection.hasCursor = function() { var cur = Y.all('#' + Y.Selection.CUR_WRAPID); * Called from Editor keydown to remove the "extra" space before the cursor. Y.Selection.cleanCursor = function() { var cur = Y.config.doc.getElementById(Y.Selection.CUR_WRAPID); if (cur.innerHTML == ' ' || cur.innerHTML == '<br>') { cur.parentNode.removeChild(cur); var cur = Y.all('#' + Y.Selection.CUR_WRAPID); var html = c.get('innerHTML'); if (html == ' ' || html == '<br>') { 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; 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); * 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) { 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', '<span id="' + Y.Selection.CUR_WRAPID + '"> </span>'); * 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 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]; 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(); inst.config.doc.execCommand(action, false, value); * 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); * 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()) { 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); * 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 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. forecolor: function(cmd, val) { var inst = this.getInstance(), sel = new inst.Selection(), n; this._command('styleWithCSS', 'true'); 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); this._command('styleWithCSS', false); backcolor: function(cmd, val) { var inst = this.getInstance(), sel = new inst.Selection(), n; if (Y.UA.gecko || Y.UA.opera) { this._command('styleWithCSS', 'true'); 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); this._command('styleWithCSS', false); * 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. //Removed from commands list, so the default is executed.. fontname_enh: function(cmd, val) { var inst = this.getInstance(), sel = new inst.Selection(), n; if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) { sel.anchorNode.setStyle('fontFamily', val); n = this.command('inserthtml', '<span style="font-
family:
' + val + '">' + inst.Selection.CURSOR + '</span>'); sel.focusCursor(true, true); return this._command('fontname', 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. //Removed from commands list, so the default is executed.. fontsize_enh: function(cmd, val) { var inst = this.getInstance(), sel = new inst.Selection(), n, prev; n = this.command('inserthtml', '<font size="' + val + '"> </font>'); prev = n.get('previousSibling'); if (prev && prev.get('nodeType') === 3) { if (prev.get('length') < 2) { sel.selectNode(n.get('firstChild'), true, false); return this._command('fontsize', val); 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) { 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); this.get('host')._execCommand(cmd, url); sel = new inst.Selection(); if (!sel.isCollapsed && out.size()) { a = out.item(0).one('a'); //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); 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'), }).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) { 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'; * Holder for the selection bookmark in IE. * @property _lastBookmark * The default handler for the nodeChange event. * @method _defNodeChangeFn * @param {Event} e The event _defNodeChangeFn: function(e) { var startTime = (new Date()).getTime(); var inst = this.getInstance(), sel; sel = inst.config.doc.selection.createRange(); this._lastBookmark = sel.getBookmark(); * 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.Selection.cleanCursor(); //Webkit doesn't support shift+enter as a BR, this fixes that. if (e.changedEvent.shiftKey) { this.execCommand('insertbr'); e.changedEvent.preventDefault(); if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) { e.changedEvent.preventDefault(); var sel = new inst.Selection(); var cur = sel.getCursor(); cur.insert(EditorBase.TABKEY, 'before'); if (e.changedNode.test('p')) { var prev = e.changedNode.previous(), lc, lc2, found = false; lc = prev.one(':last-child'); lc2 = lc.one(':last-child'); this.copyStyles(lc, e.changedNode); 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:keyup', Y.bind(this._onFrameKeyUp, this)); this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this)); this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this)); //this.frame.on('dom:keydown', Y.throttle(Y.bind(this._onFrameKeyDown, this), 500)); * Moves the cached selection bookmark back so IE can place the cursor in the right place. * @method _onFrameActivate * @method _onFrameMouseUp * @method _onFrameMouseDown * Caches a copy of the selection for key events. Only creating the selection on keydown * @property _currentSelection * @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 * @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 * The value of the dir attribute on the HTML element of the frame. Default: ltr * @description A string of CSS to add to the Head of the Editor * @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'],
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. //li.setStyle('border', '1px solid red'); * 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. * 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 YUI.
add(
'editor',
function(Y){},
'@VERSION@' ,{
use:[
'frame',
'selection',
'exec-command',
'editor-base'],
skinnable:
false});