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