578 lines
17 KiB
JavaScript
578 lines
17 KiB
JavaScript
|
define("dijit/_editor/plugins/ViewSource", [
|
||
|
"dojo/_base/array", // array.forEach
|
||
|
"dojo/_base/declare", // declare
|
||
|
"dojo/dom-attr", // domAttr.set
|
||
|
"dojo/dom-construct", // domConstruct.create domConstruct.place
|
||
|
"dojo/dom-geometry", // domGeometry.setMarginBox domGeometry.position
|
||
|
"dojo/dom-style", // domStyle.set
|
||
|
"dojo/_base/event", // event.stop
|
||
|
"dojo/i18n", // i18n.getLocalization
|
||
|
"dojo/keys", // keys.F12
|
||
|
"dojo/_base/lang", // lang.hitch
|
||
|
"dojo/on", // on()
|
||
|
"dojo/sniff", // has("ie") has("webkit")
|
||
|
"dojo/_base/window", // win.body win.global
|
||
|
"dojo/window", // winUtils.getBox
|
||
|
"../../focus", // focus.focus()
|
||
|
"../_Plugin",
|
||
|
"../../form/ToggleButton",
|
||
|
"../..", // dijit._scopeName
|
||
|
"../../registry", // registry.getEnclosingWidget()
|
||
|
"dojo/aspect", // Aspect commands for adice
|
||
|
"dojo/i18n!../nls/commands"
|
||
|
], function(array, declare, domAttr, domConstruct, domGeometry, domStyle, event, i18n, keys, lang, on, has, win,
|
||
|
winUtils, focus, _Plugin, ToggleButton, dijit, registry, aspect){
|
||
|
|
||
|
// module:
|
||
|
// dijit/_editor/plugins/ViewSource
|
||
|
|
||
|
|
||
|
var ViewSource = declare("dijit._editor.plugins.ViewSource",_Plugin, {
|
||
|
// summary:
|
||
|
// This plugin provides a simple view source capability. When view
|
||
|
// source mode is enabled, it disables all other buttons/plugins on the RTE.
|
||
|
// It also binds to the hotkey: CTRL-SHIFT-F11 for toggling ViewSource mode.
|
||
|
|
||
|
// stripScripts: [public] Boolean
|
||
|
// Boolean flag used to indicate if script tags should be stripped from the document.
|
||
|
// Defaults to true.
|
||
|
stripScripts: true,
|
||
|
|
||
|
// stripComments: [public] Boolean
|
||
|
// Boolean flag used to indicate if comment tags should be stripped from the document.
|
||
|
// Defaults to true.
|
||
|
stripComments: true,
|
||
|
|
||
|
// stripComments: [public] Boolean
|
||
|
// Boolean flag used to indicate if iframe tags should be stripped from the document.
|
||
|
// Defaults to true.
|
||
|
stripIFrames: true,
|
||
|
|
||
|
// readOnly: [const] Boolean
|
||
|
// Boolean flag used to indicate if the source view should be readonly or not.
|
||
|
// Cannot be changed after initialization of the plugin.
|
||
|
// Defaults to false.
|
||
|
readOnly: false,
|
||
|
|
||
|
// _fsPlugin: [private] Object
|
||
|
// Reference to a registered fullscreen plugin so that viewSource knows
|
||
|
// how to scale.
|
||
|
_fsPlugin: null,
|
||
|
|
||
|
toggle: function(){
|
||
|
// summary:
|
||
|
// Function to allow programmatic toggling of the view.
|
||
|
|
||
|
// For Webkit, we have to focus a very particular way.
|
||
|
// when swapping views, otherwise focus doesn't shift right
|
||
|
// but can't focus this way all the time, only for VS changes.
|
||
|
// If we did it all the time, buttons like bold, italic, etc
|
||
|
// break.
|
||
|
if(has("webkit")){this._vsFocused = true;}
|
||
|
this.button.set("checked", !this.button.get("checked"));
|
||
|
|
||
|
},
|
||
|
|
||
|
_initButton: function(){
|
||
|
// summary:
|
||
|
// Over-ride for creation of the resize button.
|
||
|
var strings = i18n.getLocalization("dijit._editor", "commands"),
|
||
|
editor = this.editor;
|
||
|
this.button = new ToggleButton({
|
||
|
label: strings["viewSource"],
|
||
|
ownerDocument: editor.ownerDocument,
|
||
|
dir: editor.dir,
|
||
|
lang: editor.lang,
|
||
|
showLabel: false,
|
||
|
iconClass: this.iconClassPrefix + " " + this.iconClassPrefix + "ViewSource",
|
||
|
tabIndex: "-1",
|
||
|
onChange: lang.hitch(this, "_showSource")
|
||
|
});
|
||
|
|
||
|
// IE 7 has a horrible bug with zoom, so we have to create this node
|
||
|
// to cross-check later. Sigh.
|
||
|
if(has("ie") == 7){
|
||
|
this._ieFixNode = domConstruct.create("div", {
|
||
|
style: {
|
||
|
opacity: "0",
|
||
|
zIndex: "-1000",
|
||
|
position: "absolute",
|
||
|
top: "-1000px"
|
||
|
}
|
||
|
}, editor.ownerDocumentBody);
|
||
|
}
|
||
|
// Make sure readonly mode doesn't make the wrong cursor appear over the button.
|
||
|
this.button.set("readOnly", false);
|
||
|
},
|
||
|
|
||
|
|
||
|
setEditor: function(/*dijit/Editor*/ editor){
|
||
|
// summary:
|
||
|
// Tell the plugin which Editor it is associated with.
|
||
|
// editor: Object
|
||
|
// The editor object to attach the print capability to.
|
||
|
this.editor = editor;
|
||
|
this._initButton();
|
||
|
|
||
|
this.editor.addKeyHandler(keys.F12, true, true, lang.hitch(this, function(e){
|
||
|
// Move the focus before switching
|
||
|
// It'll focus back. Hiding a focused
|
||
|
// node causes issues.
|
||
|
this.button.focus();
|
||
|
this.toggle();
|
||
|
event.stop(e);
|
||
|
|
||
|
// Call the focus shift outside of the handler.
|
||
|
setTimeout(lang.hitch(this, function(){
|
||
|
// We over-ride focus, so we just need to call.
|
||
|
this.editor.focus();
|
||
|
}), 100);
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
_showSource: function(source){
|
||
|
// summary:
|
||
|
// Function to toggle between the source and RTE views.
|
||
|
// source: boolean
|
||
|
// Boolean value indicating if it should be in source mode or not.
|
||
|
// tags:
|
||
|
// private
|
||
|
var ed = this.editor;
|
||
|
var edPlugins = ed._plugins;
|
||
|
var html;
|
||
|
this._sourceShown = source;
|
||
|
var self = this;
|
||
|
try{
|
||
|
if(!this.sourceArea){
|
||
|
this._createSourceView();
|
||
|
}
|
||
|
if(source){
|
||
|
// Update the QueryCommandEnabled function to disable everything but
|
||
|
// the source view mode. Have to over-ride a function, then kick all
|
||
|
// plugins to check their state.
|
||
|
ed._sourceQueryCommandEnabled = ed.queryCommandEnabled;
|
||
|
ed.queryCommandEnabled = function(cmd){
|
||
|
return cmd.toLowerCase() === "viewsource";
|
||
|
};
|
||
|
this.editor.onDisplayChanged();
|
||
|
html = ed.get("value");
|
||
|
html = this._filter(html);
|
||
|
ed.set("value", html);
|
||
|
array.forEach(edPlugins, function(p){
|
||
|
// Turn off any plugins not controlled by queryCommandenabled.
|
||
|
if(p && !(p instanceof ViewSource) && p.isInstanceOf(_Plugin)){
|
||
|
p.set("disabled", true)
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// We actually do need to trap this plugin and adjust how we
|
||
|
// display the textarea.
|
||
|
if(this._fsPlugin){
|
||
|
this._fsPlugin._getAltViewNode = function(){
|
||
|
return self.sourceArea;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
this.sourceArea.value = html;
|
||
|
|
||
|
// Since neither iframe nor textarea have margin, border, or padding,
|
||
|
// just set sizes equal
|
||
|
this.sourceArea.style.height = ed.iframe.style.height;
|
||
|
this.sourceArea.style.width = ed.iframe.style.width;
|
||
|
domStyle.set(ed.iframe, "display", "none");
|
||
|
domStyle.set(this.sourceArea, {
|
||
|
display: "block"
|
||
|
});
|
||
|
|
||
|
var resizer = function(){
|
||
|
// function to handle resize events.
|
||
|
// Will check current VP and only resize if
|
||
|
// different.
|
||
|
var vp = winUtils.getBox(ed.ownerDocument);
|
||
|
|
||
|
if("_prevW" in this && "_prevH" in this){
|
||
|
// No actual size change, ignore.
|
||
|
if(vp.w === this._prevW && vp.h === this._prevH){
|
||
|
return;
|
||
|
}else{
|
||
|
this._prevW = vp.w;
|
||
|
this._prevH = vp.h;
|
||
|
}
|
||
|
}else{
|
||
|
this._prevW = vp.w;
|
||
|
this._prevH = vp.h;
|
||
|
}
|
||
|
if(this._resizer){
|
||
|
clearTimeout(this._resizer);
|
||
|
delete this._resizer;
|
||
|
}
|
||
|
// Timeout it to help avoid spamming resize on IE.
|
||
|
// Works for all browsers.
|
||
|
this._resizer = setTimeout(lang.hitch(this, function(){
|
||
|
delete this._resizer;
|
||
|
this._resize();
|
||
|
}), 10);
|
||
|
};
|
||
|
this._resizeHandle = on(window, "resize", lang.hitch(this, resizer));
|
||
|
|
||
|
//Call this on a delay once to deal with IE glitchiness on initial size.
|
||
|
setTimeout(lang.hitch(this, this._resize), 100);
|
||
|
|
||
|
//Trigger a check for command enablement/disablement.
|
||
|
this.editor.onNormalizedDisplayChanged();
|
||
|
|
||
|
this.editor.__oldGetValue = this.editor.getValue;
|
||
|
this.editor.getValue = lang.hitch(this, function(){
|
||
|
var txt = this.sourceArea.value;
|
||
|
txt = this._filter(txt);
|
||
|
return txt;
|
||
|
});
|
||
|
|
||
|
this._setListener = aspect.after(this.editor, "setValue", lang.hitch(this, function(htmlTxt){
|
||
|
htmlTxt = htmlTxt || "";
|
||
|
htmlTxt = this._filter(htmlTxt);
|
||
|
this.sourceArea.value = htmlTxt;
|
||
|
}), true);
|
||
|
}else{
|
||
|
// First check that we were in source view before doing anything.
|
||
|
// corner case for being called with a value of false and we hadn't
|
||
|
// actually been in source display mode.
|
||
|
if(!ed._sourceQueryCommandEnabled){
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Remove the set listener.
|
||
|
this._setListener.remove();
|
||
|
delete this._setListener;
|
||
|
|
||
|
this._resizeHandle.remove();
|
||
|
delete this._resizeHandle;
|
||
|
|
||
|
if(this.editor.__oldGetValue){
|
||
|
this.editor.getValue = this.editor.__oldGetValue;
|
||
|
delete this.editor.__oldGetValue;
|
||
|
}
|
||
|
|
||
|
// Restore all the plugin buttons state.
|
||
|
ed.queryCommandEnabled = ed._sourceQueryCommandEnabled;
|
||
|
if(!this._readOnly){
|
||
|
html = this.sourceArea.value;
|
||
|
html = this._filter(html);
|
||
|
ed.beginEditing();
|
||
|
ed.set("value", html);
|
||
|
ed.endEditing();
|
||
|
}
|
||
|
|
||
|
array.forEach(edPlugins, function(p){
|
||
|
// Turn back on any plugins we turned off.
|
||
|
if(p && p.isInstanceOf(_Plugin)){
|
||
|
p.set("disabled", false);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
domStyle.set(this.sourceArea, "display", "none");
|
||
|
domStyle.set(ed.iframe, "display", "block");
|
||
|
delete ed._sourceQueryCommandEnabled;
|
||
|
|
||
|
//Trigger a check for command enablement/disablement.
|
||
|
this.editor.onDisplayChanged();
|
||
|
}
|
||
|
// Call a delayed resize to wait for some things to display in header/footer.
|
||
|
setTimeout(lang.hitch(this, function(){
|
||
|
// Make resize calls.
|
||
|
var parent = ed.domNode.parentNode;
|
||
|
if(parent){
|
||
|
var container = registry.getEnclosingWidget(parent);
|
||
|
if(container && container.resize){
|
||
|
container.resize();
|
||
|
}
|
||
|
}
|
||
|
ed.resize();
|
||
|
}), 300);
|
||
|
}catch(e){
|
||
|
console.log(e);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
updateState: function(){
|
||
|
// summary:
|
||
|
// Over-ride for button state control for disabled to work.
|
||
|
this.button.set("disabled", this.get("disabled"));
|
||
|
},
|
||
|
|
||
|
_resize: function(){
|
||
|
// summary:
|
||
|
// Internal function to resize the source view
|
||
|
// tags:
|
||
|
// private
|
||
|
var ed = this.editor;
|
||
|
var tbH = ed.getHeaderHeight();
|
||
|
var fH = ed.getFooterHeight();
|
||
|
var eb = domGeometry.position(ed.domNode);
|
||
|
|
||
|
// Styles are now applied to the internal source container, so we have
|
||
|
// to subtract them off.
|
||
|
var containerPadding = domGeometry.getPadBorderExtents(ed.iframe.parentNode);
|
||
|
var containerMargin = domGeometry.getMarginExtents(ed.iframe.parentNode);
|
||
|
|
||
|
var extents = domGeometry.getPadBorderExtents(ed.domNode);
|
||
|
var edb = {
|
||
|
w: eb.w - extents.w,
|
||
|
h: eb.h - (tbH + extents.h + fH)
|
||
|
};
|
||
|
|
||
|
// Fullscreen gets odd, so we need to check for the FS plugin and
|
||
|
// adapt.
|
||
|
if(this._fsPlugin && this._fsPlugin.isFullscreen){
|
||
|
//Okay, probably in FS, adjust.
|
||
|
var vp = winUtils.getBox(ed.ownerDocument);
|
||
|
edb.w = (vp.w - extents.w);
|
||
|
edb.h = (vp.h - (tbH + extents.h + fH));
|
||
|
}
|
||
|
|
||
|
if(has("ie")){
|
||
|
// IE is always off by 2px, so we have to adjust here
|
||
|
// Note that IE ZOOM is broken here. I can't get
|
||
|
//it to scale right.
|
||
|
edb.h -= 2;
|
||
|
}
|
||
|
|
||
|
// IE has a horrible zoom bug. So, we have to try and account for
|
||
|
// it and fix up the scaling.
|
||
|
if(this._ieFixNode){
|
||
|
var _ie7zoom = -this._ieFixNode.offsetTop / 1000;
|
||
|
edb.w = Math.floor((edb.w + 0.9) / _ie7zoom);
|
||
|
edb.h = Math.floor((edb.h + 0.9) / _ie7zoom);
|
||
|
}
|
||
|
|
||
|
domGeometry.setMarginBox(this.sourceArea, {
|
||
|
w: edb.w - (containerPadding.w + containerMargin.w),
|
||
|
h: edb.h - (containerPadding.h + containerMargin.h)
|
||
|
});
|
||
|
|
||
|
// Scale the parent container too in this case.
|
||
|
domGeometry.setMarginBox(ed.iframe.parentNode, {
|
||
|
h: edb.h
|
||
|
});
|
||
|
},
|
||
|
|
||
|
_createSourceView: function(){
|
||
|
// summary:
|
||
|
// Internal function for creating the source view area.
|
||
|
// tags:
|
||
|
// private
|
||
|
var ed = this.editor;
|
||
|
var edPlugins = ed._plugins;
|
||
|
this.sourceArea = domConstruct.create("textarea");
|
||
|
if(this.readOnly){
|
||
|
domAttr.set(this.sourceArea, "readOnly", true);
|
||
|
this._readOnly = true;
|
||
|
}
|
||
|
domStyle.set(this.sourceArea, {
|
||
|
padding: "0px",
|
||
|
margin: "0px",
|
||
|
borderWidth: "0px",
|
||
|
borderStyle: "none"
|
||
|
});
|
||
|
domConstruct.place(this.sourceArea, ed.iframe, "before");
|
||
|
|
||
|
if(has("ie") && ed.iframe.parentNode.lastChild !== ed.iframe){
|
||
|
// There's some weirdo div in IE used for focus control
|
||
|
// But is messed up scaling the textarea if we don't config
|
||
|
// it some so it doesn't have a varying height.
|
||
|
domStyle.set(ed.iframe.parentNode.lastChild,{
|
||
|
width: "0px",
|
||
|
height: "0px",
|
||
|
padding: "0px",
|
||
|
margin: "0px",
|
||
|
borderWidth: "0px",
|
||
|
borderStyle: "none"
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// We also need to take over editor focus a bit here, so that focus calls to
|
||
|
// focus the editor will focus to the right node when VS is active.
|
||
|
ed._viewsource_oldFocus = ed.focus;
|
||
|
var self = this;
|
||
|
ed.focus = function(){
|
||
|
if(self._sourceShown){
|
||
|
self.setSourceAreaCaret();
|
||
|
}else{
|
||
|
try{
|
||
|
if(this._vsFocused){
|
||
|
delete this._vsFocused;
|
||
|
// Must focus edit node in this case (webkit only) or
|
||
|
// focus doesn't shift right, but in normal
|
||
|
// cases we focus with the regular function.
|
||
|
focus.focus(ed.editNode);
|
||
|
}else{
|
||
|
ed._viewsource_oldFocus();
|
||
|
}
|
||
|
}catch(e){
|
||
|
console.log(e);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var i, p;
|
||
|
for(i = 0; i < edPlugins.length; i++){
|
||
|
// We actually do need to trap this plugin and adjust how we
|
||
|
// display the textarea.
|
||
|
p = edPlugins[i];
|
||
|
if(p && (p.declaredClass === "dijit._editor.plugins.FullScreen" ||
|
||
|
p.declaredClass === (dijit._scopeName +
|
||
|
"._editor.plugins.FullScreen"))){
|
||
|
this._fsPlugin = p;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if(this._fsPlugin){
|
||
|
// Found, we need to over-ride the alt-view node function
|
||
|
// on FullScreen with our own, chain up to parent call when appropriate.
|
||
|
this._fsPlugin._viewsource_getAltViewNode = this._fsPlugin._getAltViewNode;
|
||
|
this._fsPlugin._getAltViewNode = function(){
|
||
|
return self._sourceShown?self.sourceArea:this._viewsource_getAltViewNode();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// Listen to the source area for key events as well, as we need to be able to hotkey toggle
|
||
|
// it from there too.
|
||
|
this.connect(this.sourceArea, "onkeydown", lang.hitch(this, function(e){
|
||
|
if(this._sourceShown && e.keyCode == keys.F12 && e.ctrlKey && e.shiftKey){
|
||
|
this.button.focus();
|
||
|
this.button.set("checked", false);
|
||
|
setTimeout(lang.hitch(this, function(){ed.focus();}), 100);
|
||
|
event.stop(e);
|
||
|
}
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
_stripScripts: function(html){
|
||
|
// summary:
|
||
|
// Strips out script tags from the HTML used in editor.
|
||
|
// html: String
|
||
|
// The HTML to filter
|
||
|
// tags:
|
||
|
// private
|
||
|
if(html){
|
||
|
// Look for closed and unclosed (malformed) script attacks.
|
||
|
html = html.replace(/<\s*script[^>]*>((.|\s)*?)<\\?\/\s*script\s*>/ig, "");
|
||
|
html = html.replace(/<\s*script\b([^<>]|\s)*>?/ig, "");
|
||
|
html = html.replace(/<[^>]*=(\s|)*[("|')]javascript:[^$1][(\s|.)]*[$1][^>]*>/ig, "");
|
||
|
}
|
||
|
return html;
|
||
|
},
|
||
|
|
||
|
_stripComments: function(html){
|
||
|
// summary:
|
||
|
// Strips out comments from the HTML used in editor.
|
||
|
// html: String
|
||
|
// The HTML to filter
|
||
|
// tags:
|
||
|
// private
|
||
|
if(html){
|
||
|
html = html.replace(/<!--(.|\s){1,}?-->/g, "");
|
||
|
}
|
||
|
return html;
|
||
|
},
|
||
|
|
||
|
_stripIFrames: function(html){
|
||
|
// summary:
|
||
|
// Strips out iframe tags from the content, to avoid iframe script
|
||
|
// style injection attacks.
|
||
|
// html: String
|
||
|
// The HTML to filter
|
||
|
// tags:
|
||
|
// private
|
||
|
if(html){
|
||
|
html = html.replace(/<\s*iframe[^>]*>((.|\s)*?)<\\?\/\s*iframe\s*>/ig, "");
|
||
|
}
|
||
|
return html;
|
||
|
},
|
||
|
|
||
|
_filter: function(html){
|
||
|
// summary:
|
||
|
// Internal function to perform some filtering on the HTML.
|
||
|
// html: String
|
||
|
// The HTML to filter
|
||
|
// tags:
|
||
|
// private
|
||
|
if(html){
|
||
|
if(this.stripScripts){
|
||
|
html = this._stripScripts(html);
|
||
|
}
|
||
|
if(this.stripComments){
|
||
|
html = this._stripComments(html);
|
||
|
}
|
||
|
if(this.stripIFrames){
|
||
|
html = this._stripIFrames(html);
|
||
|
}
|
||
|
}
|
||
|
return html;
|
||
|
},
|
||
|
|
||
|
setSourceAreaCaret: function(){
|
||
|
// summary:
|
||
|
// Internal function to set the caret in the sourceArea
|
||
|
// to 0x0
|
||
|
var global = win.global;
|
||
|
var elem = this.sourceArea;
|
||
|
focus.focus(elem);
|
||
|
if(this._sourceShown && !this.readOnly){
|
||
|
if(has("ie")){
|
||
|
if(this.sourceArea.createTextRange){
|
||
|
var range = elem.createTextRange();
|
||
|
range.collapse(true);
|
||
|
range.moveStart("character", -99999); // move to 0
|
||
|
range.moveStart("character", 0); // delta from 0 is the correct position
|
||
|
range.moveEnd("character", 0);
|
||
|
range.select();
|
||
|
}
|
||
|
}else if(global.getSelection){
|
||
|
if(elem.setSelectionRange){
|
||
|
elem.setSelectionRange(0,0);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
destroy: function(){
|
||
|
// summary:
|
||
|
// Over-ride to remove the node used to correct for IE's
|
||
|
// zoom bug.
|
||
|
if(this._ieFixNode){
|
||
|
domConstruct.destroy(this._ieFixNode);
|
||
|
}
|
||
|
if(this._resizer){
|
||
|
clearTimeout(this._resizer);
|
||
|
delete this._resizer;
|
||
|
}
|
||
|
if(this._resizeHandle){
|
||
|
this._resizeHandle.remove();
|
||
|
delete this._resizeHandle;
|
||
|
}
|
||
|
if(this._setListener){
|
||
|
this._setListener.remove();
|
||
|
delete this._setListener;
|
||
|
}
|
||
|
this.inherited(arguments);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Register this plugin.
|
||
|
// For back-compat accept "viewsource" (all lowercase) too, remove in 2.0
|
||
|
_Plugin.registry["viewSource"] = _Plugin.registry["viewsource"] = function(args){
|
||
|
return new ViewSource({
|
||
|
readOnly: ("readOnly" in args)?args.readOnly:false,
|
||
|
stripComments: ("stripComments" in args)?args.stripComments:true,
|
||
|
stripScripts: ("stripScripts" in args)?args.stripScripts:true,
|
||
|
stripIFrames: ("stripIFrames" in args)?args.stripIFrames:true
|
||
|
});
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
return ViewSource;
|
||
|
});
|