RichText.js 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dijit._editor.RichText"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit._editor.RichText"] = true;
  8. dojo.provide("dijit._editor.RichText");
  9. dojo.require("dijit._Widget");
  10. dojo.require("dijit._CssStateMixin");
  11. dojo.require("dijit._editor.selection");
  12. dojo.require("dijit._editor.range");
  13. dojo.require("dijit._editor.html");
  14. // used to restore content when user leaves this page then comes back
  15. // but do not try doing dojo.doc.write if we are using xd loading.
  16. // dojo.doc.write will only work if RichText.js is included in the dojo.js
  17. // file. If it is included in dojo.js and you want to allow rich text saving
  18. // for back/forward actions, then set dojo.config.allowXdRichTextSave = true.
  19. if(!dojo.config["useXDomain"] || dojo.config["allowXdRichTextSave"]){
  20. if(dojo._postLoad){
  21. (function(){
  22. var savetextarea = dojo.doc.createElement('textarea');
  23. savetextarea.id = dijit._scopeName + "._editor.RichText.value";
  24. dojo.style(savetextarea, {
  25. display:'none',
  26. position:'absolute',
  27. top:"-100px",
  28. height:"3px",
  29. width:"3px"
  30. });
  31. dojo.body().appendChild(savetextarea);
  32. })();
  33. }else{
  34. //dojo.body() is not available before onLoad is fired
  35. try{
  36. dojo.doc.write('<textarea id="' + dijit._scopeName + '._editor.RichText.value" ' +
  37. 'style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>');
  38. }catch(e){ }
  39. }
  40. }
  41. dojo.declare("dijit._editor.RichText", [dijit._Widget, dijit._CssStateMixin], {
  42. constructor: function(params){
  43. // summary:
  44. // dijit._editor.RichText is the core of dijit.Editor, which provides basic
  45. // WYSIWYG editing features.
  46. //
  47. // description:
  48. // dijit._editor.RichText is the core of dijit.Editor, which provides basic
  49. // WYSIWYG editing features. It also encapsulates the differences
  50. // of different js engines for various browsers. Do not use this widget
  51. // with an HTML &lt;TEXTAREA&gt; tag, since the browser unescapes XML escape characters,
  52. // like &lt;. This can have unexpected behavior and lead to security issues
  53. // such as scripting attacks.
  54. //
  55. // tags:
  56. // private
  57. // contentPreFilters: Function(String)[]
  58. // Pre content filter function register array.
  59. // these filters will be executed before the actual
  60. // editing area gets the html content.
  61. this.contentPreFilters = [];
  62. // contentPostFilters: Function(String)[]
  63. // post content filter function register array.
  64. // These will be used on the resulting html
  65. // from contentDomPostFilters. The resulting
  66. // content is the final html (returned by getValue()).
  67. this.contentPostFilters = [];
  68. // contentDomPreFilters: Function(DomNode)[]
  69. // Pre content dom filter function register array.
  70. // These filters are applied after the result from
  71. // contentPreFilters are set to the editing area.
  72. this.contentDomPreFilters = [];
  73. // contentDomPostFilters: Function(DomNode)[]
  74. // Post content dom filter function register array.
  75. // These filters are executed on the editing area dom.
  76. // The result from these will be passed to contentPostFilters.
  77. this.contentDomPostFilters = [];
  78. // editingAreaStyleSheets: dojo._URL[]
  79. // array to store all the stylesheets applied to the editing area
  80. this.editingAreaStyleSheets = [];
  81. // Make a copy of this.events before we start writing into it, otherwise we
  82. // will modify the prototype which leads to bad things on pages w/multiple editors
  83. this.events = [].concat(this.events);
  84. this._keyHandlers = {};
  85. if(params && dojo.isString(params.value)){
  86. this.value = params.value;
  87. }
  88. this.onLoadDeferred = new dojo.Deferred();
  89. },
  90. baseClass: "dijitEditor",
  91. // inheritWidth: Boolean
  92. // whether to inherit the parent's width or simply use 100%
  93. inheritWidth: false,
  94. // focusOnLoad: [deprecated] Boolean
  95. // Focus into this widget when the page is loaded
  96. focusOnLoad: false,
  97. // name: String?
  98. // Specifies the name of a (hidden) <textarea> node on the page that's used to save
  99. // the editor content on page leave. Used to restore editor contents after navigating
  100. // to a new page and then hitting the back button.
  101. name: "",
  102. // styleSheets: [const] String
  103. // semicolon (";") separated list of css files for the editing area
  104. styleSheets: "",
  105. // height: String
  106. // Set height to fix the editor at a specific height, with scrolling.
  107. // By default, this is 300px. If you want to have the editor always
  108. // resizes to accommodate the content, use AlwaysShowToolbar plugin
  109. // and set height="". If this editor is used within a layout widget,
  110. // set height="100%".
  111. height: "300px",
  112. // minHeight: String
  113. // The minimum height that the editor should have.
  114. minHeight: "1em",
  115. // isClosed: [private] Boolean
  116. isClosed: true,
  117. // isLoaded: [private] Boolean
  118. isLoaded: false,
  119. // _SEPARATOR: [private] String
  120. // Used to concat contents from multiple editors into a single string,
  121. // so they can be saved into a single <textarea> node. See "name" attribute.
  122. _SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@",
  123. // _NAME_CONTENT_SEP: [private] String
  124. // USed to separate name from content. Just a colon isn't safe.
  125. _NAME_CONTENT_SEP: "@@**%%:%%**@@",
  126. // onLoadDeferred: [readonly] dojo.Deferred
  127. // Deferred which is fired when the editor finishes loading.
  128. // Call myEditor.onLoadDeferred.then(callback) it to be informed
  129. // when the rich-text area initialization is finalized.
  130. onLoadDeferred: null,
  131. // isTabIndent: Boolean
  132. // Make tab key and shift-tab indent and outdent rather than navigating.
  133. // Caution: sing this makes web pages inaccessible to users unable to use a mouse.
  134. isTabIndent: false,
  135. // disableSpellCheck: [const] Boolean
  136. // When true, disables the browser's native spell checking, if supported.
  137. // Works only in Firefox.
  138. disableSpellCheck: false,
  139. postCreate: function(){
  140. if("textarea" == this.domNode.tagName.toLowerCase()){
  141. console.warn("RichText should not be used with the TEXTAREA tag. See dijit._editor.RichText docs.");
  142. }
  143. // Push in the builtin filters now, making them the first executed, but not over-riding anything
  144. // users passed in. See: #6062
  145. this.contentPreFilters = [dojo.hitch(this, "_preFixUrlAttributes")].concat(this.contentPreFilters);
  146. if(dojo.isMoz){
  147. this.contentPreFilters = [this._normalizeFontStyle].concat(this.contentPreFilters);
  148. this.contentPostFilters = [this._removeMozBogus].concat(this.contentPostFilters);
  149. }
  150. if(dojo.isWebKit){
  151. // Try to clean up WebKit bogus artifacts. The inserted classes
  152. // made by WebKit sometimes messes things up.
  153. this.contentPreFilters = [this._removeWebkitBogus].concat(this.contentPreFilters);
  154. this.contentPostFilters = [this._removeWebkitBogus].concat(this.contentPostFilters);
  155. }
  156. if(dojo.isIE){
  157. // IE generates <strong> and <em> but we want to normalize to <b> and <i>
  158. this.contentPostFilters = [this._normalizeFontStyle].concat(this.contentPostFilters);
  159. }
  160. this.inherited(arguments);
  161. dojo.publish(dijit._scopeName + "._editor.RichText::init", [this]);
  162. this.open();
  163. this.setupDefaultShortcuts();
  164. },
  165. setupDefaultShortcuts: function(){
  166. // summary:
  167. // Add some default key handlers
  168. // description:
  169. // Overwrite this to setup your own handlers. The default
  170. // implementation does not use Editor commands, but directly
  171. // executes the builtin commands within the underlying browser
  172. // support.
  173. // tags:
  174. // protected
  175. var exec = dojo.hitch(this, function(cmd, arg){
  176. return function(){
  177. return !this.execCommand(cmd,arg);
  178. };
  179. });
  180. var ctrlKeyHandlers = {
  181. b: exec("bold"),
  182. i: exec("italic"),
  183. u: exec("underline"),
  184. a: exec("selectall"),
  185. s: function(){ this.save(true); },
  186. m: function(){ this.isTabIndent = !this.isTabIndent; },
  187. "1": exec("formatblock", "h1"),
  188. "2": exec("formatblock", "h2"),
  189. "3": exec("formatblock", "h3"),
  190. "4": exec("formatblock", "h4"),
  191. "\\": exec("insertunorderedlist")
  192. };
  193. if(!dojo.isIE){
  194. ctrlKeyHandlers.Z = exec("redo"); //FIXME: undo?
  195. }
  196. for(var key in ctrlKeyHandlers){
  197. this.addKeyHandler(key, true, false, ctrlKeyHandlers[key]);
  198. }
  199. },
  200. // events: [private] String[]
  201. // events which should be connected to the underlying editing area
  202. events: ["onKeyPress", "onKeyDown", "onKeyUp"], // onClick handled specially
  203. // captureEvents: [deprecated] String[]
  204. // Events which should be connected to the underlying editing
  205. // area, events in this array will be addListener with
  206. // capture=true.
  207. // TODO: looking at the code I don't see any distinction between events and captureEvents,
  208. // so get rid of this for 2.0 if not sooner
  209. captureEvents: [],
  210. _editorCommandsLocalized: false,
  211. _localizeEditorCommands: function(){
  212. // summary:
  213. // When IE is running in a non-English locale, the API actually changes,
  214. // so that we have to say (for example) danraku instead of p (for paragraph).
  215. // Handle that here.
  216. // tags:
  217. // private
  218. if(dijit._editor._editorCommandsLocalized){
  219. // Use the already generate cache of mappings.
  220. this._local2NativeFormatNames = dijit._editor._local2NativeFormatNames;
  221. this._native2LocalFormatNames = dijit._editor._native2LocalFormatNames;
  222. return;
  223. }
  224. dijit._editor._editorCommandsLocalized = true;
  225. dijit._editor._local2NativeFormatNames = {};
  226. dijit._editor._native2LocalFormatNames = {};
  227. this._local2NativeFormatNames = dijit._editor._local2NativeFormatNames;
  228. this._native2LocalFormatNames = dijit._editor._native2LocalFormatNames;
  229. //in IE, names for blockformat is locale dependent, so we cache the values here
  230. //put p after div, so if IE returns Normal, we show it as paragraph
  231. //We can distinguish p and div if IE returns Normal, however, in order to detect that,
  232. //we have to call this.document.selection.createRange().parentElement() or such, which
  233. //could slow things down. Leave it as it is for now
  234. var formats = ['div', 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'address'];
  235. var localhtml = "", format, i=0;
  236. while((format=formats[i++])){
  237. //append a <br> after each element to separate the elements more reliably
  238. if(format.charAt(1) !== 'l'){
  239. localhtml += "<"+format+"><span>content</span></"+format+"><br/>";
  240. }else{
  241. localhtml += "<"+format+"><li>content</li></"+format+"><br/>";
  242. }
  243. }
  244. // queryCommandValue returns empty if we hide editNode, so move it out of screen temporary
  245. // Also, IE9 does weird stuff unless we do it inside the editor iframe.
  246. var style = { position: "absolute", top: "0px", zIndex: 10, opacity: 0.01 };
  247. var div = dojo.create('div', {style: style, innerHTML: localhtml});
  248. dojo.body().appendChild(div);
  249. // IE9 has a timing issue with doing this right after setting
  250. // the inner HTML, so put a delay in.
  251. var inject = dojo.hitch(this, function(){
  252. var node = div.firstChild;
  253. while(node){
  254. try{
  255. dijit._editor.selection.selectElement(node.firstChild);
  256. var nativename = node.tagName.toLowerCase();
  257. this._local2NativeFormatNames[nativename] = document.queryCommandValue("formatblock");
  258. this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename;
  259. node = node.nextSibling.nextSibling;
  260. //console.log("Mapped: ", nativename, " to: ", this._local2NativeFormatNames[nativename]);
  261. }catch(e) { /*Sqelch the occasional IE9 error */ }
  262. }
  263. div.parentNode.removeChild(div);
  264. div.innerHTML = "";
  265. });
  266. setTimeout(inject, 0);
  267. },
  268. open: function(/*DomNode?*/ element){
  269. // summary:
  270. // Transforms the node referenced in this.domNode into a rich text editing
  271. // node.
  272. // description:
  273. // Sets up the editing area asynchronously. This will result in
  274. // the creation and replacement with an iframe.
  275. // tags:
  276. // private
  277. if(!this.onLoadDeferred || this.onLoadDeferred.fired >= 0){
  278. this.onLoadDeferred = new dojo.Deferred();
  279. }
  280. if(!this.isClosed){ this.close(); }
  281. dojo.publish(dijit._scopeName + "._editor.RichText::open", [ this ]);
  282. if(arguments.length == 1 && element.nodeName){ // else unchanged
  283. this.domNode = element;
  284. }
  285. var dn = this.domNode;
  286. // "html" will hold the innerHTML of the srcNodeRef and will be used to
  287. // initialize the editor.
  288. var html;
  289. if(dojo.isString(this.value)){
  290. // Allow setting the editor content programmatically instead of
  291. // relying on the initial content being contained within the target
  292. // domNode.
  293. html = this.value;
  294. delete this.value;
  295. dn.innerHTML = "";
  296. }else if(dn.nodeName && dn.nodeName.toLowerCase() == "textarea"){
  297. // if we were created from a textarea, then we need to create a
  298. // new editing harness node.
  299. var ta = (this.textarea = dn);
  300. this.name = ta.name;
  301. html = ta.value;
  302. dn = this.domNode = dojo.doc.createElement("div");
  303. dn.setAttribute('widgetId', this.id);
  304. ta.removeAttribute('widgetId');
  305. dn.cssText = ta.cssText;
  306. dn.className += " " + ta.className;
  307. dojo.place(dn, ta, "before");
  308. var tmpFunc = dojo.hitch(this, function(){
  309. //some browsers refuse to submit display=none textarea, so
  310. //move the textarea off screen instead
  311. dojo.style(ta, {
  312. display: "block",
  313. position: "absolute",
  314. top: "-1000px"
  315. });
  316. if(dojo.isIE){ //nasty IE bug: abnormal formatting if overflow is not hidden
  317. var s = ta.style;
  318. this.__overflow = s.overflow;
  319. s.overflow = "hidden";
  320. }
  321. });
  322. if(dojo.isIE){
  323. setTimeout(tmpFunc, 10);
  324. }else{
  325. tmpFunc();
  326. }
  327. if(ta.form){
  328. var resetValue = ta.value;
  329. this.reset = function(){
  330. var current = this.getValue();
  331. if(current != resetValue){
  332. this.replaceValue(resetValue);
  333. }
  334. };
  335. dojo.connect(ta.form, "onsubmit", this, function(){
  336. // Copy value to the <textarea> so it gets submitted along with form.
  337. // FIXME: should we be calling close() here instead?
  338. dojo.attr(ta, 'disabled', this.disabled); // don't submit the value if disabled
  339. ta.value = this.getValue();
  340. });
  341. }
  342. }else{
  343. html = dijit._editor.getChildrenHtml(dn);
  344. dn.innerHTML = "";
  345. }
  346. var content = dojo.contentBox(dn);
  347. this._oldHeight = content.h;
  348. this._oldWidth = content.w;
  349. this.value = html;
  350. // If we're a list item we have to put in a blank line to force the
  351. // bullet to nicely align at the top of text
  352. if(dn.nodeName && dn.nodeName == "LI"){
  353. dn.innerHTML = " <br>";
  354. }
  355. // Construct the editor div structure.
  356. this.header = dn.ownerDocument.createElement("div");
  357. dn.appendChild(this.header);
  358. this.editingArea = dn.ownerDocument.createElement("div");
  359. dn.appendChild(this.editingArea);
  360. this.footer = dn.ownerDocument.createElement("div");
  361. dn.appendChild(this.footer);
  362. if(!this.name){
  363. this.name = this.id + "_AUTOGEN";
  364. }
  365. // User has pressed back/forward button so we lost the text in the editor, but it's saved
  366. // in a hidden <textarea> (which contains the data for all the editors on this page),
  367. // so get editor value from there
  368. if(this.name !== "" && (!dojo.config["useXDomain"] || dojo.config["allowXdRichTextSave"])){
  369. var saveTextarea = dojo.byId(dijit._scopeName + "._editor.RichText.value");
  370. if(saveTextarea && saveTextarea.value !== ""){
  371. var datas = saveTextarea.value.split(this._SEPARATOR), i=0, dat;
  372. while((dat=datas[i++])){
  373. var data = dat.split(this._NAME_CONTENT_SEP);
  374. if(data[0] == this.name){
  375. html = data[1];
  376. datas = datas.splice(i, 1);
  377. saveTextarea.value = datas.join(this._SEPARATOR);
  378. break;
  379. }
  380. }
  381. }
  382. if(!dijit._editor._globalSaveHandler){
  383. dijit._editor._globalSaveHandler = {};
  384. dojo.addOnUnload(function() {
  385. var id;
  386. for(id in dijit._editor._globalSaveHandler){
  387. var f = dijit._editor._globalSaveHandler[id];
  388. if(dojo.isFunction(f)){
  389. f();
  390. }
  391. }
  392. });
  393. }
  394. dijit._editor._globalSaveHandler[this.id] = dojo.hitch(this, "_saveContent");
  395. }
  396. this.isClosed = false;
  397. var ifr = (this.editorObject = this.iframe = dojo.doc.createElement('iframe'));
  398. ifr.id = this.id+"_iframe";
  399. this._iframeSrc = this._getIframeDocTxt();
  400. ifr.style.border = "none";
  401. ifr.style.width = "100%";
  402. if(this._layoutMode){
  403. // iframe should be 100% height, thus getting it's height from surrounding
  404. // <div> (which has the correct height set by Editor)
  405. ifr.style.height = "100%";
  406. }else{
  407. if(dojo.isIE >= 7){
  408. if(this.height){
  409. ifr.style.height = this.height;
  410. }
  411. if(this.minHeight){
  412. ifr.style.minHeight = this.minHeight;
  413. }
  414. }else{
  415. ifr.style.height = this.height ? this.height : this.minHeight;
  416. }
  417. }
  418. ifr.frameBorder = 0;
  419. ifr._loadFunc = dojo.hitch( this, function(win){
  420. this.window = win;
  421. this.document = this.window.document;
  422. if(dojo.isIE){
  423. this._localizeEditorCommands();
  424. }
  425. // Do final setup and set initial contents of editor
  426. this.onLoad(html);
  427. });
  428. // Set the iframe's initial (blank) content.
  429. var s = 'javascript:parent.' + dijit._scopeName + '.byId("'+this.id+'")._iframeSrc';
  430. ifr.setAttribute('src', s);
  431. this.editingArea.appendChild(ifr);
  432. if(dojo.isSafari <= 4){
  433. var src = ifr.getAttribute("src");
  434. if(!src || src.indexOf("javascript") == -1){
  435. // Safari 4 and earlier sometimes act oddly
  436. // So we have to set it again.
  437. setTimeout(function(){ifr.setAttribute('src', s);},0);
  438. }
  439. }
  440. // TODO: this is a guess at the default line-height, kinda works
  441. if(dn.nodeName == "LI"){
  442. dn.lastChild.style.marginTop = "-1.2em";
  443. }
  444. dojo.addClass(this.domNode, this.baseClass);
  445. },
  446. //static cache variables shared among all instance of this class
  447. _local2NativeFormatNames: {},
  448. _native2LocalFormatNames: {},
  449. _getIframeDocTxt: function(){
  450. // summary:
  451. // Generates the boilerplate text of the document inside the iframe (ie, <html><head>...</head><body/></html>).
  452. // Editor content (if not blank) should be added afterwards.
  453. // tags:
  454. // private
  455. var _cs = dojo.getComputedStyle(this.domNode);
  456. // The contents inside of <body>. The real contents are set later via a call to setValue().
  457. var html = "";
  458. var setBodyId = true;
  459. if(dojo.isIE || dojo.isWebKit || (!this.height && !dojo.isMoz)){
  460. // In auto-expand mode, need a wrapper div for AlwaysShowToolbar plugin to correctly
  461. // expand/contract the editor as the content changes.
  462. html = "<div id='dijitEditorBody'></div>";
  463. setBodyId = false;
  464. }else if(dojo.isMoz){
  465. // workaround bug where can't select then delete text (until user types something
  466. // into the editor)... and/or issue where typing doesn't erase selected text
  467. this._cursorToStart = true;
  468. html = "&nbsp;";
  469. }
  470. var font = [ _cs.fontWeight, _cs.fontSize, _cs.fontFamily ].join(" ");
  471. // line height is tricky - applying a units value will mess things up.
  472. // if we can't get a non-units value, bail out.
  473. var lineHeight = _cs.lineHeight;
  474. if(lineHeight.indexOf("px") >= 0){
  475. lineHeight = parseFloat(lineHeight)/parseFloat(_cs.fontSize);
  476. // console.debug(lineHeight);
  477. }else if(lineHeight.indexOf("em")>=0){
  478. lineHeight = parseFloat(lineHeight);
  479. }else{
  480. // If we can't get a non-units value, just default
  481. // it to the CSS spec default of 'normal'. Seems to
  482. // work better, esp on IE, than '1.0'
  483. lineHeight = "normal";
  484. }
  485. var userStyle = "";
  486. var self = this;
  487. this.style.replace(/(^|;)\s*(line-|font-?)[^;]+/ig, function(match){
  488. match = match.replace(/^;/ig,"") + ';';
  489. var s = match.split(":")[0];
  490. if(s){
  491. s = dojo.trim(s);
  492. s = s.toLowerCase();
  493. var i;
  494. var sC = "";
  495. for(i = 0; i < s.length; i++){
  496. var c = s.charAt(i);
  497. switch(c){
  498. case "-":
  499. i++;
  500. c = s.charAt(i).toUpperCase();
  501. default:
  502. sC += c;
  503. }
  504. }
  505. dojo.style(self.domNode, sC, "");
  506. }
  507. userStyle += match + ';';
  508. });
  509. // need to find any associated label element and update iframe document title
  510. var label=dojo.query('label[for="'+this.id+'"]');
  511. return [
  512. this.isLeftToRight() ? "<html>\n<head>\n" : "<html dir='rtl'>\n<head>\n",
  513. (dojo.isMoz && label.length ? "<title>" + label[0].innerHTML + "</title>\n" : ""),
  514. "<meta http-equiv='Content-Type' content='text/html'>\n",
  515. "<style>\n",
  516. "\tbody,html {\n",
  517. "\t\tbackground:transparent;\n",
  518. "\t\tpadding: 1px 0 0 0;\n",
  519. "\t\tmargin: -1px 0 0 0;\n", // remove extraneous vertical scrollbar on safari and firefox
  520. // Set the html/body sizing. Webkit always needs this, other browsers
  521. // only set it when height is defined (not auto-expanding), otherwise
  522. // scrollers do not appear.
  523. ((dojo.isWebKit)?"\t\twidth: 100%;\n":""),
  524. ((dojo.isWebKit)?"\t\theight: 100%;\n":""),
  525. "\t}\n",
  526. // TODO: left positioning will cause contents to disappear out of view
  527. // if it gets too wide for the visible area
  528. "\tbody{\n",
  529. "\t\ttop:0px;\n",
  530. "\t\tleft:0px;\n",
  531. "\t\tright:0px;\n",
  532. "\t\tfont:", font, ";\n",
  533. ((this.height||dojo.isOpera) ? "" : "\t\tposition: fixed;\n"),
  534. // FIXME: IE 6 won't understand min-height?
  535. "\t\tmin-height:", this.minHeight, ";\n",
  536. "\t\tline-height:", lineHeight,";\n",
  537. "\t}\n",
  538. "\tp{ margin: 1em 0; }\n",
  539. // Determine how scrollers should be applied. In autoexpand mode (height = "") no scrollers on y at all.
  540. // But in fixed height mode we want both x/y scrollers. Also, if it's using wrapping div and in auto-expand
  541. // (Mainly IE) we need to kill the y scroller on body and html.
  542. (!setBodyId && !this.height ? "\tbody,html {overflow-y: hidden;}\n" : ""),
  543. "\t#dijitEditorBody{overflow-x: auto; overflow-y:" + (this.height ? "auto;" : "hidden;") + " outline: 0px;}\n",
  544. "\tli > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; }\n",
  545. // Can't set min-height in IE9, it puts layout on li, which puts move/resize handles.
  546. (!dojo.isIE ? "\tli{ min-height:1.2em; }\n" : ""),
  547. "</style>\n",
  548. this._applyEditingAreaStyleSheets(),"\n",
  549. "</head>\n<body ",
  550. (setBodyId?"id='dijitEditorBody' ":""),
  551. "onload='frameElement._loadFunc(window,document)' style='"+userStyle+"'>", html, "</body>\n</html>"
  552. ].join(""); // String
  553. },
  554. _applyEditingAreaStyleSheets: function(){
  555. // summary:
  556. // apply the specified css files in styleSheets
  557. // tags:
  558. // private
  559. var files = [];
  560. if(this.styleSheets){
  561. files = this.styleSheets.split(';');
  562. this.styleSheets = '';
  563. }
  564. //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
  565. files = files.concat(this.editingAreaStyleSheets);
  566. this.editingAreaStyleSheets = [];
  567. var text='', i=0, url;
  568. while((url=files[i++])){
  569. var abstring = (new dojo._Url(dojo.global.location, url)).toString();
  570. this.editingAreaStyleSheets.push(abstring);
  571. text += '<link rel="stylesheet" type="text/css" href="'+abstring+'"/>';
  572. }
  573. return text;
  574. },
  575. addStyleSheet: function(/*dojo._Url*/ uri){
  576. // summary:
  577. // add an external stylesheet for the editing area
  578. // uri:
  579. // A dojo.uri.Uri pointing to the url of the external css file
  580. var url=uri.toString();
  581. //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
  582. if(url.charAt(0) == '.' || (url.charAt(0) != '/' && !uri.host)){
  583. url = (new dojo._Url(dojo.global.location, url)).toString();
  584. }
  585. if(dojo.indexOf(this.editingAreaStyleSheets, url) > -1){
  586. // console.debug("dijit._editor.RichText.addStyleSheet: Style sheet "+url+" is already applied");
  587. return;
  588. }
  589. this.editingAreaStyleSheets.push(url);
  590. this.onLoadDeferred.addCallback(dojo.hitch(this, function(){
  591. if(this.document.createStyleSheet){ //IE
  592. this.document.createStyleSheet(url);
  593. }else{ //other browser
  594. var head = this.document.getElementsByTagName("head")[0];
  595. var stylesheet = this.document.createElement("link");
  596. stylesheet.rel="stylesheet";
  597. stylesheet.type="text/css";
  598. stylesheet.href=url;
  599. head.appendChild(stylesheet);
  600. }
  601. }));
  602. },
  603. removeStyleSheet: function(/*dojo._Url*/ uri){
  604. // summary:
  605. // remove an external stylesheet for the editing area
  606. var url=uri.toString();
  607. //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
  608. if(url.charAt(0) == '.' || (url.charAt(0) != '/' && !uri.host)){
  609. url = (new dojo._Url(dojo.global.location, url)).toString();
  610. }
  611. var index = dojo.indexOf(this.editingAreaStyleSheets, url);
  612. if(index == -1){
  613. // console.debug("dijit._editor.RichText.removeStyleSheet: Style sheet "+url+" has not been applied");
  614. return;
  615. }
  616. delete this.editingAreaStyleSheets[index];
  617. dojo.withGlobal(this.window,'query', dojo, ['link:[href="'+url+'"]']).orphan();
  618. },
  619. // disabled: Boolean
  620. // The editor is disabled; the text cannot be changed.
  621. disabled: false,
  622. _mozSettingProps: {'styleWithCSS':false},
  623. _setDisabledAttr: function(/*Boolean*/ value){
  624. value = !!value;
  625. this._set("disabled", value);
  626. if(!this.isLoaded){ return; } // this method requires init to be complete
  627. if(dojo.isIE || dojo.isWebKit || dojo.isOpera){
  628. var preventIEfocus = dojo.isIE && (this.isLoaded || !this.focusOnLoad);
  629. if(preventIEfocus){ this.editNode.unselectable = "on"; }
  630. this.editNode.contentEditable = !value;
  631. if(preventIEfocus){
  632. var _this = this;
  633. setTimeout(function(){ _this.editNode.unselectable = "off"; }, 0);
  634. }
  635. }else{ //moz
  636. try{
  637. this.document.designMode=(value?'off':'on');
  638. }catch(e){ return; } // ! _disabledOK
  639. if(!value && this._mozSettingProps){
  640. var ps = this._mozSettingProps;
  641. for(var n in ps){
  642. if(ps.hasOwnProperty(n)){
  643. try{
  644. this.document.execCommand(n,false,ps[n]);
  645. }catch(e2){}
  646. }
  647. }
  648. }
  649. // this.document.execCommand('contentReadOnly', false, value);
  650. // if(value){
  651. // this.blur(); //to remove the blinking caret
  652. // }
  653. }
  654. this._disabledOK = true;
  655. },
  656. /* Event handlers
  657. *****************/
  658. onLoad: function(/*String*/ html){
  659. // summary:
  660. // Handler after the iframe finishes loading.
  661. // html: String
  662. // Editor contents should be set to this value
  663. // tags:
  664. // protected
  665. // TODO: rename this to _onLoad, make empty public onLoad() method, deprecate/make protected onLoadDeferred handler?
  666. if(!this.window.__registeredWindow){
  667. this.window.__registeredWindow = true;
  668. this._iframeRegHandle = dijit.registerIframe(this.iframe);
  669. }
  670. if(!dojo.isIE && !dojo.isWebKit && (this.height || dojo.isMoz)){
  671. this.editNode=this.document.body;
  672. }else{
  673. // there's a wrapper div around the content, see _getIframeDocTxt().
  674. this.editNode=this.document.body.firstChild;
  675. var _this = this;
  676. if(dojo.isIE){ // #4996 IE wants to focus the BODY tag
  677. this.tabStop = dojo.create('div', { tabIndex: -1 }, this.editingArea);
  678. this.iframe.onfocus = function(){ _this.editNode.setActive(); };
  679. }
  680. }
  681. this.focusNode = this.editNode; // for InlineEditBox
  682. var events = this.events.concat(this.captureEvents);
  683. var ap = this.iframe ? this.document : this.editNode;
  684. dojo.forEach(events, function(item){
  685. this.connect(ap, item.toLowerCase(), item);
  686. }, this);
  687. this.connect(ap, "onmouseup", "onClick"); // mouseup in the margin does not generate an onclick event
  688. if(dojo.isIE){ // IE contentEditable
  689. this.connect(this.document, "onmousedown", "_onIEMouseDown"); // #4996 fix focus
  690. // give the node Layout on IE
  691. // TODO: this may no longer be needed, since we've reverted IE to using an iframe,
  692. // not contentEditable. Removing it would also probably remove the need for creating
  693. // the extra <div> in _getIframeDocTxt()
  694. this.editNode.style.zoom = 1.0;
  695. }else{
  696. this.connect(this.document, "onmousedown", function(){
  697. // Clear the moveToStart focus, as mouse
  698. // down will set cursor point. Required to properly
  699. // work with selection/position driven plugins and clicks in
  700. // the window. refs: #10678
  701. delete this._cursorToStart;
  702. });
  703. }
  704. if(dojo.isWebKit){
  705. //WebKit sometimes doesn't fire right on selections, so the toolbar
  706. //doesn't update right. Therefore, help it out a bit with an additional
  707. //listener. A mouse up will typically indicate a display change, so fire this
  708. //and get the toolbar to adapt. Reference: #9532
  709. this._webkitListener = this.connect(this.document, "onmouseup", "onDisplayChanged");
  710. this.connect(this.document, "onmousedown", function(e){
  711. var t = e.target;
  712. if(t && (t === this.document.body || t === this.document)){
  713. // Since WebKit uses the inner DIV, we need to check and set position.
  714. // See: #12024 as to why the change was made.
  715. setTimeout(dojo.hitch(this, "placeCursorAtEnd"), 0);
  716. }
  717. });
  718. }
  719. if(dojo.isIE){
  720. // Try to make sure 'hidden' elements aren't visible in edit mode (like browsers other than IE
  721. // do). See #9103
  722. try{
  723. this.document.execCommand('RespectVisibilityInDesign', true, null);
  724. }catch(e){/* squelch */}
  725. }
  726. this.isLoaded = true;
  727. this.set('disabled', this.disabled); // initialize content to editable (or not)
  728. // Note that setValue() call will only work after isLoaded is set to true (above)
  729. // Set up a function to allow delaying the setValue until a callback is fired
  730. // This ensures extensions like dijit.Editor have a way to hold the value set
  731. // until plugins load (and do things like register filters).
  732. var setContent = dojo.hitch(this, function(){
  733. this.setValue(html);
  734. if(this.onLoadDeferred){
  735. this.onLoadDeferred.callback(true);
  736. }
  737. this.onDisplayChanged();
  738. if(this.focusOnLoad){
  739. // after the document loads, then set focus after updateInterval expires so that
  740. // onNormalizedDisplayChanged has run to avoid input caret issues
  741. dojo.addOnLoad(dojo.hitch(this, function(){ setTimeout(dojo.hitch(this, "focus"), this.updateInterval); }));
  742. }
  743. // Save off the initial content now
  744. this.value = this.getValue(true);
  745. });
  746. if(this.setValueDeferred){
  747. this.setValueDeferred.addCallback(setContent);
  748. }else{
  749. setContent();
  750. }
  751. },
  752. onKeyDown: function(/* Event */ e){
  753. // summary:
  754. // Handler for onkeydown event
  755. // tags:
  756. // protected
  757. // we need this event at the moment to get the events from control keys
  758. // such as the backspace. It might be possible to add this to Dojo, so that
  759. // keyPress events can be emulated by the keyDown and keyUp detection.
  760. if(e.keyCode === dojo.keys.TAB && this.isTabIndent ){
  761. dojo.stopEvent(e); //prevent tab from moving focus out of editor
  762. // FIXME: this is a poor-man's indent/outdent. It would be
  763. // better if it added 4 "&nbsp;" chars in an undoable way.
  764. // Unfortunately pasteHTML does not prove to be undoable
  765. if(this.queryCommandEnabled((e.shiftKey ? "outdent" : "indent"))){
  766. this.execCommand((e.shiftKey ? "outdent" : "indent"));
  767. }
  768. }
  769. if(dojo.isIE){
  770. if(e.keyCode == dojo.keys.TAB && !this.isTabIndent){
  771. if(e.shiftKey && !e.ctrlKey && !e.altKey){
  772. // focus the BODY so the browser will tab away from it instead
  773. this.iframe.focus();
  774. }else if(!e.shiftKey && !e.ctrlKey && !e.altKey){
  775. // focus the BODY so the browser will tab away from it instead
  776. this.tabStop.focus();
  777. }
  778. }else if(e.keyCode === dojo.keys.BACKSPACE && this.document.selection.type === "Control"){
  779. // IE has a bug where if a non-text object is selected in the editor,
  780. // hitting backspace would act as if the browser's back button was
  781. // clicked instead of deleting the object. see #1069
  782. dojo.stopEvent(e);
  783. this.execCommand("delete");
  784. }else if((65 <= e.keyCode && e.keyCode <= 90) ||
  785. (e.keyCode>=37 && e.keyCode<=40) // FIXME: get this from connect() instead!
  786. ){ //arrow keys
  787. e.charCode = e.keyCode;
  788. this.onKeyPress(e);
  789. }
  790. }
  791. return true;
  792. },
  793. onKeyUp: function(e){
  794. // summary:
  795. // Handler for onkeyup event
  796. // tags:
  797. // callback
  798. return;
  799. },
  800. setDisabled: function(/*Boolean*/ disabled){
  801. // summary:
  802. // Deprecated, use set('disabled', ...) instead.
  803. // tags:
  804. // deprecated
  805. dojo.deprecated('dijit.Editor::setDisabled is deprecated','use dijit.Editor::attr("disabled",boolean) instead', 2.0);
  806. this.set('disabled',disabled);
  807. },
  808. _setValueAttr: function(/*String*/ value){
  809. // summary:
  810. // Registers that attr("value", foo) should call setValue(foo)
  811. this.setValue(value);
  812. },
  813. _setDisableSpellCheckAttr: function(/*Boolean*/ disabled){
  814. if(this.document){
  815. dojo.attr(this.document.body, "spellcheck", !disabled);
  816. }else{
  817. // try again after the editor is finished loading
  818. this.onLoadDeferred.addCallback(dojo.hitch(this, function(){
  819. dojo.attr(this.document.body, "spellcheck", !disabled);
  820. }));
  821. }
  822. this._set("disableSpellCheck", disabled);
  823. },
  824. onKeyPress: function(e){
  825. // summary:
  826. // Handle the various key events
  827. // tags:
  828. // protected
  829. var c = (e.keyChar && e.keyChar.toLowerCase()) || e.keyCode,
  830. handlers = this._keyHandlers[c],
  831. args = arguments;
  832. if(handlers && !e.altKey){
  833. dojo.some(handlers, function(h){
  834. // treat meta- same as ctrl-, for benefit of mac users
  835. if(!(h.shift ^ e.shiftKey) && !(h.ctrl ^ (e.ctrlKey||e.metaKey))){
  836. if(!h.handler.apply(this, args)){
  837. e.preventDefault();
  838. }
  839. return true;
  840. }
  841. }, this);
  842. }
  843. // function call after the character has been inserted
  844. if(!this._onKeyHitch){
  845. this._onKeyHitch = dojo.hitch(this, "onKeyPressed");
  846. }
  847. setTimeout(this._onKeyHitch, 1);
  848. return true;
  849. },
  850. addKeyHandler: function(/*String*/ key, /*Boolean*/ ctrl, /*Boolean*/ shift, /*Function*/ handler){
  851. // summary:
  852. // Add a handler for a keyboard shortcut
  853. // description:
  854. // The key argument should be in lowercase if it is a letter character
  855. // tags:
  856. // protected
  857. if(!dojo.isArray(this._keyHandlers[key])){
  858. this._keyHandlers[key] = [];
  859. }
  860. //TODO: would be nice to make this a hash instead of an array for quick lookups
  861. this._keyHandlers[key].push({
  862. shift: shift || false,
  863. ctrl: ctrl || false,
  864. handler: handler
  865. });
  866. },
  867. onKeyPressed: function(){
  868. // summary:
  869. // Handler for after the user has pressed a key, and the display has been updated.
  870. // (Runs on a timer so that it runs after the display is updated)
  871. // tags:
  872. // private
  873. this.onDisplayChanged(/*e*/); // can't pass in e
  874. },
  875. onClick: function(/*Event*/ e){
  876. // summary:
  877. // Handler for when the user clicks.
  878. // tags:
  879. // private
  880. // console.info('onClick',this._tryDesignModeOn);
  881. this.onDisplayChanged(e);
  882. },
  883. _onIEMouseDown: function(/*Event*/ e){
  884. // summary:
  885. // IE only to prevent 2 clicks to focus
  886. // tags:
  887. // protected
  888. if(!this._focused && !this.disabled){
  889. this.focus();
  890. }
  891. },
  892. _onBlur: function(e){
  893. // summary:
  894. // Called from focus manager when focus has moved away from this editor
  895. // tags:
  896. // protected
  897. // console.info('_onBlur')
  898. this.inherited(arguments);
  899. var newValue = this.getValue(true);
  900. if(newValue != this.value){
  901. this.onChange(newValue);
  902. }
  903. this._set("value", newValue);
  904. },
  905. _onFocus: function(/*Event*/ e){
  906. // summary:
  907. // Called from focus manager when focus has moved into this editor
  908. // tags:
  909. // protected
  910. // console.info('_onFocus')
  911. if(!this.disabled){
  912. if(!this._disabledOK){
  913. this.set('disabled', false);
  914. }
  915. this.inherited(arguments);
  916. }
  917. },
  918. // TODO: remove in 2.0
  919. blur: function(){
  920. // summary:
  921. // Remove focus from this instance.
  922. // tags:
  923. // deprecated
  924. if(!dojo.isIE && this.window.document.documentElement && this.window.document.documentElement.focus){
  925. this.window.document.documentElement.focus();
  926. }else if(dojo.doc.body.focus){
  927. dojo.doc.body.focus();
  928. }
  929. },
  930. focus: function(){
  931. // summary:
  932. // Move focus to this editor
  933. if(!this.isLoaded){
  934. this.focusOnLoad = true;
  935. return;
  936. }
  937. if(this._cursorToStart){
  938. delete this._cursorToStart;
  939. if(this.editNode.childNodes){
  940. this.placeCursorAtStart(); // this calls focus() so return
  941. return;
  942. }
  943. }
  944. if(!dojo.isIE){
  945. dijit.focus(this.iframe);
  946. }else if(this.editNode && this.editNode.focus){
  947. // editNode may be hidden in display:none div, lets just punt in this case
  948. //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
  949. // if we fire the event manually and let the browser handle the focusing, the latest
  950. // cursor position is focused like in FF
  951. this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject only in IE
  952. // }else{
  953. // TODO: should we throw here?
  954. // console.debug("Have no idea how to focus into the editor!");
  955. }
  956. },
  957. // _lastUpdate: 0,
  958. updateInterval: 200,
  959. _updateTimer: null,
  960. onDisplayChanged: function(/*Event*/ e){
  961. // summary:
  962. // This event will be fired everytime the display context
  963. // changes and the result needs to be reflected in the UI.
  964. // description:
  965. // If you don't want to have update too often,
  966. // onNormalizedDisplayChanged should be used instead
  967. // tags:
  968. // private
  969. // var _t=new Date();
  970. if(this._updateTimer){
  971. clearTimeout(this._updateTimer);
  972. }
  973. if(!this._updateHandler){
  974. this._updateHandler = dojo.hitch(this,"onNormalizedDisplayChanged");
  975. }
  976. this._updateTimer = setTimeout(this._updateHandler, this.updateInterval);
  977. // Technically this should trigger a call to watch("value", ...) registered handlers,
  978. // but getValue() is too slow to call on every keystroke so we don't.
  979. },
  980. onNormalizedDisplayChanged: function(){
  981. // summary:
  982. // This event is fired every updateInterval ms or more
  983. // description:
  984. // If something needs to happen immediately after a
  985. // user change, please use onDisplayChanged instead.
  986. // tags:
  987. // private
  988. delete this._updateTimer;
  989. },
  990. onChange: function(newContent){
  991. // summary:
  992. // This is fired if and only if the editor loses focus and
  993. // the content is changed.
  994. },
  995. _normalizeCommand: function(/*String*/ cmd, /*Anything?*/argument){
  996. // summary:
  997. // Used as the advice function by dojo.connect to map our
  998. // normalized set of commands to those supported by the target
  999. // browser.
  1000. // tags:
  1001. // private
  1002. var command = cmd.toLowerCase();
  1003. if(command == "formatblock"){
  1004. if(dojo.isSafari && argument === undefined){ command = "heading"; }
  1005. }else if(command == "hilitecolor" && !dojo.isMoz){
  1006. command = "backcolor";
  1007. }
  1008. return command;
  1009. },
  1010. _qcaCache: {},
  1011. queryCommandAvailable: function(/*String*/ command){
  1012. // summary:
  1013. // Tests whether a command is supported by the host. Clients
  1014. // SHOULD check whether a command is supported before attempting
  1015. // to use it, behaviour for unsupported commands is undefined.
  1016. // command:
  1017. // The command to test for
  1018. // tags:
  1019. // private
  1020. // memoizing version. See _queryCommandAvailable for computing version
  1021. var ca = this._qcaCache[command];
  1022. if(ca !== undefined){ return ca; }
  1023. return (this._qcaCache[command] = this._queryCommandAvailable(command));
  1024. },
  1025. _queryCommandAvailable: function(/*String*/ command){
  1026. // summary:
  1027. // See queryCommandAvailable().
  1028. // tags:
  1029. // private
  1030. var ie = 1;
  1031. var mozilla = 1 << 1;
  1032. var webkit = 1 << 2;
  1033. var opera = 1 << 3;
  1034. function isSupportedBy(browsers){
  1035. return {
  1036. ie: Boolean(browsers & ie),
  1037. mozilla: Boolean(browsers & mozilla),
  1038. webkit: Boolean(browsers & webkit),
  1039. opera: Boolean(browsers & opera)
  1040. };
  1041. }
  1042. var supportedBy = null;
  1043. switch(command.toLowerCase()){
  1044. case "bold": case "italic": case "underline":
  1045. case "subscript": case "superscript":
  1046. case "fontname": case "fontsize":
  1047. case "forecolor": case "hilitecolor":
  1048. case "justifycenter": case "justifyfull": case "justifyleft":
  1049. case "justifyright": case "delete": case "selectall": case "toggledir":
  1050. supportedBy = isSupportedBy(mozilla | ie | webkit | opera);
  1051. break;
  1052. case "createlink": case "unlink": case "removeformat":
  1053. case "inserthorizontalrule": case "insertimage":
  1054. case "insertorderedlist": case "insertunorderedlist":
  1055. case "indent": case "outdent": case "formatblock":
  1056. case "inserthtml": case "undo": case "redo": case "strikethrough": case "tabindent":
  1057. supportedBy = isSupportedBy(mozilla | ie | opera | webkit);
  1058. break;
  1059. case "blockdirltr": case "blockdirrtl":
  1060. case "dirltr": case "dirrtl":
  1061. case "inlinedirltr": case "inlinedirrtl":
  1062. supportedBy = isSupportedBy(ie);
  1063. break;
  1064. case "cut": case "copy": case "paste":
  1065. supportedBy = isSupportedBy( ie | mozilla | webkit);
  1066. break;
  1067. case "inserttable":
  1068. supportedBy = isSupportedBy(mozilla | ie);
  1069. break;
  1070. case "insertcell": case "insertcol": case "insertrow":
  1071. case "deletecells": case "deletecols": case "deleterows":
  1072. case "mergecells": case "splitcell":
  1073. supportedBy = isSupportedBy(ie | mozilla);
  1074. break;
  1075. default: return false;
  1076. }
  1077. return (dojo.isIE && supportedBy.ie) ||
  1078. (dojo.isMoz && supportedBy.mozilla) ||
  1079. (dojo.isWebKit && supportedBy.webkit) ||
  1080. (dojo.isOpera && supportedBy.opera); // Boolean return true if the command is supported, false otherwise
  1081. },
  1082. execCommand: function(/*String*/ command, argument){
  1083. // summary:
  1084. // Executes a command in the Rich Text area
  1085. // command:
  1086. // The command to execute
  1087. // argument:
  1088. // An optional argument to the command
  1089. // tags:
  1090. // protected
  1091. var returnValue;
  1092. //focus() is required for IE to work
  1093. //In addition, focus() makes sure after the execution of
  1094. //the command, the editor receives the focus as expected
  1095. this.focus();
  1096. command = this._normalizeCommand(command, argument);
  1097. if(argument !== undefined){
  1098. if(command == "heading"){
  1099. throw new Error("unimplemented");
  1100. }else if((command == "formatblock") && dojo.isIE){
  1101. argument = '<'+argument+'>';
  1102. }
  1103. }
  1104. //Check to see if we have any over-rides for commands, they will be functions on this
  1105. //widget of the form _commandImpl. If we don't, fall through to the basic native
  1106. //exec command of the browser.
  1107. var implFunc = "_" + command + "Impl";
  1108. if(this[implFunc]){
  1109. returnValue = this[implFunc](argument);
  1110. }else{
  1111. argument = arguments.length > 1 ? argument : null;
  1112. if(argument || command!="createlink"){
  1113. returnValue = this.document.execCommand(command, false, argument);
  1114. }
  1115. }
  1116. this.onDisplayChanged();
  1117. return returnValue;
  1118. },
  1119. queryCommandEnabled: function(/*String*/ command){
  1120. // summary:
  1121. // Check whether a command is enabled or not.
  1122. // tags:
  1123. // protected
  1124. if(this.disabled || !this._disabledOK){ return false; }
  1125. command = this._normalizeCommand(command);
  1126. if(dojo.isMoz || dojo.isWebKit){
  1127. if(command == "unlink"){ // mozilla returns true always
  1128. // console.debug(this._sCall("hasAncestorElement", ['a']));
  1129. return this._sCall("hasAncestorElement", ["a"]);
  1130. }else if(command == "inserttable"){
  1131. return true;
  1132. }
  1133. }
  1134. //see #4109
  1135. if(dojo.isWebKit){
  1136. if(command == "cut" || command == "copy") {
  1137. // WebKit deems clipboard activity as a security threat and natively would return false
  1138. var sel = this.window.getSelection();
  1139. if(sel){ sel = sel.toString(); }
  1140. return !!sel;
  1141. }else if(command == "paste"){
  1142. return true;
  1143. }
  1144. }
  1145. var elem = dojo.isIE ? this.document.selection.createRange() : this.document;
  1146. try{
  1147. return elem.queryCommandEnabled(command);
  1148. }catch(e){
  1149. //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
  1150. return false;
  1151. }
  1152. },
  1153. queryCommandState: function(command){
  1154. // summary:
  1155. // Check the state of a given command and returns true or false.
  1156. // tags:
  1157. // protected
  1158. if(this.disabled || !this._disabledOK){ return false; }
  1159. command = this._normalizeCommand(command);
  1160. try{
  1161. return this.document.queryCommandState(command);
  1162. }catch(e){
  1163. //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
  1164. return false;
  1165. }
  1166. },
  1167. queryCommandValue: function(command){
  1168. // summary:
  1169. // Check the value of a given command. This matters most for
  1170. // custom selections and complex values like font value setting.
  1171. // tags:
  1172. // protected
  1173. if(this.disabled || !this._disabledOK){ return false; }
  1174. var r;
  1175. command = this._normalizeCommand(command);
  1176. if(dojo.isIE && command == "formatblock"){
  1177. r = this._native2LocalFormatNames[this.document.queryCommandValue(command)];
  1178. }else if(dojo.isMoz && command === "hilitecolor"){
  1179. var oldValue;
  1180. try{
  1181. oldValue = this.document.queryCommandValue("styleWithCSS");
  1182. }catch(e){
  1183. oldValue = false;
  1184. }
  1185. this.document.execCommand("styleWithCSS", false, true);
  1186. r = this.document.queryCommandValue(command);
  1187. this.document.execCommand("styleWithCSS", false, oldValue);
  1188. }else{
  1189. r = this.document.queryCommandValue(command);
  1190. }
  1191. return r;
  1192. },
  1193. // Misc.
  1194. _sCall: function(name, args){
  1195. // summary:
  1196. // Run the named method of dijit._editor.selection over the
  1197. // current editor instance's window, with the passed args.
  1198. // tags:
  1199. // private
  1200. return dojo.withGlobal(this.window, name, dijit._editor.selection, args);
  1201. },
  1202. // FIXME: this is a TON of code duplication. Why?
  1203. placeCursorAtStart: function(){
  1204. // summary:
  1205. // Place the cursor at the start of the editing area.
  1206. // tags:
  1207. // private
  1208. this.focus();
  1209. //see comments in placeCursorAtEnd
  1210. var isvalid=false;
  1211. if(dojo.isMoz){
  1212. // TODO: Is this branch even necessary?
  1213. var first=this.editNode.firstChild;
  1214. while(first){
  1215. if(first.nodeType == 3){
  1216. if(first.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
  1217. isvalid=true;
  1218. this._sCall("selectElement", [ first ]);
  1219. break;
  1220. }
  1221. }else if(first.nodeType == 1){
  1222. isvalid=true;
  1223. var tg = first.tagName ? first.tagName.toLowerCase() : "";
  1224. // Collapse before childless tags.
  1225. if(/br|input|img|base|meta|area|basefont|hr|link/.test(tg)){
  1226. this._sCall("selectElement", [ first ]);
  1227. }else{
  1228. // Collapse inside tags with children.
  1229. this._sCall("selectElementChildren", [ first ]);
  1230. }
  1231. break;
  1232. }
  1233. first = first.nextSibling;
  1234. }
  1235. }else{
  1236. isvalid=true;
  1237. this._sCall("selectElementChildren", [ this.editNode ]);
  1238. }
  1239. if(isvalid){
  1240. this._sCall("collapse", [ true ]);
  1241. }
  1242. },
  1243. placeCursorAtEnd: function(){
  1244. // summary:
  1245. // Place the cursor at the end of the editing area.
  1246. // tags:
  1247. // private
  1248. this.focus();
  1249. //In mozilla, if last child is not a text node, we have to use
  1250. // selectElementChildren on this.editNode.lastChild otherwise the
  1251. // cursor would be placed at the end of the closing tag of
  1252. //this.editNode.lastChild
  1253. var isvalid=false;
  1254. if(dojo.isMoz){
  1255. var last=this.editNode.lastChild;
  1256. while(last){
  1257. if(last.nodeType == 3){
  1258. if(last.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
  1259. isvalid=true;
  1260. this._sCall("selectElement", [ last ]);
  1261. break;
  1262. }
  1263. }else if(last.nodeType == 1){
  1264. isvalid=true;
  1265. if(last.lastChild){
  1266. this._sCall("selectElement", [ last.lastChild ]);
  1267. }else{
  1268. this._sCall("selectElement", [ last ]);
  1269. }
  1270. break;
  1271. }
  1272. last = last.previousSibling;
  1273. }
  1274. }else{
  1275. isvalid=true;
  1276. this._sCall("selectElementChildren", [ this.editNode ]);
  1277. }
  1278. if(isvalid){
  1279. this._sCall("collapse", [ false ]);
  1280. }
  1281. },
  1282. getValue: function(/*Boolean?*/ nonDestructive){
  1283. // summary:
  1284. // Return the current content of the editing area (post filters
  1285. // are applied). Users should call get('value') instead.
  1286. // nonDestructive:
  1287. // defaults to false. Should the post-filtering be run over a copy
  1288. // of the live DOM? Most users should pass "true" here unless they
  1289. // *really* know that none of the installed filters are going to
  1290. // mess up the editing session.
  1291. // tags:
  1292. // private
  1293. if(this.textarea){
  1294. if(this.isClosed || !this.isLoaded){
  1295. return this.textarea.value;
  1296. }
  1297. }
  1298. return this._postFilterContent(null, nonDestructive);
  1299. },
  1300. _getValueAttr: function(){
  1301. // summary:
  1302. // Hook to make attr("value") work
  1303. return this.getValue(true);
  1304. },
  1305. setValue: function(/*String*/ html){
  1306. // summary:
  1307. // This function sets the content. No undo history is preserved.
  1308. // Users should use set('value', ...) instead.
  1309. // tags:
  1310. // deprecated
  1311. // TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
  1312. if(!this.isLoaded){
  1313. // try again after the editor is finished loading
  1314. this.onLoadDeferred.addCallback(dojo.hitch(this, function(){
  1315. this.setValue(html);
  1316. }));
  1317. return;
  1318. }
  1319. this._cursorToStart = true;
  1320. if(this.textarea && (this.isClosed || !this.isLoaded)){
  1321. this.textarea.value=html;
  1322. }else{
  1323. html = this._preFilterContent(html);
  1324. var node = this.isClosed ? this.domNode : this.editNode;
  1325. if(html && dojo.isMoz && html.toLowerCase() == "<p></p>"){
  1326. html = "<p>&nbsp;</p>";
  1327. }
  1328. // Use &nbsp; to avoid webkit problems where editor is disabled until the user clicks it
  1329. if(!html && dojo.isWebKit){
  1330. html = "&nbsp;";
  1331. }
  1332. node.innerHTML = html;
  1333. this._preDomFilterContent(node);
  1334. }
  1335. this.onDisplayChanged();
  1336. this._set("value", this.getValue(true));
  1337. },
  1338. replaceValue: function(/*String*/ html){
  1339. // summary:
  1340. // This function set the content while trying to maintain the undo stack
  1341. // (now only works fine with Moz, this is identical to setValue in all
  1342. // other browsers)
  1343. // tags:
  1344. // protected
  1345. if(this.isClosed){
  1346. this.setValue(html);
  1347. }else if(this.window && this.window.getSelection && !dojo.isMoz){ // Safari
  1348. // look ma! it's a totally f'd browser!
  1349. this.setValue(html);
  1350. }else if(this.window && this.window.getSelection){ // Moz
  1351. html = this._preFilterContent(html);
  1352. this.execCommand("selectall");
  1353. if(!html){
  1354. this._cursorToStart = true;
  1355. html = "&nbsp;";
  1356. }
  1357. this.execCommand("inserthtml", html);
  1358. this._preDomFilterContent(this.editNode);
  1359. }else if(this.document && this.document.selection){//IE
  1360. //In IE, when the first element is not a text node, say
  1361. //an <a> tag, when replacing the content of the editing
  1362. //area, the <a> tag will be around all the content
  1363. //so for now, use setValue for IE too
  1364. this.setValue(html);
  1365. }
  1366. this._set("value", this.getValue(true));
  1367. },
  1368. _preFilterContent: function(/*String*/ html){
  1369. // summary:
  1370. // Filter the input before setting the content of the editing
  1371. // area. DOM pre-filtering may happen after this
  1372. // string-based filtering takes place but as of 1.2, this is not
  1373. // guaranteed for operations such as the inserthtml command.
  1374. // tags:
  1375. // private
  1376. var ec = html;
  1377. dojo.forEach(this.contentPreFilters, function(ef){ if(ef){ ec = ef(ec); } });
  1378. return ec;
  1379. },
  1380. _preDomFilterContent: function(/*DomNode*/ dom){
  1381. // summary:
  1382. // filter the input's live DOM. All filter operations should be
  1383. // considered to be "live" and operating on the DOM that the user
  1384. // will be interacting with in their editing session.
  1385. // tags:
  1386. // private
  1387. dom = dom || this.editNode;
  1388. dojo.forEach(this.contentDomPreFilters, function(ef){
  1389. if(ef && dojo.isFunction(ef)){
  1390. ef(dom);
  1391. }
  1392. }, this);
  1393. },
  1394. _postFilterContent: function(
  1395. /*DomNode|DomNode[]|String?*/ dom,
  1396. /*Boolean?*/ nonDestructive){
  1397. // summary:
  1398. // filter the output after getting the content of the editing area
  1399. //
  1400. // description:
  1401. // post-filtering allows plug-ins and users to specify any number
  1402. // of transforms over the editor's content, enabling many common
  1403. // use-cases such as transforming absolute to relative URLs (and
  1404. // vice-versa), ensuring conformance with a particular DTD, etc.
  1405. // The filters are registered in the contentDomPostFilters and
  1406. // contentPostFilters arrays. Each item in the
  1407. // contentDomPostFilters array is a function which takes a DOM
  1408. // Node or array of nodes as its only argument and returns the
  1409. // same. It is then passed down the chain for further filtering.
  1410. // The contentPostFilters array behaves the same way, except each
  1411. // member operates on strings. Together, the DOM and string-based
  1412. // filtering allow the full range of post-processing that should
  1413. // be necessaray to enable even the most agressive of post-editing
  1414. // conversions to take place.
  1415. //
  1416. // If nonDestructive is set to "true", the nodes are cloned before
  1417. // filtering proceeds to avoid potentially destructive transforms
  1418. // to the content which may still needed to be edited further.
  1419. // Once DOM filtering has taken place, the serialized version of
  1420. // the DOM which is passed is run through each of the
  1421. // contentPostFilters functions.
  1422. //
  1423. // dom:
  1424. // a node, set of nodes, which to filter using each of the current
  1425. // members of the contentDomPostFilters and contentPostFilters arrays.
  1426. //
  1427. // nonDestructive:
  1428. // defaults to "false". If true, ensures that filtering happens on
  1429. // a clone of the passed-in content and not the actual node
  1430. // itself.
  1431. //
  1432. // tags:
  1433. // private
  1434. var ec;
  1435. if(!dojo.isString(dom)){
  1436. dom = dom || this.editNode;
  1437. if(this.contentDomPostFilters.length){
  1438. if(nonDestructive){
  1439. dom = dojo.clone(dom);
  1440. }
  1441. dojo.forEach(this.contentDomPostFilters, function(ef){
  1442. dom = ef(dom);
  1443. });
  1444. }
  1445. ec = dijit._editor.getChildrenHtml(dom);
  1446. }else{
  1447. ec = dom;
  1448. }
  1449. if(!dojo.trim(ec.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length){
  1450. ec = "";
  1451. }
  1452. // if(dojo.isIE){
  1453. // //removing appended <P>&nbsp;</P> for IE
  1454. // ec = ec.replace(/(?:<p>&nbsp;</p>[\n\r]*)+$/i,"");
  1455. // }
  1456. dojo.forEach(this.contentPostFilters, function(ef){
  1457. ec = ef(ec);
  1458. });
  1459. return ec;
  1460. },
  1461. _saveContent: function(/*Event*/ e){
  1462. // summary:
  1463. // Saves the content in an onunload event if the editor has not been closed
  1464. // tags:
  1465. // private
  1466. var saveTextarea = dojo.byId(dijit._scopeName + "._editor.RichText.value");
  1467. if(saveTextarea.value){
  1468. saveTextarea.value += this._SEPARATOR;
  1469. }
  1470. saveTextarea.value += this.name + this._NAME_CONTENT_SEP + this.getValue(true);
  1471. },
  1472. escapeXml: function(/*String*/ str, /*Boolean*/ noSingleQuotes){
  1473. // summary:
  1474. // Adds escape sequences for special characters in XML.
  1475. // Optionally skips escapes for single quotes
  1476. // tags:
  1477. // private
  1478. str = str.replace(/&/gm, "&amp;").replace(/</gm, "&lt;").replace(/>/gm, "&gt;").replace(/"/gm, "&quot;");
  1479. if(!noSingleQuotes){
  1480. str = str.replace(/'/gm, "&#39;");
  1481. }
  1482. return str; // string
  1483. },
  1484. getNodeHtml: function(/* DomNode */ node){
  1485. // summary:
  1486. // Deprecated. Use dijit._editor._getNodeHtml() instead.
  1487. // tags:
  1488. // deprecated
  1489. dojo.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit._editor.getNodeHtml instead', 2);
  1490. return dijit._editor.getNodeHtml(node); // String
  1491. },
  1492. getNodeChildrenHtml: function(/* DomNode */ dom){
  1493. // summary:
  1494. // Deprecated. Use dijit._editor.getChildrenHtml() instead.
  1495. // tags:
  1496. // deprecated
  1497. dojo.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit._editor.getChildrenHtml instead', 2);
  1498. return dijit._editor.getChildrenHtml(dom);
  1499. },
  1500. close: function(/*Boolean?*/ save){
  1501. // summary:
  1502. // Kills the editor and optionally writes back the modified contents to the
  1503. // element from which it originated.
  1504. // save:
  1505. // Whether or not to save the changes. If false, the changes are discarded.
  1506. // tags:
  1507. // private
  1508. if(this.isClosed){ return; }
  1509. if(!arguments.length){ save = true; }
  1510. if(save){
  1511. this._set("value", this.getValue(true));
  1512. }
  1513. // line height is squashed for iframes
  1514. // FIXME: why was this here? if (this.iframe){ this.domNode.style.lineHeight = null; }
  1515. if(this.interval){ clearInterval(this.interval); }
  1516. if(this._webkitListener){
  1517. //Cleaup of WebKit fix: #9532
  1518. this.disconnect(this._webkitListener);
  1519. delete this._webkitListener;
  1520. }
  1521. // Guard against memory leaks on IE (see #9268)
  1522. if(dojo.isIE){
  1523. this.iframe.onfocus = null;
  1524. }
  1525. this.iframe._loadFunc = null;
  1526. if(this._iframeRegHandle){
  1527. dijit.unregisterIframe(this._iframeRegHandle);
  1528. delete this._iframeRegHandle;
  1529. }
  1530. if(this.textarea){
  1531. var s = this.textarea.style;
  1532. s.position = "";
  1533. s.left = s.top = "";
  1534. if(dojo.isIE){
  1535. s.overflow = this.__overflow;
  1536. this.__overflow = null;
  1537. }
  1538. this.textarea.value = this.value;
  1539. dojo.destroy(this.domNode);
  1540. this.domNode = this.textarea;
  1541. }else{
  1542. // Note that this destroys the iframe
  1543. this.domNode.innerHTML = this.value;
  1544. }
  1545. delete this.iframe;
  1546. dojo.removeClass(this.domNode, this.baseClass);
  1547. this.isClosed = true;
  1548. this.isLoaded = false;
  1549. delete this.editNode;
  1550. delete this.focusNode;
  1551. if(this.window && this.window._frameElement){
  1552. this.window._frameElement = null;
  1553. }
  1554. this.window = null;
  1555. this.document = null;
  1556. this.editingArea = null;
  1557. this.editorObject = null;
  1558. },
  1559. destroy: function(){
  1560. if(!this.isClosed){ this.close(false); }
  1561. this.inherited(arguments);
  1562. if(dijit._editor._globalSaveHandler){
  1563. delete dijit._editor._globalSaveHandler[this.id];
  1564. }
  1565. },
  1566. _removeMozBogus: function(/* String */ html){
  1567. // summary:
  1568. // Post filter to remove unwanted HTML attributes generated by mozilla
  1569. // tags:
  1570. // private
  1571. return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi,''); // String
  1572. },
  1573. _removeWebkitBogus: function(/* String */ html){
  1574. // summary:
  1575. // Post filter to remove unwanted HTML attributes generated by webkit
  1576. // tags:
  1577. // private
  1578. html = html.replace(/\sclass="webkit-block-placeholder"/gi, '');
  1579. html = html.replace(/\sclass="apple-style-span"/gi, '');
  1580. // For some reason copy/paste sometime adds extra meta tags for charset on
  1581. // webkit (chrome) on mac.They need to be removed. See: #12007"
  1582. html = html.replace(/<meta charset=\"utf-8\" \/>/gi, '');
  1583. return html; // String
  1584. },
  1585. _normalizeFontStyle: function(/* String */ html){
  1586. // summary:
  1587. // Convert 'strong' and 'em' to 'b' and 'i'.
  1588. // description:
  1589. // Moz can not handle strong/em tags correctly, so to help
  1590. // mozilla and also to normalize output, convert them to 'b' and 'i'.
  1591. //
  1592. // Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
  1593. // tags:
  1594. // private
  1595. return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
  1596. .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
  1597. },
  1598. _preFixUrlAttributes: function(/* String */ html){
  1599. // summary:
  1600. // Pre-filter to do fixing to href attributes on <a> and <img> tags
  1601. // tags:
  1602. // private
  1603. return html.replace(/(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi,
  1604. '$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
  1605. .replace(/(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi,
  1606. '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String
  1607. },
  1608. /*****************************************************************************
  1609. The following functions implement HTML manipulation commands for various
  1610. browser/contentEditable implementations. The goal of them is to enforce
  1611. standard behaviors of them.
  1612. ******************************************************************************/
  1613. _inserthorizontalruleImpl: function(argument){
  1614. // summary:
  1615. // This function implements the insertion of HTML 'HR' tags.
  1616. // into a point on the page. IE doesn't to it right, so
  1617. // we have to use an alternate form
  1618. // argument:
  1619. // arguments to the exec command, if any.
  1620. // tags:
  1621. // protected
  1622. if(dojo.isIE){
  1623. return this._inserthtmlImpl("<hr>");
  1624. }
  1625. return this.document.execCommand("inserthorizontalrule", false, argument);
  1626. },
  1627. _unlinkImpl: function(argument){
  1628. // summary:
  1629. // This function implements the unlink of an 'a' tag.
  1630. // argument:
  1631. // arguments to the exec command, if any.
  1632. // tags:
  1633. // protected
  1634. if((this.queryCommandEnabled("unlink")) && (dojo.isMoz || dojo.isWebKit)){
  1635. var a = this._sCall("getAncestorElement", [ "a" ]);
  1636. this._sCall("selectElement", [ a ]);
  1637. return this.document.execCommand("unlink", false, null);
  1638. }
  1639. return this.document.execCommand("unlink", false, argument);
  1640. },
  1641. _hilitecolorImpl: function(argument){
  1642. // summary:
  1643. // This function implements the hilitecolor command
  1644. // argument:
  1645. // arguments to the exec command, if any.
  1646. // tags:
  1647. // protected
  1648. var returnValue;
  1649. if(dojo.isMoz){
  1650. // mozilla doesn't support hilitecolor properly when useCSS is
  1651. // set to false (bugzilla #279330)
  1652. this.document.execCommand("styleWithCSS", false, true);
  1653. returnValue = this.document.execCommand("hilitecolor", false, argument);
  1654. this.document.execCommand("styleWithCSS", false, false);
  1655. }else{
  1656. returnValue = this.document.execCommand("hilitecolor", false, argument);
  1657. }
  1658. return returnValue;
  1659. },
  1660. _backcolorImpl: function(argument){
  1661. // summary:
  1662. // This function implements the backcolor command
  1663. // argument:
  1664. // arguments to the exec command, if any.
  1665. // tags:
  1666. // protected
  1667. if(dojo.isIE){
  1668. // Tested under IE 6 XP2, no problem here, comment out
  1669. // IE weirdly collapses ranges when we exec these commands, so prevent it
  1670. // var tr = this.document.selection.createRange();
  1671. argument = argument ? argument : null;
  1672. }
  1673. return this.document.execCommand("backcolor", false, argument);
  1674. },
  1675. _forecolorImpl: function(argument){
  1676. // summary:
  1677. // This function implements the forecolor command
  1678. // argument:
  1679. // arguments to the exec command, if any.
  1680. // tags:
  1681. // protected
  1682. if(dojo.isIE){
  1683. // Tested under IE 6 XP2, no problem here, comment out
  1684. // IE weirdly collapses ranges when we exec these commands, so prevent it
  1685. // var tr = this.document.selection.createRange();
  1686. argument = argument? argument : null;
  1687. }
  1688. return this.document.execCommand("forecolor", false, argument);
  1689. },
  1690. _inserthtmlImpl: function(argument){
  1691. // summary:
  1692. // This function implements the insertion of HTML content into
  1693. // a point on the page.
  1694. // argument:
  1695. // The content to insert, if any.
  1696. // tags:
  1697. // protected
  1698. argument = this._preFilterContent(argument);
  1699. var rv = true;
  1700. if(dojo.isIE){
  1701. var insertRange = this.document.selection.createRange();
  1702. if(this.document.selection.type.toUpperCase() == 'CONTROL'){
  1703. var n=insertRange.item(0);
  1704. while(insertRange.length){
  1705. insertRange.remove(insertRange.item(0));
  1706. }
  1707. n.outerHTML=argument;
  1708. }else{
  1709. insertRange.pasteHTML(argument);
  1710. }
  1711. insertRange.select();
  1712. //insertRange.collapse(true);
  1713. }else if(dojo.isMoz && !argument.length){
  1714. //mozilla can not inserthtml an empty html to delete current selection
  1715. //so we delete the selection instead in this case
  1716. this._sCall("remove"); // FIXME
  1717. }else{
  1718. rv = this.document.execCommand("inserthtml", false, argument);
  1719. }
  1720. return rv;
  1721. },
  1722. _boldImpl: function(argument){
  1723. // summary:
  1724. // This function implements an over-ride of the bold command.
  1725. // argument:
  1726. // Not used, operates by selection.
  1727. // tags:
  1728. // protected
  1729. if(dojo.isIE){
  1730. this._adaptIESelection()
  1731. }
  1732. return this.document.execCommand("bold", false, argument);
  1733. },
  1734. _italicImpl: function(argument){
  1735. // summary:
  1736. // This function implements an over-ride of the italic command.
  1737. // argument:
  1738. // Not used, operates by selection.
  1739. // tags:
  1740. // protected
  1741. if(dojo.isIE){
  1742. this._adaptIESelection()
  1743. }
  1744. return this.document.execCommand("italic", false, argument);
  1745. },
  1746. _underlineImpl: function(argument){
  1747. // summary:
  1748. // This function implements an over-ride of the underline command.
  1749. // argument:
  1750. // Not used, operates by selection.
  1751. // tags:
  1752. // protected
  1753. if(dojo.isIE){
  1754. this._adaptIESelection()
  1755. }
  1756. return this.document.execCommand("underline", false, argument);
  1757. },
  1758. _strikethroughImpl: function(argument){
  1759. // summary:
  1760. // This function implements an over-ride of the strikethrough command.
  1761. // argument:
  1762. // Not used, operates by selection.
  1763. // tags:
  1764. // protected
  1765. if(dojo.isIE){
  1766. this._adaptIESelection()
  1767. }
  1768. return this.document.execCommand("strikethrough", false, argument);
  1769. },
  1770. getHeaderHeight: function(){
  1771. // summary:
  1772. // A function for obtaining the height of the header node
  1773. return this._getNodeChildrenHeight(this.header); // Number
  1774. },
  1775. getFooterHeight: function(){
  1776. // summary:
  1777. // A function for obtaining the height of the footer node
  1778. return this._getNodeChildrenHeight(this.footer); // Number
  1779. },
  1780. _getNodeChildrenHeight: function(node){
  1781. // summary:
  1782. // An internal function for computing the cumulative height of all child nodes of 'node'
  1783. // node:
  1784. // The node to process the children of;
  1785. var h = 0;
  1786. if(node && node.childNodes){
  1787. // IE didn't compute it right when position was obtained on the node directly is some cases,
  1788. // so we have to walk over all the children manually.
  1789. var i;
  1790. for(i = 0; i < node.childNodes.length; i++){
  1791. var size = dojo.position(node.childNodes[i]);
  1792. h += size.h;
  1793. }
  1794. }
  1795. return h; // Number
  1796. },
  1797. _isNodeEmpty: function(node, startOffset){
  1798. // summary:
  1799. // Function to test if a node is devoid of real content.
  1800. // node:
  1801. // The node to check.
  1802. // tags:
  1803. // private.
  1804. if(node.nodeType == 1/*element*/){
  1805. if(node.childNodes.length > 0){
  1806. return this._isNodeEmpty(node.childNodes[0], startOffset);
  1807. }
  1808. return true;
  1809. }else if(node.nodeType == 3/*text*/){
  1810. return (node.nodeValue.substring(startOffset) == "");
  1811. }
  1812. return false;
  1813. },
  1814. _removeStartingRangeFromRange: function(node, range){
  1815. // summary:
  1816. // Function to adjust selection range by removing the current
  1817. // start node.
  1818. // node:
  1819. // The node to remove from the starting range.
  1820. // range:
  1821. // The range to adapt.
  1822. // tags:
  1823. // private
  1824. if(node.nextSibling){
  1825. range.setStart(node.nextSibling,0);
  1826. }else{
  1827. var parent = node.parentNode;
  1828. while(parent && parent.nextSibling == null){
  1829. //move up the tree until we find a parent that has another node, that node will be the next node
  1830. parent = parent.parentNode;
  1831. }
  1832. if(parent){
  1833. range.setStart(parent.nextSibling,0);
  1834. }
  1835. }
  1836. return range;
  1837. },
  1838. _adaptIESelection: function(){
  1839. // summary:
  1840. // Function to adapt the IE range by removing leading 'newlines'
  1841. // Needed to fix issue with bold/italics/underline not working if
  1842. // range included leading 'newlines'.
  1843. // In IE, if a user starts a selection at the very end of a line,
  1844. // then the native browser commands will fail to execute correctly.
  1845. // To work around the issue, we can remove all empty nodes from
  1846. // the start of the range selection.
  1847. var selection = dijit.range.getSelection(this.window);
  1848. if(selection && selection.rangeCount && !selection.isCollapsed){
  1849. var range = selection.getRangeAt(0);
  1850. var firstNode = range.startContainer;
  1851. var startOffset = range.startOffset;
  1852. while(firstNode.nodeType == 3/*text*/ && startOffset >= firstNode.length && firstNode.nextSibling){
  1853. //traverse the text nodes until we get to the one that is actually highlighted
  1854. startOffset = startOffset - firstNode.length;
  1855. firstNode = firstNode.nextSibling;
  1856. }
  1857. //Remove the starting ranges until the range does not start with an empty node.
  1858. var lastNode=null;
  1859. while(this._isNodeEmpty(firstNode, startOffset) && firstNode != lastNode){
  1860. lastNode =firstNode; //this will break the loop in case we can't find the next sibling
  1861. range = this._removeStartingRangeFromRange(firstNode, range); //move the start container to the next node in the range
  1862. firstNode = range.startContainer;
  1863. startOffset = 0; //start at the beginning of the new starting range
  1864. }
  1865. selection.removeAllRanges();// this will work as long as users cannot select multiple ranges. I have not been able to do that in the editor.
  1866. selection.addRange(range);
  1867. }
  1868. }
  1869. });
  1870. }