ViewSource.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. define("dijit/_editor/plugins/ViewSource", [
  2. "dojo/_base/array", // array.forEach
  3. "dojo/_base/declare", // declare
  4. "dojo/dom-attr", // domAttr.set
  5. "dojo/dom-construct", // domConstruct.create domConstruct.place
  6. "dojo/dom-geometry", // domGeometry.setMarginBox domGeometry.position
  7. "dojo/dom-style", // domStyle.set
  8. "dojo/_base/event", // event.stop
  9. "dojo/i18n", // i18n.getLocalization
  10. "dojo/keys", // keys.F12
  11. "dojo/_base/lang", // lang.hitch
  12. "dojo/on", // on()
  13. "dojo/_base/sniff", // has("ie") has("webkit")
  14. "dojo/_base/window", // win.body win.global
  15. "dojo/window", // winUtils.getBox
  16. "../../focus", // focus.focus()
  17. "../_Plugin",
  18. "../../form/ToggleButton",
  19. "../..", // dijit._scopeName
  20. "../../registry", // registry.getEnclosingWidget()
  21. "dojo/i18n!../nls/commands"
  22. ], function(array, declare, domAttr, domConstruct, domGeometry, domStyle, event, i18n, keys, lang, on, has, win,
  23. winUtils, focus, _Plugin, ToggleButton, dijit, registry){
  24. /*=====
  25. var _Plugin = dijit._editor._Plugin;
  26. =====*/
  27. // module:
  28. // dijit/_editor/plugins/ViewSource
  29. // summary:
  30. // This plugin provides a simple view source capability.
  31. var ViewSource = declare("dijit._editor.plugins.ViewSource",_Plugin, {
  32. // summary:
  33. // This plugin provides a simple view source capability. When view
  34. // source mode is enabled, it disables all other buttons/plugins on the RTE.
  35. // It also binds to the hotkey: CTRL-SHIFT-F11 for toggling ViewSource mode.
  36. // stripScripts: [public] Boolean
  37. // Boolean flag used to indicate if script tags should be stripped from the document.
  38. // Defaults to true.
  39. stripScripts: true,
  40. // stripComments: [public] Boolean
  41. // Boolean flag used to indicate if comment tags should be stripped from the document.
  42. // Defaults to true.
  43. stripComments: true,
  44. // stripComments: [public] Boolean
  45. // Boolean flag used to indicate if iframe tags should be stripped from the document.
  46. // Defaults to true.
  47. stripIFrames: true,
  48. // readOnly: [const] Boolean
  49. // Boolean flag used to indicate if the source view should be readonly or not.
  50. // Cannot be changed after initialization of the plugin.
  51. // Defaults to false.
  52. readOnly: false,
  53. // _fsPlugin: [private] Object
  54. // Reference to a registered fullscreen plugin so that viewSource knows
  55. // how to scale.
  56. _fsPlugin: null,
  57. toggle: function(){
  58. // summary:
  59. // Function to allow programmatic toggling of the view.
  60. // For Webkit, we have to focus a very particular way.
  61. // when swapping views, otherwise focus doesn't shift right
  62. // but can't focus this way all the time, only for VS changes.
  63. // If we did it all the time, buttons like bold, italic, etc
  64. // break.
  65. if(has("webkit")){this._vsFocused = true;}
  66. this.button.set("checked", !this.button.get("checked"));
  67. },
  68. _initButton: function(){
  69. // summary:
  70. // Over-ride for creation of the resize button.
  71. var strings = i18n.getLocalization("dijit._editor", "commands"),
  72. editor = this.editor;
  73. this.button = new ToggleButton({
  74. label: strings["viewSource"],
  75. dir: editor.dir,
  76. lang: editor.lang,
  77. showLabel: false,
  78. iconClass: this.iconClassPrefix + " " + this.iconClassPrefix + "ViewSource",
  79. tabIndex: "-1",
  80. onChange: lang.hitch(this, "_showSource")
  81. });
  82. // IE 7 has a horrible bug with zoom, so we have to create this node
  83. // to cross-check later. Sigh.
  84. if(has("ie") == 7){
  85. this._ieFixNode = domConstruct.create("div", {
  86. style: {
  87. opacity: "0",
  88. zIndex: "-1000",
  89. position: "absolute",
  90. top: "-1000px"
  91. }
  92. }, win.body());
  93. }
  94. // Make sure readonly mode doesn't make the wrong cursor appear over the button.
  95. this.button.set("readOnly", false);
  96. },
  97. setEditor: function(/*dijit.Editor*/ editor){
  98. // summary:
  99. // Tell the plugin which Editor it is associated with.
  100. // editor: Object
  101. // The editor object to attach the print capability to.
  102. this.editor = editor;
  103. this._initButton();
  104. this.editor.addKeyHandler(keys.F12, true, true, lang.hitch(this, function(e){
  105. // Move the focus before switching
  106. // It'll focus back. Hiding a focused
  107. // node causes issues.
  108. this.button.focus();
  109. this.toggle();
  110. event.stop(e);
  111. // Call the focus shift outside of the handler.
  112. setTimeout(lang.hitch(this, function(){
  113. // Focus the textarea... unless focus has moved outside of the editor completely during the timeout.
  114. // Since we override focus, so we just need to call it.
  115. if(this.editor.focused){
  116. this.editor.focus();
  117. }
  118. }), 100);
  119. }));
  120. },
  121. _showSource: function(source){
  122. // summary:
  123. // Function to toggle between the source and RTE views.
  124. // source: boolean
  125. // Boolean value indicating if it should be in source mode or not.
  126. // tags:
  127. // private
  128. var ed = this.editor;
  129. var edPlugins = ed._plugins;
  130. var html;
  131. this._sourceShown = source;
  132. var self = this;
  133. try{
  134. if(!this.sourceArea){
  135. this._createSourceView();
  136. }
  137. if(source){
  138. // Update the QueryCommandEnabled function to disable everything but
  139. // the source view mode. Have to over-ride a function, then kick all
  140. // plugins to check their state.
  141. ed._sourceQueryCommandEnabled = ed.queryCommandEnabled;
  142. ed.queryCommandEnabled = function(cmd){
  143. return cmd.toLowerCase() === "viewsource";
  144. };
  145. this.editor.onDisplayChanged();
  146. html = ed.get("value");
  147. html = this._filter(html);
  148. ed.set("value", html);
  149. array.forEach(edPlugins, function(p){
  150. // Turn off any plugins not controlled by queryCommandenabled.
  151. if(!(p instanceof ViewSource)){
  152. p.set("disabled", true)
  153. }
  154. });
  155. // We actually do need to trap this plugin and adjust how we
  156. // display the textarea.
  157. if(this._fsPlugin){
  158. this._fsPlugin._getAltViewNode = function(){
  159. return self.sourceArea;
  160. };
  161. }
  162. this.sourceArea.value = html;
  163. // Since neither iframe nor textarea have margin, border, or padding,
  164. // just set sizes equal
  165. this.sourceArea.style.height = ed.iframe.style.height;
  166. this.sourceArea.style.width = ed.iframe.style.width;
  167. domStyle.set(ed.iframe, "display", "none");
  168. domStyle.set(this.sourceArea, {
  169. display: "block"
  170. });
  171. var resizer = function(){
  172. // function to handle resize events.
  173. // Will check current VP and only resize if
  174. // different.
  175. var vp = winUtils.getBox();
  176. if("_prevW" in this && "_prevH" in this){
  177. // No actual size change, ignore.
  178. if(vp.w === this._prevW && vp.h === this._prevH){
  179. return;
  180. }else{
  181. this._prevW = vp.w;
  182. this._prevH = vp.h;
  183. }
  184. }else{
  185. this._prevW = vp.w;
  186. this._prevH = vp.h;
  187. }
  188. if(this._resizer){
  189. clearTimeout(this._resizer);
  190. delete this._resizer;
  191. }
  192. // Timeout it to help avoid spamming resize on IE.
  193. // Works for all browsers.
  194. this._resizer = setTimeout(lang.hitch(this, function(){
  195. delete this._resizer;
  196. this._resize();
  197. }), 10);
  198. };
  199. this._resizeHandle = on(window, "resize", lang.hitch(this, resizer));
  200. //Call this on a delay once to deal with IE glitchiness on initial size.
  201. setTimeout(lang.hitch(this, this._resize), 100);
  202. //Trigger a check for command enablement/disablement.
  203. this.editor.onNormalizedDisplayChanged();
  204. this.editor.__oldGetValue = this.editor.getValue;
  205. this.editor.getValue = lang.hitch(this, function(){
  206. var txt = this.sourceArea.value;
  207. txt = this._filter(txt);
  208. return txt;
  209. });
  210. }else{
  211. // First check that we were in source view before doing anything.
  212. // corner case for being called with a value of false and we hadn't
  213. // actually been in source display mode.
  214. if(!ed._sourceQueryCommandEnabled){
  215. return;
  216. }
  217. this._resizeHandle.remove();
  218. delete this._resizeHandle;
  219. if(this.editor.__oldGetValue){
  220. this.editor.getValue = this.editor.__oldGetValue;
  221. delete this.editor.__oldGetValue;
  222. }
  223. // Restore all the plugin buttons state.
  224. ed.queryCommandEnabled = ed._sourceQueryCommandEnabled;
  225. if(!this._readOnly){
  226. html = this.sourceArea.value;
  227. html = this._filter(html);
  228. ed.beginEditing();
  229. ed.set("value", html);
  230. ed.endEditing();
  231. }
  232. array.forEach(edPlugins, function(p){
  233. // Turn back on any plugins we turned off.
  234. p.set("disabled", false);
  235. });
  236. domStyle.set(this.sourceArea, "display", "none");
  237. domStyle.set(ed.iframe, "display", "block");
  238. delete ed._sourceQueryCommandEnabled;
  239. //Trigger a check for command enablement/disablement.
  240. this.editor.onDisplayChanged();
  241. }
  242. // Call a delayed resize to wait for some things to display in header/footer.
  243. setTimeout(lang.hitch(this, function(){
  244. // Make resize calls.
  245. var parent = ed.domNode.parentNode;
  246. if(parent){
  247. var container = registry.getEnclosingWidget(parent);
  248. if(container && container.resize){
  249. container.resize();
  250. }
  251. }
  252. ed.resize();
  253. }), 300);
  254. }catch(e){
  255. console.log(e);
  256. }
  257. },
  258. updateState: function(){
  259. // summary:
  260. // Over-ride for button state control for disabled to work.
  261. this.button.set("disabled", this.get("disabled"));
  262. },
  263. _resize: function(){
  264. // summary:
  265. // Internal function to resize the source view
  266. // tags:
  267. // private
  268. var ed = this.editor;
  269. var tbH = ed.getHeaderHeight();
  270. var fH = ed.getFooterHeight();
  271. var eb = domGeometry.position(ed.domNode);
  272. // Styles are now applied to the internal source container, so we have
  273. // to subtract them off.
  274. var containerPadding = domGeometry.getPadBorderExtents(ed.iframe.parentNode);
  275. var containerMargin = domGeometry.getMarginExtents(ed.iframe.parentNode);
  276. var extents = domGeometry.getPadBorderExtents(ed.domNode);
  277. var edb = {
  278. w: eb.w - extents.w,
  279. h: eb.h - (tbH + extents.h + + fH)
  280. };
  281. // Fullscreen gets odd, so we need to check for the FS plugin and
  282. // adapt.
  283. if(this._fsPlugin && this._fsPlugin.isFullscreen){
  284. //Okay, probably in FS, adjust.
  285. var vp = winUtils.getBox();
  286. edb.w = (vp.w - extents.w);
  287. edb.h = (vp.h - (tbH + extents.h + fH));
  288. }
  289. if(has("ie")){
  290. // IE is always off by 2px, so we have to adjust here
  291. // Note that IE ZOOM is broken here. I can't get
  292. //it to scale right.
  293. edb.h -= 2;
  294. }
  295. // IE has a horrible zoom bug. So, we have to try and account for
  296. // it and fix up the scaling.
  297. if(this._ieFixNode){
  298. var _ie7zoom = -this._ieFixNode.offsetTop / 1000;
  299. edb.w = Math.floor((edb.w + 0.9) / _ie7zoom);
  300. edb.h = Math.floor((edb.h + 0.9) / _ie7zoom);
  301. }
  302. domGeometry.setMarginBox(this.sourceArea, {
  303. w: edb.w - (containerPadding.w + containerMargin.w),
  304. h: edb.h - (containerPadding.h + containerMargin.h)
  305. });
  306. // Scale the parent container too in this case.
  307. domGeometry.setMarginBox(ed.iframe.parentNode, {
  308. h: edb.h
  309. });
  310. },
  311. _createSourceView: function(){
  312. // summary:
  313. // Internal function for creating the source view area.
  314. // tags:
  315. // private
  316. var ed = this.editor;
  317. var edPlugins = ed._plugins;
  318. this.sourceArea = domConstruct.create("textarea");
  319. if(this.readOnly){
  320. domAttr.set(this.sourceArea, "readOnly", true);
  321. this._readOnly = true;
  322. }
  323. domStyle.set(this.sourceArea, {
  324. padding: "0px",
  325. margin: "0px",
  326. borderWidth: "0px",
  327. borderStyle: "none"
  328. });
  329. domAttr.set(this.sourceArea, "aria-label", this.editor.id);
  330. domConstruct.place(this.sourceArea, ed.iframe, "before");
  331. if(has("ie") && ed.iframe.parentNode.lastChild !== ed.iframe){
  332. // There's some weirdo div in IE used for focus control
  333. // But is messed up scaling the textarea if we don't config
  334. // it some so it doesn't have a varying height.
  335. domStyle.set(ed.iframe.parentNode.lastChild,{
  336. width: "0px",
  337. height: "0px",
  338. padding: "0px",
  339. margin: "0px",
  340. borderWidth: "0px",
  341. borderStyle: "none"
  342. });
  343. }
  344. // We also need to take over editor focus a bit here, so that focus calls to
  345. // focus the editor will focus to the right node when VS is active.
  346. ed._viewsource_oldFocus = ed.focus;
  347. var self = this;
  348. ed.focus = function(){
  349. if(self._sourceShown){
  350. self.setSourceAreaCaret();
  351. }else{
  352. try{
  353. if(this._vsFocused){
  354. delete this._vsFocused;
  355. // Must focus edit node in this case (webkit only) or
  356. // focus doesn't shift right, but in normal
  357. // cases we focus with the regular function.
  358. focus.focus(ed.editNode);
  359. }else{
  360. ed._viewsource_oldFocus();
  361. }
  362. }catch(e){
  363. console.log(e);
  364. }
  365. }
  366. };
  367. var i, p;
  368. for(i = 0; i < edPlugins.length; i++){
  369. // We actually do need to trap this plugin and adjust how we
  370. // display the textarea.
  371. p = edPlugins[i];
  372. if(p && (p.declaredClass === "dijit._editor.plugins.FullScreen" ||
  373. p.declaredClass === (dijit._scopeName +
  374. "._editor.plugins.FullScreen"))){
  375. this._fsPlugin = p;
  376. break;
  377. }
  378. }
  379. if(this._fsPlugin){
  380. // Found, we need to over-ride the alt-view node function
  381. // on FullScreen with our own, chain up to parent call when appropriate.
  382. this._fsPlugin._viewsource_getAltViewNode = this._fsPlugin._getAltViewNode;
  383. this._fsPlugin._getAltViewNode = function(){
  384. return self._sourceShown?self.sourceArea:this._viewsource_getAltViewNode();
  385. };
  386. }
  387. // Listen to the source area for key events as well, as we need to be able to hotkey toggle
  388. // it from there too.
  389. this.connect(this.sourceArea, "onkeydown", lang.hitch(this, function(e){
  390. if(this._sourceShown && e.keyCode == keys.F12 && e.ctrlKey && e.shiftKey){
  391. this.button.focus();
  392. this.button.set("checked", false);
  393. setTimeout(lang.hitch(this, function(){ed.focus();}), 100);
  394. event.stop(e);
  395. }
  396. }));
  397. },
  398. _stripScripts: function(html){
  399. // summary:
  400. // Strips out script tags from the HTML used in editor.
  401. // html: String
  402. // The HTML to filter
  403. // tags:
  404. // private
  405. if(html){
  406. // Look for closed and unclosed (malformed) script attacks.
  407. html = html.replace(/<\s*script[^>]*>((.|\s)*?)<\\?\/\s*script\s*>/ig, "");
  408. html = html.replace(/<\s*script\b([^<>]|\s)*>?/ig, "");
  409. html = html.replace(/<[^>]*=(\s|)*[("|')]javascript:[^$1][(\s|.)]*[$1][^>]*>/ig, "");
  410. }
  411. return html;
  412. },
  413. _stripComments: function(html){
  414. // summary:
  415. // Strips out comments from the HTML used in editor.
  416. // html: String
  417. // The HTML to filter
  418. // tags:
  419. // private
  420. if(html){
  421. html = html.replace(/<!--(.|\s){1,}?-->/g, "");
  422. }
  423. return html;
  424. },
  425. _stripIFrames: function(html){
  426. // summary:
  427. // Strips out iframe tags from the content, to avoid iframe script
  428. // style injection attacks.
  429. // html: String
  430. // The HTML to filter
  431. // tags:
  432. // private
  433. if(html){
  434. html = html.replace(/<\s*iframe[^>]*>((.|\s)*?)<\\?\/\s*iframe\s*>/ig, "");
  435. }
  436. return html;
  437. },
  438. _filter: function(html){
  439. // summary:
  440. // Internal function to perform some filtering on the HTML.
  441. // html: String
  442. // The HTML to filter
  443. // tags:
  444. // private
  445. if(html){
  446. if(this.stripScripts){
  447. html = this._stripScripts(html);
  448. }
  449. if(this.stripComments){
  450. html = this._stripComments(html);
  451. }
  452. if(this.stripIFrames){
  453. html = this._stripIFrames(html);
  454. }
  455. }
  456. return html;
  457. },
  458. setSourceAreaCaret: function(){
  459. // summary:
  460. // Internal function to set the caret in the sourceArea
  461. // to 0x0
  462. var global = win.global;
  463. var elem = this.sourceArea;
  464. focus.focus(elem);
  465. if(this._sourceShown && !this.readOnly){
  466. if(has("ie")){
  467. if(this.sourceArea.createTextRange){
  468. var range = elem.createTextRange();
  469. range.collapse(true);
  470. range.moveStart("character", -99999); // move to 0
  471. range.moveStart("character", 0); // delta from 0 is the correct position
  472. range.moveEnd("character", 0);
  473. range.select();
  474. }
  475. }else if(global.getSelection){
  476. if(elem.setSelectionRange){
  477. elem.setSelectionRange(0,0);
  478. }
  479. }
  480. }
  481. },
  482. destroy: function(){
  483. // summary:
  484. // Over-ride to remove the node used to correct for IE's
  485. // zoom bug.
  486. if(this._ieFixNode){
  487. win.body().removeChild(this._ieFixNode);
  488. }
  489. if(this._resizer){
  490. clearTimeout(this._resizer);
  491. delete this._resizer;
  492. }
  493. if(this._resizeHandle){
  494. this._resizeHandle.remove();
  495. delete this._resizeHandle;
  496. }
  497. this.inherited(arguments);
  498. }
  499. });
  500. // Register this plugin.
  501. // For back-compat accept "viewsource" (all lowercase) too, remove in 2.0
  502. _Plugin.registry["viewSource"] = _Plugin.registry["viewsource"] = function(args){
  503. return new ViewSource({
  504. readOnly: ("readOnly" in args)?args.readOnly:false,
  505. stripComments: ("stripComments" in args)?args.stripComments:true,
  506. stripScripts: ("stripScripts" in args)?args.stripScripts:true,
  507. stripIFrames: ("stripIFrames" in args)?args.stripIFrames:true
  508. });
  509. };
  510. return ViewSource;
  511. });