Dashboard sipadu mbip
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

exec-command.js 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. /*
  2. Copyright (c) 2010, Yahoo! Inc. All rights reserved.
  3. Code licensed under the BSD License:
  4. http://developer.yahoo.com/yui/license.html
  5. version: 3.4.0
  6. build: nightly
  7. */
  8. YUI.add('exec-command', function(Y) {
  9. /**
  10. * Plugin for the frame module to handle execCommands for Editor
  11. * @class Plugin.ExecCommand
  12. * @extends Base
  13. * @constructor
  14. * @module editor
  15. * @submodule exec-command
  16. */
  17. var ExecCommand = function() {
  18. ExecCommand.superclass.constructor.apply(this, arguments);
  19. };
  20. Y.extend(ExecCommand, Y.Base, {
  21. /**
  22. * An internal reference to the keyCode of the last key that was pressed.
  23. * @private
  24. * @property _lastKey
  25. */
  26. _lastKey: null,
  27. /**
  28. * An internal reference to the instance of the frame plugged into.
  29. * @private
  30. * @property _inst
  31. */
  32. _inst: null,
  33. /**
  34. * Execute a command on the frame's document.
  35. * @method command
  36. * @param {String} action The action to perform (bold, italic, fontname)
  37. * @param {String} value The optional value (helvetica)
  38. * @return {Node/NodeList} Should return the Node/Nodelist affected
  39. */
  40. command: function(action, value) {
  41. var fn = ExecCommand.COMMANDS[action];
  42. if (fn) {
  43. return fn.call(this, action, value);
  44. } else {
  45. return this._command(action, value);
  46. }
  47. },
  48. /**
  49. * The private version of execCommand that doesn't filter for overrides.
  50. * @private
  51. * @method _command
  52. * @param {String} action The action to perform (bold, italic, fontname)
  53. * @param {String} value The optional value (helvetica)
  54. */
  55. _command: function(action, value) {
  56. var inst = this.getInstance();
  57. try {
  58. try {
  59. inst.config.doc.execCommand('styleWithCSS', null, 1);
  60. } catch (e1) {
  61. try {
  62. inst.config.doc.execCommand('useCSS', null, 0);
  63. } catch (e2) {
  64. }
  65. }
  66. inst.config.doc.execCommand(action, null, value);
  67. } catch (e) {
  68. }
  69. },
  70. /**
  71. * Get's the instance of YUI bound to the parent frame
  72. * @method getInstance
  73. * @return {YUI} The YUI instance bound to the parent frame
  74. */
  75. getInstance: function() {
  76. if (!this._inst) {
  77. this._inst = this.get('host').getInstance();
  78. }
  79. return this._inst;
  80. },
  81. initializer: function() {
  82. Y.mix(this.get('host'), {
  83. execCommand: function(action, value) {
  84. return this.exec.command(action, value);
  85. },
  86. _execCommand: function(action, value) {
  87. return this.exec._command(action, value);
  88. }
  89. });
  90. this.get('host').on('dom:keypress', Y.bind(function(e) {
  91. this._lastKey = e.keyCode;
  92. }, this));
  93. }
  94. }, {
  95. /**
  96. * execCommand
  97. * @property NAME
  98. * @static
  99. */
  100. NAME: 'execCommand',
  101. /**
  102. * exec
  103. * @property NS
  104. * @static
  105. */
  106. NS: 'exec',
  107. ATTRS: {
  108. host: {
  109. value: false
  110. }
  111. },
  112. /**
  113. * Static object literal of execCommand overrides
  114. * @property COMMANDS
  115. * @static
  116. */
  117. COMMANDS: {
  118. /**
  119. * Wraps the content with a new element of type (tag)
  120. * @method COMMANDS.wrap
  121. * @static
  122. * @param {String} cmd The command executed: wrap
  123. * @param {String} tag The tag to wrap the selection with
  124. * @return {NodeList} NodeList of the items touched by this command.
  125. */
  126. wrap: function(cmd, tag) {
  127. var inst = this.getInstance();
  128. return (new inst.Selection()).wrapContent(tag);
  129. },
  130. /**
  131. * Inserts the provided HTML at the cursor, should be a single element.
  132. * @method COMMANDS.inserthtml
  133. * @static
  134. * @param {String} cmd The command executed: inserthtml
  135. * @param {String} html The html to insert
  136. * @return {Node} Node instance of the item touched by this command.
  137. */
  138. inserthtml: function(cmd, html) {
  139. var inst = this.getInstance();
  140. if (inst.Selection.hasCursor() || Y.UA.ie) {
  141. return (new inst.Selection()).insertContent(html);
  142. } else {
  143. this._command('inserthtml', html);
  144. }
  145. },
  146. /**
  147. * Inserts the provided HTML at the cursor, and focuses the cursor afterwards.
  148. * @method COMMANDS.insertandfocus
  149. * @static
  150. * @param {String} cmd The command executed: insertandfocus
  151. * @param {String} html The html to insert
  152. * @return {Node} Node instance of the item touched by this command.
  153. */
  154. insertandfocus: function(cmd, html) {
  155. var inst = this.getInstance(), out, sel;
  156. if (inst.Selection.hasCursor()) {
  157. html += inst.Selection.CURSOR;
  158. out = this.command('inserthtml', html);
  159. sel = new inst.Selection();
  160. sel.focusCursor(true, true);
  161. } else {
  162. this.command('inserthtml', html);
  163. }
  164. return out;
  165. },
  166. /**
  167. * Inserts a BR at the current cursor position
  168. * @method COMMANDS.insertbr
  169. * @static
  170. * @param {String} cmd The command executed: insertbr
  171. */
  172. insertbr: function(cmd) {
  173. var inst = this.getInstance(),
  174. sel = new inst.Selection(),
  175. html = '<var>|</var>', last = null,
  176. q = (Y.UA.webkit) ? 'span.Apple-style-span,var' : 'var';
  177. if (sel._selection.pasteHTML) {
  178. sel._selection.pasteHTML(html);
  179. } else {
  180. this._command('inserthtml', html);
  181. }
  182. var insert = function(n) {
  183. var c = inst.Node.create('<br>');
  184. n.insert(c, 'before');
  185. return c;
  186. };
  187. inst.all(q).each(function(n) {
  188. var g = true;
  189. if (Y.UA.webkit) {
  190. g = false;
  191. if (n.get('innerHTML') === '|') {
  192. g = true;
  193. }
  194. }
  195. if (g) {
  196. last = insert(n);
  197. if ((!last.previous() || !last.previous().test('br')) && Y.UA.gecko) {
  198. var s = last.cloneNode();
  199. last.insert(s, 'after');
  200. last = s;
  201. }
  202. n.remove();
  203. }
  204. });
  205. if (Y.UA.webkit && last) {
  206. insert(last);
  207. sel.selectNode(last);
  208. }
  209. },
  210. /**
  211. * Inserts an image at the cursor position
  212. * @method COMMANDS.insertimage
  213. * @static
  214. * @param {String} cmd The command executed: insertimage
  215. * @param {String} img The url of the image to be inserted
  216. * @return {Node} Node instance of the item touched by this command.
  217. */
  218. insertimage: function(cmd, img) {
  219. return this.command('inserthtml', '<img src="' + img + '">');
  220. },
  221. /**
  222. * Add a class to all of the elements in the selection
  223. * @method COMMANDS.addclass
  224. * @static
  225. * @param {String} cmd The command executed: addclass
  226. * @param {String} cls The className to add
  227. * @return {NodeList} NodeList of the items touched by this command.
  228. */
  229. addclass: function(cmd, cls) {
  230. var inst = this.getInstance();
  231. return (new inst.Selection()).getSelected().addClass(cls);
  232. },
  233. /**
  234. * Remove a class from all of the elements in the selection
  235. * @method COMMANDS.removeclass
  236. * @static
  237. * @param {String} cmd The command executed: removeclass
  238. * @param {String} cls The className to remove
  239. * @return {NodeList} NodeList of the items touched by this command.
  240. */
  241. removeclass: function(cmd, cls) {
  242. var inst = this.getInstance();
  243. return (new inst.Selection()).getSelected().removeClass(cls);
  244. },
  245. /**
  246. * Adds a forecolor to the current selection, or creates a new element and applies it
  247. * @method COMMANDS.forecolor
  248. * @static
  249. * @param {String} cmd The command executed: forecolor
  250. * @param {String} val The color value to apply
  251. * @return {NodeList} NodeList of the items touched by this command.
  252. */
  253. forecolor: function(cmd, val) {
  254. var inst = this.getInstance(),
  255. sel = new inst.Selection(), n;
  256. if (!Y.UA.ie) {
  257. this._command('useCSS', false);
  258. }
  259. if (inst.Selection.hasCursor()) {
  260. if (sel.isCollapsed) {
  261. if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === '&nbsp;')) {
  262. sel.anchorNode.setStyle('color', val);
  263. n = sel.anchorNode;
  264. } else {
  265. n = this.command('inserthtml', '<span style="color: ' + val + '">' + inst.Selection.CURSOR + '</span>');
  266. sel.focusCursor(true, true);
  267. }
  268. return n;
  269. } else {
  270. return this._command(cmd, val);
  271. }
  272. } else {
  273. this._command(cmd, val);
  274. }
  275. },
  276. /**
  277. * Adds a background color to the current selection, or creates a new element and applies it
  278. * @method COMMANDS.backcolor
  279. * @static
  280. * @param {String} cmd The command executed: backcolor
  281. * @param {String} val The color value to apply
  282. * @return {NodeList} NodeList of the items touched by this command.
  283. */
  284. backcolor: function(cmd, val) {
  285. var inst = this.getInstance(),
  286. sel = new inst.Selection(), n;
  287. if (Y.UA.gecko || Y.UA.opera) {
  288. cmd = 'hilitecolor';
  289. }
  290. if (!Y.UA.ie) {
  291. this._command('useCSS', false);
  292. }
  293. if (inst.Selection.hasCursor()) {
  294. if (sel.isCollapsed) {
  295. if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === '&nbsp;')) {
  296. sel.anchorNode.setStyle('backgroundColor', val);
  297. n = sel.anchorNode;
  298. } else {
  299. n = this.command('inserthtml', '<span style="background-color: ' + val + '">' + inst.Selection.CURSOR + '</span>');
  300. sel.focusCursor(true, true);
  301. }
  302. return n;
  303. } else {
  304. return this._command(cmd, val);
  305. }
  306. } else {
  307. this._command(cmd, val);
  308. }
  309. },
  310. /**
  311. * Sugar method, calles backcolor
  312. * @method COMMANDS.hilitecolor
  313. * @static
  314. * @param {String} cmd The command executed: backcolor
  315. * @param {String} val The color value to apply
  316. * @return {NodeList} NodeList of the items touched by this command.
  317. */
  318. hilitecolor: function() {
  319. return ExecCommand.COMMANDS.backcolor.apply(this, arguments);
  320. },
  321. /**
  322. * Adds a font name to the current selection, or creates a new element and applies it
  323. * @method COMMANDS.fontname2
  324. * @deprecated
  325. * @static
  326. * @param {String} cmd The command executed: fontname
  327. * @param {String} val The font name to apply
  328. * @return {NodeList} NodeList of the items touched by this command.
  329. */
  330. fontname2: function(cmd, val) {
  331. this._command('fontname', val);
  332. var inst = this.getInstance(),
  333. sel = new inst.Selection();
  334. if (sel.isCollapsed && (this._lastKey != 32)) {
  335. if (sel.anchorNode.test('font')) {
  336. sel.anchorNode.set('face', val);
  337. }
  338. }
  339. },
  340. /**
  341. * Adds a fontsize to the current selection, or creates a new element and applies it
  342. * @method COMMANDS.fontsize2
  343. * @deprecated
  344. * @static
  345. * @param {String} cmd The command executed: fontsize
  346. * @param {String} val The font size to apply
  347. * @return {NodeList} NodeList of the items touched by this command.
  348. */
  349. fontsize2: function(cmd, val) {
  350. this._command('fontsize', val);
  351. var inst = this.getInstance(),
  352. sel = new inst.Selection();
  353. if (sel.isCollapsed && sel.anchorNode && (this._lastKey != 32)) {
  354. if (Y.UA.webkit) {
  355. if (sel.anchorNode.getStyle('lineHeight')) {
  356. sel.anchorNode.setStyle('lineHeight', '');
  357. }
  358. }
  359. if (sel.anchorNode.test('font')) {
  360. sel.anchorNode.set('size', val);
  361. } else if (Y.UA.gecko) {
  362. var p = sel.anchorNode.ancestor(inst.Selection.DEFAULT_BLOCK_TAG);
  363. if (p) {
  364. p.setStyle('fontSize', '');
  365. }
  366. }
  367. }
  368. },
  369. /**
  370. * Overload for COMMANDS.list
  371. * @method COMMANDS.insertorderedlist
  372. * @static
  373. * @param {String} cmd The command executed: list, ul
  374. */
  375. insertunorderedlist: function(cmd) {
  376. this.command('list', 'ul');
  377. },
  378. /**
  379. * Overload for COMMANDS.list
  380. * @method COMMANDS.insertunorderedlist
  381. * @static
  382. * @param {String} cmd The command executed: list, ol
  383. */
  384. insertorderedlist: function(cmd) {
  385. this.command('list', 'ol');
  386. },
  387. /**
  388. * Noramlizes lists creation/destruction for IE. All others pass through to native calls
  389. * @method COMMANDS.list
  390. * @static
  391. * @param {String} cmd The command executed: list (not used)
  392. * @param {String} tag The tag to deal with
  393. */
  394. list: function(cmd, tag) {
  395. var inst = this.getInstance(), html,
  396. DIR = 'dir', cls = 'yui3-touched',
  397. dir, range, div, elm, n, str, s, par, list, lis,
  398. useP = (inst.host.editorPara ? true : false),
  399. sel = new inst.Selection();
  400. cmd = 'insert' + ((tag === 'ul') ? 'un' : '') + 'orderedlist';
  401. if (Y.UA.ie && !sel.isCollapsed) {
  402. range = sel._selection;
  403. html = range.htmlText;
  404. div = inst.Node.create(html);
  405. if (div.test('li') || div.one('li')) {
  406. this._command(cmd, null);
  407. return;
  408. }
  409. if (div.test(tag)) {
  410. elm = range.item ? range.item(0) : range.parentElement();
  411. n = inst.one(elm);
  412. lis = n.all('li');
  413. str = '<div>';
  414. lis.each(function(l) {
  415. if (useP) {
  416. str += '<p>' + l.get('innerHTML') + '</p>';
  417. } else {
  418. str += l.get('innerHTML') + '<br>';
  419. }
  420. });
  421. str += '</div>';
  422. s = inst.Node.create(str);
  423. if (n.get('parentNode').test('div')) {
  424. n = n.get('parentNode');
  425. }
  426. if (n && n.hasAttribute(DIR)) {
  427. if (useP) {
  428. s.all('p').setAttribute(DIR, n.getAttribute(DIR));
  429. } else {
  430. s.setAttribute(DIR, n.getAttribute(DIR));
  431. }
  432. }
  433. if (useP) {
  434. n.replace(s.get('innerHTML'));
  435. } else {
  436. n.replace(s);
  437. }
  438. if (range.moveToElementText) {
  439. range.moveToElementText(s._node);
  440. }
  441. range.select();
  442. } else {
  443. par = Y.one(range.parentElement());
  444. if (!par.test(inst.Selection.BLOCKS)) {
  445. par = par.ancestor(inst.Selection.BLOCKS);
  446. }
  447. if (par) {
  448. if (par.hasAttribute(DIR)) {
  449. dir = par.getAttribute(DIR);
  450. }
  451. }
  452. if (html.indexOf('<br>') > -1) {
  453. html = html.split(/<br>/i);
  454. } else {
  455. var tmp = inst.Node.create(html),
  456. ps = tmp.all('p');
  457. if (ps.size()) {
  458. html = [];
  459. ps.each(function(n) {
  460. html.push(n.get('innerHTML'));
  461. });
  462. } else {
  463. html = [html];
  464. }
  465. }
  466. list = '<' + tag + ' id="ie-list">';
  467. Y.each(html, function(v) {
  468. var a = inst.Node.create(v);
  469. if (a.test('p')) {
  470. if (a.hasAttribute(DIR)) {
  471. dir = a.getAttribute(DIR);
  472. }
  473. v = a.get('innerHTML');
  474. }
  475. list += '<li>' + v + '</li>';
  476. });
  477. list += '</' + tag + '>';
  478. range.pasteHTML(list);
  479. elm = inst.config.doc.getElementById('ie-list');
  480. elm.id = '';
  481. if (dir) {
  482. elm.setAttribute(DIR, dir);
  483. }
  484. if (range.moveToElementText) {
  485. range.moveToElementText(elm);
  486. }
  487. range.select();
  488. }
  489. } else if (Y.UA.ie) {
  490. par = inst.one(sel._selection.parentElement());
  491. if (par.test('p')) {
  492. if (par && par.hasAttribute(DIR)) {
  493. dir = par.getAttribute(DIR);
  494. }
  495. html = Y.Selection.getText(par);
  496. if (html === '') {
  497. var sdir = '';
  498. if (dir) {
  499. sdir = ' dir="' + dir + '"';
  500. }
  501. list = inst.Node.create(Y.Lang.sub('<{tag}{dir}><li></li></{tag}>', { tag: tag, dir: sdir }));
  502. par.replace(list);
  503. sel.selectNode(list.one('li'));
  504. } else {
  505. this._command(cmd, null);
  506. }
  507. } else {
  508. this._command(cmd, null);
  509. }
  510. } else {
  511. inst.all(tag).addClass(cls);
  512. if (sel.anchorNode.test(inst.Selection.BLOCKS)) {
  513. par = sel.anchorNode;
  514. } else {
  515. par = sel.anchorNode.ancestor(inst.Selection.BLOCKS);
  516. }
  517. if (!par) { //No parent, find the first block under the anchorNode
  518. par = sel.anchorNode.one(inst.Selection.BLOCKS);
  519. }
  520. if (par && par.hasAttribute(DIR)) {
  521. dir = par.getAttribute(DIR);
  522. }
  523. if (par && par.test(tag)) {
  524. html = inst.Node.create('<div/>');
  525. elm = par.all('li');
  526. elm.each(function(h) {
  527. if (useP) {
  528. html.append('<p>' + h.get('innerHTML') + '</p>');
  529. } else {
  530. html.append(h.get('innerHTML') + '<br>');
  531. }
  532. });
  533. if (dir) {
  534. if (useP) {
  535. html.all('p').setAttribute(DIR, dir);
  536. } else {
  537. html.setAttribute(DIR, dir);
  538. }
  539. }
  540. if (useP) {
  541. par.replace(html.get('innerHTML'));
  542. } else {
  543. par.replace(html);
  544. }
  545. sel.selectNode(html.get('firstChild'));
  546. } else {
  547. this._command(cmd, null);
  548. }
  549. list = inst.all(tag);
  550. if (dir) {
  551. if (list.size()) {
  552. //Changed to a List
  553. list.each(function(n) {
  554. if (!n.hasClass(cls)) {
  555. n.setAttribute(DIR, dir);
  556. }
  557. });
  558. }
  559. }
  560. list.removeClass(cls);
  561. }
  562. },
  563. /**
  564. * Noramlizes alignment for Webkit Browsers
  565. * @method COMMANDS.justify
  566. * @static
  567. * @param {String} cmd The command executed: justify (not used)
  568. * @param {String} val The actual command from the justify{center,all,left,right} stubs
  569. */
  570. justify: function(cmd, val) {
  571. if (Y.UA.webkit) {
  572. var inst = this.getInstance(),
  573. sel = new inst.Selection(),
  574. aNode = sel.anchorNode;
  575. var bgColor = aNode.getStyle('backgroundColor');
  576. this._command(val);
  577. sel = new inst.Selection();
  578. if (sel.anchorNode.test('div')) {
  579. var html = '<span>' + sel.anchorNode.get('innerHTML') + '</span>';
  580. sel.anchorNode.set('innerHTML', html);
  581. sel.anchorNode.one('span').setStyle('backgroundColor', bgColor);
  582. sel.selectNode(sel.anchorNode.one('span'));
  583. }
  584. } else {
  585. this._command(val);
  586. }
  587. },
  588. /**
  589. * Override method for COMMANDS.justify
  590. * @method COMMANDS.justifycenter
  591. * @static
  592. */
  593. justifycenter: function(cmd) {
  594. this.command('justify', 'justifycenter');
  595. },
  596. /**
  597. * Override method for COMMANDS.justify
  598. * @method COMMANDS.justifyleft
  599. * @static
  600. */
  601. justifyleft: function(cmd) {
  602. this.command('justify', 'justifyleft');
  603. },
  604. /**
  605. * Override method for COMMANDS.justify
  606. * @method COMMANDS.justifyright
  607. * @static
  608. */
  609. justifyright: function(cmd) {
  610. this.command('justify', 'justifyright');
  611. },
  612. /**
  613. * Override method for COMMANDS.justify
  614. * @method COMMANDS.justifyfull
  615. * @static
  616. */
  617. justifyfull: function(cmd) {
  618. this.command('justify', 'justifyfull');
  619. }
  620. }
  621. });
  622. /**
  623. * This method is meant to normalize IE's in ability to exec the proper command on elements with CSS styling.
  624. * @method fixIETags
  625. * @protected
  626. * @param {String} cmd The command to execute
  627. * @param {String} tag The tag to create
  628. * @param {String} rule The rule that we are looking for.
  629. */
  630. var fixIETags = function(cmd, tag, rule) {
  631. var inst = this.getInstance(),
  632. doc = inst.config.doc,
  633. sel = doc.selection.createRange(),
  634. o = doc.queryCommandValue(cmd),
  635. html, reg, m, p, d, s, c;
  636. if (o) {
  637. html = sel.htmlText;
  638. reg = new RegExp(rule, 'g');
  639. m = html.match(reg);
  640. if (m) {
  641. html = html.replace(rule + ';', '').replace(rule, '');
  642. sel.pasteHTML('<var id="yui-ie-bs">');
  643. p = doc.getElementById('yui-ie-bs');
  644. d = doc.createElement('div');
  645. s = doc.createElement(tag);
  646. d.innerHTML = html;
  647. if (p.parentNode !== inst.config.doc.body) {
  648. p = p.parentNode;
  649. }
  650. c = d.childNodes;
  651. p.parentNode.replaceChild(s, p);
  652. Y.each(c, function(f) {
  653. s.appendChild(f);
  654. });
  655. sel.collapse();
  656. if (sel.moveToElementText) {
  657. sel.moveToElementText(s);
  658. }
  659. sel.select();
  660. }
  661. }
  662. this._command(cmd);
  663. };
  664. if (Y.UA.ie) {
  665. ExecCommand.COMMANDS.bold = function() {
  666. fixIETags.call(this, 'bold', 'b', 'FONT-WEIGHT: bold');
  667. };
  668. ExecCommand.COMMANDS.italic = function() {
  669. fixIETags.call(this, 'italic', 'i', 'FONT-STYLE: italic');
  670. };
  671. ExecCommand.COMMANDS.underline = function() {
  672. fixIETags.call(this, 'underline', 'u', 'TEXT-DECORATION: underline');
  673. };
  674. }
  675. Y.namespace('Plugin');
  676. Y.Plugin.ExecCommand = ExecCommand;
  677. }, '3.4.0' ,{skinnable:false, requires:['frame']});