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