Blockquote.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. define("dojox/editor/plugins/Blockquote", [
  2. "dojo",
  3. "dijit",
  4. "dojox",
  5. "dijit/_editor/range",
  6. "dijit/_editor/selection",
  7. "dijit/_editor/_Plugin",
  8. "dijit/form/ToggleButton",
  9. "dojo/_base/connect",
  10. "dojo/_base/declare",
  11. "dojo/i18n",
  12. "dojo/i18n!dojox/editor/plugins/nls/Blockquote"
  13. ], function(dojo, dijit, dojox) {
  14. dojo.declare("dojox.editor.plugins.Blockquote",dijit._editor._Plugin,{
  15. // summary:
  16. // This plugin provides Blockquote cabability to the editor.
  17. // window/tab
  18. // iconClassPrefix: [const] String
  19. // The CSS class name for the button node icon.
  20. iconClassPrefix: "dijitAdditionalEditorIcon",
  21. _initButton: function(){
  22. // summary:
  23. // Over-ride for creation of the preview button.
  24. this._nlsResources = dojo.i18n.getLocalization("dojox.editor.plugins", "Blockquote");
  25. this.button = new dijit.form.ToggleButton({
  26. label: this._nlsResources["blockquote"],
  27. showLabel: false,
  28. iconClass: this.iconClassPrefix + " " + this.iconClassPrefix + "Blockquote",
  29. tabIndex: "-1",
  30. onClick: dojo.hitch(this, "_toggleQuote")
  31. });
  32. },
  33. setEditor: function(editor){
  34. // summary:
  35. // Over-ride for the setting of the editor.
  36. // editor: Object
  37. // The editor to configure for this plugin to use.
  38. this.editor = editor;
  39. this._initButton();
  40. this.connect(this.editor, "onNormalizedDisplayChanged", "updateState");
  41. // We need the custom undo code since we manipulate the dom
  42. // outside of the browser natives and only customUndo really handles
  43. // that. It will incur a performance hit, but should hopefully be
  44. // relatively small.
  45. editor.customUndo = true;
  46. },
  47. _toggleQuote: function(arg){
  48. // summary:
  49. // Function to trigger previewing of the editor document
  50. // tags:
  51. // private
  52. try{
  53. var ed = this.editor;
  54. ed.focus();
  55. var quoteIt = this.button.get("checked");
  56. var sel = dijit.range.getSelection(ed.window);
  57. var range, elem, start, end;
  58. if(sel && sel.rangeCount > 0){
  59. range = sel.getRangeAt(0);
  60. }
  61. if(range){
  62. ed.beginEditing();
  63. if(quoteIt){
  64. // Lets see what we've got as a selection...
  65. var bq, tag;
  66. if(range.startContainer === range.endContainer){
  67. // No selection, just cursor point, we need to see if we're
  68. // in an indentable block, or similar.
  69. if(this._isRootInline(range.startContainer)){
  70. // Text at the 'root' of the document, so we need to gather all of it.,
  71. // First, we need to find the toplevel inline element that is rooted
  72. // to the document 'editNode'
  73. start = range.startContainer;
  74. while(start && start.parentNode !== ed.editNode){
  75. start = start.parentNode;
  76. }
  77. // Now we need to walk up its siblings and look for the first one in the rooting
  78. // that isn't inline or text, as we want to grab all of that for indent.
  79. while(start && start.previousSibling && (
  80. this._isTextElement(start) ||
  81. (start.nodeType === 1 &&
  82. this._isInlineFormat(this._getTagName(start))
  83. ))){
  84. start = start.previousSibling;
  85. }
  86. if(start && start.nodeType === 1 &&
  87. !this._isInlineFormat(this._getTagName(start))){
  88. // Adjust slightly, we're one node too far back in this case.
  89. start = start.nextSibling;
  90. }
  91. // Okay, we have a configured start, lets grab everything following it that's
  92. // inline and make it part of the blockquote!
  93. if(start){
  94. bq = ed.document.createElement("blockquote");
  95. dojo.place(bq, start, "after");
  96. bq.appendChild(start);
  97. end = bq.nextSibling;
  98. while(end && (
  99. this._isTextElement(end) ||
  100. (end.nodeType === 1 &&
  101. this._isInlineFormat(this._getTagName(end)))
  102. )){
  103. // Add it.
  104. bq.appendChild(end);
  105. end = bq.nextSibling;
  106. }
  107. }
  108. }else{
  109. // Figure out what to do when not root inline....
  110. var node = range.startContainer;
  111. while ((this._isTextElement(node) ||
  112. this._isInlineFormat(this._getTagName(node))
  113. || this._getTagName(node) === "li") &&
  114. node !== ed.editNode && node !== ed.document.body){
  115. node = node.parentNode;
  116. }
  117. if(node !== ed.editNode && node !== node.ownerDocument.documentElement){
  118. bq = ed.document.createElement("blockquote");
  119. dojo.place(bq, node, "after");
  120. bq.appendChild(node);
  121. }
  122. }
  123. if(bq){
  124. dojo.withGlobal(ed.window,
  125. "selectElementChildren", dijit._editor.selection, [bq]);
  126. dojo.withGlobal(ed.window,
  127. "collapse", dijit._editor.selection, [true]);
  128. }
  129. }else{
  130. var curNode;
  131. // multi-node select. We need to scan over them.
  132. // Find the two containing nodes at start and end.
  133. // then move the end one node past. Then ... lets see
  134. // what we can blockquote!
  135. start = range.startContainer;
  136. end = range.endContainer;
  137. // Find the non-text nodes.
  138. while(start && this._isTextElement(start) && start.parentNode !== ed.editNode){
  139. start = start.parentNode;
  140. }
  141. // Try to find the end node. We have to check the selection junk
  142. curNode = start;
  143. while(curNode.nextSibling && dojo.withGlobal(ed.window,
  144. "inSelection", dijit._editor.selection, [curNode])){
  145. curNode = curNode.nextSibling;
  146. }
  147. end = curNode;
  148. if(end === ed.editNode || end === ed.document.body){
  149. // Unable to determine real selection end, so just make it
  150. // a single node indent of start + all following inline styles, if
  151. // present, then just exit.
  152. bq = ed.document.createElement("blockquote");
  153. dojo.place(bq, start, "after");
  154. tag = this._getTagName(start);
  155. if(this._isTextElement(start) || this._isInlineFormat(tag)){
  156. // inline element or textnode
  157. // Find and move all inline tags following the one we inserted also into the
  158. // blockquote so we don't split up content funny.
  159. var next = start;
  160. while(next && (
  161. this._isTextElement(next) ||
  162. (next.nodeType === 1 &&
  163. this._isInlineFormat(this._getTagName(next))))){
  164. bq.appendChild(next);
  165. next = bq.nextSibling;
  166. }
  167. }else{
  168. bq.appendChild(start);
  169. }
  170. return;
  171. }
  172. // Has a definite end somewhere, so lets try to blockquote up to it.
  173. // requires looking at the selections and in some cases, moving nodes
  174. // into separate blockquotes.
  175. end = end.nextSibling;
  176. curNode = start;
  177. while(curNode && curNode !== end){
  178. if(curNode.nodeType === 1){
  179. tag = this._getTagName(curNode);
  180. if(tag !== "br"){
  181. if(!window.getSelection){
  182. // IE sometimes inserts blank P tags, which we want to skip
  183. // as they end up blockquoted, which messes up layout.
  184. if(tag === "p" && this._isEmpty(curNode)){
  185. curNode = curNode.nextSibling;
  186. continue;
  187. }
  188. }
  189. if(this._isInlineFormat(tag)){
  190. // inline tag.
  191. if(!bq){
  192. bq = ed.document.createElement("blockquote");
  193. dojo.place(bq, curNode, "after");
  194. bq.appendChild(curNode);
  195. }else{
  196. bq.appendChild(curNode);
  197. }
  198. curNode = bq;
  199. }else{
  200. if(bq){
  201. if(this._isEmpty(bq)){
  202. bq.parentNode.removeChild(bq);
  203. }
  204. }
  205. bq = ed.document.createElement("blockquote");
  206. dojo.place(bq, curNode, "after");
  207. bq.appendChild(curNode);
  208. curNode = bq;
  209. }
  210. }
  211. }else if(this._isTextElement(curNode)){
  212. if(!bq){
  213. bq = ed.document.createElement("blockquote");
  214. dojo.place(bq, curNode, "after");
  215. bq.appendChild(curNode);
  216. }else{
  217. bq.appendChild(curNode);
  218. }
  219. curNode = bq;
  220. }
  221. curNode = curNode.nextSibling;
  222. }
  223. // Okay, check the last bq, remove it if no content.
  224. if(bq){
  225. if(this._isEmpty(bq)){
  226. bq.parentNode.removeChild(bq);
  227. }else{
  228. dojo.withGlobal(ed.window,
  229. "selectElementChildren", dijit._editor.selection, [bq]);
  230. dojo.withGlobal(ed.window,
  231. "collapse", dijit._editor.selection, [true]);
  232. }
  233. bq = null;
  234. }
  235. }
  236. }else{
  237. var found = false;
  238. if(range.startContainer === range.endContainer){
  239. elem = range.endContainer;
  240. // Okay, now see if we can find one of the formatting types we're in.
  241. while(elem && elem !== ed.editNode && elem !== ed.document.body){
  242. var tg = elem.tagName?elem.tagName.toLowerCase():"";
  243. if(tg === "blockquote"){
  244. found = true;
  245. break;
  246. }
  247. elem = elem.parentNode;
  248. }
  249. if(found){
  250. var lastChild;
  251. while(elem.firstChild){
  252. lastChild = elem.firstChild;
  253. dojo.place(lastChild, elem, "before");
  254. }
  255. elem.parentNode.removeChild(elem);
  256. if(lastChild){
  257. dojo.withGlobal(ed.window,
  258. "selectElementChildren", dijit._editor.selection, [lastChild]);
  259. dojo.withGlobal(ed.window,
  260. "collapse", dijit._editor.selection, [true]);
  261. }
  262. }
  263. }else{
  264. // Multi-select! Gotta find all the blockquotes contained within the selection area.
  265. start = range.startContainer;
  266. end = range.endContainer;
  267. while(start && this._isTextElement(start) && start.parentNode !== ed.editNode){
  268. start = start.parentNode;
  269. }
  270. var selectedNodes = [];
  271. var cNode = start;
  272. while(cNode && cNode.nextSibling && dojo.withGlobal(ed.window,
  273. "inSelection", dijit._editor.selection, [cNode])){
  274. if(cNode.parentNode && this._getTagName(cNode.parentNode) === "blockquote"){
  275. cNode = cNode.parentNode;
  276. }
  277. selectedNodes.push(cNode);
  278. cNode = cNode.nextSibling;
  279. }
  280. // Find all the blocknodes now that we know the selection area.
  281. var bnNodes = this._findBlockQuotes(selectedNodes);
  282. while(bnNodes.length){
  283. var bn = bnNodes.pop();
  284. if(bn.parentNode){
  285. // Make sure we haven't seen this before and removed it.
  286. while(bn.firstChild){
  287. dojo.place(bn.firstChild, bn, "before");
  288. }
  289. bn.parentNode.removeChild(bn);
  290. }
  291. }
  292. }
  293. }
  294. ed.endEditing();
  295. }
  296. ed.onNormalizedDisplayChanged();
  297. }catch(e){ /* Squelch */ }
  298. },
  299. updateState: function(){
  300. // summary:
  301. // Overrides _Plugin.updateState(). This controls whether or not the current
  302. // cursor position should toggle on the quote button or not.
  303. // tags:
  304. // protected
  305. var ed = this.editor;
  306. var disabled = this.get("disabled");
  307. if(!ed || !ed.isLoaded){ return; }
  308. if(this.button){
  309. this.button.set("disabled", disabled);
  310. if(disabled){
  311. return;
  312. }
  313. // Some browsers (WebKit) doesn't actually get the tag info right.
  314. // So ... lets check it manually.
  315. var elem;
  316. var found = false;
  317. // Try to find the ansestor element (and see if it is blockquote)
  318. var sel = dijit.range.getSelection(ed.window);
  319. if(sel && sel.rangeCount > 0){
  320. var range = sel.getRangeAt(0);
  321. if(range){
  322. elem = range.endContainer;
  323. }
  324. }
  325. // Okay, now see if we can find one of the formatting types we're in.
  326. while(elem && elem !== ed.editNode && elem !== ed.document){
  327. var tg = elem.tagName?elem.tagName.toLowerCase():"";
  328. if(tg === "blockquote"){
  329. found = true;
  330. break;
  331. }
  332. elem = elem.parentNode;
  333. }
  334. // toggle whether or not the current selection is blockquoted.
  335. this.button.set("checked", found);
  336. }
  337. },
  338. _findBlockQuotes: function(nodeList){
  339. // summary:
  340. // function to find a ll the blocknode elements in a collection of
  341. // nodes
  342. // nodeList:
  343. // The list of nodes.
  344. // tags:
  345. // private
  346. var bnList = [];
  347. if(nodeList){
  348. var i;
  349. for(i = 0; i < nodeList.length; i++){
  350. var node = nodeList[i];
  351. if(node.nodeType === 1){
  352. if(this._getTagName(node) === "blockquote"){
  353. bnList.push(node);
  354. }
  355. if(node.childNodes && node.childNodes.length > 0){
  356. bnList = bnList.concat(this._findBlockQuotes(node.childNodes));
  357. }
  358. }
  359. }
  360. }
  361. return bnList;
  362. },
  363. /*****************************************************************/
  364. /* Functions borrowed from NormalizeIndentOutdent */
  365. /*****************************************************************/
  366. _getTagName: function(node){
  367. // summary:
  368. // Internal function to get the tag name of an element
  369. // if any.
  370. // node:
  371. // The node to look at.
  372. // tags:
  373. // private
  374. var tag = "";
  375. if(node && node.nodeType === 1){
  376. tag = node.tagName?node.tagName.toLowerCase():"";
  377. }
  378. return tag;
  379. },
  380. _isRootInline: function(node){
  381. // summary:
  382. // This functions tests whether an indicated node is in root as inline
  383. // or rooted inline elements in the page.
  384. // node:
  385. // The node to start at.
  386. // tags:
  387. // private
  388. var ed = this.editor;
  389. if(this._isTextElement(node) && node.parentNode === ed.editNode){
  390. return true;
  391. }else if(node.nodeType === 1 && this._isInlineFormat(node) && node.parentNode === ed.editNode){
  392. return true;
  393. }else if(this._isTextElement(node) && this._isInlineFormat(this._getTagName(node.parentNode))){
  394. node = node.parentNode;
  395. while(node && node !== ed.editNode && this._isInlineFormat(this._getTagName(node))){
  396. node = node.parentNode;
  397. }
  398. if(node === ed.editNode){
  399. return true;
  400. }
  401. }
  402. return false;
  403. },
  404. _isTextElement: function(node){
  405. // summary:
  406. // Helper function to check for text nodes.
  407. // node:
  408. // The node to check.
  409. // tags:
  410. // private
  411. if(node && node.nodeType === 3 || node.nodeType === 4){
  412. return true;
  413. }
  414. return false;
  415. },
  416. _isEmpty: function(node){
  417. // summary:
  418. // Internal function to determine if a node is 'empty'
  419. // Eg, contains only blank text. Used to determine if
  420. // an empty list element should be removed or not.
  421. // node:
  422. // The node to check.
  423. // tags:
  424. // private
  425. if(node.childNodes){
  426. var empty = true;
  427. var i;
  428. for(i = 0; i < node.childNodes.length; i++){
  429. var n = node.childNodes[i];
  430. if(n.nodeType === 1){
  431. if(this._getTagName(n) === "p"){
  432. if(!dojo.trim(n.innerHTML)){
  433. continue;
  434. }
  435. }
  436. empty = false;
  437. break;
  438. }else if(this._isTextElement(n)){
  439. // Check for empty text.
  440. var nv = dojo.trim(n.nodeValue);
  441. if(nv && nv !=="&nbsp;" && nv !== "\u00A0"){
  442. empty = false;
  443. break;
  444. }
  445. }else{
  446. empty = false;
  447. break;
  448. }
  449. }
  450. return empty;
  451. }else{
  452. return true;
  453. }
  454. },
  455. _isInlineFormat: function(tag){
  456. // summary:
  457. // Function to determine if the current tag is an inline
  458. // element that does formatting, as we don't want to
  459. // break/indent around it, as it can screw up text.
  460. // tag:
  461. // The tag to examine
  462. // tags:
  463. // private
  464. switch(tag){
  465. case "a":
  466. case "b":
  467. case "strong":
  468. case "s":
  469. case "strike":
  470. case "i":
  471. case "u":
  472. case "em":
  473. case "sup":
  474. case "sub":
  475. case "span":
  476. case "font":
  477. case "big":
  478. case "cite":
  479. case "q":
  480. case "img":
  481. case "small":
  482. return true;
  483. default:
  484. return false;
  485. }
  486. }
  487. });
  488. // Register this plugin.
  489. dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){
  490. if(o.plugin){ return; }
  491. var name = o.args.name.toLowerCase();
  492. if(name === "blockquote"){
  493. o.plugin = new dojox.editor.plugins.Blockquote({});
  494. }
  495. });
  496. return dojox.editor.plugins.Blockquote;
  497. });