RichText.js 92 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923
  1. define("dijit/_editor/RichText", [
  2. "dojo/_base/array", // array.forEach array.indexOf array.some
  3. "dojo/_base/config", // config
  4. "dojo/_base/declare", // declare
  5. "dojo/_base/Deferred", // Deferred
  6. "dojo/dom", // dom.byId
  7. "dojo/dom-attr", // domAttr.set or get
  8. "dojo/dom-class", // domClass.add domClass.remove
  9. "dojo/dom-construct", // domConstruct.create domConstruct.destroy domConstruct.place
  10. "dojo/dom-geometry", // domGeometry.position
  11. "dojo/dom-style", // domStyle.getComputedStyle domStyle.set
  12. "dojo/_base/event", // event.stop
  13. "dojo/_base/kernel", // kernel.deprecated
  14. "dojo/keys", // keys.BACKSPACE keys.TAB
  15. "dojo/_base/lang", // lang.clone lang.hitch lang.isArray lang.isFunction lang.isString lang.trim
  16. "dojo/on", // on()
  17. "dojo/query", // query
  18. "dojo/ready", // ready
  19. "dojo/_base/sniff", // has("ie") has("mozilla") has("opera") has("safari") has("webkit")
  20. "dojo/topic", // topic.publish() (publish)
  21. "dojo/_base/unload", // unload
  22. "dojo/_base/url", // url
  23. "dojo/_base/window", // win.body win.doc.body.focus win.doc.createElement win.global.location win.withGlobal
  24. "../_Widget",
  25. "../_CssStateMixin",
  26. "./selection",
  27. "./range",
  28. "./html",
  29. "../focus",
  30. ".." // dijit._scopeName
  31. ], function(array, config, declare, Deferred, dom, domAttr, domClass, domConstruct, domGeometry, domStyle,
  32. event, kernel, keys, lang, on, query, ready, has, topic, unload, _Url, win,
  33. _Widget, _CssStateMixin, selectionapi, rangeapi, htmlapi, focus, dijit){
  34. /*=====
  35. var _Widget = dijit._Widget;
  36. var _CssStateMixin = dijit._CssStateMixin;
  37. =====*/
  38. // module:
  39. // dijit/_editor/RichText
  40. // summary:
  41. // dijit._editor.RichText is the core of dijit.Editor, which provides basic
  42. // WYSIWYG editing features.
  43. // if you want to allow for rich text saving with back/forward actions, you must add a text area to your page with
  44. // the id==dijit._scopeName + "._editor.RichText.value" (typically "dijit._editor.RichText.value). For example,
  45. // something like this will work:
  46. //
  47. // <textarea id="dijit._editor.RichText.value" style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>
  48. //
  49. var RichText = declare("dijit._editor.RichText", [_Widget, _CssStateMixin], {
  50. // summary:
  51. // dijit._editor.RichText is the core of dijit.Editor, which provides basic
  52. // WYSIWYG editing features.
  53. //
  54. // description:
  55. // dijit._editor.RichText is the core of dijit.Editor, which provides basic
  56. // WYSIWYG editing features. It also encapsulates the differences
  57. // of different js engines for various browsers. Do not use this widget
  58. // with an HTML &lt;TEXTAREA&gt; tag, since the browser unescapes XML escape characters,
  59. // like &lt;. This can have unexpected behavior and lead to security issues
  60. // such as scripting attacks.
  61. //
  62. // tags:
  63. // private
  64. constructor: function(params){
  65. // contentPreFilters: Function(String)[]
  66. // Pre content filter function register array.
  67. // these filters will be executed before the actual
  68. // editing area gets the html content.
  69. this.contentPreFilters = [];
  70. // contentPostFilters: Function(String)[]
  71. // post content filter function register array.
  72. // These will be used on the resulting html
  73. // from contentDomPostFilters. The resulting
  74. // content is the final html (returned by getValue()).
  75. this.contentPostFilters = [];
  76. // contentDomPreFilters: Function(DomNode)[]
  77. // Pre content dom filter function register array.
  78. // These filters are applied after the result from
  79. // contentPreFilters are set to the editing area.
  80. this.contentDomPreFilters = [];
  81. // contentDomPostFilters: Function(DomNode)[]
  82. // Post content dom filter function register array.
  83. // These filters are executed on the editing area dom.
  84. // The result from these will be passed to contentPostFilters.
  85. this.contentDomPostFilters = [];
  86. // editingAreaStyleSheets: dojo._URL[]
  87. // array to store all the stylesheets applied to the editing area
  88. this.editingAreaStyleSheets = [];
  89. // Make a copy of this.events before we start writing into it, otherwise we
  90. // will modify the prototype which leads to bad things on pages w/multiple editors
  91. this.events = [].concat(this.events);
  92. this._keyHandlers = {};
  93. if(params && lang.isString(params.value)){
  94. this.value = params.value;
  95. }
  96. this.onLoadDeferred = new Deferred();
  97. },
  98. baseClass: "dijitEditor",
  99. // inheritWidth: Boolean
  100. // whether to inherit the parent's width or simply use 100%
  101. inheritWidth: false,
  102. // focusOnLoad: [deprecated] Boolean
  103. // Focus into this widget when the page is loaded
  104. focusOnLoad: false,
  105. // name: String?
  106. // Specifies the name of a (hidden) <textarea> node on the page that's used to save
  107. // the editor content on page leave. Used to restore editor contents after navigating
  108. // to a new page and then hitting the back button.
  109. name: "",
  110. // styleSheets: [const] String
  111. // semicolon (";") separated list of css files for the editing area
  112. styleSheets: "",
  113. // height: String
  114. // Set height to fix the editor at a specific height, with scrolling.
  115. // By default, this is 300px. If you want to have the editor always
  116. // resizes to accommodate the content, use AlwaysShowToolbar plugin
  117. // and set height="". If this editor is used within a layout widget,
  118. // set height="100%".
  119. height: "300px",
  120. // minHeight: String
  121. // The minimum height that the editor should have.
  122. minHeight: "1em",
  123. // isClosed: [private] Boolean
  124. isClosed: true,
  125. // isLoaded: [private] Boolean
  126. isLoaded: false,
  127. // _SEPARATOR: [private] String
  128. // Used to concat contents from multiple editors into a single string,
  129. // so they can be saved into a single <textarea> node. See "name" attribute.
  130. _SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@",
  131. // _NAME_CONTENT_SEP: [private] String
  132. // USed to separate name from content. Just a colon isn't safe.
  133. _NAME_CONTENT_SEP: "@@**%%:%%**@@",
  134. // onLoadDeferred: [readonly] dojo.Deferred
  135. // Deferred which is fired when the editor finishes loading.
  136. // Call myEditor.onLoadDeferred.then(callback) it to be informed
  137. // when the rich-text area initialization is finalized.
  138. onLoadDeferred: null,
  139. // isTabIndent: Boolean
  140. // Make tab key and shift-tab indent and outdent rather than navigating.
  141. // Caution: sing this makes web pages inaccessible to users unable to use a mouse.
  142. isTabIndent: false,
  143. // disableSpellCheck: [const] Boolean
  144. // When true, disables the browser's native spell checking, if supported.
  145. // Works only in Firefox.
  146. disableSpellCheck: false,
  147. postCreate: function(){
  148. if("textarea" === this.domNode.tagName.toLowerCase()){
  149. console.warn("RichText should not be used with the TEXTAREA tag. See dijit._editor.RichText docs.");
  150. }
  151. // Push in the builtin filters now, making them the first executed, but not over-riding anything
  152. // users passed in. See: #6062
  153. this.contentPreFilters = [lang.hitch(this, "_preFixUrlAttributes")].concat(this.contentPreFilters);
  154. if(has("mozilla")){
  155. this.contentPreFilters = [this._normalizeFontStyle].concat(this.contentPreFilters);
  156. this.contentPostFilters = [this._removeMozBogus].concat(this.contentPostFilters);
  157. }
  158. if(has("webkit")){
  159. // Try to clean up WebKit bogus artifacts. The inserted classes
  160. // made by WebKit sometimes messes things up.
  161. this.contentPreFilters = [this._removeWebkitBogus].concat(this.contentPreFilters);
  162. this.contentPostFilters = [this._removeWebkitBogus].concat(this.contentPostFilters);
  163. }
  164. if(has("ie") || has("trident")){
  165. // IE generates <strong> and <em> but we want to normalize to <b> and <i>
  166. // Still happens in IE11!
  167. this.contentPostFilters = [this._normalizeFontStyle].concat(this.contentPostFilters);
  168. this.contentDomPostFilters = [lang.hitch(this, this._stripBreakerNodes)].concat(this.contentDomPostFilters);
  169. }
  170. this.inherited(arguments);
  171. topic.publish(dijit._scopeName + "._editor.RichText::init", this);
  172. this.open();
  173. this.setupDefaultShortcuts();
  174. },
  175. setupDefaultShortcuts: function(){
  176. // summary:
  177. // Add some default key handlers
  178. // description:
  179. // Overwrite this to setup your own handlers. The default
  180. // implementation does not use Editor commands, but directly
  181. // executes the builtin commands within the underlying browser
  182. // support.
  183. // tags:
  184. // protected
  185. var exec = lang.hitch(this, function(cmd, arg){
  186. return function(){
  187. return !this.execCommand(cmd,arg);
  188. };
  189. });
  190. var ctrlKeyHandlers = {
  191. b: exec("bold"),
  192. i: exec("italic"),
  193. u: exec("underline"),
  194. a: exec("selectall"),
  195. s: function(){ this.save(true); },
  196. m: function(){ this.isTabIndent = !this.isTabIndent; },
  197. "1": exec("formatblock", "h1"),
  198. "2": exec("formatblock", "h2"),
  199. "3": exec("formatblock", "h3"),
  200. "4": exec("formatblock", "h4"),
  201. "\\": exec("insertunorderedlist")
  202. };
  203. if(!has("ie")){
  204. ctrlKeyHandlers.Z = exec("redo"); //FIXME: undo?
  205. }
  206. var key;
  207. for(key in ctrlKeyHandlers){
  208. this.addKeyHandler(key, true, false, ctrlKeyHandlers[key]);
  209. }
  210. },
  211. // events: [private] String[]
  212. // events which should be connected to the underlying editing area
  213. events: ["onKeyPress", "onKeyDown", "onKeyUp"], // onClick handled specially
  214. // captureEvents: [deprecated] String[]
  215. // Events which should be connected to the underlying editing
  216. // area, events in this array will be addListener with
  217. // capture=true.
  218. // TODO: looking at the code I don't see any distinction between events and captureEvents,
  219. // so get rid of this for 2.0 if not sooner
  220. captureEvents: [],
  221. _editorCommandsLocalized: false,
  222. _localizeEditorCommands: function(){
  223. // summary:
  224. // When IE is running in a non-English locale, the API actually changes,
  225. // so that we have to say (for example) danraku instead of p (for paragraph).
  226. // Handle that here.
  227. // tags:
  228. // private
  229. if(RichText._editorCommandsLocalized){
  230. // Use the already generate cache of mappings.
  231. this._local2NativeFormatNames = RichText._local2NativeFormatNames;
  232. this._native2LocalFormatNames = RichText._native2LocalFormatNames;
  233. return;
  234. }
  235. RichText._editorCommandsLocalized = true;
  236. RichText._local2NativeFormatNames = {};
  237. RichText._native2LocalFormatNames = {};
  238. this._local2NativeFormatNames = RichText._local2NativeFormatNames;
  239. this._native2LocalFormatNames = RichText._native2LocalFormatNames;
  240. //in IE, names for blockformat is locale dependent, so we cache the values here
  241. //put p after div, so if IE returns Normal, we show it as paragraph
  242. //We can distinguish p and div if IE returns Normal, however, in order to detect that,
  243. //we have to call this.document.selection.createRange().parentElement() or such, which
  244. //could slow things down. Leave it as it is for now
  245. var formats = ['div', 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'address'];
  246. var localhtml = "", format, i=0;
  247. while((format=formats[i++])){
  248. //append a <br> after each element to separate the elements more reliably
  249. if(format.charAt(1) !== 'l'){
  250. localhtml += "<"+format+"><span>content</span></"+format+"><br/>";
  251. }else{
  252. localhtml += "<"+format+"><li>content</li></"+format+"><br/>";
  253. }
  254. }
  255. // queryCommandValue returns empty if we hide editNode, so move it out of screen temporary
  256. // Also, IE9 does weird stuff unless we do it inside the editor iframe.
  257. var style = { position: "absolute", top: "0px", zIndex: 10, opacity: 0.01 };
  258. var div = domConstruct.create('div', {style: style, innerHTML: localhtml});
  259. win.body().appendChild(div);
  260. // IE9 has a timing issue with doing this right after setting
  261. // the inner HTML, so put a delay in.
  262. var inject = lang.hitch(this, function(){
  263. var node = div.firstChild;
  264. while(node){
  265. try{
  266. selectionapi.selectElement(node.firstChild);
  267. var nativename = node.tagName.toLowerCase();
  268. this._local2NativeFormatNames[nativename] = document.queryCommandValue("formatblock");
  269. this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename;
  270. node = node.nextSibling.nextSibling;
  271. //console.log("Mapped: ", nativename, " to: ", this._local2NativeFormatNames[nativename]);
  272. }catch(e){ /*Sqelch the occasional IE9 error */ }
  273. }
  274. div.parentNode.removeChild(div);
  275. div.innerHTML = "";
  276. });
  277. setTimeout(inject, 0);
  278. },
  279. open: function(/*DomNode?*/ element){
  280. // summary:
  281. // Transforms the node referenced in this.domNode into a rich text editing
  282. // node.
  283. // description:
  284. // Sets up the editing area asynchronously. This will result in
  285. // the creation and replacement with an iframe.
  286. // tags:
  287. // private
  288. if(!this.onLoadDeferred || this.onLoadDeferred.fired >= 0){
  289. this.onLoadDeferred = new Deferred();
  290. }
  291. if(!this.isClosed){ this.close(); }
  292. topic.publish(dijit._scopeName + "._editor.RichText::open", this);
  293. if(arguments.length === 1 && element.nodeName){ // else unchanged
  294. this.domNode = element;
  295. }
  296. var dn = this.domNode;
  297. // "html" will hold the innerHTML of the srcNodeRef and will be used to
  298. // initialize the editor.
  299. var html;
  300. if(lang.isString(this.value)){
  301. // Allow setting the editor content programmatically instead of
  302. // relying on the initial content being contained within the target
  303. // domNode.
  304. html = this.value;
  305. delete this.value;
  306. dn.innerHTML = "";
  307. }else if(dn.nodeName && dn.nodeName.toLowerCase() == "textarea"){
  308. // if we were created from a textarea, then we need to create a
  309. // new editing harness node.
  310. var ta = (this.textarea = dn);
  311. this.name = ta.name;
  312. html = ta.value;
  313. dn = this.domNode = win.doc.createElement("div");
  314. dn.setAttribute('widgetId', this.id);
  315. ta.removeAttribute('widgetId');
  316. dn.cssText = ta.cssText;
  317. dn.className += " " + ta.className;
  318. domConstruct.place(dn, ta, "before");
  319. var tmpFunc = lang.hitch(this, function(){
  320. //some browsers refuse to submit display=none textarea, so
  321. //move the textarea off screen instead
  322. domStyle.set(ta, {
  323. display: "block",
  324. position: "absolute",
  325. top: "-1000px"
  326. });
  327. if(has("ie")){ //nasty IE bug: abnormal formatting if overflow is not hidden
  328. var s = ta.style;
  329. this.__overflow = s.overflow;
  330. s.overflow = "hidden";
  331. }
  332. });
  333. if(has("ie")){
  334. setTimeout(tmpFunc, 10);
  335. }else{
  336. tmpFunc();
  337. }
  338. if(ta.form){
  339. var resetValue = ta.value;
  340. this.reset = function(){
  341. var current = this.getValue();
  342. if(current !== resetValue){
  343. this.replaceValue(resetValue);
  344. }
  345. };
  346. on(ta.form, "submit", lang.hitch(this, function(){
  347. // Copy value to the <textarea> so it gets submitted along with form.
  348. // FIXME: should we be calling close() here instead?
  349. domAttr.set(ta, 'disabled', this.disabled); // don't submit the value if disabled
  350. ta.value = this.getValue();
  351. }));
  352. }
  353. }else{
  354. html = htmlapi.getChildrenHtml(dn);
  355. dn.innerHTML = "";
  356. }
  357. this.value = html;
  358. // If we're a list item we have to put in a blank line to force the
  359. // bullet to nicely align at the top of text
  360. if(dn.nodeName && dn.nodeName === "LI"){
  361. dn.innerHTML = " <br>";
  362. }
  363. // Construct the editor div structure.
  364. this.header = dn.ownerDocument.createElement("div");
  365. dn.appendChild(this.header);
  366. this.editingArea = dn.ownerDocument.createElement("div");
  367. dn.appendChild(this.editingArea);
  368. this.footer = dn.ownerDocument.createElement("div");
  369. dn.appendChild(this.footer);
  370. if(!this.name){
  371. this.name = this.id + "_AUTOGEN";
  372. }
  373. // User has pressed back/forward button so we lost the text in the editor, but it's saved
  374. // in a hidden <textarea> (which contains the data for all the editors on this page),
  375. // so get editor value from there
  376. if(this.name !== "" && (!config["useXDomain"] || config["allowXdRichTextSave"])){
  377. var saveTextarea = dom.byId(dijit._scopeName + "._editor.RichText.value");
  378. if(saveTextarea && saveTextarea.value !== ""){
  379. var datas = saveTextarea.value.split(this._SEPARATOR), i=0, dat;
  380. while((dat=datas[i++])){
  381. var data = dat.split(this._NAME_CONTENT_SEP);
  382. if(data[0] === this.name){
  383. html = data[1];
  384. datas = datas.splice(i, 1);
  385. saveTextarea.value = datas.join(this._SEPARATOR);
  386. break;
  387. }
  388. }
  389. }
  390. if(!RichText._globalSaveHandler){
  391. RichText._globalSaveHandler = {};
  392. unload.addOnUnload(function(){
  393. var id;
  394. for(id in RichText._globalSaveHandler){
  395. var f = RichText._globalSaveHandler[id];
  396. if(lang.isFunction(f)){
  397. f();
  398. }
  399. }
  400. });
  401. }
  402. RichText._globalSaveHandler[this.id] = lang.hitch(this, "_saveContent");
  403. }
  404. this.isClosed = false;
  405. var ifr = (this.editorObject = this.iframe = win.doc.createElement('iframe'));
  406. ifr.id = this.id+"_iframe";
  407. ifr.style.border = "none";
  408. ifr.style.width = "100%";
  409. if(this._layoutMode){
  410. // iframe should be 100% height, thus getting it's height from surrounding
  411. // <div> (which has the correct height set by Editor)
  412. ifr.style.height = "100%";
  413. }else{
  414. if(has("ie") >= 7){
  415. if(this.height){
  416. ifr.style.height = this.height;
  417. }
  418. if(this.minHeight){
  419. ifr.style.minHeight = this.minHeight;
  420. }
  421. }else{
  422. ifr.style.height = this.height ? this.height : this.minHeight;
  423. }
  424. }
  425. ifr.frameBorder = 0;
  426. ifr._loadFunc = lang.hitch( this, function(w){
  427. this.window = w;
  428. this.document = w.document;
  429. if(has("ie")){
  430. this._localizeEditorCommands();
  431. }
  432. // Do final setup and set initial contents of editor
  433. this.onLoad(html);
  434. });
  435. // Attach iframe to document, and set the initial (blank) content.
  436. var src = this._getIframeDocTxt().replace(/\\/g, "\\\\").replace(/'/g, "\\'"),
  437. s;
  438. // IE10 and earlier will throw an "Access is denied" error when attempting to access the parent frame if
  439. // document.domain has been set, unless the child frame also has the same document.domain set. The child frame
  440. // can only set document.domain while the document is being constructed using open/write/close; attempting to
  441. // set it later results in a different "This method can't be used in this context" error. See #17529
  442. if (has("ie") < 11) {
  443. s = 'javascript:document.open();try{parent.window;}catch(e){document.domain="' + document.domain + '";}' +
  444. 'document.write(\'' + src + '\');document.close()';
  445. }
  446. else {
  447. s = "javascript: '" + src + "'";
  448. }
  449. if(has("ie") == 9){
  450. // On IE9, attach to document before setting the content, to avoid problem w/iframe running in
  451. // wrong security context, see #16633.
  452. this.editingArea.appendChild(ifr);
  453. ifr.src = s;
  454. }else{
  455. // For other browsers, set src first, especially for IE6/7 where attaching first gives a warning on
  456. // https:// about "this page contains secure and insecure items, do you want to view both?"
  457. ifr.setAttribute('src', s);
  458. this.editingArea.appendChild(ifr);
  459. }
  460. if(has("safari") <= 4){
  461. src = ifr.getAttribute("src");
  462. if(!src || src.indexOf("javascript") === -1){
  463. // Safari 4 and earlier sometimes act oddly
  464. // So we have to set it again.
  465. setTimeout(function(){ifr.setAttribute('src', s);},0);
  466. }
  467. }
  468. // TODO: this is a guess at the default line-height, kinda works
  469. if(dn.nodeName === "LI"){
  470. dn.lastChild.style.marginTop = "-1.2em";
  471. }
  472. domClass.add(this.domNode, this.baseClass);
  473. },
  474. //static cache variables shared among all instance of this class
  475. _local2NativeFormatNames: {},
  476. _native2LocalFormatNames: {},
  477. _getIframeDocTxt: function(){
  478. // summary:
  479. // Generates the boilerplate text of the document inside the iframe (ie, <html><head>...</head><body/></html>).
  480. // Editor content (if not blank) should be added afterwards.
  481. // tags:
  482. // private
  483. var _cs = domStyle.getComputedStyle(this.domNode);
  484. // The contents inside of <body>. The real contents are set later via a call to setValue().
  485. // In auto-expand mode, need a wrapper div for AlwaysShowToolbar plugin to correctly
  486. // expand/contract the editor as the content changes.
  487. var html = "<div id='dijitEditorBody'></div>";
  488. var font = [ _cs.fontWeight, _cs.fontSize, _cs.fontFamily ].join(" ");
  489. // line height is tricky - applying a units value will mess things up.
  490. // if we can't get a non-units value, bail out.
  491. var lineHeight = _cs.lineHeight;
  492. if(lineHeight.indexOf("px") >= 0){
  493. lineHeight = parseFloat(lineHeight)/parseFloat(_cs.fontSize);
  494. // console.debug(lineHeight);
  495. }else if(lineHeight.indexOf("em")>=0){
  496. lineHeight = parseFloat(lineHeight);
  497. }else{
  498. // If we can't get a non-units value, just default
  499. // it to the CSS spec default of 'normal'. Seems to
  500. // work better, esp on IE, than '1.0'
  501. lineHeight = "normal";
  502. }
  503. var userStyle = "";
  504. var self = this;
  505. this.style.replace(/(^|;)\s*(line-|font-?)[^;]+/ig, function(match){
  506. match = match.replace(/^;/ig,"") + ';';
  507. var s = match.split(":")[0];
  508. if(s){
  509. s = lang.trim(s);
  510. s = s.toLowerCase();
  511. var i;
  512. var sC = "";
  513. for(i = 0; i < s.length; i++){
  514. var c = s.charAt(i);
  515. switch(c){
  516. case "-":
  517. i++;
  518. c = s.charAt(i).toUpperCase();
  519. default:
  520. sC += c;
  521. }
  522. }
  523. domStyle.set(self.domNode, sC, "");
  524. }
  525. userStyle += match + ';';
  526. });
  527. // need to find any associated label element and update iframe document title
  528. var label=query('label[for="'+this.id+'"]');
  529. return [
  530. this.isLeftToRight() ? "<html>\n<head>\n" : "<html dir='rtl'>\n<head>\n",
  531. (has("mozilla") && label.length ? "<title>" + label[0].innerHTML + "</title>\n" : ""),
  532. "<meta http-equiv='Content-Type' content='text/html'>\n",
  533. "<style>\n",
  534. "\tbody,html {\n",
  535. "\t\tbackground:transparent;\n",
  536. "\t\tpadding: 1px 0 0 0;\n",
  537. "\t\tmargin: -1px 0 0 0;\n", // remove extraneous vertical scrollbar on safari and firefox
  538. "\t}\n",
  539. "\tbody,html,#dijitEditorBody { outline: none; }",
  540. // Set <body> to expand to full size of editor, so clicking anywhere will work.
  541. // Except in auto-expand mode, in which case the editor expands to the size of <body>.
  542. // Also determine how scrollers should be applied. In autoexpand mode (height = "") no scrollers on y at all.
  543. // But in fixed height mode we want both x/y scrollers.
  544. // Scrollers go on <body> since it's been set to height: 100%.
  545. "html { height: 100%; width: 100%; overflow: hidden; }\n", // scroll bar is on #dijitEditorBody, shouldn't be on <html>
  546. this.height ? "\tbody,#dijitEditorBody { height: 100%; width: 100%; overflow: auto; }\n" :
  547. "\tbody,#dijitEditorBody { min-height: " + this.minHeight + "; width: 100%; overflow-x: auto; overflow-y: hidden; }\n",
  548. // TODO: left positioning will cause contents to disappear out of view
  549. // if it gets too wide for the visible area
  550. "\tbody{\n",
  551. "\t\ttop:0px;\n",
  552. "\t\tleft:0px;\n",
  553. "\t\tright:0px;\n",
  554. "\t\tfont:", font, ";\n",
  555. ((this.height||has("opera")) ? "" : "\t\tposition: fixed;\n"),
  556. "\t\tline-height:", lineHeight,";\n",
  557. "\t}\n",
  558. "\tp{ margin: 1em 0; }\n",
  559. "\tli > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; }\n",
  560. // Can't set min-height in IE>=9, it puts layout on li, which puts move/resize handles.
  561. (has("ie") || has("trident") ? "" : "\tli{ min-height:1.2em; }\n"),
  562. "</style>\n",
  563. this._applyEditingAreaStyleSheets(),"\n",
  564. "</head>\n<body ",
  565. "</head>\n<body role='main' ",
  566. // Onload handler fills in real editor content.
  567. // On IE9, sometimes onload is called twice, and the first time frameElement is null (test_FullScreen.html)
  568. "onload='frameElement && frameElement._loadFunc(window,document)' ",
  569. "style='"+userStyle+"'>", html, "</body>\n</html>"
  570. ].join(""); // String
  571. },
  572. _applyEditingAreaStyleSheets: function(){
  573. // summary:
  574. // apply the specified css files in styleSheets
  575. // tags:
  576. // private
  577. var files = [];
  578. if(this.styleSheets){
  579. files = this.styleSheets.split(';');
  580. this.styleSheets = '';
  581. }
  582. //empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
  583. files = files.concat(this.editingAreaStyleSheets);
  584. this.editingAreaStyleSheets = [];
  585. var text='', i=0, url;
  586. while((url=files[i++])){
  587. var abstring = (new _Url(win.global.location, url)).toString();
  588. this.editingAreaStyleSheets.push(abstring);
  589. text += '<link rel="stylesheet" type="text/css" href="'+abstring+'"/>';
  590. }
  591. return text;
  592. },
  593. addStyleSheet: function(/*dojo._Url*/ uri){
  594. // summary:
  595. // add an external stylesheet for the editing area
  596. // uri:
  597. // A dojo.uri.Uri pointing to the url of the external css file
  598. var url=uri.toString();
  599. //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
  600. if(url.charAt(0) === '.' || (url.charAt(0) !== '/' && !uri.host)){
  601. url = (new _Url(win.global.location, url)).toString();
  602. }
  603. if(array.indexOf(this.editingAreaStyleSheets, url) > -1){
  604. // console.debug("dijit._editor.RichText.addStyleSheet: Style sheet "+url+" is already applied");
  605. return;
  606. }
  607. this.editingAreaStyleSheets.push(url);
  608. this.onLoadDeferred.addCallback(lang.hitch(this, function(){
  609. if(this.document.createStyleSheet){ //IE
  610. this.document.createStyleSheet(url);
  611. }else{ //other browser
  612. var head = this.document.getElementsByTagName("head")[0];
  613. var stylesheet = this.document.createElement("link");
  614. stylesheet.rel="stylesheet";
  615. stylesheet.type="text/css";
  616. stylesheet.href=url;
  617. head.appendChild(stylesheet);
  618. }
  619. }));
  620. },
  621. removeStyleSheet: function(/*dojo._Url*/ uri){
  622. // summary:
  623. // remove an external stylesheet for the editing area
  624. var url=uri.toString();
  625. //if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
  626. if(url.charAt(0) === '.' || (url.charAt(0) !== '/' && !uri.host)){
  627. url = (new _Url(win.global.location, url)).toString();
  628. }
  629. var index = array.indexOf(this.editingAreaStyleSheets, url);
  630. if(index === -1){
  631. // console.debug("dijit._editor.RichText.removeStyleSheet: Style sheet "+url+" has not been applied");
  632. return;
  633. }
  634. delete this.editingAreaStyleSheets[index];
  635. win.withGlobal(this.window,'query', dojo, ['link:[href="'+url+'"]']).orphan();
  636. },
  637. // disabled: Boolean
  638. // The editor is disabled; the text cannot be changed.
  639. disabled: false,
  640. _mozSettingProps: {'styleWithCSS':false},
  641. _setDisabledAttr: function(/*Boolean*/ value){
  642. value = !!value;
  643. this._set("disabled", value);
  644. if(!this.isLoaded){
  645. return;
  646. } // this method requires init to be complete
  647. var preventIEfocus = has("ie") && (this.isLoaded || !this.focusOnLoad);
  648. if(preventIEfocus){
  649. this.editNode.unselectable = "on";
  650. }
  651. this.editNode.contentEditable = !value;
  652. this.editNode.tabIndex = value ? "-1" : this.tabIndex;
  653. if(preventIEfocus){
  654. this.defer(function(){
  655. if(this.editNode){ // guard in case widget destroyed before timeout
  656. this.editNode.unselectable = "off";
  657. }
  658. });
  659. }
  660. if(has("mozilla") && !value && this._mozSettingProps){
  661. var ps = this._mozSettingProps;
  662. var n;
  663. for(n in ps){
  664. if(ps.hasOwnProperty(n)){
  665. try{
  666. this.document.execCommand(n, false, ps[n]);
  667. }catch(e2){
  668. }
  669. }
  670. }
  671. }
  672. this._disabledOK = true;
  673. },
  674. /* Event handlers
  675. *****************/
  676. onLoad: function(/*String*/ html){
  677. // summary:
  678. // Handler after the iframe finishes loading.
  679. // html: String
  680. // Editor contents should be set to this value
  681. // tags:
  682. // protected
  683. // TODO: rename this to _onLoad, make empty public onLoad() method, deprecate/make protected onLoadDeferred handler?
  684. if(!this.window.__registeredWindow){
  685. this.window.__registeredWindow = true;
  686. this._iframeRegHandle = focus.registerIframe(this.iframe);
  687. }
  688. // there's a wrapper div around the content, see _getIframeDocTxt().
  689. this.editNode = this.document.body.firstChild;
  690. var _this = this;
  691. // Helper code so IE and FF skip over focusing on the <iframe> and just focus on the inner <div>.
  692. // See #4996 IE wants to focus the BODY tag.
  693. this.beforeIframeNode = domConstruct.place("<div tabIndex=-1></div>", this.iframe, "before");
  694. this.afterIframeNode = domConstruct.place("<div tabIndex=-1></div>", this.iframe, "after");
  695. this.iframe.onfocus = this.document.onfocus = function(){
  696. _this.editNode.focus();
  697. };
  698. this.focusNode = this.editNode; // for InlineEditBox
  699. var events = this.events.concat(this.captureEvents);
  700. var ap = this.iframe ? this.document : this.editNode;
  701. array.forEach(events, function(item){
  702. this.connect(ap, item.toLowerCase(), item);
  703. }, this);
  704. this.connect(ap, "onmouseup", "onClick"); // mouseup in the margin does not generate an onclick event
  705. if(has("ie")){ // IE contentEditable
  706. this.connect(this.document, "onmousedown", "_onIEMouseDown"); // #4996 fix focus
  707. // give the node Layout on IE
  708. // TODO: this may no longer be needed, since we've reverted IE to using an iframe,
  709. // not contentEditable. Removing it would also probably remove the need for creating
  710. // the extra <div> in _getIframeDocTxt()
  711. this.editNode.style.zoom = 1.0;
  712. }else{
  713. this.connect(this.document, "onmousedown", function(){
  714. // Clear the moveToStart focus, as mouse
  715. // down will set cursor point. Required to properly
  716. // work with selection/position driven plugins and clicks in
  717. // the window. refs: #10678
  718. delete this._cursorToStart;
  719. });
  720. }
  721. if(has("webkit")){
  722. //WebKit sometimes doesn't fire right on selections, so the toolbar
  723. //doesn't update right. Therefore, help it out a bit with an additional
  724. //listener. A mouse up will typically indicate a display change, so fire this
  725. //and get the toolbar to adapt. Reference: #9532
  726. this._webkitListener = this.connect(this.document, "onmouseup", "onDisplayChanged");
  727. this.connect(this.document, "onmousedown", function(e){
  728. var t = e.target;
  729. if(t && (t === this.document.body || t === this.document)){
  730. // Since WebKit uses the inner DIV, we need to check and set position.
  731. // See: #12024 as to why the change was made.
  732. setTimeout(lang.hitch(this, "placeCursorAtEnd"), 0);
  733. }
  734. });
  735. }
  736. if(has("ie")){
  737. // Try to make sure 'hidden' elements aren't visible in edit mode (like browsers other than IE
  738. // do). See #9103
  739. try{
  740. this.document.execCommand('RespectVisibilityInDesign', true, null);
  741. }catch(e){/* squelch */}
  742. }
  743. this.isLoaded = true;
  744. this.set('disabled', this.disabled); // initialize content to editable (or not)
  745. // Note that setValue() call will only work after isLoaded is set to true (above)
  746. // Set up a function to allow delaying the setValue until a callback is fired
  747. // This ensures extensions like dijit.Editor have a way to hold the value set
  748. // until plugins load (and do things like register filters).
  749. var setContent = lang.hitch(this, function(){
  750. this.setValue(html);
  751. if(this.onLoadDeferred){
  752. this.onLoadDeferred.callback(true);
  753. }
  754. this.onDisplayChanged();
  755. if(this.focusOnLoad){
  756. // after the document loads, then set focus after updateInterval expires so that
  757. // onNormalizedDisplayChanged has run to avoid input caret issues
  758. ready(lang.hitch(this, function(){ setTimeout(lang.hitch(this, "focus"), this.updateInterval); }));
  759. }
  760. // Save off the initial content now
  761. this.value = this.getValue(true);
  762. });
  763. if(this.setValueDeferred){
  764. this.setValueDeferred.addCallback(setContent);
  765. }else{
  766. setContent();
  767. }
  768. },
  769. onKeyDown: function(/* Event */ e){
  770. // summary:
  771. // Handler for onkeydown event
  772. // tags:
  773. // protected
  774. // we need this event at the moment to get the events from control keys
  775. // such as the backspace. It might be possible to add this to Dojo, so that
  776. // keyPress events can be emulated by the keyDown and keyUp detection.
  777. if(e.keyCode === keys.TAB && this.isTabIndent){
  778. event.stop(e); //prevent tab from moving focus out of editor
  779. // FIXME: this is a poor-man's indent/outdent. It would be
  780. // better if it added 4 "&nbsp;" chars in an undoable way.
  781. // Unfortunately pasteHTML does not prove to be undoable
  782. if(this.queryCommandEnabled((e.shiftKey ? "outdent" : "indent"))){
  783. this.execCommand((e.shiftKey ? "outdent" : "indent"));
  784. }
  785. }
  786. // Make tab and shift-tab skip over the <iframe>, going from the nested <div> to the toolbar
  787. // or next element after the editor. Needed on IE<9 and firefox.
  788. if(e.keyCode == keys.TAB && !this.isTabIndent){
  789. if(e.shiftKey && !e.ctrlKey && !e.altKey){
  790. // focus the <iframe> so the browser will shift-tab away from it instead
  791. this.beforeIframeNode.focus();
  792. }else if(!e.shiftKey && !e.ctrlKey && !e.altKey){
  793. // focus node after the <iframe> so the browser will tab away from it instead
  794. this.afterIframeNode.focus();
  795. }
  796. }
  797. if(has("ie") < 9 && e.keyCode === keys.BACKSPACE && this.document.selection.type === "Control"){
  798. // IE has a bug where if a non-text object is selected in the editor,
  799. // hitting backspace would act as if the browser's back button was
  800. // clicked instead of deleting the object. see #1069
  801. e.stopPropagation();
  802. e.preventDefault();
  803. this.execCommand("delete");
  804. }
  805. if(has("ff")){
  806. if(e.keyCode === keys.PAGE_UP || e.keyCode === keys.PAGE_DOWN ){
  807. if(this.editNode.clientHeight >= this.editNode.scrollHeight){
  808. // Stop the event to prevent firefox from trapping the cursor when there is no scroll bar.
  809. e.preventDefault();
  810. }
  811. }
  812. }
  813. return true;
  814. },
  815. onKeyUp: function(/*===== e =====*/){
  816. // summary:
  817. // Handler for onkeyup event
  818. // tags:
  819. // callback
  820. },
  821. setDisabled: function(/*Boolean*/ disabled){
  822. // summary:
  823. // Deprecated, use set('disabled', ...) instead.
  824. // tags:
  825. // deprecated
  826. kernel.deprecated('dijit.Editor::setDisabled is deprecated','use dijit.Editor::attr("disabled",boolean) instead', 2.0);
  827. this.set('disabled',disabled);
  828. },
  829. _setValueAttr: function(/*String*/ value){
  830. // summary:
  831. // Registers that attr("value", foo) should call setValue(foo)
  832. this.setValue(value);
  833. },
  834. _setDisableSpellCheckAttr: function(/*Boolean*/ disabled){
  835. if(this.document){
  836. domAttr.set(this.document.body, "spellcheck", !disabled);
  837. }else{
  838. // try again after the editor is finished loading
  839. this.onLoadDeferred.addCallback(lang.hitch(this, function(){
  840. domAttr.set(this.document.body, "spellcheck", !disabled);
  841. }));
  842. }
  843. this._set("disableSpellCheck", disabled);
  844. },
  845. onKeyPress: function(e){
  846. // summary:
  847. // Handle the various key events
  848. // tags:
  849. // protected
  850. if(e.keyCode === keys.SHIFT ||
  851. e.keyCode === keys.ALT ||
  852. e.keyCode === keys.META ||
  853. e.keyCode === keys.CTRL ||
  854. (e.keyCode == keys.TAB && !this.isTabIndent && !e.ctrlKey && !e.altKey)){
  855. return true;
  856. }
  857. var c = (e.keyChar && e.keyChar.toLowerCase()) || e.keyCode,
  858. handlers = this._keyHandlers[c],
  859. args = arguments;
  860. if(handlers && !e.altKey){
  861. array.some(handlers, function(h){
  862. // treat meta- same as ctrl-, for benefit of mac users
  863. if(!(h.shift ^ e.shiftKey) && !(h.ctrl ^ (e.ctrlKey||e.metaKey))){
  864. if(!h.handler.apply(this, args)){
  865. e.preventDefault();
  866. }
  867. return true;
  868. }
  869. }, this);
  870. }
  871. // function call after the character has been inserted
  872. if(!this._onKeyHitch){
  873. this._onKeyHitch = lang.hitch(this, "onKeyPressed");
  874. }
  875. setTimeout(this._onKeyHitch, 1);
  876. return true;
  877. },
  878. addKeyHandler: function(/*String*/ key, /*Boolean*/ ctrl, /*Boolean*/ shift, /*Function*/ handler){
  879. // summary:
  880. // Add a handler for a keyboard shortcut
  881. // description:
  882. // The key argument should be in lowercase if it is a letter character
  883. // tags:
  884. // protected
  885. if(!lang.isArray(this._keyHandlers[key])){
  886. this._keyHandlers[key] = [];
  887. }
  888. //TODO: would be nice to make this a hash instead of an array for quick lookups
  889. this._keyHandlers[key].push({
  890. shift: shift || false,
  891. ctrl: ctrl || false,
  892. handler: handler
  893. });
  894. },
  895. onKeyPressed: function(){
  896. // summary:
  897. // Handler for after the user has pressed a key, and the display has been updated.
  898. // (Runs on a timer so that it runs after the display is updated)
  899. // tags:
  900. // private
  901. this.onDisplayChanged(/*e*/); // can't pass in e
  902. },
  903. onClick: function(/*Event*/ e){
  904. // summary:
  905. // Handler for when the user clicks.
  906. // tags:
  907. // private
  908. // console.info('onClick',this._tryDesignModeOn);
  909. this.onDisplayChanged(e);
  910. },
  911. _onIEMouseDown: function(){
  912. // summary:
  913. // IE only to prevent 2 clicks to focus
  914. // tags:
  915. // protected
  916. if(!this.focused && !this.disabled){
  917. this.focus();
  918. }
  919. },
  920. _onBlur: function(e){
  921. // summary:
  922. // Called from focus manager when focus has moved away from this editor
  923. // tags:
  924. // protected
  925. // console.info('_onBlur')
  926. this.inherited(arguments);
  927. var newValue = this.getValue(true);
  928. if(newValue !== this.value){
  929. this.onChange(newValue);
  930. }
  931. this._set("value", newValue);
  932. },
  933. _onFocus: function(/*Event*/ e){
  934. // summary:
  935. // Called from focus manager when focus has moved into this editor
  936. // tags:
  937. // protected
  938. // console.info('_onFocus')
  939. if(!this.disabled){
  940. if(!this._disabledOK){
  941. this.set('disabled', false);
  942. }
  943. this.inherited(arguments);
  944. }
  945. },
  946. // TODO: remove in 2.0
  947. blur: function(){
  948. // summary:
  949. // Remove focus from this instance.
  950. // tags:
  951. // deprecated
  952. if(!has("ie") && this.window.document.documentElement && this.window.document.documentElement.focus){
  953. this.window.document.documentElement.focus();
  954. }else if(win.doc.body.focus){
  955. win.doc.body.focus();
  956. }
  957. },
  958. focus: function(){
  959. // summary:
  960. // Move focus to this editor
  961. if(!this.isLoaded){
  962. this.focusOnLoad = true;
  963. return;
  964. }
  965. if(this._cursorToStart){
  966. delete this._cursorToStart;
  967. if(this.editNode.childNodes){
  968. this.placeCursorAtStart(); // this calls focus() so return
  969. return;
  970. }
  971. }
  972. if(has("ie") < 9){
  973. //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
  974. // if we fire the event manually and let the browser handle the focusing, the latest
  975. // cursor position is focused like in FF
  976. this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject/fireEvent only in IE < 11
  977. }else{
  978. // Firefox and chrome
  979. this.editNode.focus();
  980. }
  981. },
  982. // _lastUpdate: 0,
  983. updateInterval: 200,
  984. _updateTimer: null,
  985. onDisplayChanged: function(/*Event*/ /*===== e =====*/){
  986. // summary:
  987. // This event will be fired every time the display context
  988. // changes and the result needs to be reflected in the UI.
  989. // description:
  990. // If you don't want to have update too often,
  991. // onNormalizedDisplayChanged should be used instead
  992. // tags:
  993. // private
  994. // var _t=new Date();
  995. if(this._updateTimer){
  996. clearTimeout(this._updateTimer);
  997. }
  998. if(!this._updateHandler){
  999. this._updateHandler = lang.hitch(this,"onNormalizedDisplayChanged");
  1000. }
  1001. this._updateTimer = setTimeout(this._updateHandler, this.updateInterval);
  1002. // Technically this should trigger a call to watch("value", ...) registered handlers,
  1003. // but getValue() is too slow to call on every keystroke so we don't.
  1004. },
  1005. onNormalizedDisplayChanged: function(){
  1006. // summary:
  1007. // This event is fired every updateInterval ms or more
  1008. // description:
  1009. // If something needs to happen immediately after a
  1010. // user change, please use onDisplayChanged instead.
  1011. // tags:
  1012. // private
  1013. delete this._updateTimer;
  1014. },
  1015. onChange: function(/*===== newContent =====*/){
  1016. // summary:
  1017. // This is fired if and only if the editor loses focus and
  1018. // the content is changed.
  1019. },
  1020. _normalizeCommand: function(/*String*/ cmd, /*Anything?*/argument){
  1021. // summary:
  1022. // Used as the advice function to map our
  1023. // normalized set of commands to those supported by the target
  1024. // browser.
  1025. // tags:
  1026. // private
  1027. var command = cmd.toLowerCase();
  1028. if(command === "formatblock"){
  1029. if(has("safari") && argument === undefined){ command = "heading"; }
  1030. }else if(command === "hilitecolor" && !has("mozilla")){
  1031. command = "backcolor";
  1032. }
  1033. return command;
  1034. },
  1035. _qcaCache: {},
  1036. queryCommandAvailable: function(/*String*/ command){
  1037. // summary:
  1038. // Tests whether a command is supported by the host. Clients
  1039. // SHOULD check whether a command is supported before attempting
  1040. // to use it, behaviour for unsupported commands is undefined.
  1041. // command:
  1042. // The command to test for
  1043. // tags:
  1044. // private
  1045. // memoizing version. See _queryCommandAvailable for computing version
  1046. var ca = this._qcaCache[command];
  1047. if(ca !== undefined){ return ca; }
  1048. return (this._qcaCache[command] = this._queryCommandAvailable(command));
  1049. },
  1050. _queryCommandAvailable: function(/*String*/ command){
  1051. // summary:
  1052. // See queryCommandAvailable().
  1053. // tags:
  1054. // private
  1055. var ie = 1;
  1056. var mozilla = 1 << 1;
  1057. var webkit = 1 << 2;
  1058. var opera = 1 << 3;
  1059. function isSupportedBy(browsers){
  1060. return {
  1061. ie: Boolean(browsers & ie),
  1062. mozilla: Boolean(browsers & mozilla),
  1063. webkit: Boolean(browsers & webkit),
  1064. opera: Boolean(browsers & opera)
  1065. };
  1066. }
  1067. var supportedBy = null;
  1068. switch(command.toLowerCase()){
  1069. case "bold": case "italic": case "underline":
  1070. case "subscript": case "superscript":
  1071. case "fontname": case "fontsize":
  1072. case "forecolor": case "hilitecolor":
  1073. case "justifycenter": case "justifyfull": case "justifyleft":
  1074. case "justifyright": case "delete": case "selectall": case "toggledir":
  1075. supportedBy = isSupportedBy(mozilla | ie | webkit | opera);
  1076. break;
  1077. case "createlink": case "unlink": case "removeformat":
  1078. case "inserthorizontalrule": case "insertimage":
  1079. case "insertorderedlist": case "insertunorderedlist":
  1080. case "indent": case "outdent": case "formatblock":
  1081. case "inserthtml": case "undo": case "redo": case "strikethrough": case "tabindent":
  1082. supportedBy = isSupportedBy(mozilla | ie | opera | webkit);
  1083. break;
  1084. case "blockdirltr": case "blockdirrtl":
  1085. case "dirltr": case "dirrtl":
  1086. case "inlinedirltr": case "inlinedirrtl":
  1087. supportedBy = isSupportedBy(ie);
  1088. break;
  1089. case "cut": case "copy": case "paste":
  1090. supportedBy = isSupportedBy( ie | mozilla | webkit);
  1091. break;
  1092. case "inserttable":
  1093. supportedBy = isSupportedBy(mozilla | ie);
  1094. break;
  1095. case "insertcell": case "insertcol": case "insertrow":
  1096. case "deletecells": case "deletecols": case "deleterows":
  1097. case "mergecells": case "splitcell":
  1098. supportedBy = isSupportedBy(ie | mozilla);
  1099. break;
  1100. default: return false;
  1101. }
  1102. return ((has("ie") || has("trident")) && supportedBy.ie) ||
  1103. (has("mozilla") && supportedBy.mozilla) ||
  1104. (has("webkit") && supportedBy.webkit) ||
  1105. (has("opera") && supportedBy.opera); // Boolean return true if the command is supported, false otherwise
  1106. },
  1107. execCommand: function(/*String*/ command, argument){
  1108. // summary:
  1109. // Executes a command in the Rich Text area
  1110. // command:
  1111. // The command to execute
  1112. // argument:
  1113. // An optional argument to the command
  1114. // tags:
  1115. // protected
  1116. var returnValue;
  1117. //focus() is required for IE to work
  1118. //In addition, focus() makes sure after the execution of
  1119. //the command, the editor receives the focus as expected
  1120. if(this.focused){
  1121. // put focus back in the iframe, unless focus has somehow been shifted out of the editor completely
  1122. this.focus();
  1123. }
  1124. command = this._normalizeCommand(command, argument);
  1125. if(argument !== undefined){
  1126. if(command === "heading"){
  1127. throw new Error("unimplemented");
  1128. }else if(command === "formatblock" && (has("ie") || has("trident"))){
  1129. argument = '<'+argument+'>';
  1130. }
  1131. }
  1132. //Check to see if we have any over-rides for commands, they will be functions on this
  1133. //widget of the form _commandImpl. If we don't, fall through to the basic native
  1134. //exec command of the browser.
  1135. var implFunc = "_" + command + "Impl";
  1136. if(this[implFunc]){
  1137. returnValue = this[implFunc](argument);
  1138. }else{
  1139. argument = arguments.length > 1 ? argument : null;
  1140. if(argument || command !== "createlink"){
  1141. returnValue = this.document.execCommand(command, false, argument);
  1142. }
  1143. }
  1144. this.onDisplayChanged();
  1145. return returnValue;
  1146. },
  1147. queryCommandEnabled: function(/*String*/ command){
  1148. // summary:
  1149. // Check whether a command is enabled or not.
  1150. // command:
  1151. // The command to execute
  1152. // tags:
  1153. // protected
  1154. if(this.disabled || !this._disabledOK){ return false; }
  1155. command = this._normalizeCommand(command);
  1156. //Check to see if we have any over-rides for commands, they will be functions on this
  1157. //widget of the form _commandEnabledImpl. If we don't, fall through to the basic native
  1158. //command of the browser.
  1159. var implFunc = "_" + command + "EnabledImpl";
  1160. if(this[implFunc]){
  1161. return this[implFunc](command);
  1162. }else{
  1163. return this._browserQueryCommandEnabled(command);
  1164. }
  1165. },
  1166. queryCommandState: function(command){
  1167. // summary:
  1168. // Check the state of a given command and returns true or false.
  1169. // tags:
  1170. // protected
  1171. if(this.disabled || !this._disabledOK){ return false; }
  1172. command = this._normalizeCommand(command);
  1173. try{
  1174. return this.document.queryCommandState(command);
  1175. }catch(e){
  1176. //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
  1177. return false;
  1178. }
  1179. },
  1180. queryCommandValue: function(command){
  1181. // summary:
  1182. // Check the value of a given command. This matters most for
  1183. // custom selections and complex values like font value setting.
  1184. // tags:
  1185. // protected
  1186. if(this.disabled || !this._disabledOK){ return false; }
  1187. var r;
  1188. command = this._normalizeCommand(command);
  1189. if((has("ie") || has("trident")) && command === "formatblock"){
  1190. r = this._native2LocalFormatNames[this.document.queryCommandValue(command)];
  1191. }else if(has("mozilla") && command === "hilitecolor"){
  1192. var oldValue;
  1193. try{
  1194. oldValue = this.document.queryCommandValue("styleWithCSS");
  1195. }catch(e){
  1196. oldValue = false;
  1197. }
  1198. this.document.execCommand("styleWithCSS", false, true);
  1199. r = this.document.queryCommandValue(command);
  1200. this.document.execCommand("styleWithCSS", false, oldValue);
  1201. }else{
  1202. r = this.document.queryCommandValue(command);
  1203. }
  1204. return r;
  1205. },
  1206. // Misc.
  1207. _sCall: function(name, args){
  1208. // summary:
  1209. // Run the named method of dijit._editor.selection over the
  1210. // current editor instance's window, with the passed args.
  1211. // tags:
  1212. // private
  1213. return win.withGlobal(this.window, name, selectionapi, args);
  1214. },
  1215. // FIXME: this is a TON of code duplication. Why?
  1216. placeCursorAtStart: function(){
  1217. // summary:
  1218. // Place the cursor at the start of the editing area.
  1219. // tags:
  1220. // private
  1221. this.focus();
  1222. //see comments in placeCursorAtEnd
  1223. var isvalid=false;
  1224. if(has("mozilla")){
  1225. // TODO: Is this branch even necessary?
  1226. var first=this.editNode.firstChild;
  1227. while(first){
  1228. if(first.nodeType === 3){
  1229. if(first.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
  1230. isvalid=true;
  1231. this._sCall("selectElement", [ first ]);
  1232. break;
  1233. }
  1234. }else if(first.nodeType === 1){
  1235. isvalid=true;
  1236. var tg = first.tagName ? first.tagName.toLowerCase() : "";
  1237. // Collapse before childless tags.
  1238. if(/br|input|img|base|meta|area|basefont|hr|link/.test(tg)){
  1239. this._sCall("selectElement", [ first ]);
  1240. }else{
  1241. // Collapse inside tags with children.
  1242. this._sCall("selectElementChildren", [ first ]);
  1243. }
  1244. break;
  1245. }
  1246. first = first.nextSibling;
  1247. }
  1248. }else{
  1249. isvalid=true;
  1250. this._sCall("selectElementChildren", [ this.editNode ]);
  1251. }
  1252. if(isvalid){
  1253. this._sCall("collapse", [ true ]);
  1254. }
  1255. },
  1256. placeCursorAtEnd: function(){
  1257. // summary:
  1258. // Place the cursor at the end of the editing area.
  1259. // tags:
  1260. // private
  1261. this.focus();
  1262. //In mozilla, if last child is not a text node, we have to use
  1263. // selectElementChildren on this.editNode.lastChild otherwise the
  1264. // cursor would be placed at the end of the closing tag of
  1265. //this.editNode.lastChild
  1266. var isvalid=false;
  1267. if(has("mozilla")){
  1268. var last=this.editNode.lastChild;
  1269. while(last){
  1270. if(last.nodeType === 3){
  1271. if(last.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
  1272. isvalid=true;
  1273. this._sCall("selectElement", [ last ]);
  1274. break;
  1275. }
  1276. }else if(last.nodeType === 1){
  1277. isvalid=true;
  1278. if(last.lastChild){
  1279. this._sCall("selectElement", [ last.lastChild ]);
  1280. }else{
  1281. this._sCall("selectElement", [ last ]);
  1282. }
  1283. break;
  1284. }
  1285. last = last.previousSibling;
  1286. }
  1287. }else{
  1288. isvalid=true;
  1289. this._sCall("selectElementChildren", [ this.editNode ]);
  1290. }
  1291. if(isvalid){
  1292. this._sCall("collapse", [ false ]);
  1293. }
  1294. },
  1295. getValue: function(/*Boolean?*/ nonDestructive){
  1296. // summary:
  1297. // Return the current content of the editing area (post filters
  1298. // are applied). Users should call get('value') instead.
  1299. // nonDestructive:
  1300. // defaults to false. Should the post-filtering be run over a copy
  1301. // of the live DOM? Most users should pass "true" here unless they
  1302. // *really* know that none of the installed filters are going to
  1303. // mess up the editing session.
  1304. // tags:
  1305. // private
  1306. if(this.textarea){
  1307. if(this.isClosed || !this.isLoaded){
  1308. return this.textarea.value;
  1309. }
  1310. }
  1311. return this._postFilterContent(null, nonDestructive);
  1312. },
  1313. _getValueAttr: function(){
  1314. // summary:
  1315. // Hook to make attr("value") work
  1316. return this.getValue(true);
  1317. },
  1318. setValue: function(/*String*/ html){
  1319. // summary:
  1320. // This function sets the content. No undo history is preserved.
  1321. // Users should use set('value', ...) instead.
  1322. // tags:
  1323. // deprecated
  1324. // TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
  1325. if(!this.isLoaded){
  1326. // try again after the editor is finished loading
  1327. this.onLoadDeferred.addCallback(lang.hitch(this, function(){
  1328. this.setValue(html);
  1329. }));
  1330. return;
  1331. }
  1332. this._cursorToStart = true;
  1333. if(this.textarea && (this.isClosed || !this.isLoaded)){
  1334. this.textarea.value=html;
  1335. }else{
  1336. html = this._preFilterContent(html);
  1337. var node = this.isClosed ? this.domNode : this.editNode;
  1338. // Use &nbsp; to avoid webkit problems where editor is disabled until the user clicks it
  1339. if(!html && has("webkit")){
  1340. html = "&#160;"; // &nbsp;
  1341. }
  1342. node.innerHTML = html;
  1343. this._preDomFilterContent(node);
  1344. }
  1345. this.onDisplayChanged();
  1346. this._set("value", this.getValue(true));
  1347. },
  1348. replaceValue: function(/*String*/ html){
  1349. // summary:
  1350. // This function set the content while trying to maintain the undo stack
  1351. // (now only works fine with Moz, this is identical to setValue in all
  1352. // other browsers)
  1353. // tags:
  1354. // protected
  1355. if(this.isClosed){
  1356. this.setValue(html);
  1357. }else if(this.window && this.window.getSelection && !has("mozilla")){ // Safari
  1358. // look ma! it's a totally f'd browser!
  1359. this.setValue(html);
  1360. }else if(this.window && this.window.getSelection){ // Moz
  1361. html = this._preFilterContent(html);
  1362. this.execCommand("selectall");
  1363. this.execCommand("inserthtml", html);
  1364. this._preDomFilterContent(this.editNode);
  1365. }else if(this.document && this.document.selection){//IE
  1366. //In IE, when the first element is not a text node, say
  1367. //an <a> tag, when replacing the content of the editing
  1368. //area, the <a> tag will be around all the content
  1369. //so for now, use setValue for IE too
  1370. this.setValue(html);
  1371. }
  1372. this._set("value", this.getValue(true));
  1373. },
  1374. _preFilterContent: function(/*String*/ html){
  1375. // summary:
  1376. // Filter the input before setting the content of the editing
  1377. // area. DOM pre-filtering may happen after this
  1378. // string-based filtering takes place but as of 1.2, this is not
  1379. // guaranteed for operations such as the inserthtml command.
  1380. // tags:
  1381. // private
  1382. var ec = html;
  1383. array.forEach(this.contentPreFilters, function(ef){ if(ef){ ec = ef(ec); } });
  1384. return ec;
  1385. },
  1386. _preDomFilterContent: function(/*DomNode*/ dom){
  1387. // summary:
  1388. // filter the input's live DOM. All filter operations should be
  1389. // considered to be "live" and operating on the DOM that the user
  1390. // will be interacting with in their editing session.
  1391. // tags:
  1392. // private
  1393. dom = dom || this.editNode;
  1394. array.forEach(this.contentDomPreFilters, function(ef){
  1395. if(ef && lang.isFunction(ef)){
  1396. ef(dom);
  1397. }
  1398. }, this);
  1399. },
  1400. _postFilterContent: function(
  1401. /*DomNode|DomNode[]|String?*/ dom,
  1402. /*Boolean?*/ nonDestructive){
  1403. // summary:
  1404. // filter the output after getting the content of the editing area
  1405. //
  1406. // description:
  1407. // post-filtering allows plug-ins and users to specify any number
  1408. // of transforms over the editor's content, enabling many common
  1409. // use-cases such as transforming absolute to relative URLs (and
  1410. // vice-versa), ensuring conformance with a particular DTD, etc.
  1411. // The filters are registered in the contentDomPostFilters and
  1412. // contentPostFilters arrays. Each item in the
  1413. // contentDomPostFilters array is a function which takes a DOM
  1414. // Node or array of nodes as its only argument and returns the
  1415. // same. It is then passed down the chain for further filtering.
  1416. // The contentPostFilters array behaves the same way, except each
  1417. // member operates on strings. Together, the DOM and string-based
  1418. // filtering allow the full range of post-processing that should
  1419. // be necessaray to enable even the most agressive of post-editing
  1420. // conversions to take place.
  1421. //
  1422. // If nonDestructive is set to "true", the nodes are cloned before
  1423. // filtering proceeds to avoid potentially destructive transforms
  1424. // to the content which may still needed to be edited further.
  1425. // Once DOM filtering has taken place, the serialized version of
  1426. // the DOM which is passed is run through each of the
  1427. // contentPostFilters functions.
  1428. //
  1429. // dom:
  1430. // a node, set of nodes, which to filter using each of the current
  1431. // members of the contentDomPostFilters and contentPostFilters arrays.
  1432. //
  1433. // nonDestructive:
  1434. // defaults to "false". If true, ensures that filtering happens on
  1435. // a clone of the passed-in content and not the actual node
  1436. // itself.
  1437. //
  1438. // tags:
  1439. // private
  1440. var ec;
  1441. if(!lang.isString(dom)){
  1442. dom = dom || this.editNode;
  1443. if(this.contentDomPostFilters.length){
  1444. if(nonDestructive){
  1445. dom = lang.clone(dom);
  1446. }
  1447. array.forEach(this.contentDomPostFilters, function(ef){
  1448. dom = ef(dom);
  1449. });
  1450. }
  1451. ec = htmlapi.getChildrenHtml(dom);
  1452. }else{
  1453. ec = dom;
  1454. }
  1455. if(!lang.trim(ec.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length){
  1456. ec = "";
  1457. }
  1458. // if(has("ie")){
  1459. // //removing appended <P>&nbsp;</P> for IE
  1460. // ec = ec.replace(/(?:<p>&nbsp;</p>[\n\r]*)+$/i,"");
  1461. // }
  1462. array.forEach(this.contentPostFilters, function(ef){
  1463. ec = ef(ec);
  1464. });
  1465. return ec;
  1466. },
  1467. _saveContent: function(){
  1468. // summary:
  1469. // Saves the content in an onunload event if the editor has not been closed
  1470. // tags:
  1471. // private
  1472. var saveTextarea = dom.byId(dijit._scopeName + "._editor.RichText.value");
  1473. if(saveTextarea){
  1474. if(saveTextarea.value){
  1475. saveTextarea.value += this._SEPARATOR;
  1476. }
  1477. saveTextarea.value += this.name + this._NAME_CONTENT_SEP + this.getValue(true);
  1478. }
  1479. },
  1480. escapeXml: function(/*String*/ str, /*Boolean*/ noSingleQuotes){
  1481. // summary:
  1482. // Adds escape sequences for special characters in XML.
  1483. // Optionally skips escapes for single quotes
  1484. // tags:
  1485. // private
  1486. str = str.replace(/&/gm, "&amp;").replace(/</gm, "&lt;").replace(/>/gm, "&gt;").replace(/"/gm, "&quot;");
  1487. if(!noSingleQuotes){
  1488. str = str.replace(/'/gm, "&#39;");
  1489. }
  1490. return str; // string
  1491. },
  1492. getNodeHtml: function(/* DomNode */ node){
  1493. // summary:
  1494. // Deprecated. Use dijit/_editor/html::_getNodeHtml() instead.
  1495. // tags:
  1496. // deprecated
  1497. kernel.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit/_editor/html::getNodeHtml instead', 2);
  1498. return htmlapi.getNodeHtml(node); // String
  1499. },
  1500. getNodeChildrenHtml: function(/* DomNode */ dom){
  1501. // summary:
  1502. // Deprecated. Use dijit/_editor/html::getChildrenHtml() instead.
  1503. // tags:
  1504. // deprecated
  1505. kernel.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit/_editor/html::getChildrenHtml instead', 2);
  1506. return htmlapi.getChildrenHtml(dom);
  1507. },
  1508. close: function(/*Boolean?*/ save){
  1509. // summary:
  1510. // Kills the editor and optionally writes back the modified contents to the
  1511. // element from which it originated.
  1512. // save:
  1513. // Whether or not to save the changes. If false, the changes are discarded.
  1514. // tags:
  1515. // private
  1516. if(this.isClosed){ return; }
  1517. if(!arguments.length){ save = true; }
  1518. if(save){
  1519. this._set("value", this.getValue(true));
  1520. }
  1521. // line height is squashed for iframes
  1522. // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
  1523. if(this.interval){ clearInterval(this.interval); }
  1524. if(this._webkitListener){
  1525. //Cleaup of WebKit fix: #9532
  1526. this.disconnect(this._webkitListener);
  1527. delete this._webkitListener;
  1528. }
  1529. // Guard against memory leaks on IE (see #9268)
  1530. if(has("ie")){
  1531. this.iframe.onfocus = null;
  1532. }
  1533. this.iframe._loadFunc = null;
  1534. if(this._iframeRegHandle){
  1535. this._iframeRegHandle.remove();
  1536. delete this._iframeRegHandle;
  1537. }
  1538. if(this.textarea){
  1539. var s = this.textarea.style;
  1540. s.position = "";
  1541. s.left = s.top = "";
  1542. if(has("ie")){
  1543. s.overflow = this.__overflow;
  1544. this.__overflow = null;
  1545. }
  1546. this.textarea.value = this.value;
  1547. domConstruct.destroy(this.domNode);
  1548. this.domNode = this.textarea;
  1549. }else{
  1550. // Note that this destroys the iframe
  1551. this.domNode.innerHTML = this.value;
  1552. }
  1553. delete this.iframe;
  1554. domClass.remove(this.domNode, this.baseClass);
  1555. this.isClosed = true;
  1556. this.isLoaded = false;
  1557. delete this.editNode;
  1558. delete this.focusNode;
  1559. if(this.window && this.window._frameElement){
  1560. this.window._frameElement = null;
  1561. }
  1562. this.window = null;
  1563. this.document = null;
  1564. this.editingArea = null;
  1565. this.editorObject = null;
  1566. },
  1567. destroy: function(){
  1568. if(!this.isClosed){ this.close(false); }
  1569. if(this._updateTimer){
  1570. clearTimeout(this._updateTimer);
  1571. }
  1572. this.inherited(arguments);
  1573. if(RichText._globalSaveHandler){
  1574. delete RichText._globalSaveHandler[this.id];
  1575. }
  1576. },
  1577. _removeMozBogus: function(/* String */ html){
  1578. // summary:
  1579. // Post filter to remove unwanted HTML attributes generated by mozilla
  1580. // tags:
  1581. // private
  1582. return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi,''); // String
  1583. },
  1584. _removeWebkitBogus: function(/* String */ html){
  1585. // summary:
  1586. // Post filter to remove unwanted HTML attributes generated by webkit
  1587. // tags:
  1588. // private
  1589. html = html.replace(/\sclass="webkit-block-placeholder"/gi, '');
  1590. html = html.replace(/\sclass="apple-style-span"/gi, '');
  1591. // For some reason copy/paste sometime adds extra meta tags for charset on
  1592. // webkit (chrome) on mac.They need to be removed. See: #12007"
  1593. html = html.replace(/<meta charset=\"utf-8\" \/>/gi, '');
  1594. return html; // String
  1595. },
  1596. _normalizeFontStyle: function(/* String */ html){
  1597. // summary:
  1598. // Convert 'strong' and 'em' to 'b' and 'i'.
  1599. // description:
  1600. // Moz can not handle strong/em tags correctly, so to help
  1601. // mozilla and also to normalize output, convert them to 'b' and 'i'.
  1602. //
  1603. // Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
  1604. // tags:
  1605. // private
  1606. return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
  1607. .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
  1608. },
  1609. _preFixUrlAttributes: function(/* String */ html){
  1610. // summary:
  1611. // Pre-filter to do fixing to href attributes on <a> and <img> tags
  1612. // tags:
  1613. // private
  1614. return html.replace(/(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi,
  1615. '$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
  1616. .replace(/(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi,
  1617. '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String
  1618. },
  1619. /*****************************************************************************
  1620. The following functions implement HTML manipulation commands for various
  1621. browser/contentEditable implementations. The goal of them is to enforce
  1622. standard behaviors of them.
  1623. ******************************************************************************/
  1624. /*** queryCommandEnabled implementations ***/
  1625. _browserQueryCommandEnabled: function(command){
  1626. // summary:
  1627. // Implementation to call to the native queryCommandEnabled of the browser.
  1628. // command:
  1629. // The command to check.
  1630. // tags:
  1631. // protected
  1632. if(!command) { return false; }
  1633. var elem = has("ie") < 9 ? this.document.selection.createRange() : this.document;
  1634. try{
  1635. return elem.queryCommandEnabled(command);
  1636. }catch(e){
  1637. return false;
  1638. }
  1639. },
  1640. _createlinkEnabledImpl: function(/*===== argument =====*/){
  1641. // summary:
  1642. // This function implements the test for if the create link
  1643. // command should be enabled or not.
  1644. // argument:
  1645. // arguments to the exec command, if any.
  1646. // tags:
  1647. // protected
  1648. var enabled = true;
  1649. if(has("opera")){
  1650. var sel = this.window.getSelection();
  1651. if(sel.isCollapsed){
  1652. enabled = true;
  1653. }else{
  1654. enabled = this.document.queryCommandEnabled("createlink");
  1655. }
  1656. }else{
  1657. enabled = this._browserQueryCommandEnabled("createlink");
  1658. }
  1659. return enabled;
  1660. },
  1661. _unlinkEnabledImpl: function(/*===== argument =====*/){
  1662. // summary:
  1663. // This function implements the test for if the unlink
  1664. // command should be enabled or not.
  1665. // argument:
  1666. // arguments to the exec command, if any.
  1667. // tags:
  1668. // protected
  1669. var enabled = true;
  1670. if(has("mozilla") || has("webkit")){
  1671. enabled = this._sCall("hasAncestorElement", ["a"]);
  1672. }else{
  1673. enabled = this._browserQueryCommandEnabled("unlink");
  1674. }
  1675. return enabled;
  1676. },
  1677. _inserttableEnabledImpl: function(/*===== argument =====*/){
  1678. // summary:
  1679. // This function implements the test for if the inserttable
  1680. // command should be enabled or not.
  1681. // argument:
  1682. // arguments to the exec command, if any.
  1683. // tags:
  1684. // protected
  1685. var enabled = true;
  1686. if(has("mozilla") || has("webkit")){
  1687. enabled = true;
  1688. }else{
  1689. enabled = this._browserQueryCommandEnabled("inserttable");
  1690. }
  1691. return enabled;
  1692. },
  1693. _cutEnabledImpl: function(/*===== argument =====*/){
  1694. // summary:
  1695. // This function implements the test for if the cut
  1696. // command should be enabled or not.
  1697. // argument:
  1698. // arguments to the exec command, if any.
  1699. // tags:
  1700. // protected
  1701. var enabled = true;
  1702. if(has("webkit")){
  1703. // WebKit deems clipboard activity as a security threat and natively would return false
  1704. var sel = this.window.getSelection();
  1705. if(sel){ sel = sel.toString(); }
  1706. enabled = !!sel;
  1707. }else{
  1708. enabled = this._browserQueryCommandEnabled("cut");
  1709. }
  1710. return enabled;
  1711. },
  1712. _copyEnabledImpl: function(/*===== argument =====*/){
  1713. // summary:
  1714. // This function implements the test for if the copy
  1715. // command should be enabled or not.
  1716. // argument:
  1717. // arguments to the exec command, if any.
  1718. // tags:
  1719. // protected
  1720. var enabled = true;
  1721. if(has("webkit")){
  1722. // WebKit deems clipboard activity as a security threat and natively would return false
  1723. var sel = this.window.getSelection();
  1724. if(sel){ sel = sel.toString(); }
  1725. enabled = !!sel;
  1726. }else{
  1727. enabled = this._browserQueryCommandEnabled("copy");
  1728. }
  1729. return enabled;
  1730. },
  1731. _pasteEnabledImpl: function(/*===== argument =====*/){
  1732. // summary:c
  1733. // This function implements the test for if the paste
  1734. // command should be enabled or not.
  1735. // argument:
  1736. // arguments to the exec command, if any.
  1737. // tags:
  1738. // protected
  1739. var enabled = true;
  1740. if(has("webkit")){
  1741. return true;
  1742. }else{
  1743. enabled = this._browserQueryCommandEnabled("paste");
  1744. }
  1745. return enabled;
  1746. },
  1747. /*** execCommand implementations ***/
  1748. _inserthorizontalruleImpl: function(argument){
  1749. // summary:
  1750. // This function implements the insertion of HTML 'HR' tags.
  1751. // into a point on the page. IE doesn't to it right, so
  1752. // we have to use an alternate form
  1753. // argument:
  1754. // arguments to the exec command, if any.
  1755. // tags:
  1756. // protected
  1757. if(has("ie")){
  1758. return this._inserthtmlImpl("<hr>");
  1759. }
  1760. return this.document.execCommand("inserthorizontalrule", false, argument);
  1761. },
  1762. _unlinkImpl: function(argument){
  1763. // summary:
  1764. // This function implements the unlink of an 'a' tag.
  1765. // argument:
  1766. // arguments to the exec command, if any.
  1767. // tags:
  1768. // protected
  1769. if((this.queryCommandEnabled("unlink")) && (has("mozilla") || has("webkit"))){
  1770. var a = this._sCall("getAncestorElement", [ "a" ]);
  1771. this._sCall("selectElement", [ a ]);
  1772. return this.document.execCommand("unlink", false, null);
  1773. }
  1774. return this.document.execCommand("unlink", false, argument);
  1775. },
  1776. _hilitecolorImpl: function(argument){
  1777. // summary:
  1778. // This function implements the hilitecolor command
  1779. // argument:
  1780. // arguments to the exec command, if any.
  1781. // tags:
  1782. // protected
  1783. var returnValue;
  1784. var isApplied = this._handleTextColorOrProperties("hilitecolor", argument);
  1785. if(!isApplied){
  1786. if(has("mozilla")){
  1787. // mozilla doesn't support hilitecolor properly when useCSS is
  1788. // set to false (bugzilla #279330)
  1789. this.document.execCommand("styleWithCSS", false, true);
  1790. console.log("Executing color command.");
  1791. returnValue = this.document.execCommand("hilitecolor", false, argument);
  1792. this.document.execCommand("styleWithCSS", false, false);
  1793. }else{
  1794. returnValue = this.document.execCommand("hilitecolor", false, argument);
  1795. }
  1796. }
  1797. return returnValue;
  1798. },
  1799. _backcolorImpl: function(argument){
  1800. // summary:
  1801. // This function implements the backcolor command
  1802. // argument:
  1803. // arguments to the exec command, if any.
  1804. // tags:
  1805. // protected
  1806. if(has("ie")){
  1807. // Tested under IE 6 XP2, no problem here, comment out
  1808. // IE weirdly collapses ranges when we exec these commands, so prevent it
  1809. // var tr = this.document.selection.createRange();
  1810. argument = argument ? argument : null;
  1811. }
  1812. var isApplied = this._handleTextColorOrProperties("backcolor", argument);
  1813. if(!isApplied){
  1814. isApplied = this.document.execCommand("backcolor", false, argument);
  1815. }
  1816. return isApplied;
  1817. },
  1818. _forecolorImpl: function(argument){
  1819. // summary:
  1820. // This function implements the forecolor command
  1821. // argument:
  1822. // arguments to the exec command, if any.
  1823. // tags:
  1824. // protected
  1825. if(has("ie")){
  1826. // Tested under IE 6 XP2, no problem here, comment out
  1827. // IE weirdly collapses ranges when we exec these commands, so prevent it
  1828. // var tr = this.document.selection.createRange();
  1829. argument = argument? argument : null;
  1830. }
  1831. var isApplied = false;
  1832. isApplied = this._handleTextColorOrProperties("forecolor", argument);
  1833. if(!isApplied){
  1834. isApplied = this.document.execCommand("forecolor", false, argument);
  1835. }
  1836. return isApplied;
  1837. },
  1838. _inserthtmlImpl: function(argument){
  1839. // summary:
  1840. // This function implements the insertion of HTML content into
  1841. // a point on the page.
  1842. // argument:
  1843. // The content to insert, if any.
  1844. // tags:
  1845. // protected
  1846. argument = this._preFilterContent(argument);
  1847. var rv = true;
  1848. if(has("ie") < 9){
  1849. var insertRange = this.document.selection.createRange();
  1850. if(this.document.selection.type.toUpperCase() === 'CONTROL'){
  1851. var n = insertRange.item(0);
  1852. while(insertRange.length){
  1853. insertRange.remove(insertRange.item(0));
  1854. }
  1855. n.outerHTML = argument;
  1856. }else{
  1857. insertRange.pasteHTML(argument);
  1858. }
  1859. insertRange.select();
  1860. }else if(has("trident") < 8){
  1861. var insertRange;
  1862. var selection = rangeapi.getSelection(this.window);
  1863. if(selection && selection.rangeCount && selection.getRangeAt){
  1864. insertRange = selection.getRangeAt(0);
  1865. insertRange.deleteContents();
  1866. var div = domConstruct.create('div');
  1867. div.innerHTML = argument;
  1868. var node, lastNode;
  1869. var n = this.document.createDocumentFragment();
  1870. while((node = div.firstChild)){
  1871. lastNode = n.appendChild(node);
  1872. }
  1873. insertRange.insertNode(n);
  1874. if(lastNode) {
  1875. insertRange = insertRange.cloneRange();
  1876. insertRange.setStartAfter(lastNode);
  1877. insertRange.collapse(false);
  1878. selection.removeAllRanges();
  1879. selection.addRange(insertRange);
  1880. }
  1881. }
  1882. }else if(has("mozilla") && !argument.length){
  1883. //mozilla can not inserthtml an empty html to delete current selection
  1884. //so we delete the selection instead in this case
  1885. this._sCall("remove"); // FIXME
  1886. }else{
  1887. rv = this.document.execCommand("inserthtml", false, argument);
  1888. }
  1889. return rv;
  1890. },
  1891. _boldImpl: function(argument){
  1892. // summary:
  1893. // This function implements an over-ride of the bold command.
  1894. // argument:
  1895. // Not used, operates by selection.
  1896. // tags:
  1897. // protected
  1898. var applied = false;
  1899. if(has("ie") || has("trident")){
  1900. this._adaptIESelection();
  1901. applied = this._adaptIEFormatAreaAndExec("bold");
  1902. }
  1903. if(!applied){
  1904. applied = this.document.execCommand("bold", false, argument);
  1905. }
  1906. return applied;
  1907. },
  1908. _italicImpl: function(argument){
  1909. // summary:
  1910. // This function implements an over-ride of the italic command.
  1911. // argument:
  1912. // Not used, operates by selection.
  1913. // tags:
  1914. // protected
  1915. var applied = false;
  1916. if(has("ie") || has("trident")){
  1917. this._adaptIESelection();
  1918. applied = this._adaptIEFormatAreaAndExec("italic");
  1919. }
  1920. if(!applied){
  1921. applied = this.document.execCommand("italic", false, argument);
  1922. }
  1923. return applied;
  1924. },
  1925. _underlineImpl: function(argument){
  1926. // summary:
  1927. // This function implements an over-ride of the underline command.
  1928. // argument:
  1929. // Not used, operates by selection.
  1930. // tags:
  1931. // protected
  1932. var applied = false;
  1933. if(has("ie") || has("trident")){
  1934. this._adaptIESelection();
  1935. applied = this._adaptIEFormatAreaAndExec("underline");
  1936. }
  1937. if(!applied){
  1938. applied = this.document.execCommand("underline", false, argument);
  1939. }
  1940. return applied;
  1941. },
  1942. _strikethroughImpl: function(argument){
  1943. // summary:
  1944. // This function implements an over-ride of the strikethrough command.
  1945. // argument:
  1946. // Not used, operates by selection.
  1947. // tags:
  1948. // protected
  1949. var applied = false;
  1950. if(has("ie") || has("trident")){
  1951. this._adaptIESelection();
  1952. applied = this._adaptIEFormatAreaAndExec("strikethrough");
  1953. }
  1954. if(!applied){
  1955. applied = this.document.execCommand("strikethrough", false, argument);
  1956. }
  1957. return applied;
  1958. },
  1959. _superscriptImpl: function(argument){
  1960. // summary:
  1961. // This function implements an over-ride of the superscript command.
  1962. // argument:
  1963. // Not used, operates by selection.
  1964. // tags:
  1965. // protected
  1966. var applied = false;
  1967. if(has("ie") || has("trident")){
  1968. this._adaptIESelection();
  1969. applied = this._adaptIEFormatAreaAndExec("superscript");
  1970. }
  1971. if(!applied){
  1972. applied = this.document.execCommand("superscript", false, argument);
  1973. }
  1974. return applied;
  1975. },
  1976. _subscriptImpl: function(argument){
  1977. // summary:
  1978. // This function implements an over-ride of the superscript command.
  1979. // argument:
  1980. // Not used, operates by selection.
  1981. // tags:
  1982. // protected
  1983. var applied = false;
  1984. if(has("ie") || has("trident")){
  1985. this._adaptIESelection();
  1986. applied = this._adaptIEFormatAreaAndExec("subscript");
  1987. }
  1988. if(!applied){
  1989. applied = this.document.execCommand("subscript", false, argument);
  1990. }
  1991. return applied;
  1992. },
  1993. _fontnameImpl: function(argument){
  1994. // summary:
  1995. // This function implements the fontname command
  1996. // argument:
  1997. // arguments to the exec command, if any.
  1998. // tags:
  1999. // protected
  2000. var isApplied;
  2001. if(has("ie") || has("trident")){
  2002. isApplied = this._handleTextColorOrProperties("fontname", argument);
  2003. }
  2004. if(!isApplied){
  2005. isApplied = this.document.execCommand("fontname", false, argument);
  2006. }
  2007. return isApplied;
  2008. },
  2009. _fontsizeImpl: function(argument){
  2010. // summary:
  2011. // This function implements the fontsize command
  2012. // argument:
  2013. // arguments to the exec command, if any.
  2014. // tags:
  2015. // protected
  2016. var isApplied;
  2017. if(has("ie") || has("trident")){
  2018. isApplied = this._handleTextColorOrProperties("fontsize", argument);
  2019. }
  2020. if(!isApplied){
  2021. isApplied = this.document.execCommand("fontsize", false, argument);
  2022. }
  2023. return isApplied;
  2024. },
  2025. _insertorderedlistImpl: function(argument){
  2026. // summary:
  2027. // This function implements the insertorderedlist command
  2028. // argument:
  2029. // arguments to the exec command, if any.
  2030. // tags:
  2031. // protected
  2032. var applied = false;
  2033. if(has("ie") || has("trident")){
  2034. applied = this._adaptIEList("insertorderedlist", argument);
  2035. }
  2036. if(!applied){
  2037. applied = this.document.execCommand("insertorderedlist", false, argument);
  2038. }
  2039. return applied;
  2040. },
  2041. _insertunorderedlistImpl: function(argument){
  2042. // summary:
  2043. // This function implements the insertunorderedlist command
  2044. // argument:
  2045. // arguments to the exec command, if any.
  2046. // tags:
  2047. // protected
  2048. var applied = false;
  2049. if(has("ie") || has("trident")){
  2050. applied = this._adaptIEList("insertunorderedlist", argument);
  2051. }
  2052. if(!applied){
  2053. applied = this.document.execCommand("insertunorderedlist", false, argument);
  2054. }
  2055. return applied;
  2056. },
  2057. getHeaderHeight: function(){
  2058. // summary:
  2059. // A function for obtaining the height of the header node
  2060. return this._getNodeChildrenHeight(this.header); // Number
  2061. },
  2062. getFooterHeight: function(){
  2063. // summary:
  2064. // A function for obtaining the height of the footer node
  2065. return this._getNodeChildrenHeight(this.footer); // Number
  2066. },
  2067. _getNodeChildrenHeight: function(node){
  2068. // summary:
  2069. // An internal function for computing the cumulative height of all child nodes of 'node'
  2070. // node:
  2071. // The node to process the children of;
  2072. var h = 0;
  2073. if(node && node.childNodes){
  2074. // IE didn't compute it right when position was obtained on the node directly is some cases,
  2075. // so we have to walk over all the children manually.
  2076. var i;
  2077. for(i = 0; i < node.childNodes.length; i++){
  2078. var size = domGeometry.position(node.childNodes[i]);
  2079. h += size.h;
  2080. }
  2081. }
  2082. return h; // Number
  2083. },
  2084. _isNodeEmpty: function(node, startOffset){
  2085. // summary:
  2086. // Function to test if a node is devoid of real content.
  2087. // node:
  2088. // The node to check.
  2089. // tags:
  2090. // private.
  2091. if(node.nodeType === 1/*element*/){
  2092. if(node.childNodes.length > 0){
  2093. return this._isNodeEmpty(node.childNodes[0], startOffset);
  2094. }
  2095. return true;
  2096. }else if(node.nodeType === 3/*text*/){
  2097. return (node.nodeValue.substring(startOffset) === "");
  2098. }
  2099. return false;
  2100. },
  2101. _removeStartingRangeFromRange: function(node, range){
  2102. // summary:
  2103. // Function to adjust selection range by removing the current
  2104. // start node.
  2105. // node:
  2106. // The node to remove from the starting range.
  2107. // range:
  2108. // The range to adapt.
  2109. // tags:
  2110. // private
  2111. if(node.nextSibling){
  2112. range.setStart(node.nextSibling,0);
  2113. }else{
  2114. var parent = node.parentNode;
  2115. while(parent && parent.nextSibling == null){
  2116. //move up the tree until we find a parent that has another node, that node will be the next node
  2117. parent = parent.parentNode;
  2118. }
  2119. if(parent){
  2120. range.setStart(parent.nextSibling,0);
  2121. }
  2122. }
  2123. return range;
  2124. },
  2125. _adaptIESelection: function(){
  2126. // summary:
  2127. // Function to adapt the IE range by removing leading 'newlines'
  2128. // Needed to fix issue with bold/italics/underline not working if
  2129. // range included leading 'newlines'.
  2130. // In IE, if a user starts a selection at the very end of a line,
  2131. // then the native browser commands will fail to execute correctly.
  2132. // To work around the issue, we can remove all empty nodes from
  2133. // the start of the range selection.
  2134. var selection = rangeapi.getSelection(this.window);
  2135. if(selection && selection.rangeCount && !selection.isCollapsed){
  2136. var range = selection.getRangeAt(0);
  2137. var firstNode = range.startContainer;
  2138. var startOffset = range.startOffset;
  2139. while(firstNode.nodeType === 3/*text*/ && startOffset >= firstNode.length && firstNode.nextSibling){
  2140. //traverse the text nodes until we get to the one that is actually highlighted
  2141. startOffset = startOffset - firstNode.length;
  2142. firstNode = firstNode.nextSibling;
  2143. }
  2144. //Remove the starting ranges until the range does not start with an empty node.
  2145. var lastNode=null;
  2146. while(this._isNodeEmpty(firstNode, startOffset) && firstNode !== lastNode){
  2147. lastNode =firstNode; //this will break the loop in case we can't find the next sibling
  2148. range = this._removeStartingRangeFromRange(firstNode, range); //move the start container to the next node in the range
  2149. firstNode = range.startContainer;
  2150. startOffset = 0; //start at the beginning of the new starting range
  2151. }
  2152. selection.removeAllRanges();// this will work as long as users cannot select multiple ranges. I have not been able to do that in the editor.
  2153. selection.addRange(range);
  2154. }
  2155. },
  2156. _adaptIEFormatAreaAndExec: function(command){
  2157. // summary:
  2158. // Function to handle IE's quirkiness regarding how it handles
  2159. // format commands on a word. This involves a lit of node splitting
  2160. // and format cloning.
  2161. // command:
  2162. // The format command, needed to check if the desired
  2163. // command is true or not.
  2164. var selection = rangeapi.getSelection(this.window);
  2165. var doc = this.document;
  2166. var rs, ret, range, txt, startNode, endNode, breaker, sNode;
  2167. if(command && selection && selection.isCollapsed){
  2168. var isApplied = this.queryCommandValue(command);
  2169. if(isApplied){
  2170. // We have to split backwards until we hit the format
  2171. var nNames = this._tagNamesForCommand(command);
  2172. range = selection.getRangeAt(0);
  2173. var fs = range.startContainer;
  2174. if(fs.nodeType === 3){
  2175. var offset = range.endOffset;
  2176. if(fs.length < offset){
  2177. //We are not looking from the right node, try to locate the correct one
  2178. ret = this._adjustNodeAndOffset(rs, offset);
  2179. fs = ret.node;
  2180. offset = ret.offset;
  2181. }
  2182. }
  2183. var topNode;
  2184. while(fs && fs !== this.editNode){
  2185. // We have to walk back and see if this is still a format or not.
  2186. // Hm, how do I do this?
  2187. var tName = fs.tagName? fs.tagName.toLowerCase() : "";
  2188. if(array.indexOf(nNames, tName) > -1){
  2189. topNode = fs;
  2190. break;
  2191. }
  2192. fs = fs.parentNode;
  2193. }
  2194. // Okay, we have a stopping place, time to split things apart.
  2195. if(topNode){
  2196. // Okay, we know how far we have to split backwards, so we have to split now.
  2197. rs = range.startContainer;
  2198. var newblock = doc.createElement(topNode.tagName);
  2199. domConstruct.place(newblock, topNode, "after");
  2200. if(rs && rs.nodeType === 3){
  2201. // Text node, we have to split it.
  2202. var nodeToMove, tNode;
  2203. var endOffset = range.endOffset;
  2204. if(rs.length < endOffset){
  2205. //We are not splitting the right node, try to locate the correct one
  2206. ret = this._adjustNodeAndOffset(rs, endOffset);
  2207. rs = ret.node;
  2208. endOffset = ret.offset;
  2209. }
  2210. txt = rs.nodeValue;
  2211. startNode = doc.createTextNode(txt.substring(0, endOffset));
  2212. var endText = txt.substring(endOffset, txt.length);
  2213. if(endText){
  2214. endNode = doc.createTextNode(endText);
  2215. }
  2216. // Place the split, then remove original nodes.
  2217. domConstruct.place(startNode, rs, "before");
  2218. if(endNode){
  2219. breaker = doc.createElement("span");
  2220. breaker.className = "ieFormatBreakerSpan";
  2221. domConstruct.place(breaker, rs, "after");
  2222. domConstruct.place(endNode, breaker, "after");
  2223. endNode = breaker;
  2224. }
  2225. domConstruct.destroy(rs);
  2226. // Okay, we split the text. Now we need to see if we're
  2227. // parented to the block element we're splitting and if
  2228. // not, we have to split all the way up. Ugh.
  2229. var parentC = startNode.parentNode;
  2230. var tagList = [];
  2231. var tagData;
  2232. while(parentC !== topNode){
  2233. var tg = parentC.tagName;
  2234. tagData = {tagName: tg};
  2235. tagList.push(tagData);
  2236. var newTg = doc.createElement(tg);
  2237. // Clone over any 'style' data.
  2238. if(parentC.style){
  2239. if(newTg.style){
  2240. if(parentC.style.cssText){
  2241. newTg.style.cssText = parentC.style.cssText;
  2242. tagData.cssText = parentC.style.cssText;
  2243. }
  2244. }
  2245. }
  2246. // If font also need to clone over any font data.
  2247. if(parentC.tagName === "FONT"){
  2248. if(parentC.color){
  2249. newTg.color = parentC.color;
  2250. tagData.color = parentC.color;
  2251. }
  2252. if(parentC.face){
  2253. newTg.face = parentC.face;
  2254. tagData.face = parentC.face;
  2255. }
  2256. if(parentC.size){ // this check was necessary on IE
  2257. newTg.size = parentC.size;
  2258. tagData.size = parentC.size;
  2259. }
  2260. }
  2261. if(parentC.className){
  2262. newTg.className = parentC.className;
  2263. tagData.className = parentC.className;
  2264. }
  2265. // Now move end node and every sibling
  2266. // after it over into the new tag.
  2267. if(endNode){
  2268. nodeToMove = endNode;
  2269. while(nodeToMove){
  2270. tNode = nodeToMove.nextSibling;
  2271. newTg.appendChild(nodeToMove);
  2272. nodeToMove = tNode;
  2273. }
  2274. }
  2275. if(newTg.tagName == parentC.tagName){
  2276. breaker = doc.createElement("span");
  2277. breaker.className = "ieFormatBreakerSpan";
  2278. domConstruct.place(breaker, parentC, "after");
  2279. domConstruct.place(newTg, breaker, "after");
  2280. }else{
  2281. domConstruct.place(newTg, parentC, "after");
  2282. }
  2283. startNode = parentC;
  2284. endNode = newTg;
  2285. parentC = parentC.parentNode;
  2286. }
  2287. // Lastly, move the split out all the split tags
  2288. // to the new block as they should now be split properly.
  2289. if(endNode){
  2290. nodeToMove = endNode;
  2291. if(nodeToMove.nodeType === 1 || (nodeToMove.nodeType === 3 && nodeToMove.nodeValue)){
  2292. // Non-blank text and non-text nodes need to clear out that blank space
  2293. // before moving the contents.
  2294. newblock.innerHTML = "";
  2295. }
  2296. while(nodeToMove){
  2297. tNode = nodeToMove.nextSibling;
  2298. newblock.appendChild(nodeToMove);
  2299. nodeToMove = tNode;
  2300. }
  2301. }
  2302. // We had intermediate tags, we have to now recreate them inbetween the split
  2303. // and restore what styles, classnames, etc, we can.
  2304. if(tagList.length){
  2305. tagData = tagList.pop();
  2306. var newContTag = doc.createElement(tagData.tagName);
  2307. if(tagData.cssText && newContTag.style){
  2308. newContTag.style.cssText = tagData.cssText;
  2309. }
  2310. if(tagData.className){
  2311. newContTag.className = tagData.className;
  2312. }
  2313. if(tagData.tagName === "FONT"){
  2314. if(tagData.color){
  2315. newContTag.color = tagData.color;
  2316. }
  2317. if(tagData.face){
  2318. newContTag.face = tagData.face;
  2319. }
  2320. if(tagData.size){
  2321. newContTag.size = tagData.size;
  2322. }
  2323. }
  2324. domConstruct.place(newContTag, newblock, "before");
  2325. while(tagList.length){
  2326. tagData = tagList.pop();
  2327. var newTgNode = doc.createElement(tagData.tagName);
  2328. if(tagData.cssText && newTgNode.style){
  2329. newTgNode.style.cssText = tagData.cssText;
  2330. }
  2331. if(tagData.className){
  2332. newTgNode.className = tagData.className;
  2333. }
  2334. if(tagData.tagName === "FONT"){
  2335. if(tagData.color){
  2336. newTgNode.color = tagData.color;
  2337. }
  2338. if(tagData.face){
  2339. newTgNode.face = tagData.face;
  2340. }
  2341. if(tagData.size){
  2342. newTgNode.size = tagData.size;
  2343. }
  2344. }
  2345. newContTag.appendChild(newTgNode);
  2346. newContTag = newTgNode;
  2347. }
  2348. // Okay, everything is theoretically split apart and removed from the content
  2349. // so insert the dummy text to select, select it, then
  2350. // clear to position cursor.
  2351. sNode = doc.createTextNode(".");
  2352. breaker.appendChild(sNode);
  2353. newContTag.appendChild(sNode);
  2354. win.withGlobal(this.window, lang.hitch(this, function(){
  2355. var newrange = rangeapi.create();
  2356. newrange.setStart(sNode, 0);
  2357. newrange.setEnd(sNode, sNode.length);
  2358. selection.removeAllRanges();
  2359. selection.addRange(newrange);
  2360. selectionapi.collapse(false);
  2361. sNode.parentNode.innerHTML = "";
  2362. }));
  2363. }else{
  2364. // No extra tags, so we have to insert a breaker point and rely
  2365. // on filters to remove it later.
  2366. breaker = doc.createElement("span");
  2367. breaker.className="ieFormatBreakerSpan";
  2368. sNode = doc.createTextNode(".");
  2369. breaker.appendChild(sNode);
  2370. domConstruct.place(breaker, newblock, "before");
  2371. win.withGlobal(this.window, lang.hitch(this, function(){
  2372. var newrange = rangeapi.create();
  2373. newrange.setStart(sNode, 0);
  2374. newrange.setEnd(sNode, sNode.length);
  2375. selection.removeAllRanges();
  2376. selection.addRange(newrange);
  2377. selectionapi.collapse(false);
  2378. sNode.parentNode.innerHTML = "";
  2379. }));
  2380. }
  2381. if(!newblock.firstChild){
  2382. // Empty, we don't need it. Split was at end or similar
  2383. // So, remove it.
  2384. domConstruct.destroy(newblock);
  2385. }
  2386. return true;
  2387. }
  2388. }
  2389. return false;
  2390. }else{
  2391. range = selection.getRangeAt(0);
  2392. rs = range.startContainer;
  2393. if(rs && rs.nodeType === 3){
  2394. // Text node, we have to split it.
  2395. win.withGlobal(this.window, lang.hitch(this, function(){
  2396. var offset = range.startOffset;
  2397. if(rs.length < offset){
  2398. //We are not splitting the right node, try to locate the correct one
  2399. ret = this._adjustNodeAndOffset(rs, offset);
  2400. rs = ret.node;
  2401. offset = ret.offset;
  2402. }
  2403. txt = rs.nodeValue;
  2404. startNode = doc.createTextNode(txt.substring(0, offset));
  2405. var endText = txt.substring(offset);
  2406. if(endText !== ""){
  2407. endNode = doc.createTextNode(txt.substring(offset));
  2408. }
  2409. // Create a space, we'll select and bold it, so
  2410. // the whole word doesn't get bolded
  2411. breaker = doc.createElement("span");
  2412. sNode = doc.createTextNode(".");
  2413. breaker.appendChild(sNode);
  2414. if(startNode.length){
  2415. domConstruct.place(startNode, rs, "after");
  2416. }else{
  2417. startNode = rs;
  2418. }
  2419. domConstruct.place(breaker, startNode, "after");
  2420. if(endNode){
  2421. domConstruct.place(endNode, breaker, "after");
  2422. }
  2423. domConstruct.destroy(rs);
  2424. var newrange = rangeapi.create();
  2425. newrange.setStart(sNode, 0);
  2426. newrange.setEnd(sNode, sNode.length);
  2427. selection.removeAllRanges();
  2428. selection.addRange(newrange);
  2429. doc.execCommand(command);
  2430. domConstruct.place(breaker.firstChild, breaker, "before");
  2431. domConstruct.destroy(breaker);
  2432. newrange.setStart(sNode, 0);
  2433. newrange.setEnd(sNode, sNode.length);
  2434. selection.removeAllRanges();
  2435. selection.addRange(newrange);
  2436. selectionapi.collapse(false);
  2437. sNode.parentNode.innerHTML = "";
  2438. }));
  2439. return true;
  2440. }
  2441. }
  2442. }else{
  2443. return false;
  2444. }
  2445. },
  2446. _adaptIEList: function(command /*===== , argument =====*/){
  2447. // summary:
  2448. // This function handles normalizing the IE list behavior as
  2449. // much as possible.
  2450. // command:
  2451. // The list command to execute.
  2452. // argument:
  2453. // Any additional argument.
  2454. // tags:
  2455. // private
  2456. var selection = rangeapi.getSelection(this.window);
  2457. if(selection.isCollapsed){
  2458. // In the case of no selection, lets commonize the behavior and
  2459. // make sure that it indents if needed.
  2460. if(selection.rangeCount && !this.queryCommandValue(command)){
  2461. var range = selection.getRangeAt(0);
  2462. var sc = range.startContainer;
  2463. if(sc && sc.nodeType == 3){
  2464. // text node. Lets see if there is a node before it that isn't
  2465. // some sort of breaker.
  2466. if(!range.startOffset){
  2467. // We're at the beginning of a text area. It may have been br split
  2468. // Who knows? In any event, we must create the list manually
  2469. // or IE may shove too much into the list element. It seems to
  2470. // grab content before the text node too if it's br split.
  2471. // Why can't IE work like everyone else?
  2472. win.withGlobal(this.window, lang.hitch(this, function(){
  2473. // Create a space, we'll select and bold it, so
  2474. // the whole word doesn't get bolded
  2475. var lType = "ul";
  2476. if(command === "insertorderedlist"){
  2477. lType = "ol";
  2478. }
  2479. var list = domConstruct.create(lType);
  2480. var li = domConstruct.create("li", null, list);
  2481. domConstruct.place(list, sc, "before");
  2482. // Move in the text node as part of the li.
  2483. li.appendChild(sc);
  2484. // We need a br after it or the enter key handler
  2485. // sometimes throws errors.
  2486. domConstruct.create("br", null, list, "after");
  2487. // Okay, now lets move our cursor to the beginning.
  2488. var newrange = rangeapi.create();
  2489. newrange.setStart(sc, 0);
  2490. newrange.setEnd(sc, sc.length);
  2491. selection.removeAllRanges();
  2492. selection.addRange(newrange);
  2493. selectionapi.collapse(true);
  2494. }));
  2495. return true;
  2496. }
  2497. }
  2498. }
  2499. }
  2500. return false;
  2501. },
  2502. _handleTextColorOrProperties: function(command, argument){
  2503. // summary:
  2504. // This function handles appplying text color as best it is
  2505. // able to do so when the selection is collapsed, making the
  2506. // behavior cross-browser consistent. It also handles the name
  2507. // and size for IE.
  2508. // command:
  2509. // The command.
  2510. // argument:
  2511. // Any additional arguments.
  2512. // tags:
  2513. // private
  2514. var selection = rangeapi.getSelection(this.window);
  2515. var doc = this.document;
  2516. var rs, ret, range, txt, startNode, endNode, breaker, sNode;
  2517. argument = argument || null;
  2518. if(command && selection && selection.isCollapsed){
  2519. if(selection.rangeCount){
  2520. range = selection.getRangeAt(0);
  2521. rs = range.startContainer;
  2522. if(rs && rs.nodeType === 3){
  2523. // Text node, we have to split it.
  2524. win.withGlobal(this.window, lang.hitch(this, function(){
  2525. var offset = range.startOffset;
  2526. if(rs.length < offset){
  2527. //We are not splitting the right node, try to locate the correct one
  2528. ret = this._adjustNodeAndOffset(rs, offset);
  2529. rs = ret.node;
  2530. offset = ret.offset;
  2531. }
  2532. txt = rs.nodeValue;
  2533. startNode = doc.createTextNode(txt.substring(0, offset));
  2534. var endText = txt.substring(offset);
  2535. if(endText !== ""){
  2536. endNode = doc.createTextNode(txt.substring(offset));
  2537. }
  2538. // Create a space, we'll select and bold it, so
  2539. // the whole word doesn't get bolded
  2540. breaker = domConstruct.create("span");
  2541. sNode = doc.createTextNode(".");
  2542. breaker.appendChild(sNode);
  2543. // Create a junk node to avoid it trying to stlye the breaker.
  2544. // This will get destroyed later.
  2545. var extraSpan = domConstruct.create("span");
  2546. breaker.appendChild(extraSpan);
  2547. if(startNode.length){
  2548. domConstruct.place(startNode, rs, "after");
  2549. }else{
  2550. startNode = rs;
  2551. }
  2552. domConstruct.place(breaker, startNode, "after");
  2553. if(endNode){
  2554. domConstruct.place(endNode, breaker, "after");
  2555. }
  2556. domConstruct.destroy(rs);
  2557. var newrange = rangeapi.create();
  2558. newrange.setStart(sNode, 0);
  2559. newrange.setEnd(sNode, sNode.length);
  2560. selection.removeAllRanges();
  2561. selection.addRange(newrange);
  2562. if(has("webkit")){
  2563. // WebKit is frustrating with positioning the cursor.
  2564. // It stinks to have a selected space, but there really
  2565. // isn't much choice here.
  2566. var style = "color";
  2567. if(command === "hilitecolor" || command === "backcolor"){
  2568. style = "backgroundColor";
  2569. }
  2570. domStyle.set(breaker, style, argument);
  2571. selectionapi.remove();
  2572. domConstruct.destroy(extraSpan);
  2573. breaker.innerHTML = "&#160;"; // &nbsp;
  2574. selectionapi.selectElement(breaker);
  2575. this.focus();
  2576. }else{
  2577. this.execCommand(command, argument);
  2578. domConstruct.place(breaker.firstChild, breaker, "before");
  2579. domConstruct.destroy(breaker);
  2580. newrange.setStart(sNode, 0);
  2581. newrange.setEnd(sNode, sNode.length);
  2582. selection.removeAllRanges();
  2583. selection.addRange(newrange);
  2584. selectionapi.collapse(false);
  2585. sNode.parentNode.removeChild(sNode);
  2586. }
  2587. }));
  2588. return true;
  2589. }
  2590. }
  2591. }
  2592. return false;
  2593. },
  2594. _adjustNodeAndOffset: function(/*DomNode*/node, /*Int*/offset){
  2595. // summary:
  2596. // In the case there are multiple text nodes in a row the offset may not be within the node.
  2597. // If the offset is larger than the node length, it will attempt to find
  2598. // the next text sibling until it locates the text node in which the offset refers to
  2599. // node:
  2600. // The node to check.
  2601. // offset:
  2602. // The position to find within the text node
  2603. // tags:
  2604. // private.
  2605. while(node.length < offset && node.nextSibling && node.nextSibling.nodeType === 3){
  2606. //Adjust the offset and node in the case of multiple text nodes in a row
  2607. offset = offset - node.length;
  2608. node = node.nextSibling;
  2609. }
  2610. return {"node": node, "offset": offset};
  2611. },
  2612. _tagNamesForCommand: function(command){
  2613. // summary:
  2614. // Function to return the tab names that are associated
  2615. // with a particular style.
  2616. // command: String
  2617. // The command to return tags for.
  2618. // tags:
  2619. // private
  2620. if(command === "bold"){
  2621. return ["b", "strong"];
  2622. }else if(command === "italic"){
  2623. return ["i","em"];
  2624. }else if(command === "strikethrough"){
  2625. return ["s", "strike"];
  2626. }else if(command === "superscript"){
  2627. return ["sup"];
  2628. }else if(command === "subscript"){
  2629. return ["sub"];
  2630. }else if(command === "underline"){
  2631. return ["u"];
  2632. }
  2633. return [];
  2634. },
  2635. _stripBreakerNodes: function(node){
  2636. // summary:
  2637. // Function for stripping out the breaker spans inserted by the formatting command.
  2638. // Registered as a filter for IE, handles the breaker spans needed to fix up
  2639. // How bold/italic/etc, work when selection is collapsed (single cursor).
  2640. win.withGlobal(this.window, lang.hitch(this, function(){
  2641. var breakers = query(".ieFormatBreakerSpan", node);
  2642. var i;
  2643. for(i = 0; i < breakers.length; i++){
  2644. var b = breakers[i];
  2645. while(b.firstChild){
  2646. domConstruct.place(b.firstChild, b, "before");
  2647. }
  2648. domConstruct.destroy(b);
  2649. }
  2650. }));
  2651. return node;
  2652. }
  2653. });
  2654. return RichText;
  2655. });