/*jshint maxlen: 500 */ /** * Creates a component to work with an elemment. * @class ContentEditable * @for ContentEditable * @extends Y.Plugin.Base * @constructor * @module editor * @submodule content-editable */ var Lang = Y.Lang, YNode = Y.Node, EVENT_CONTENT_READY = 'contentready', EVENT_READY = 'ready', TAG_PARAGRAPH = 'p', BLUR = 'blur', CONTAINER = 'container', CONTENT_EDITABLE = 'contentEditable', EMPTY = '', FOCUS = 'focus', HOST = 'host', INNER_HTML = 'innerHTML', KEY = 'key', PARENT_NODE = 'parentNode', PASTE = 'paste', TEXT = 'Text', USE = 'use', ContentEditable = function() { ContentEditable.superclass.constructor.apply(this, arguments); }; Y.extend(ContentEditable, Y.Plugin.Base, { /** * Internal reference set when render is called. * @private * @property _rendered * @type Boolean */ _rendered: null, /** * Internal reference to the YUI instance bound to the element * @private * @property _instance * @type YUI */ _instance: null, /** * Initializes the ContentEditable instance * @protected * @method initializer */ initializer: function() { var host = this.get(HOST); if (host) { host.frame = this; } this._eventHandles = []; this.publish(EVENT_READY, { emitFacade: true, defaultFn: this._defReadyFn }); }, /** * Destroys the instance. * @protected * @method destructor */ destructor: function() { new Y.EventHandle(this._eventHandles).detach(); this._container.removeAttribute(CONTENT_EDITABLE); }, /** * Generic handler for all DOM events fired by the Editor container. This handler * takes the current EventFacade and augments it to fire on the ContentEditable host. It adds two new properties * to the EventFacade called frameX and frameY which adds the scroll and xy position of the ContentEditable element * to the original pageX and pageY of the event so external nodes can be positioned over the element. * In case of ContentEditable element these will be equal to pageX and pageY of the container. * @private * @method _onDomEvent * @param {EventFacade} e */ _onDomEvent: function(e) { var xy; e.frameX = e.frameY = 0; if (e.pageX > 0 || e.pageY > 0) { if (e.type.substring(0, 3) !== KEY) { xy = this._container.getXY(); e.frameX = xy[0]; e.frameY = xy[1]; } } e.frameTarget = e.target; e.frameCurrentTarget = e.currentTarget; e.frameEvent = e; this.fire('dom:' + e.type, e); }, /** * Simple pass thru handler for the paste event so we can do content cleanup * @private * @method _DOMPaste * @param {EventFacade} e */ _DOMPaste: function(e) { var inst = this.getInstance(), data = EMPTY, win = inst.config.win; if (e._event.originalTarget) { data = e._event.originalTarget; } if (e._event.clipboardData) { data = e._event.clipboardData.getData(TEXT); } if (win.clipboardData) { data = win.clipboardData.getData(TEXT); if (data === EMPTY) { // Could be empty, or failed // Verify failure if (!win.clipboardData.setData(TEXT, data)) { data = null; } } } e.frameTarget = e.target; e.frameCurrentTarget = e.currentTarget; e.frameEvent = e; if (data) { e.clipboardData = { data: data, getData: function() { return data; } }; } else { Y.log('Failed to collect clipboard data', 'warn', 'contenteditable'); e.clipboardData = null; } this.fire('dom:paste', e); }, /** * Binds DOM events and fires the ready event * @private * @method _defReadyFn */ _defReadyFn: function() { var inst = this.getInstance(), container = this.get(CONTAINER); Y.each( ContentEditable.DOM_EVENTS, function(value, key) { var fn = Y.bind(this._onDomEvent, this), kfn = ((Y.UA.ie && ContentEditable.THROTTLE_TIME > 0) ? Y.throttle(fn, ContentEditable.THROTTLE_TIME) : fn); if (!inst.Node.DOM_EVENTS[key]) { inst.Node.DOM_EVENTS[key] = 1; } if (value === 1) { if (key !== FOCUS && key !== BLUR && key !== PASTE) { if (key.substring(0, 3) === KEY) { //Throttle key events in IE this._eventHandles.push(container.on(key, kfn, container)); } else { this._eventHandles.push(container.on(key, fn, container)); } } } }, this ); inst.Node.DOM_EVENTS.paste = 1; this._eventHandles.push( container.on(PASTE, Y.bind(this._DOMPaste, this), container), container.on(FOCUS, Y.bind(this._onDomEvent, this), container), container.on(BLUR, Y.bind(this._onDomEvent, this), container) ); inst.__use = inst.use; inst.use = Y.bind(this.use, this); }, /** * Called once the content is available in the ContentEditable element and calls the final use call * @private * @method _onContentReady * on the internal instance so that the modules are loaded properly. */ _onContentReady: function(event) { if (!this._ready) { this._ready = true; var inst = this.getInstance(), args = Y.clone(this.get(USE)); this.fire(EVENT_CONTENT_READY); Y.log('On content available', 'info', 'contenteditable'); if (event) { inst.config.doc = YNode.getDOMNode(event.target); } args.push(Y.bind(function() { Y.log('Callback from final internal use call', 'info', 'contenteditable'); if (inst.EditorSelection) { inst.EditorSelection.DEFAULT_BLOCK_TAG = this.get('defaultblock'); inst.EditorSelection.ROOT = this.get(CONTAINER); } this.fire(EVENT_READY); }, this)); Y.log('Calling use on internal instance: ' + args, 'info', 'contentEditable'); inst.use.apply(inst, args); } }, /** * Retrieves defaultblock value from host attribute * @private * @method _getDefaultBlock * @return {String} */ _getDefaultBlock: function() { return this._getHostValue('defaultblock'); }, /** * Retrieves dir value from host attribute * @private * @method _getDir * @return {String} */ _getDir: function() { return this._getHostValue('dir'); }, /** * Retrieves extracss value from host attribute * @private * @method _getExtraCSS * @return {String} */ _getExtraCSS: function() { return this._getHostValue('extracss'); }, /** * Get the content from the container * @private * @method _getHTML * @param {String} html The raw HTML from the container. * @return {String} */ _getHTML: function() { var html, container; if (this._ready) { container = this.get(CONTAINER); html = container.get(INNER_HTML); } return html; }, /** * Retrieves a value from host attribute * @private * @method _getHostValue * @param {attr} The attribute which value should be returned from the host * @return {String|Object} */ _getHostValue: function(attr) { var host = this.get(HOST); if (host) { return host.get(attr); } }, /** * Set the content of the container * @private * @method _setHTML * @param {String} html The raw HTML to set to the container. * @return {String} */ _setHTML: function(html) { if (this._ready) { var container = this.get(CONTAINER); container.set(INNER_HTML, html); } else { //This needs to be wrapped in a contentready callback for the !_ready state this.once(EVENT_CONTENT_READY, Y.bind(this._setHTML, this, html)); } return html; }, /** * Sets the linked CSS on the instance. * @private * @method _setLinkedCSS * @param {String} css The linkedcss value * @return {String} */ _setLinkedCSS: function(css) { if (this._ready) { var inst = this.getInstance(); inst.Get.css(css); } else { //This needs to be wrapped in a contentready callback for the !_ready state this.once(EVENT_CONTENT_READY, Y.bind(this._setLinkedCSS, this, css)); } return css; }, /** * Sets the dir (language direction) attribute on the container. * @private * @method _setDir * @param {String} value The language direction * @return {String} */ _setDir: function(value) { var container; if (this._ready) { container = this.get(CONTAINER); container.setAttribute('dir', value); } else { //This needs to be wrapped in a contentready callback for the !_ready state this.once(EVENT_CONTENT_READY, Y.bind(this._setDir, this, value)); } return value; }, /** * Set's the extra CSS on the instance. * @private * @method _setExtraCSS * @param {String} css The CSS style to be set as extra css * @return {String} */ _setExtraCSS: function(css) { if (this._ready) { if (css) { var inst = this.getInstance(), head = inst.one('head'); if (this._extraCSSNode) { this._extraCSSNode.remove(); } this._extraCSSNode = YNode.create('<style>' + css + '</style>'); head.append(this._extraCSSNode); } } else { //This needs to be wrapped in a contentready callback for the !_ready state this.once(EVENT_CONTENT_READY, Y.bind(this._setExtraCSS, this, css)); } return css; }, /** * Sets the language value on the instance. * @private * @method _setLang * @param {String} value The language to be set * @return {String} */ _setLang: function(value) { var container; if (this._ready) { container = this.get(CONTAINER); container.setAttribute('lang', value); } else { //This needs to be wrapped in a contentready callback for the !_ready state this.once(EVENT_CONTENT_READY, Y.bind(this._setLang, this, value)); } return value; }, /** * Called from the first YUI instance that sets up the internal instance. * This loads the content into the ContentEditable element and attaches the contentready event. * @private * @method _instanceLoaded * @param {YUI} inst The internal YUI instance bound to the ContentEditable element */ _instanceLoaded: function(inst) { this._instance = inst; this._onContentReady(); var doc = this._instance.config.doc; if (!Y.UA.ie) { try { //Force other browsers into non CSS styling doc.execCommand('styleWithCSS', false, false); doc.execCommand('insertbronreturn', false, false); } catch (err) {} } }, /** * Validates linkedcss property * * @method _validateLinkedCSS * @private */ _validateLinkedCSS: function(value) { return Lang.isString(value) || Lang.isArray(value); }, //BEGIN PUBLIC METHODS /** * This is a scoped version of the normal YUI.use method & is bound to the ContentEditable element * At setup, the inst.use method is mapped to this method. * @method use */ use: function() { Y.log('Calling augmented use after ready', 'info', 'contenteditable'); var inst = this.getInstance(), args = Y.Array(arguments), callback = false; if (Lang.isFunction(args[args.length - 1])) { callback = args.pop(); } if (callback) { args.push(function() { Y.log('Internal callback from augmented use', 'info', 'contenteditable'); callback.apply(inst, arguments); }); } return inst.__use.apply(inst, args); }, /** * A delegate method passed to the instance's delegate method * @method delegate * @param {String} type The type of event to listen for * @param {Function} fn The method to attach * @param {String, Node} cont The container to act as a delegate, if no "sel" passed, the container is assumed. * @param {String} sel The selector to match in the event (optional) * @return {EventHandle} The Event handle returned from Y.delegate */ delegate: function(type, fn, cont, sel) { var inst = this.getInstance(); if (!inst) { Y.log('Delegate events can not be attached until after the ready event has fired.', 'error', 'contenteditable'); return false; } if (!sel) { sel = cont; cont = this.get(CONTAINER); } return inst.delegate(type, fn, cont, sel); }, /** * Get a reference to the internal YUI instance. * @method getInstance * @return {YUI} The internal YUI instance */ getInstance: function() { return this._instance; }, /** * @method render * @param {String/HTMLElement/Node} node The node to render to * @return {ContentEditable} * @chainable */ render: function(node) { var args, inst, fn; if (this._rendered) { Y.log('Container already rendered.', 'warn', 'contentEditable'); return this; } if (node) { this.set(CONTAINER, node); } container = this.get(CONTAINER); if (!container) { container = YNode.create(ContentEditable.HTML); Y.one('body').prepend(container); this.set(CONTAINER, container); } this._rendered = true; this._container.setAttribute(CONTENT_EDITABLE, true); args = Y.clone(this.get(USE)); fn = Y.bind(function() { inst = YUI(); inst.host = this.get(HOST); //Cross reference to Editor inst.log = Y.log; //Dump the instance logs to the parent instance. Y.log('Creating new internal instance with node-base only', 'info', 'contenteditable'); inst.use('node-base', Y.bind(this._instanceLoaded, this)); }, this); args.push(fn); Y.log('Adding new modules to main instance: ' + args, 'info', 'contenteditable'); Y.use.apply(Y, args); return this; }, /** * Set the focus to the container * @method focus * @param {Function} fn Callback function to execute after focus happens * @return {ContentEditable} * @chainable */ focus: function() { this._container.focus(); return this; }, /** * Show the iframe instance * @method show * @return {ContentEditable} * @chainable */ show: function() { this._container.show(); this.focus(); return this; }, /** * Hide the iframe instance * @method hide * @return {ContentEditable} * @chainable */ hide: function() { this._container.hide(); return this; } }, { /** * The throttle time for key events in IE * @static * @property THROTTLE_TIME * @type Number * @default 100 */ THROTTLE_TIME: 100, /** * The DomEvents that the frame automatically attaches and bubbles * @static * @property DOM_EVENTS * @type Object */ DOM_EVENTS: { click: 1, dblclick: 1, focusin: 1, focusout: 1, keydown: 1, keypress: 1, keyup: 1, mousedown: 1, mouseup: 1, paste: 1 }, /** * The template string used to create the ContentEditable element * @static * @property HTML * @type String */ HTML: '<div></div>', /** * The name of the class (contentEditable) * @static * @property NAME * @type String */ NAME: 'contentEditable', /** * The namespace on which ContentEditable plugin will reside. * * @property NS * @type String * @default 'contentEditable' * @static */ NS: CONTENT_EDITABLE, ATTRS: { /** * The default text direction for this ContentEditable element. Default: ltr * @attribute dir * @type String */ dir: { lazyAdd: false, validator: Lang.isString, setter: '_setDir', valueFn: '_getDir' }, /** * The container to set contentEditable=true or to create on render. * @attribute container * @type String/HTMLElement/Node */ container: { setter: function(n) { this._container = Y.one(n); return this._container; } }, /** * The string to inject as Editor content. Default '<br>' * @attribute content * @type String */ content: { getter: '_getHTML', lazyAdd: false, setter: '_setHTML', validator: Lang.isString, value: '<br>' }, /** * The default tag to use for block level items, defaults to: p * @attribute defaultblock * @type String */ defaultblock: { validator: Lang.isString, value: TAG_PARAGRAPH, valueFn: '_getDefaultBlock' }, /** * A string of CSS to add to the Head of the Editor * @attribute extracss * @type String */ extracss: { lazyAdd: false, setter: '_setExtraCSS', validator: Lang.isString, valueFn: '_getExtraCSS' }, /** * Set the id of the new Node. (optional) * @attribute id * @type String * @writeonce */ id: { writeOnce: true, getter: function(id) { if (!id) { id = 'inlineedit-' + Y.guid(); } return id; } }, /** * The default language. Default: en-US * @attribute lang * @type String */ lang: { validator: Lang.isString, setter: '_setLang', lazyAdd: false, value: 'en-US' }, /** * An array of url's to external linked style sheets * @attribute linkedcss * @type String|Array */ linkedcss: { setter: '_setLinkedCSS', validator: '_validateLinkedCSS' //value: '' }, /** * The Node instance of the container. * @attribute node * @type Node */ node: { readOnly: true, value: null, getter: function() { return this._container; } }, /** * Array of modules to include in the scoped YUI instance at render time. Default: ['node-base', 'editor-selection', 'stylesheet'] * @attribute use * @writeonce * @type Array */ use: { validator: Lang.isArray, writeOnce: true, value: ['node-base', 'editor-selection', 'stylesheet'] } } }); Y.namespace('Plugin'); Y.Plugin.ContentEditable = ContentEditable;