- /*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;
-
-