API Docs for: 3.17.2
Show:

File: editor/js/content-editable.js

  1. /*jshint maxlen: 500 */
  2. /**
  3. * Creates a component to work with an elemment.
  4. * @class ContentEditable
  5. * @for ContentEditable
  6. * @extends Y.Plugin.Base
  7. * @constructor
  8. * @module editor
  9. * @submodule content-editable
  10. */
  11.  
  12. var Lang = Y.Lang,
  13. YNode = Y.Node,
  14.  
  15. EVENT_CONTENT_READY = 'contentready',
  16. EVENT_READY = 'ready',
  17.  
  18. TAG_PARAGRAPH = 'p',
  19.  
  20. BLUR = 'blur',
  21. CONTAINER = 'container',
  22. CONTENT_EDITABLE = 'contentEditable',
  23. EMPTY = '',
  24. FOCUS = 'focus',
  25. HOST = 'host',
  26. INNER_HTML = 'innerHTML',
  27. KEY = 'key',
  28. PARENT_NODE = 'parentNode',
  29. PASTE = 'paste',
  30. TEXT = 'Text',
  31. USE = 'use',
  32.  
  33. ContentEditable = function() {
  34. ContentEditable.superclass.constructor.apply(this, arguments);
  35. };
  36.  
  37. Y.extend(ContentEditable, Y.Plugin.Base, {
  38.  
  39. /**
  40. * Internal reference set when render is called.
  41. * @private
  42. * @property _rendered
  43. * @type Boolean
  44. */
  45. _rendered: null,
  46.  
  47. /**
  48. * Internal reference to the YUI instance bound to the element
  49. * @private
  50. * @property _instance
  51. * @type YUI
  52. */
  53. _instance: null,
  54.  
  55. /**
  56. * Initializes the ContentEditable instance
  57. * @protected
  58. * @method initializer
  59. */
  60. initializer: function() {
  61. var host = this.get(HOST);
  62.  
  63. if (host) {
  64. host.frame = this;
  65. }
  66.  
  67. this._eventHandles = [];
  68.  
  69. this.publish(EVENT_READY, {
  70. emitFacade: true,
  71. defaultFn: this._defReadyFn
  72. });
  73. },
  74.  
  75. /**
  76. * Destroys the instance.
  77. * @protected
  78. * @method destructor
  79. */
  80. destructor: function() {
  81. new Y.EventHandle(this._eventHandles).detach();
  82.  
  83. this._container.removeAttribute(CONTENT_EDITABLE);
  84. },
  85.  
  86. /**
  87. * Generic handler for all DOM events fired by the Editor container. This handler
  88. * takes the current EventFacade and augments it to fire on the ContentEditable host. It adds two new properties
  89. * to the EventFacade called frameX and frameY which adds the scroll and xy position of the ContentEditable element
  90. * to the original pageX and pageY of the event so external nodes can be positioned over the element.
  91. * In case of ContentEditable element these will be equal to pageX and pageY of the container.
  92. * @private
  93. * @method _onDomEvent
  94. * @param {EventFacade} e
  95. */
  96. _onDomEvent: function(e) {
  97. var xy;
  98.  
  99. e.frameX = e.frameY = 0;
  100.  
  101. if (e.pageX > 0 || e.pageY > 0) {
  102. if (e.type.substring(0, 3) !== KEY) {
  103. xy = this._container.getXY();
  104.  
  105. e.frameX = xy[0];
  106. e.frameY = xy[1];
  107. }
  108. }
  109.  
  110. e.frameTarget = e.target;
  111. e.frameCurrentTarget = e.currentTarget;
  112. e.frameEvent = e;
  113.  
  114. this.fire('dom:' + e.type, e);
  115. },
  116.  
  117. /**
  118. * Simple pass thru handler for the paste event so we can do content cleanup
  119. * @private
  120. * @method _DOMPaste
  121. * @param {EventFacade} e
  122. */
  123. _DOMPaste: function(e) {
  124. var inst = this.getInstance(),
  125. data = EMPTY, win = inst.config.win;
  126.  
  127. if (e._event.originalTarget) {
  128. data = e._event.originalTarget;
  129. }
  130.  
  131. if (e._event.clipboardData) {
  132. data = e._event.clipboardData.getData(TEXT);
  133. }
  134.  
  135. if (win.clipboardData) {
  136. data = win.clipboardData.getData(TEXT);
  137.  
  138. if (data === EMPTY) { // Could be empty, or failed
  139. // Verify failure
  140. if (!win.clipboardData.setData(TEXT, data)) {
  141. data = null;
  142. }
  143. }
  144. }
  145.  
  146. e.frameTarget = e.target;
  147. e.frameCurrentTarget = e.currentTarget;
  148. e.frameEvent = e;
  149.  
  150. if (data) {
  151. e.clipboardData = {
  152. data: data,
  153. getData: function() {
  154. return data;
  155. }
  156. };
  157. } else {
  158. Y.log('Failed to collect clipboard data', 'warn', 'contenteditable');
  159.  
  160. e.clipboardData = null;
  161. }
  162.  
  163. this.fire('dom:paste', e);
  164. },
  165.  
  166. /**
  167. * Binds DOM events and fires the ready event
  168. * @private
  169. * @method _defReadyFn
  170. */
  171. _defReadyFn: function() {
  172. var inst = this.getInstance(),
  173. container = this.get(CONTAINER);
  174.  
  175. Y.each(
  176. ContentEditable.DOM_EVENTS,
  177. function(value, key) {
  178. var fn = Y.bind(this._onDomEvent, this),
  179. kfn = ((Y.UA.ie && ContentEditable.THROTTLE_TIME > 0) ? Y.throttle(fn, ContentEditable.THROTTLE_TIME) : fn);
  180.  
  181. if (!inst.Node.DOM_EVENTS[key]) {
  182. inst.Node.DOM_EVENTS[key] = 1;
  183. }
  184.  
  185. if (value === 1) {
  186. if (key !== FOCUS && key !== BLUR && key !== PASTE) {
  187. if (key.substring(0, 3) === KEY) {
  188. //Throttle key events in IE
  189. this._eventHandles.push(container.on(key, kfn, container));
  190. } else {
  191. this._eventHandles.push(container.on(key, fn, container));
  192. }
  193. }
  194. }
  195. },
  196. this
  197. );
  198.  
  199. inst.Node.DOM_EVENTS.paste = 1;
  200.  
  201. this._eventHandles.push(
  202. container.on(PASTE, Y.bind(this._DOMPaste, this), container),
  203. container.on(FOCUS, Y.bind(this._onDomEvent, this), container),
  204. container.on(BLUR, Y.bind(this._onDomEvent, this), container)
  205. );
  206.  
  207. inst.__use = inst.use;
  208.  
  209. inst.use = Y.bind(this.use, this);
  210. },
  211.  
  212. /**
  213. * Called once the content is available in the ContentEditable element and calls the final use call
  214. * @private
  215. * @method _onContentReady
  216. * on the internal instance so that the modules are loaded properly.
  217. */
  218. _onContentReady: function(event) {
  219. if (!this._ready) {
  220. this._ready = true;
  221.  
  222. var inst = this.getInstance(),
  223. args = Y.clone(this.get(USE));
  224.  
  225. this.fire(EVENT_CONTENT_READY);
  226.  
  227. Y.log('On content available', 'info', 'contenteditable');
  228.  
  229. if (event) {
  230. inst.config.doc = YNode.getDOMNode(event.target);
  231. }
  232.  
  233. args.push(Y.bind(function() {
  234. Y.log('Callback from final internal use call', 'info', 'contenteditable');
  235.  
  236. if (inst.EditorSelection) {
  237. inst.EditorSelection.DEFAULT_BLOCK_TAG = this.get('defaultblock');
  238.  
  239. inst.EditorSelection.ROOT = this.get(CONTAINER);
  240. }
  241.  
  242. this.fire(EVENT_READY);
  243. }, this));
  244.  
  245. Y.log('Calling use on internal instance: ' + args, 'info', 'contentEditable');
  246.  
  247. inst.use.apply(inst, args);
  248. }
  249. },
  250.  
  251. /**
  252. * Retrieves defaultblock value from host attribute
  253. * @private
  254. * @method _getDefaultBlock
  255. * @return {String}
  256. */
  257. _getDefaultBlock: function() {
  258. return this._getHostValue('defaultblock');
  259. },
  260.  
  261. /**
  262. * Retrieves dir value from host attribute
  263. * @private
  264. * @method _getDir
  265. * @return {String}
  266. */
  267. _getDir: function() {
  268. return this._getHostValue('dir');
  269. },
  270.  
  271. /**
  272. * Retrieves extracss value from host attribute
  273. * @private
  274. * @method _getExtraCSS
  275. * @return {String}
  276. */
  277. _getExtraCSS: function() {
  278. return this._getHostValue('extracss');
  279. },
  280.  
  281. /**
  282. * Get the content from the container
  283. * @private
  284. * @method _getHTML
  285. * @param {String} html The raw HTML from the container.
  286. * @return {String}
  287. */
  288. _getHTML: function() {
  289. var html, container;
  290.  
  291. if (this._ready) {
  292. container = this.get(CONTAINER);
  293.  
  294. html = container.get(INNER_HTML);
  295. }
  296.  
  297. return html;
  298. },
  299.  
  300. /**
  301. * Retrieves a value from host attribute
  302. * @private
  303. * @method _getHostValue
  304. * @param {attr} The attribute which value should be returned from the host
  305. * @return {String|Object}
  306. */
  307. _getHostValue: function(attr) {
  308. var host = this.get(HOST);
  309.  
  310. if (host) {
  311. return host.get(attr);
  312. }
  313. },
  314.  
  315. /**
  316. * Set the content of the container
  317. * @private
  318. * @method _setHTML
  319. * @param {String} html The raw HTML to set to the container.
  320. * @return {String}
  321. */
  322. _setHTML: function(html) {
  323. if (this._ready) {
  324. var container = this.get(CONTAINER);
  325.  
  326. container.set(INNER_HTML, html);
  327. } else {
  328. //This needs to be wrapped in a contentready callback for the !_ready state
  329. this.once(EVENT_CONTENT_READY, Y.bind(this._setHTML, this, html));
  330. }
  331.  
  332. return html;
  333. },
  334.  
  335. /**
  336. * Sets the linked CSS on the instance.
  337. * @private
  338. * @method _setLinkedCSS
  339. * @param {String} css The linkedcss value
  340. * @return {String}
  341. */
  342. _setLinkedCSS: function(css) {
  343. if (this._ready) {
  344. var inst = this.getInstance();
  345. inst.Get.css(css);
  346. } else {
  347. //This needs to be wrapped in a contentready callback for the !_ready state
  348. this.once(EVENT_CONTENT_READY, Y.bind(this._setLinkedCSS, this, css));
  349. }
  350.  
  351. return css;
  352. },
  353.  
  354. /**
  355. * Sets the dir (language direction) attribute on the container.
  356. * @private
  357. * @method _setDir
  358. * @param {String} value The language direction
  359. * @return {String}
  360. */
  361. _setDir: function(value) {
  362. var container;
  363.  
  364. if (this._ready) {
  365. container = this.get(CONTAINER);
  366.  
  367. container.setAttribute('dir', value);
  368. } else {
  369. //This needs to be wrapped in a contentready callback for the !_ready state
  370. this.once(EVENT_CONTENT_READY, Y.bind(this._setDir, this, value));
  371. }
  372.  
  373. return value;
  374. },
  375.  
  376. /**
  377. * Set's the extra CSS on the instance.
  378. * @private
  379. * @method _setExtraCSS
  380. * @param {String} css The CSS style to be set as extra css
  381. * @return {String}
  382. */
  383. _setExtraCSS: function(css) {
  384. if (this._ready) {
  385. if (css) {
  386. var inst = this.getInstance(),
  387. head = inst.one('head');
  388.  
  389. if (this._extraCSSNode) {
  390. this._extraCSSNode.remove();
  391. }
  392.  
  393. this._extraCSSNode = YNode.create('<style>' + css + '</style>');
  394.  
  395. head.append(this._extraCSSNode);
  396. }
  397. } else {
  398. //This needs to be wrapped in a contentready callback for the !_ready state
  399. this.once(EVENT_CONTENT_READY, Y.bind(this._setExtraCSS, this, css));
  400. }
  401.  
  402. return css;
  403. },
  404.  
  405. /**
  406. * Sets the language value on the instance.
  407. * @private
  408. * @method _setLang
  409. * @param {String} value The language to be set
  410. * @return {String}
  411. */
  412. _setLang: function(value) {
  413. var container;
  414.  
  415. if (this._ready) {
  416. container = this.get(CONTAINER);
  417.  
  418. container.setAttribute('lang', value);
  419. } else {
  420. //This needs to be wrapped in a contentready callback for the !_ready state
  421. this.once(EVENT_CONTENT_READY, Y.bind(this._setLang, this, value));
  422. }
  423.  
  424. return value;
  425. },
  426.  
  427. /**
  428. * Called from the first YUI instance that sets up the internal instance.
  429. * This loads the content into the ContentEditable element and attaches the contentready event.
  430. * @private
  431. * @method _instanceLoaded
  432. * @param {YUI} inst The internal YUI instance bound to the ContentEditable element
  433. */
  434. _instanceLoaded: function(inst) {
  435. this._instance = inst;
  436.  
  437. this._onContentReady();
  438.  
  439. var doc = this._instance.config.doc;
  440.  
  441. if (!Y.UA.ie) {
  442. try {
  443. //Force other browsers into non CSS styling
  444. doc.execCommand('styleWithCSS', false, false);
  445. doc.execCommand('insertbronreturn', false, false);
  446. } catch (err) {}
  447. }
  448. },
  449.  
  450.  
  451. /**
  452. * Validates linkedcss property
  453. *
  454. * @method _validateLinkedCSS
  455. * @private
  456. */
  457. _validateLinkedCSS: function(value) {
  458. return Lang.isString(value) || Lang.isArray(value);
  459. },
  460.  
  461. //BEGIN PUBLIC METHODS
  462. /**
  463. * This is a scoped version of the normal YUI.use method & is bound to the ContentEditable element
  464. * At setup, the inst.use method is mapped to this method.
  465. * @method use
  466. */
  467. use: function() {
  468. Y.log('Calling augmented use after ready', 'info', 'contenteditable');
  469.  
  470. var inst = this.getInstance(),
  471. args = Y.Array(arguments),
  472. callback = false;
  473.  
  474. if (Lang.isFunction(args[args.length - 1])) {
  475. callback = args.pop();
  476. }
  477.  
  478. if (callback) {
  479. args.push(function() {
  480. Y.log('Internal callback from augmented use', 'info', 'contenteditable');
  481.  
  482. callback.apply(inst, arguments);
  483. });
  484. }
  485.  
  486. return inst.__use.apply(inst, args);
  487. },
  488.  
  489. /**
  490. * A delegate method passed to the instance's delegate method
  491. * @method delegate
  492. * @param {String} type The type of event to listen for
  493. * @param {Function} fn The method to attach
  494. * @param {String, Node} cont The container to act as a delegate, if no "sel" passed, the container is assumed.
  495. * @param {String} sel The selector to match in the event (optional)
  496. * @return {EventHandle} The Event handle returned from Y.delegate
  497. */
  498. delegate: function(type, fn, cont, sel) {
  499. var inst = this.getInstance();
  500.  
  501. if (!inst) {
  502. Y.log('Delegate events can not be attached until after the ready event has fired.', 'error', 'contenteditable');
  503.  
  504. return false;
  505. }
  506.  
  507. if (!sel) {
  508. sel = cont;
  509.  
  510. cont = this.get(CONTAINER);
  511. }
  512.  
  513. return inst.delegate(type, fn, cont, sel);
  514. },
  515.  
  516. /**
  517. * Get a reference to the internal YUI instance.
  518. * @method getInstance
  519. * @return {YUI} The internal YUI instance
  520. */
  521. getInstance: function() {
  522. return this._instance;
  523. },
  524.  
  525. /**
  526. * @method render
  527. * @param {String/HTMLElement/Node} node The node to render to
  528. * @return {ContentEditable}
  529. * @chainable
  530. */
  531. render: function(node) {
  532. var args, inst, fn;
  533.  
  534. if (this._rendered) {
  535. Y.log('Container already rendered.', 'warn', 'contentEditable');
  536.  
  537. return this;
  538. }
  539.  
  540. if (node) {
  541. this.set(CONTAINER, node);
  542. }
  543.  
  544. container = this.get(CONTAINER);
  545.  
  546. if (!container) {
  547. container = YNode.create(ContentEditable.HTML);
  548.  
  549. Y.one('body').prepend(container);
  550.  
  551. this.set(CONTAINER, container);
  552. }
  553.  
  554. this._rendered = true;
  555.  
  556. this._container.setAttribute(CONTENT_EDITABLE, true);
  557.  
  558. args = Y.clone(this.get(USE));
  559.  
  560. fn = Y.bind(function() {
  561. inst = YUI();
  562.  
  563. inst.host = this.get(HOST); //Cross reference to Editor
  564.  
  565. inst.log = Y.log; //Dump the instance logs to the parent instance.
  566.  
  567. Y.log('Creating new internal instance with node-base only', 'info', 'contenteditable');
  568. inst.use('node-base', Y.bind(this._instanceLoaded, this));
  569. }, this);
  570.  
  571. args.push(fn);
  572.  
  573. Y.log('Adding new modules to main instance: ' + args, 'info', 'contenteditable');
  574. Y.use.apply(Y, args);
  575.  
  576. return this;
  577. },
  578.  
  579. /**
  580. * Set the focus to the container
  581. * @method focus
  582. * @param {Function} fn Callback function to execute after focus happens
  583. * @return {ContentEditable}
  584. * @chainable
  585. */
  586. focus: function() {
  587. this._container.focus();
  588.  
  589. return this;
  590. },
  591. /**
  592. * Show the iframe instance
  593. * @method show
  594. * @return {ContentEditable}
  595. * @chainable
  596. */
  597. show: function() {
  598. this._container.show();
  599.  
  600. this.focus();
  601.  
  602. return this;
  603. },
  604.  
  605. /**
  606. * Hide the iframe instance
  607. * @method hide
  608. * @return {ContentEditable}
  609. * @chainable
  610. */
  611. hide: function() {
  612. this._container.hide();
  613.  
  614. return this;
  615. }
  616. },
  617. {
  618. /**
  619. * The throttle time for key events in IE
  620. * @static
  621. * @property THROTTLE_TIME
  622. * @type Number
  623. * @default 100
  624. */
  625. THROTTLE_TIME: 100,
  626.  
  627. /**
  628. * The DomEvents that the frame automatically attaches and bubbles
  629. * @static
  630. * @property DOM_EVENTS
  631. * @type Object
  632. */
  633. DOM_EVENTS: {
  634. click: 1,
  635. dblclick: 1,
  636. focusin: 1,
  637. focusout: 1,
  638. keydown: 1,
  639. keypress: 1,
  640. keyup: 1,
  641. mousedown: 1,
  642. mouseup: 1,
  643. paste: 1
  644. },
  645.  
  646. /**
  647. * The template string used to create the ContentEditable element
  648. * @static
  649. * @property HTML
  650. * @type String
  651. */
  652. HTML: '<div></div>',
  653.  
  654. /**
  655. * The name of the class (contentEditable)
  656. * @static
  657. * @property NAME
  658. * @type String
  659. */
  660. NAME: 'contentEditable',
  661.  
  662. /**
  663. * The namespace on which ContentEditable plugin will reside.
  664. *
  665. * @property NS
  666. * @type String
  667. * @default 'contentEditable'
  668. * @static
  669. */
  670. NS: CONTENT_EDITABLE,
  671.  
  672. ATTRS: {
  673. /**
  674. * The default text direction for this ContentEditable element. Default: ltr
  675. * @attribute dir
  676. * @type String
  677. */
  678. dir: {
  679. lazyAdd: false,
  680. validator: Lang.isString,
  681. setter: '_setDir',
  682. valueFn: '_getDir'
  683. },
  684.  
  685. /**
  686. * The container to set contentEditable=true or to create on render.
  687. * @attribute container
  688. * @type String/HTMLElement/Node
  689. */
  690. container: {
  691. setter: function(n) {
  692. this._container = Y.one(n);
  693.  
  694. return this._container;
  695. }
  696. },
  697.  
  698. /**
  699. * The string to inject as Editor content. Default '<br>'
  700. * @attribute content
  701. * @type String
  702. */
  703. content: {
  704. getter: '_getHTML',
  705. lazyAdd: false,
  706. setter: '_setHTML',
  707. validator: Lang.isString,
  708. value: '<br>'
  709. },
  710.  
  711. /**
  712. * The default tag to use for block level items, defaults to: p
  713. * @attribute defaultblock
  714. * @type String
  715. */
  716. defaultblock: {
  717. validator: Lang.isString,
  718. value: TAG_PARAGRAPH,
  719. valueFn: '_getDefaultBlock'
  720. },
  721.  
  722. /**
  723. * A string of CSS to add to the Head of the Editor
  724. * @attribute extracss
  725. * @type String
  726. */
  727. extracss: {
  728. lazyAdd: false,
  729. setter: '_setExtraCSS',
  730. validator: Lang.isString,
  731. valueFn: '_getExtraCSS'
  732. },
  733.  
  734. /**
  735. * Set the id of the new Node. (optional)
  736. * @attribute id
  737. * @type String
  738. * @writeonce
  739. */
  740. id: {
  741. writeOnce: true,
  742. getter: function(id) {
  743. if (!id) {
  744. id = 'inlineedit-' + Y.guid();
  745. }
  746.  
  747. return id;
  748. }
  749. },
  750.  
  751. /**
  752. * The default language. Default: en-US
  753. * @attribute lang
  754. * @type String
  755. */
  756. lang: {
  757. validator: Lang.isString,
  758. setter: '_setLang',
  759. lazyAdd: false,
  760. value: 'en-US'
  761. },
  762.  
  763. /**
  764. * An array of url's to external linked style sheets
  765. * @attribute linkedcss
  766. * @type String|Array
  767. */
  768. linkedcss: {
  769. setter: '_setLinkedCSS',
  770. validator: '_validateLinkedCSS'
  771. //value: ''
  772. },
  773.  
  774. /**
  775. * The Node instance of the container.
  776. * @attribute node
  777. * @type Node
  778. */
  779. node: {
  780. readOnly: true,
  781. value: null,
  782. getter: function() {
  783. return this._container;
  784. }
  785. },
  786.  
  787. /**
  788. * Array of modules to include in the scoped YUI instance at render time. Default: ['node-base', 'editor-selection', 'stylesheet']
  789. * @attribute use
  790. * @writeonce
  791. * @type Array
  792. */
  793. use: {
  794. validator: Lang.isArray,
  795. writeOnce: true,
  796. value: ['node-base', 'editor-selection', 'stylesheet']
  797. }
  798. }
  799. });
  800.  
  801. Y.namespace('Plugin');
  802.  
  803. Y.Plugin.ContentEditable = ContentEditable;