BuxRichText.js 93 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937
  1. define("dijit/_editor/BuxRichText", [
  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/BuxRichText
  40. // summary:
  41. // dijit._editor.BuxRichText 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.BuxRichText.value" (typically "dijit._editor.BuxRichText.value). For example,
  45. // something like this will work:
  46. //
  47. // <textarea id="dijit._editor.BuxRichText.value" style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>
  48. //
  49. var BuxRichText = declare("dijit._editor.BuxRichText", [_Widget, _CssStateMixin], {
  50. // summary:
  51. // dijit._editor.BuxRichText is the core of dijit.Editor, which provides basic
  52. // WYSIWYG editing features.
  53. //
  54. // description:
  55. // dijit._editor.BuxRichText 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("BuxRichText should not be used with the TEXTAREA tag. See dijit._editor.BuxRichText 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.BuxRichText::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(BuxRichText._editorCommandsLocalized){
  230. // Use the already generate cache of mappings.
  231. this._local2NativeFormatNames = BuxRichText._local2NativeFormatNames;
  232. this._native2LocalFormatNames = BuxRichText._native2LocalFormatNames;
  233. return;
  234. }
  235. BuxRichText._editorCommandsLocalized = true;
  236. BuxRichText._local2NativeFormatNames = {};
  237. BuxRichText._native2LocalFormatNames = {};
  238. this._local2NativeFormatNames = BuxRichText._local2NativeFormatNames;
  239. this._native2LocalFormatNames = BuxRichText._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.BuxRichText::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.BuxRichText.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(!BuxRichText._globalSaveHandler){
  391. BuxRichText._globalSaveHandler = {};
  392. unload.addOnUnload(function(){
  393. var id;
  394. for(id in BuxRichText._globalSaveHandler){
  395. var f = BuxRichText._globalSaveHandler[id];
  396. if(lang.isFunction(f)){
  397. f();
  398. }
  399. }
  400. });
  401. }
  402. BuxRichText._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.BuxRichText.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.BuxRichText.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. var copyValue = this.value;
  751. this.setValue(html);
  752. // Defect #247392 - We added try/catch block in order to handle exception "This deferred has already been resolved".
  753. // This happens in Cognos Workspace (BUX), while changing tab's order on the dashdoard, which contain "Text Editor" widget(s).
  754. try {
  755. if(this.onLoadDeferred){
  756. this.onLoadDeferred.callback(true);
  757. }
  758. } catch (err) {
  759. // This error message is hard coded in DOJO in english language version only so we safely can use it to decrease the impact of code change.
  760. if (err.message === "This deferred has already been resolved") {
  761. this.setValue(copyValue);
  762. console.log("Caught exception: " + err.message);
  763. } else {
  764. throw err;
  765. }
  766. }
  767. this.onDisplayChanged();
  768. if(this.focusOnLoad){
  769. // after the document loads, then set focus after updateInterval expires so that
  770. // onNormalizedDisplayChanged has run to avoid input caret issues
  771. ready(lang.hitch(this, function(){ setTimeout(lang.hitch(this, "focus"), this.updateInterval); }));
  772. }
  773. // Save off the initial content now
  774. this.value = this.getValue(true);
  775. });
  776. if(this.setValueDeferred){
  777. this.setValueDeferred.addCallback(setContent);
  778. }else{
  779. setContent();
  780. }
  781. },
  782. onKeyDown: function(/* Event */ e){
  783. // summary:
  784. // Handler for onkeydown event
  785. // tags:
  786. // protected
  787. // we need this event at the moment to get the events from control keys
  788. // such as the backspace. It might be possible to add this to Dojo, so that
  789. // keyPress events can be emulated by the keyDown and keyUp detection.
  790. if(e.keyCode === keys.TAB && this.isTabIndent){
  791. event.stop(e); //prevent tab from moving focus out of editor
  792. // FIXME: this is a poor-man's indent/outdent. It would be
  793. // better if it added 4 "&nbsp;" chars in an undoable way.
  794. // Unfortunately pasteHTML does not prove to be undoable
  795. if(this.queryCommandEnabled((e.shiftKey ? "outdent" : "indent"))){
  796. this.execCommand((e.shiftKey ? "outdent" : "indent"));
  797. }
  798. }
  799. // Make tab and shift-tab skip over the <iframe>, going from the nested <div> to the toolbar
  800. // or next element after the editor. Needed on IE<9 and firefox.
  801. if(e.keyCode == keys.TAB && !this.isTabIndent){
  802. if(e.shiftKey && !e.ctrlKey && !e.altKey){
  803. // focus the <iframe> so the browser will shift-tab away from it instead
  804. this.beforeIframeNode.focus();
  805. }else if(!e.shiftKey && !e.ctrlKey && !e.altKey){
  806. // focus node after the <iframe> so the browser will tab away from it instead
  807. this.afterIframeNode.focus();
  808. }
  809. }
  810. if(has("ie") < 9 && e.keyCode === keys.BACKSPACE && this.document.selection.type === "Control"){
  811. // IE has a bug where if a non-text object is selected in the editor,
  812. // hitting backspace would act as if the browser's back button was
  813. // clicked instead of deleting the object. see #1069
  814. e.stopPropagation();
  815. e.preventDefault();
  816. this.execCommand("delete");
  817. }
  818. if(has("ff")){
  819. if(e.keyCode === keys.PAGE_UP || e.keyCode === keys.PAGE_DOWN ){
  820. if(this.editNode.clientHeight >= this.editNode.scrollHeight){
  821. // Stop the event to prevent firefox from trapping the cursor when there is no scroll bar.
  822. e.preventDefault();
  823. }
  824. }
  825. }
  826. return true;
  827. },
  828. onKeyUp: function(/*===== e =====*/){
  829. // summary:
  830. // Handler for onkeyup event
  831. // tags:
  832. // callback
  833. },
  834. setDisabled: function(/*Boolean*/ disabled){
  835. // summary:
  836. // Deprecated, use set('disabled', ...) instead.
  837. // tags:
  838. // deprecated
  839. kernel.deprecated('dijit.Editor::setDisabled is deprecated','use dijit.Editor::attr("disabled",boolean) instead', 2.0);
  840. this.set('disabled',disabled);
  841. },
  842. _setValueAttr: function(/*String*/ value){
  843. // summary:
  844. // Registers that attr("value", foo) should call setValue(foo)
  845. this.setValue(value);
  846. },
  847. _setDisableSpellCheckAttr: function(/*Boolean*/ disabled){
  848. if(this.document){
  849. domAttr.set(this.document.body, "spellcheck", !disabled);
  850. }else{
  851. // try again after the editor is finished loading
  852. this.onLoadDeferred.addCallback(lang.hitch(this, function(){
  853. domAttr.set(this.document.body, "spellcheck", !disabled);
  854. }));
  855. }
  856. this._set("disableSpellCheck", disabled);
  857. },
  858. onKeyPress: function(e){
  859. // summary:
  860. // Handle the various key events
  861. // tags:
  862. // protected
  863. if(e.keyCode === keys.SHIFT ||
  864. e.keyCode === keys.ALT ||
  865. e.keyCode === keys.META ||
  866. e.keyCode === keys.CTRL ||
  867. (e.keyCode == keys.TAB && !this.isTabIndent && !e.ctrlKey && !e.altKey)){
  868. return true;
  869. }
  870. var c = (e.keyChar && e.keyChar.toLowerCase()) || e.keyCode,
  871. handlers = this._keyHandlers[c],
  872. args = arguments;
  873. if(handlers && !e.altKey){
  874. array.some(handlers, function(h){
  875. // treat meta- same as ctrl-, for benefit of mac users
  876. if(!(h.shift ^ e.shiftKey) && !(h.ctrl ^ (e.ctrlKey||e.metaKey))){
  877. if(!h.handler.apply(this, args)){
  878. e.preventDefault();
  879. }
  880. return true;
  881. }
  882. }, this);
  883. }
  884. // function call after the character has been inserted
  885. if(!this._onKeyHitch){
  886. this._onKeyHitch = lang.hitch(this, "onKeyPressed");
  887. }
  888. setTimeout(this._onKeyHitch, 1);
  889. return true;
  890. },
  891. addKeyHandler: function(/*String*/ key, /*Boolean*/ ctrl, /*Boolean*/ shift, /*Function*/ handler){
  892. // summary:
  893. // Add a handler for a keyboard shortcut
  894. // description:
  895. // The key argument should be in lowercase if it is a letter character
  896. // tags:
  897. // protected
  898. if(!lang.isArray(this._keyHandlers[key])){
  899. this._keyHandlers[key] = [];
  900. }
  901. //TODO: would be nice to make this a hash instead of an array for quick lookups
  902. this._keyHandlers[key].push({
  903. shift: shift || false,
  904. ctrl: ctrl || false,
  905. handler: handler
  906. });
  907. },
  908. onKeyPressed: function(){
  909. // summary:
  910. // Handler for after the user has pressed a key, and the display has been updated.
  911. // (Runs on a timer so that it runs after the display is updated)
  912. // tags:
  913. // private
  914. this.onDisplayChanged(/*e*/); // can't pass in e
  915. },
  916. onClick: function(/*Event*/ e){
  917. // summary:
  918. // Handler for when the user clicks.
  919. // tags:
  920. // private
  921. // console.info('onClick',this._tryDesignModeOn);
  922. this.onDisplayChanged(e);
  923. },
  924. _onIEMouseDown: function(){
  925. // summary:
  926. // IE only to prevent 2 clicks to focus
  927. // tags:
  928. // protected
  929. if(!this.focused && !this.disabled){
  930. this.focus();
  931. }
  932. },
  933. _onBlur: function(e){
  934. // summary:
  935. // Called from focus manager when focus has moved away from this editor
  936. // tags:
  937. // protected
  938. // console.info('_onBlur')
  939. this.inherited(arguments);
  940. var newValue = this.getValue(true);
  941. if(newValue !== this.value){
  942. this.onChange(newValue);
  943. }
  944. this._set("value", newValue);
  945. },
  946. _onFocus: function(/*Event*/ e){
  947. // summary:
  948. // Called from focus manager when focus has moved into this editor
  949. // tags:
  950. // protected
  951. // console.info('_onFocus')
  952. if(!this.disabled){
  953. if(!this._disabledOK){
  954. this.set('disabled', false);
  955. }
  956. this.inherited(arguments);
  957. }
  958. },
  959. // TODO: remove in 2.0
  960. blur: function(){
  961. // summary:
  962. // Remove focus from this instance.
  963. // tags:
  964. // deprecated
  965. if(!has("ie") && this.window.document.documentElement && this.window.document.documentElement.focus){
  966. this.window.document.documentElement.focus();
  967. }else if(win.doc.body.focus){
  968. win.doc.body.focus();
  969. }
  970. },
  971. focus: function(){
  972. // summary:
  973. // Move focus to this editor
  974. if(!this.isLoaded){
  975. this.focusOnLoad = true;
  976. return;
  977. }
  978. if(this._cursorToStart){
  979. delete this._cursorToStart;
  980. if(this.editNode.childNodes){
  981. this.placeCursorAtStart(); // this calls focus() so return
  982. return;
  983. }
  984. }
  985. if(has("ie") < 9){
  986. //this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
  987. // if we fire the event manually and let the browser handle the focusing, the latest
  988. // cursor position is focused like in FF
  989. this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject/fireEvent only in IE < 11
  990. }else{
  991. // Firefox and chrome
  992. this.editNode.focus();
  993. }
  994. },
  995. // _lastUpdate: 0,
  996. updateInterval: 200,
  997. _updateTimer: null,
  998. onDisplayChanged: function(/*Event*/ /*===== e =====*/){
  999. // summary:
  1000. // This event will be fired every time the display context
  1001. // changes and the result needs to be reflected in the UI.
  1002. // description:
  1003. // If you don't want to have update too often,
  1004. // onNormalizedDisplayChanged should be used instead
  1005. // tags:
  1006. // private
  1007. // var _t=new Date();
  1008. if(this._updateTimer){
  1009. clearTimeout(this._updateTimer);
  1010. }
  1011. if(!this._updateHandler){
  1012. this._updateHandler = lang.hitch(this,"onNormalizedDisplayChanged");
  1013. }
  1014. this._updateTimer = setTimeout(this._updateHandler, this.updateInterval);
  1015. // Technically this should trigger a call to watch("value", ...) registered handlers,
  1016. // but getValue() is too slow to call on every keystroke so we don't.
  1017. },
  1018. onNormalizedDisplayChanged: function(){
  1019. // summary:
  1020. // This event is fired every updateInterval ms or more
  1021. // description:
  1022. // If something needs to happen immediately after a
  1023. // user change, please use onDisplayChanged instead.
  1024. // tags:
  1025. // private
  1026. delete this._updateTimer;
  1027. },
  1028. onChange: function(/*===== newContent =====*/){
  1029. // summary:
  1030. // This is fired if and only if the editor loses focus and
  1031. // the content is changed.
  1032. },
  1033. _normalizeCommand: function(/*String*/ cmd, /*Anything?*/argument){
  1034. // summary:
  1035. // Used as the advice function to map our
  1036. // normalized set of commands to those supported by the target
  1037. // browser.
  1038. // tags:
  1039. // private
  1040. var command = cmd.toLowerCase();
  1041. if(command === "formatblock"){
  1042. if(has("safari") && argument === undefined){ command = "heading"; }
  1043. }else if(command === "hilitecolor" && !has("mozilla")){
  1044. command = "backcolor";
  1045. }
  1046. return command;
  1047. },
  1048. _qcaCache: {},
  1049. queryCommandAvailable: function(/*String*/ command){
  1050. // summary:
  1051. // Tests whether a command is supported by the host. Clients
  1052. // SHOULD check whether a command is supported before attempting
  1053. // to use it, behaviour for unsupported commands is undefined.
  1054. // command:
  1055. // The command to test for
  1056. // tags:
  1057. // private
  1058. // memoizing version. See _queryCommandAvailable for computing version
  1059. var ca = this._qcaCache[command];
  1060. if(ca !== undefined){ return ca; }
  1061. return (this._qcaCache[command] = this._queryCommandAvailable(command));
  1062. },
  1063. _queryCommandAvailable: function(/*String*/ command){
  1064. // summary:
  1065. // See queryCommandAvailable().
  1066. // tags:
  1067. // private
  1068. var ie = 1;
  1069. var mozilla = 1 << 1;
  1070. var webkit = 1 << 2;
  1071. var opera = 1 << 3;
  1072. function isSupportedBy(browsers){
  1073. return {
  1074. ie: Boolean(browsers & ie),
  1075. mozilla: Boolean(browsers & mozilla),
  1076. webkit: Boolean(browsers & webkit),
  1077. opera: Boolean(browsers & opera)
  1078. };
  1079. }
  1080. var supportedBy = null;
  1081. switch(command.toLowerCase()){
  1082. case "bold": case "italic": case "underline":
  1083. case "subscript": case "superscript":
  1084. case "fontname": case "fontsize":
  1085. case "forecolor": case "hilitecolor":
  1086. case "justifycenter": case "justifyfull": case "justifyleft":
  1087. case "justifyright": case "delete": case "selectall": case "toggledir":
  1088. supportedBy = isSupportedBy(mozilla | ie | webkit | opera);
  1089. break;
  1090. case "createlink": case "unlink": case "removeformat":
  1091. case "inserthorizontalrule": case "insertimage":
  1092. case "insertorderedlist": case "insertunorderedlist":
  1093. case "indent": case "outdent": case "formatblock":
  1094. case "inserthtml": case "undo": case "redo": case "strikethrough": case "tabindent":
  1095. supportedBy = isSupportedBy(mozilla | ie | opera | webkit);
  1096. break;
  1097. case "blockdirltr": case "blockdirrtl":
  1098. case "dirltr": case "dirrtl":
  1099. case "inlinedirltr": case "inlinedirrtl":
  1100. supportedBy = isSupportedBy(ie);
  1101. break;
  1102. case "cut": case "copy": case "paste":
  1103. supportedBy = isSupportedBy( ie | mozilla | webkit);
  1104. break;
  1105. case "inserttable":
  1106. supportedBy = isSupportedBy(mozilla | ie);
  1107. break;
  1108. case "insertcell": case "insertcol": case "insertrow":
  1109. case "deletecells": case "deletecols": case "deleterows":
  1110. case "mergecells": case "splitcell":
  1111. supportedBy = isSupportedBy(ie | mozilla);
  1112. break;
  1113. default: return false;
  1114. }
  1115. return ((has("ie") || has("trident")) && supportedBy.ie) ||
  1116. (has("mozilla") && supportedBy.mozilla) ||
  1117. (has("webkit") && supportedBy.webkit) ||
  1118. (has("opera") && supportedBy.opera); // Boolean return true if the command is supported, false otherwise
  1119. },
  1120. execCommand: function(/*String*/ command, argument){
  1121. // summary:
  1122. // Executes a command in the Rich Text area
  1123. // command:
  1124. // The command to execute
  1125. // argument:
  1126. // An optional argument to the command
  1127. // tags:
  1128. // protected
  1129. var returnValue;
  1130. //focus() is required for IE to work
  1131. //In addition, focus() makes sure after the execution of
  1132. //the command, the editor receives the focus as expected
  1133. if(this.focused){
  1134. // put focus back in the iframe, unless focus has somehow been shifted out of the editor completely
  1135. this.focus();
  1136. }
  1137. command = this._normalizeCommand(command, argument);
  1138. if(argument !== undefined){
  1139. if(command === "heading"){
  1140. throw new Error("unimplemented");
  1141. }else if(command === "formatblock" && (has("ie") || has("trident"))){
  1142. argument = '<'+argument+'>';
  1143. }
  1144. }
  1145. //Check to see if we have any over-rides for commands, they will be functions on this
  1146. //widget of the form _commandImpl. If we don't, fall through to the basic native
  1147. //exec command of the browser.
  1148. var implFunc = "_" + command + "Impl";
  1149. if(this[implFunc]){
  1150. returnValue = this[implFunc](argument);
  1151. }else{
  1152. argument = arguments.length > 1 ? argument : null;
  1153. if(argument || command !== "createlink"){
  1154. returnValue = this.document.execCommand(command, false, argument);
  1155. }
  1156. }
  1157. this.onDisplayChanged();
  1158. return returnValue;
  1159. },
  1160. queryCommandEnabled: function(/*String*/ command){
  1161. // summary:
  1162. // Check whether a command is enabled or not.
  1163. // command:
  1164. // The command to execute
  1165. // tags:
  1166. // protected
  1167. if(this.disabled || !this._disabledOK){ return false; }
  1168. command = this._normalizeCommand(command);
  1169. //Check to see if we have any over-rides for commands, they will be functions on this
  1170. //widget of the form _commandEnabledImpl. If we don't, fall through to the basic native
  1171. //command of the browser.
  1172. var implFunc = "_" + command + "EnabledImpl";
  1173. if(this[implFunc]){
  1174. return this[implFunc](command);
  1175. }else{
  1176. return this._browserQueryCommandEnabled(command);
  1177. }
  1178. },
  1179. queryCommandState: function(command){
  1180. // summary:
  1181. // Check the state of a given command and returns true or false.
  1182. // tags:
  1183. // protected
  1184. if(this.disabled || !this._disabledOK){ return false; }
  1185. command = this._normalizeCommand(command);
  1186. try{
  1187. return this.document.queryCommandState(command);
  1188. }catch(e){
  1189. //Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
  1190. return false;
  1191. }
  1192. },
  1193. queryCommandValue: function(command){
  1194. // summary:
  1195. // Check the value of a given command. This matters most for
  1196. // custom selections and complex values like font value setting.
  1197. // tags:
  1198. // protected
  1199. if(this.disabled || !this._disabledOK){ return false; }
  1200. var r;
  1201. command = this._normalizeCommand(command);
  1202. if((has("ie") || has("trident")) && command === "formatblock"){
  1203. r = this._native2LocalFormatNames[this.document.queryCommandValue(command)];
  1204. }else if(has("mozilla") && command === "hilitecolor"){
  1205. var oldValue;
  1206. try{
  1207. oldValue = this.document.queryCommandValue("styleWithCSS");
  1208. }catch(e){
  1209. oldValue = false;
  1210. }
  1211. this.document.execCommand("styleWithCSS", false, true);
  1212. r = this.document.queryCommandValue(command);
  1213. this.document.execCommand("styleWithCSS", false, oldValue);
  1214. }else{
  1215. r = this.document.queryCommandValue(command);
  1216. }
  1217. return r;
  1218. },
  1219. // Misc.
  1220. _sCall: function(name, args){
  1221. // summary:
  1222. // Run the named method of dijit._editor.selection over the
  1223. // current editor instance's window, with the passed args.
  1224. // tags:
  1225. // private
  1226. return win.withGlobal(this.window, name, selectionapi, args);
  1227. },
  1228. // FIXME: this is a TON of code duplication. Why?
  1229. placeCursorAtStart: function(){
  1230. // summary:
  1231. // Place the cursor at the start of the editing area.
  1232. // tags:
  1233. // private
  1234. this.focus();
  1235. //see comments in placeCursorAtEnd
  1236. var isvalid=false;
  1237. if(has("mozilla")){
  1238. // TODO: Is this branch even necessary?
  1239. var first=this.editNode.firstChild;
  1240. while(first){
  1241. if(first.nodeType === 3){
  1242. if(first.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
  1243. isvalid=true;
  1244. this._sCall("selectElement", [ first ]);
  1245. break;
  1246. }
  1247. }else if(first.nodeType === 1){
  1248. isvalid=true;
  1249. var tg = first.tagName ? first.tagName.toLowerCase() : "";
  1250. // Collapse before childless tags.
  1251. if(/br|input|img|base|meta|area|basefont|hr|link/.test(tg)){
  1252. this._sCall("selectElement", [ first ]);
  1253. }else{
  1254. // Collapse inside tags with children.
  1255. this._sCall("selectElementChildren", [ first ]);
  1256. }
  1257. break;
  1258. }
  1259. first = first.nextSibling;
  1260. }
  1261. }else{
  1262. isvalid=true;
  1263. this._sCall("selectElementChildren", [ this.editNode ]);
  1264. }
  1265. if(isvalid){
  1266. this._sCall("collapse", [ true ]);
  1267. }
  1268. },
  1269. placeCursorAtEnd: function(){
  1270. // summary:
  1271. // Place the cursor at the end of the editing area.
  1272. // tags:
  1273. // private
  1274. this.focus();
  1275. //In mozilla, if last child is not a text node, we have to use
  1276. // selectElementChildren on this.editNode.lastChild otherwise the
  1277. // cursor would be placed at the end of the closing tag of
  1278. //this.editNode.lastChild
  1279. var isvalid=false;
  1280. if(has("mozilla")){
  1281. var last=this.editNode.lastChild;
  1282. while(last){
  1283. if(last.nodeType === 3){
  1284. if(last.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
  1285. isvalid=true;
  1286. this._sCall("selectElement", [ last ]);
  1287. break;
  1288. }
  1289. }else if(last.nodeType === 1){
  1290. isvalid=true;
  1291. if(last.lastChild){
  1292. this._sCall("selectElement", [ last.lastChild ]);
  1293. }else{
  1294. this._sCall("selectElement", [ last ]);
  1295. }
  1296. break;
  1297. }
  1298. last = last.previousSibling;
  1299. }
  1300. }else{
  1301. isvalid=true;
  1302. this._sCall("selectElementChildren", [ this.editNode ]);
  1303. }
  1304. if(isvalid){
  1305. this._sCall("collapse", [ false ]);
  1306. }
  1307. },
  1308. getValue: function(/*Boolean?*/ nonDestructive){
  1309. // summary:
  1310. // Return the current content of the editing area (post filters
  1311. // are applied). Users should call get('value') instead.
  1312. // nonDestructive:
  1313. // defaults to false. Should the post-filtering be run over a copy
  1314. // of the live DOM? Most users should pass "true" here unless they
  1315. // *really* know that none of the installed filters are going to
  1316. // mess up the editing session.
  1317. // tags:
  1318. // private
  1319. if(this.textarea){
  1320. if(this.isClosed || !this.isLoaded){
  1321. return this.textarea.value;
  1322. }
  1323. }
  1324. return this._postFilterContent(null, nonDestructive);
  1325. },
  1326. _getValueAttr: function(){
  1327. // summary:
  1328. // Hook to make attr("value") work
  1329. return this.getValue(true);
  1330. },
  1331. setValue: function(/*String*/ html){
  1332. // summary:
  1333. // This function sets the content. No undo history is preserved.
  1334. // Users should use set('value', ...) instead.
  1335. // tags:
  1336. // deprecated
  1337. // TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
  1338. if(!this.isLoaded){
  1339. // try again after the editor is finished loading
  1340. this.onLoadDeferred.addCallback(lang.hitch(this, function(){
  1341. this.setValue(html);
  1342. }));
  1343. return;
  1344. }
  1345. this._cursorToStart = true;
  1346. if(this.textarea && (this.isClosed || !this.isLoaded)){
  1347. this.textarea.value=html;
  1348. }else{
  1349. html = this._preFilterContent(html);
  1350. var node = this.isClosed ? this.domNode : this.editNode;
  1351. // Use &nbsp; to avoid webkit problems where editor is disabled until the user clicks it
  1352. if(!html && has("webkit")){
  1353. html = "&#160;"; // &nbsp;
  1354. }
  1355. node.innerHTML = html;
  1356. this._preDomFilterContent(node);
  1357. }
  1358. this.onDisplayChanged();
  1359. this._set("value", this.getValue(true));
  1360. },
  1361. replaceValue: function(/*String*/ html){
  1362. // summary:
  1363. // This function set the content while trying to maintain the undo stack
  1364. // (now only works fine with Moz, this is identical to setValue in all
  1365. // other browsers)
  1366. // tags:
  1367. // protected
  1368. if(this.isClosed){
  1369. this.setValue(html);
  1370. }else if(this.window && this.window.getSelection && !has("mozilla")){ // Safari
  1371. // look ma! it's a totally f'd browser!
  1372. this.setValue(html);
  1373. }else if(this.window && this.window.getSelection){ // Moz
  1374. html = this._preFilterContent(html);
  1375. this.execCommand("selectall");
  1376. this.execCommand("inserthtml", html);
  1377. this._preDomFilterContent(this.editNode);
  1378. }else if(this.document && this.document.selection){//IE
  1379. //In IE, when the first element is not a text node, say
  1380. //an <a> tag, when replacing the content of the editing
  1381. //area, the <a> tag will be around all the content
  1382. //so for now, use setValue for IE too
  1383. this.setValue(html);
  1384. }
  1385. this._set("value", this.getValue(true));
  1386. },
  1387. _preFilterContent: function(/*String*/ html){
  1388. // summary:
  1389. // Filter the input before setting the content of the editing
  1390. // area. DOM pre-filtering may happen after this
  1391. // string-based filtering takes place but as of 1.2, this is not
  1392. // guaranteed for operations such as the inserthtml command.
  1393. // tags:
  1394. // private
  1395. var ec = html;
  1396. array.forEach(this.contentPreFilters, function(ef){ if(ef){ ec = ef(ec); } });
  1397. return ec;
  1398. },
  1399. _preDomFilterContent: function(/*DomNode*/ dom){
  1400. // summary:
  1401. // filter the input's live DOM. All filter operations should be
  1402. // considered to be "live" and operating on the DOM that the user
  1403. // will be interacting with in their editing session.
  1404. // tags:
  1405. // private
  1406. dom = dom || this.editNode;
  1407. array.forEach(this.contentDomPreFilters, function(ef){
  1408. if(ef && lang.isFunction(ef)){
  1409. ef(dom);
  1410. }
  1411. }, this);
  1412. },
  1413. _postFilterContent: function(
  1414. /*DomNode|DomNode[]|String?*/ dom,
  1415. /*Boolean?*/ nonDestructive){
  1416. // summary:
  1417. // filter the output after getting the content of the editing area
  1418. //
  1419. // description:
  1420. // post-filtering allows plug-ins and users to specify any number
  1421. // of transforms over the editor's content, enabling many common
  1422. // use-cases such as transforming absolute to relative URLs (and
  1423. // vice-versa), ensuring conformance with a particular DTD, etc.
  1424. // The filters are registered in the contentDomPostFilters and
  1425. // contentPostFilters arrays. Each item in the
  1426. // contentDomPostFilters array is a function which takes a DOM
  1427. // Node or array of nodes as its only argument and returns the
  1428. // same. It is then passed down the chain for further filtering.
  1429. // The contentPostFilters array behaves the same way, except each
  1430. // member operates on strings. Together, the DOM and string-based
  1431. // filtering allow the full range of post-processing that should
  1432. // be necessaray to enable even the most agressive of post-editing
  1433. // conversions to take place.
  1434. //
  1435. // If nonDestructive is set to "true", the nodes are cloned before
  1436. // filtering proceeds to avoid potentially destructive transforms
  1437. // to the content which may still needed to be edited further.
  1438. // Once DOM filtering has taken place, the serialized version of
  1439. // the DOM which is passed is run through each of the
  1440. // contentPostFilters functions.
  1441. //
  1442. // dom:
  1443. // a node, set of nodes, which to filter using each of the current
  1444. // members of the contentDomPostFilters and contentPostFilters arrays.
  1445. //
  1446. // nonDestructive:
  1447. // defaults to "false". If true, ensures that filtering happens on
  1448. // a clone of the passed-in content and not the actual node
  1449. // itself.
  1450. //
  1451. // tags:
  1452. // private
  1453. var ec;
  1454. if(!lang.isString(dom)){
  1455. dom = dom || this.editNode;
  1456. if(this.contentDomPostFilters.length){
  1457. if(nonDestructive){
  1458. dom = lang.clone(dom);
  1459. }
  1460. array.forEach(this.contentDomPostFilters, function(ef){
  1461. dom = ef(dom);
  1462. });
  1463. }
  1464. ec = htmlapi.getChildrenHtml(dom);
  1465. }else{
  1466. ec = dom;
  1467. }
  1468. if(!lang.trim(ec.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length){
  1469. ec = "";
  1470. }
  1471. // if(has("ie")){
  1472. // //removing appended <P>&nbsp;</P> for IE
  1473. // ec = ec.replace(/(?:<p>&nbsp;</p>[\n\r]*)+$/i,"");
  1474. // }
  1475. array.forEach(this.contentPostFilters, function(ef){
  1476. ec = ef(ec);
  1477. });
  1478. return ec;
  1479. },
  1480. _saveContent: function(){
  1481. // summary:
  1482. // Saves the content in an onunload event if the editor has not been closed
  1483. // tags:
  1484. // private
  1485. var saveTextarea = dom.byId(dijit._scopeName + "._editor.BuxRichText.value");
  1486. if(saveTextarea){
  1487. if(saveTextarea.value){
  1488. saveTextarea.value += this._SEPARATOR;
  1489. }
  1490. saveTextarea.value += this.name + this._NAME_CONTENT_SEP + this.getValue(true);
  1491. }
  1492. },
  1493. escapeXml: function(/*String*/ str, /*Boolean*/ noSingleQuotes){
  1494. // summary:
  1495. // Adds escape sequences for special characters in XML.
  1496. // Optionally skips escapes for single quotes
  1497. // tags:
  1498. // private
  1499. str = str.replace(/&/gm, "&amp;").replace(/</gm, "&lt;").replace(/>/gm, "&gt;").replace(/"/gm, "&quot;");
  1500. if(!noSingleQuotes){
  1501. str = str.replace(/'/gm, "&#39;");
  1502. }
  1503. return str; // string
  1504. },
  1505. getNodeHtml: function(/* DomNode */ node){
  1506. // summary:
  1507. // Deprecated. Use dijit/_editor/html::_getNodeHtml() instead.
  1508. // tags:
  1509. // deprecated
  1510. kernel.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit/_editor/html::getNodeHtml instead', 2);
  1511. return htmlapi.getNodeHtml(node); // String
  1512. },
  1513. getNodeChildrenHtml: function(/* DomNode */ dom){
  1514. // summary:
  1515. // Deprecated. Use dijit/_editor/html::getChildrenHtml() instead.
  1516. // tags:
  1517. // deprecated
  1518. kernel.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit/_editor/html::getChildrenHtml instead', 2);
  1519. return htmlapi.getChildrenHtml(dom);
  1520. },
  1521. close: function(/*Boolean?*/ save){
  1522. // summary:
  1523. // Kills the editor and optionally writes back the modified contents to the
  1524. // element from which it originated.
  1525. // save:
  1526. // Whether or not to save the changes. If false, the changes are discarded.
  1527. // tags:
  1528. // private
  1529. if(this.isClosed){ return; }
  1530. if(!arguments.length){ save = true; }
  1531. if(save){
  1532. this._set("value", this.getValue(true));
  1533. }
  1534. // line height is squashed for iframes
  1535. // FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
  1536. if(this.interval){ clearInterval(this.interval); }
  1537. if(this._webkitListener){
  1538. //Cleaup of WebKit fix: #9532
  1539. this.disconnect(this._webkitListener);
  1540. delete this._webkitListener;
  1541. }
  1542. // Guard against memory leaks on IE (see #9268)
  1543. if(has("ie")){
  1544. this.iframe.onfocus = null;
  1545. }
  1546. this.iframe._loadFunc = null;
  1547. if(this._iframeRegHandle){
  1548. this._iframeRegHandle.remove();
  1549. delete this._iframeRegHandle;
  1550. }
  1551. if(this.textarea){
  1552. var s = this.textarea.style;
  1553. s.position = "";
  1554. s.left = s.top = "";
  1555. if(has("ie")){
  1556. s.overflow = this.__overflow;
  1557. this.__overflow = null;
  1558. }
  1559. this.textarea.value = this.value;
  1560. domConstruct.destroy(this.domNode);
  1561. this.domNode = this.textarea;
  1562. }else{
  1563. // Note that this destroys the iframe
  1564. this.domNode.innerHTML = this.value;
  1565. }
  1566. delete this.iframe;
  1567. domClass.remove(this.domNode, this.baseClass);
  1568. this.isClosed = true;
  1569. this.isLoaded = false;
  1570. delete this.editNode;
  1571. delete this.focusNode;
  1572. if(this.window && this.window._frameElement){
  1573. this.window._frameElement = null;
  1574. }
  1575. this.window = null;
  1576. this.document = null;
  1577. this.editingArea = null;
  1578. this.editorObject = null;
  1579. },
  1580. destroy: function(){
  1581. if(!this.isClosed){ this.close(false); }
  1582. if(this._updateTimer){
  1583. clearTimeout(this._updateTimer);
  1584. }
  1585. this.inherited(arguments);
  1586. if(BuxRichText._globalSaveHandler){
  1587. delete BuxRichText._globalSaveHandler[this.id];
  1588. }
  1589. },
  1590. _removeMozBogus: function(/* String */ html){
  1591. // summary:
  1592. // Post filter to remove unwanted HTML attributes generated by mozilla
  1593. // tags:
  1594. // private
  1595. return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi,''); // String
  1596. },
  1597. _removeWebkitBogus: function(/* String */ html){
  1598. // summary:
  1599. // Post filter to remove unwanted HTML attributes generated by webkit
  1600. // tags:
  1601. // private
  1602. html = html.replace(/\sclass="webkit-block-placeholder"/gi, '');
  1603. html = html.replace(/\sclass="apple-style-span"/gi, '');
  1604. // For some reason copy/paste sometime adds extra meta tags for charset on
  1605. // webkit (chrome) on mac.They need to be removed. See: #12007"
  1606. html = html.replace(/<meta charset=\"utf-8\" \/>/gi, '');
  1607. return html; // String
  1608. },
  1609. _normalizeFontStyle: function(/* String */ html){
  1610. // summary:
  1611. // Convert 'strong' and 'em' to 'b' and 'i'.
  1612. // description:
  1613. // Moz can not handle strong/em tags correctly, so to help
  1614. // mozilla and also to normalize output, convert them to 'b' and 'i'.
  1615. //
  1616. // Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
  1617. // tags:
  1618. // private
  1619. return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
  1620. .replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
  1621. },
  1622. _preFixUrlAttributes: function(/* String */ html){
  1623. // summary:
  1624. // Pre-filter to do fixing to href attributes on <a> and <img> tags
  1625. // tags:
  1626. // private
  1627. return html.replace(/(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi,
  1628. '$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
  1629. .replace(/(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi,
  1630. '$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String
  1631. },
  1632. /*****************************************************************************
  1633. The following functions implement HTML manipulation commands for various
  1634. browser/contentEditable implementations. The goal of them is to enforce
  1635. standard behaviors of them.
  1636. ******************************************************************************/
  1637. /*** queryCommandEnabled implementations ***/
  1638. _browserQueryCommandEnabled: function(command){
  1639. // summary:
  1640. // Implementation to call to the native queryCommandEnabled of the browser.
  1641. // command:
  1642. // The command to check.
  1643. // tags:
  1644. // protected
  1645. if(!command) { return false; }
  1646. var elem = has("ie") < 9 ? this.document.selection.createRange() : this.document;
  1647. try{
  1648. return elem.queryCommandEnabled(command);
  1649. }catch(e){
  1650. return false;
  1651. }
  1652. },
  1653. _createlinkEnabledImpl: function(/*===== argument =====*/){
  1654. // summary:
  1655. // This function implements the test for if the create link
  1656. // command should be enabled or not.
  1657. // argument:
  1658. // arguments to the exec command, if any.
  1659. // tags:
  1660. // protected
  1661. var enabled = true;
  1662. if(has("opera")){
  1663. var sel = this.window.getSelection();
  1664. if(sel.isCollapsed){
  1665. enabled = true;
  1666. }else{
  1667. enabled = this.document.queryCommandEnabled("createlink");
  1668. }
  1669. }else{
  1670. enabled = this._browserQueryCommandEnabled("createlink");
  1671. }
  1672. return enabled;
  1673. },
  1674. _unlinkEnabledImpl: function(/*===== argument =====*/){
  1675. // summary:
  1676. // This function implements the test for if the unlink
  1677. // command should be enabled or not.
  1678. // argument:
  1679. // arguments to the exec command, if any.
  1680. // tags:
  1681. // protected
  1682. var enabled = true;
  1683. if(has("mozilla") || has("webkit")){
  1684. enabled = this._sCall("hasAncestorElement", ["a"]);
  1685. }else{
  1686. enabled = this._browserQueryCommandEnabled("unlink");
  1687. }
  1688. return enabled;
  1689. },
  1690. _inserttableEnabledImpl: function(/*===== argument =====*/){
  1691. // summary:
  1692. // This function implements the test for if the inserttable
  1693. // command should be enabled or not.
  1694. // argument:
  1695. // arguments to the exec command, if any.
  1696. // tags:
  1697. // protected
  1698. var enabled = true;
  1699. if(has("mozilla") || has("webkit")){
  1700. enabled = true;
  1701. }else{
  1702. enabled = this._browserQueryCommandEnabled("inserttable");
  1703. }
  1704. return enabled;
  1705. },
  1706. _cutEnabledImpl: function(/*===== argument =====*/){
  1707. // summary:
  1708. // This function implements the test for if the cut
  1709. // command should be enabled or not.
  1710. // argument:
  1711. // arguments to the exec command, if any.
  1712. // tags:
  1713. // protected
  1714. var enabled = true;
  1715. if(has("webkit")){
  1716. // WebKit deems clipboard activity as a security threat and natively would return false
  1717. var sel = this.window.getSelection();
  1718. if(sel){ sel = sel.toString(); }
  1719. enabled = !!sel;
  1720. }else{
  1721. enabled = this._browserQueryCommandEnabled("cut");
  1722. }
  1723. return enabled;
  1724. },
  1725. _copyEnabledImpl: function(/*===== argument =====*/){
  1726. // summary:
  1727. // This function implements the test for if the copy
  1728. // command should be enabled or not.
  1729. // argument:
  1730. // arguments to the exec command, if any.
  1731. // tags:
  1732. // protected
  1733. var enabled = true;
  1734. if(has("webkit")){
  1735. // WebKit deems clipboard activity as a security threat and natively would return false
  1736. var sel = this.window.getSelection();
  1737. if(sel){ sel = sel.toString(); }
  1738. enabled = !!sel;
  1739. }else{
  1740. enabled = this._browserQueryCommandEnabled("copy");
  1741. }
  1742. return enabled;
  1743. },
  1744. _pasteEnabledImpl: function(/*===== argument =====*/){
  1745. // summary:c
  1746. // This function implements the test for if the paste
  1747. // command should be enabled or not.
  1748. // argument:
  1749. // arguments to the exec command, if any.
  1750. // tags:
  1751. // protected
  1752. var enabled = true;
  1753. if(has("webkit")){
  1754. return true;
  1755. }else{
  1756. enabled = this._browserQueryCommandEnabled("paste");
  1757. }
  1758. return enabled;
  1759. },
  1760. /*** execCommand implementations ***/
  1761. _inserthorizontalruleImpl: function(argument){
  1762. // summary:
  1763. // This function implements the insertion of HTML 'HR' tags.
  1764. // into a point on the page. IE doesn't to it right, so
  1765. // we have to use an alternate form
  1766. // argument:
  1767. // arguments to the exec command, if any.
  1768. // tags:
  1769. // protected
  1770. if(has("ie")){
  1771. return this._inserthtmlImpl("<hr>");
  1772. }
  1773. return this.document.execCommand("inserthorizontalrule", false, argument);
  1774. },
  1775. _unlinkImpl: function(argument){
  1776. // summary:
  1777. // This function implements the unlink of an 'a' tag.
  1778. // argument:
  1779. // arguments to the exec command, if any.
  1780. // tags:
  1781. // protected
  1782. if((this.queryCommandEnabled("unlink")) && (has("mozilla") || has("webkit"))){
  1783. var a = this._sCall("getAncestorElement", [ "a" ]);
  1784. this._sCall("selectElement", [ a ]);
  1785. return this.document.execCommand("unlink", false, null);
  1786. }
  1787. return this.document.execCommand("unlink", false, argument);
  1788. },
  1789. _hilitecolorImpl: function(argument){
  1790. // summary:
  1791. // This function implements the hilitecolor command
  1792. // argument:
  1793. // arguments to the exec command, if any.
  1794. // tags:
  1795. // protected
  1796. var returnValue;
  1797. var isApplied = this._handleTextColorOrProperties("hilitecolor", argument);
  1798. if(!isApplied){
  1799. if(has("mozilla")){
  1800. // mozilla doesn't support hilitecolor properly when useCSS is
  1801. // set to false (bugzilla #279330)
  1802. this.document.execCommand("styleWithCSS", false, true);
  1803. console.log("Executing color command.");
  1804. returnValue = this.document.execCommand("hilitecolor", false, argument);
  1805. this.document.execCommand("styleWithCSS", false, false);
  1806. }else{
  1807. returnValue = this.document.execCommand("hilitecolor", false, argument);
  1808. }
  1809. }
  1810. return returnValue;
  1811. },
  1812. _backcolorImpl: function(argument){
  1813. // summary:
  1814. // This function implements the backcolor command
  1815. // argument:
  1816. // arguments to the exec command, if any.
  1817. // tags:
  1818. // protected
  1819. if(has("ie")){
  1820. // Tested under IE 6 XP2, no problem here, comment out
  1821. // IE weirdly collapses ranges when we exec these commands, so prevent it
  1822. // var tr = this.document.selection.createRange();
  1823. argument = argument ? argument : null;
  1824. }
  1825. var isApplied = this._handleTextColorOrProperties("backcolor", argument);
  1826. if(!isApplied){
  1827. isApplied = this.document.execCommand("backcolor", false, argument);
  1828. }
  1829. return isApplied;
  1830. },
  1831. _forecolorImpl: function(argument){
  1832. // summary:
  1833. // This function implements the forecolor command
  1834. // argument:
  1835. // arguments to the exec command, if any.
  1836. // tags:
  1837. // protected
  1838. if(has("ie")){
  1839. // Tested under IE 6 XP2, no problem here, comment out
  1840. // IE weirdly collapses ranges when we exec these commands, so prevent it
  1841. // var tr = this.document.selection.createRange();
  1842. argument = argument? argument : null;
  1843. }
  1844. var isApplied = false;
  1845. isApplied = this._handleTextColorOrProperties("forecolor", argument);
  1846. if(!isApplied){
  1847. isApplied = this.document.execCommand("forecolor", false, argument);
  1848. }
  1849. return isApplied;
  1850. },
  1851. _inserthtmlImpl: function(argument){
  1852. // summary:
  1853. // This function implements the insertion of HTML content into
  1854. // a point on the page.
  1855. // argument:
  1856. // The content to insert, if any.
  1857. // tags:
  1858. // protected
  1859. argument = this._preFilterContent(argument);
  1860. var rv = true;
  1861. if(has("ie") < 9){
  1862. var insertRange = this.document.selection.createRange();
  1863. if(this.document.selection.type.toUpperCase() === 'CONTROL'){
  1864. var n = insertRange.item(0);
  1865. while(insertRange.length){
  1866. insertRange.remove(insertRange.item(0));
  1867. }
  1868. n.outerHTML = argument;
  1869. }else{
  1870. insertRange.pasteHTML(argument);
  1871. }
  1872. insertRange.select();
  1873. }else if(has("trident") < 8){
  1874. var insertRange;
  1875. var selection = rangeapi.getSelection(this.window);
  1876. if(selection && selection.rangeCount && selection.getRangeAt){
  1877. insertRange = selection.getRangeAt(0);
  1878. insertRange.deleteContents();
  1879. var div = domConstruct.create('div');
  1880. div.innerHTML = argument;
  1881. var node, lastNode;
  1882. var n = this.document.createDocumentFragment();
  1883. while((node = div.firstChild)){
  1884. lastNode = n.appendChild(node);
  1885. }
  1886. insertRange.insertNode(n);
  1887. if(lastNode) {
  1888. insertRange = insertRange.cloneRange();
  1889. insertRange.setStartAfter(lastNode);
  1890. insertRange.collapse(false);
  1891. selection.removeAllRanges();
  1892. selection.addRange(insertRange);
  1893. }
  1894. }
  1895. }else if(has("mozilla") && !argument.length){
  1896. //mozilla can not inserthtml an empty html to delete current selection
  1897. //so we delete the selection instead in this case
  1898. this._sCall("remove"); // FIXME
  1899. }else{
  1900. rv = this.document.execCommand("inserthtml", false, argument);
  1901. }
  1902. return rv;
  1903. },
  1904. _boldImpl: function(argument){
  1905. // summary:
  1906. // This function implements an over-ride of the bold command.
  1907. // argument:
  1908. // Not used, operates by selection.
  1909. // tags:
  1910. // protected
  1911. var applied = false;
  1912. if(has("ie") || has("trident")){
  1913. this._adaptIESelection();
  1914. applied = this._adaptIEFormatAreaAndExec("bold");
  1915. }
  1916. if(!applied){
  1917. applied = this.document.execCommand("bold", false, argument);
  1918. }
  1919. return applied;
  1920. },
  1921. _italicImpl: function(argument){
  1922. // summary:
  1923. // This function implements an over-ride of the italic command.
  1924. // argument:
  1925. // Not used, operates by selection.
  1926. // tags:
  1927. // protected
  1928. var applied = false;
  1929. if(has("ie") || has("trident")){
  1930. this._adaptIESelection();
  1931. applied = this._adaptIEFormatAreaAndExec("italic");
  1932. }
  1933. if(!applied){
  1934. applied = this.document.execCommand("italic", false, argument);
  1935. }
  1936. return applied;
  1937. },
  1938. _underlineImpl: function(argument){
  1939. // summary:
  1940. // This function implements an over-ride of the underline command.
  1941. // argument:
  1942. // Not used, operates by selection.
  1943. // tags:
  1944. // protected
  1945. var applied = false;
  1946. if(has("ie") || has("trident")){
  1947. this._adaptIESelection();
  1948. applied = this._adaptIEFormatAreaAndExec("underline");
  1949. }
  1950. if(!applied){
  1951. applied = this.document.execCommand("underline", false, argument);
  1952. }
  1953. return applied;
  1954. },
  1955. _strikethroughImpl: function(argument){
  1956. // summary:
  1957. // This function implements an over-ride of the strikethrough command.
  1958. // argument:
  1959. // Not used, operates by selection.
  1960. // tags:
  1961. // protected
  1962. var applied = false;
  1963. if(has("ie") || has("trident")){
  1964. this._adaptIESelection();
  1965. applied = this._adaptIEFormatAreaAndExec("strikethrough");
  1966. }
  1967. if(!applied){
  1968. applied = this.document.execCommand("strikethrough", false, argument);
  1969. }
  1970. return applied;
  1971. },
  1972. _superscriptImpl: function(argument){
  1973. // summary:
  1974. // This function implements an over-ride of the superscript command.
  1975. // argument:
  1976. // Not used, operates by selection.
  1977. // tags:
  1978. // protected
  1979. var applied = false;
  1980. if(has("ie") || has("trident")){
  1981. this._adaptIESelection();
  1982. applied = this._adaptIEFormatAreaAndExec("superscript");
  1983. }
  1984. if(!applied){
  1985. applied = this.document.execCommand("superscript", false, argument);
  1986. }
  1987. return applied;
  1988. },
  1989. _subscriptImpl: function(argument){
  1990. // summary:
  1991. // This function implements an over-ride of the superscript command.
  1992. // argument:
  1993. // Not used, operates by selection.
  1994. // tags:
  1995. // protected
  1996. var applied = false;
  1997. if(has("ie") || has("trident")){
  1998. this._adaptIESelection();
  1999. applied = this._adaptIEFormatAreaAndExec("subscript");
  2000. }
  2001. if(!applied){
  2002. applied = this.document.execCommand("subscript", false, argument);
  2003. }
  2004. return applied;
  2005. },
  2006. _fontnameImpl: function(argument){
  2007. // summary:
  2008. // This function implements the fontname command
  2009. // argument:
  2010. // arguments to the exec command, if any.
  2011. // tags:
  2012. // protected
  2013. var isApplied;
  2014. if(has("ie") || has("trident")){
  2015. isApplied = this._handleTextColorOrProperties("fontname", argument);
  2016. }
  2017. if(!isApplied){
  2018. isApplied = this.document.execCommand("fontname", false, argument);
  2019. }
  2020. return isApplied;
  2021. },
  2022. _fontsizeImpl: function(argument){
  2023. // summary:
  2024. // This function implements the fontsize command
  2025. // argument:
  2026. // arguments to the exec command, if any.
  2027. // tags:
  2028. // protected
  2029. var isApplied;
  2030. if(has("ie") || has("trident")){
  2031. isApplied = this._handleTextColorOrProperties("fontsize", argument);
  2032. }
  2033. if(!isApplied){
  2034. isApplied = this.document.execCommand("fontsize", false, argument);
  2035. }
  2036. return isApplied;
  2037. },
  2038. _insertorderedlistImpl: function(argument){
  2039. // summary:
  2040. // This function implements the insertorderedlist command
  2041. // argument:
  2042. // arguments to the exec command, if any.
  2043. // tags:
  2044. // protected
  2045. var applied = false;
  2046. if(has("ie") || has("trident")){
  2047. applied = this._adaptIEList("insertorderedlist", argument);
  2048. }
  2049. if(!applied){
  2050. applied = this.document.execCommand("insertorderedlist", false, argument);
  2051. }
  2052. return applied;
  2053. },
  2054. _insertunorderedlistImpl: function(argument){
  2055. // summary:
  2056. // This function implements the insertunorderedlist command
  2057. // argument:
  2058. // arguments to the exec command, if any.
  2059. // tags:
  2060. // protected
  2061. var applied = false;
  2062. if(has("ie") || has("trident")){
  2063. applied = this._adaptIEList("insertunorderedlist", argument);
  2064. }
  2065. if(!applied){
  2066. applied = this.document.execCommand("insertunorderedlist", false, argument);
  2067. }
  2068. return applied;
  2069. },
  2070. getHeaderHeight: function(){
  2071. // summary:
  2072. // A function for obtaining the height of the header node
  2073. return this._getNodeChildrenHeight(this.header); // Number
  2074. },
  2075. getFooterHeight: function(){
  2076. // summary:
  2077. // A function for obtaining the height of the footer node
  2078. return this._getNodeChildrenHeight(this.footer); // Number
  2079. },
  2080. _getNodeChildrenHeight: function(node){
  2081. // summary:
  2082. // An internal function for computing the cumulative height of all child nodes of 'node'
  2083. // node:
  2084. // The node to process the children of;
  2085. var h = 0;
  2086. if(node && node.childNodes){
  2087. // IE didn't compute it right when position was obtained on the node directly is some cases,
  2088. // so we have to walk over all the children manually.
  2089. var i;
  2090. for(i = 0; i < node.childNodes.length; i++){
  2091. var size = domGeometry.position(node.childNodes[i]);
  2092. h += size.h;
  2093. }
  2094. }
  2095. return h; // Number
  2096. },
  2097. _isNodeEmpty: function(node, startOffset){
  2098. // summary:
  2099. // Function to test if a node is devoid of real content.
  2100. // node:
  2101. // The node to check.
  2102. // tags:
  2103. // private.
  2104. if(node.nodeType === 1/*element*/){
  2105. if(node.childNodes.length > 0){
  2106. return this._isNodeEmpty(node.childNodes[0], startOffset);
  2107. }
  2108. return true;
  2109. }else if(node.nodeType === 3/*text*/){
  2110. return (node.nodeValue.substring(startOffset) === "");
  2111. }
  2112. return false;
  2113. },
  2114. _removeStartingRangeFromRange: function(node, range){
  2115. // summary:
  2116. // Function to adjust selection range by removing the current
  2117. // start node.
  2118. // node:
  2119. // The node to remove from the starting range.
  2120. // range:
  2121. // The range to adapt.
  2122. // tags:
  2123. // private
  2124. if(node.nextSibling){
  2125. range.setStart(node.nextSibling,0);
  2126. }else{
  2127. var parent = node.parentNode;
  2128. while(parent && parent.nextSibling == null){
  2129. //move up the tree until we find a parent that has another node, that node will be the next node
  2130. parent = parent.parentNode;
  2131. }
  2132. if(parent){
  2133. range.setStart(parent.nextSibling,0);
  2134. }
  2135. }
  2136. return range;
  2137. },
  2138. _adaptIESelection: function(){
  2139. // summary:
  2140. // Function to adapt the IE range by removing leading 'newlines'
  2141. // Needed to fix issue with bold/italics/underline not working if
  2142. // range included leading 'newlines'.
  2143. // In IE, if a user starts a selection at the very end of a line,
  2144. // then the native browser commands will fail to execute correctly.
  2145. // To work around the issue, we can remove all empty nodes from
  2146. // the start of the range selection.
  2147. var selection = rangeapi.getSelection(this.window);
  2148. if(selection && selection.rangeCount && !selection.isCollapsed){
  2149. var range = selection.getRangeAt(0);
  2150. var firstNode = range.startContainer;
  2151. var startOffset = range.startOffset;
  2152. while(firstNode.nodeType === 3/*text*/ && startOffset >= firstNode.length && firstNode.nextSibling){
  2153. //traverse the text nodes until we get to the one that is actually highlighted
  2154. startOffset = startOffset - firstNode.length;
  2155. firstNode = firstNode.nextSibling;
  2156. }
  2157. //Remove the starting ranges until the range does not start with an empty node.
  2158. var lastNode=null;
  2159. while(this._isNodeEmpty(firstNode, startOffset) && firstNode !== lastNode){
  2160. lastNode =firstNode; //this will break the loop in case we can't find the next sibling
  2161. range = this._removeStartingRangeFromRange(firstNode, range); //move the start container to the next node in the range
  2162. firstNode = range.startContainer;
  2163. startOffset = 0; //start at the beginning of the new starting range
  2164. }
  2165. selection.removeAllRanges();// this will work as long as users cannot select multiple ranges. I have not been able to do that in the editor.
  2166. selection.addRange(range);
  2167. }
  2168. },
  2169. _adaptIEFormatAreaAndExec: function(command){
  2170. // summary:
  2171. // Function to handle IE's quirkiness regarding how it handles
  2172. // format commands on a word. This involves a lit of node splitting
  2173. // and format cloning.
  2174. // command:
  2175. // The format command, needed to check if the desired
  2176. // command is true or not.
  2177. var selection = rangeapi.getSelection(this.window);
  2178. var doc = this.document;
  2179. var rs, ret, range, txt, startNode, endNode, breaker, sNode;
  2180. if(command && selection && selection.isCollapsed){
  2181. var isApplied = this.queryCommandValue(command);
  2182. if(isApplied){
  2183. // We have to split backwards until we hit the format
  2184. var nNames = this._tagNamesForCommand(command);
  2185. range = selection.getRangeAt(0);
  2186. var fs = range.startContainer;
  2187. if(fs.nodeType === 3){
  2188. var offset = range.endOffset;
  2189. if(fs.length < offset){
  2190. //We are not looking from the right node, try to locate the correct one
  2191. ret = this._adjustNodeAndOffset(rs, offset);
  2192. fs = ret.node;
  2193. offset = ret.offset;
  2194. }
  2195. }
  2196. var topNode;
  2197. while(fs && fs !== this.editNode){
  2198. // We have to walk back and see if this is still a format or not.
  2199. // Hm, how do I do this?
  2200. var tName = fs.tagName? fs.tagName.toLowerCase() : "";
  2201. if(array.indexOf(nNames, tName) > -1){
  2202. topNode = fs;
  2203. break;
  2204. }
  2205. fs = fs.parentNode;
  2206. }
  2207. // Okay, we have a stopping place, time to split things apart.
  2208. if(topNode){
  2209. // Okay, we know how far we have to split backwards, so we have to split now.
  2210. rs = range.startContainer;
  2211. var newblock = doc.createElement(topNode.tagName);
  2212. domConstruct.place(newblock, topNode, "after");
  2213. if(rs && rs.nodeType === 3){
  2214. // Text node, we have to split it.
  2215. var nodeToMove, tNode;
  2216. var endOffset = range.endOffset;
  2217. if(rs.length < endOffset){
  2218. //We are not splitting the right node, try to locate the correct one
  2219. ret = this._adjustNodeAndOffset(rs, endOffset);
  2220. rs = ret.node;
  2221. endOffset = ret.offset;
  2222. }
  2223. txt = rs.nodeValue;
  2224. startNode = doc.createTextNode(txt.substring(0, endOffset));
  2225. var endText = txt.substring(endOffset, txt.length);
  2226. if(endText){
  2227. endNode = doc.createTextNode(endText);
  2228. }
  2229. // Place the split, then remove original nodes.
  2230. domConstruct.place(startNode, rs, "before");
  2231. if(endNode){
  2232. breaker = doc.createElement("span");
  2233. breaker.className = "ieFormatBreakerSpan";
  2234. domConstruct.place(breaker, rs, "after");
  2235. domConstruct.place(endNode, breaker, "after");
  2236. endNode = breaker;
  2237. }
  2238. domConstruct.destroy(rs);
  2239. // Okay, we split the text. Now we need to see if we're
  2240. // parented to the block element we're splitting and if
  2241. // not, we have to split all the way up. Ugh.
  2242. var parentC = startNode.parentNode;
  2243. var tagList = [];
  2244. var tagData;
  2245. while(parentC !== topNode){
  2246. var tg = parentC.tagName;
  2247. tagData = {tagName: tg};
  2248. tagList.push(tagData);
  2249. var newTg = doc.createElement(tg);
  2250. // Clone over any 'style' data.
  2251. if(parentC.style){
  2252. if(newTg.style){
  2253. if(parentC.style.cssText){
  2254. newTg.style.cssText = parentC.style.cssText;
  2255. tagData.cssText = parentC.style.cssText;
  2256. }
  2257. }
  2258. }
  2259. // If font also need to clone over any font data.
  2260. if(parentC.tagName === "FONT"){
  2261. if(parentC.color){
  2262. newTg.color = parentC.color;
  2263. tagData.color = parentC.color;
  2264. }
  2265. if(parentC.face){
  2266. newTg.face = parentC.face;
  2267. tagData.face = parentC.face;
  2268. }
  2269. if(parentC.size){ // this check was necessary on IE
  2270. newTg.size = parentC.size;
  2271. tagData.size = parentC.size;
  2272. }
  2273. }
  2274. if(parentC.className){
  2275. newTg.className = parentC.className;
  2276. tagData.className = parentC.className;
  2277. }
  2278. // Now move end node and every sibling
  2279. // after it over into the new tag.
  2280. if(endNode){
  2281. nodeToMove = endNode;
  2282. while(nodeToMove){
  2283. tNode = nodeToMove.nextSibling;
  2284. newTg.appendChild(nodeToMove);
  2285. nodeToMove = tNode;
  2286. }
  2287. }
  2288. if(newTg.tagName == parentC.tagName){
  2289. breaker = doc.createElement("span");
  2290. breaker.className = "ieFormatBreakerSpan";
  2291. domConstruct.place(breaker, parentC, "after");
  2292. domConstruct.place(newTg, breaker, "after");
  2293. }else{
  2294. domConstruct.place(newTg, parentC, "after");
  2295. }
  2296. startNode = parentC;
  2297. endNode = newTg;
  2298. parentC = parentC.parentNode;
  2299. }
  2300. // Lastly, move the split out all the split tags
  2301. // to the new block as they should now be split properly.
  2302. if(endNode){
  2303. nodeToMove = endNode;
  2304. if(nodeToMove.nodeType === 1 || (nodeToMove.nodeType === 3 && nodeToMove.nodeValue)){
  2305. // Non-blank text and non-text nodes need to clear out that blank space
  2306. // before moving the contents.
  2307. newblock.innerHTML = "";
  2308. }
  2309. while(nodeToMove){
  2310. tNode = nodeToMove.nextSibling;
  2311. newblock.appendChild(nodeToMove);
  2312. nodeToMove = tNode;
  2313. }
  2314. }
  2315. // We had intermediate tags, we have to now recreate them inbetween the split
  2316. // and restore what styles, classnames, etc, we can.
  2317. if(tagList.length){
  2318. tagData = tagList.pop();
  2319. var newContTag = doc.createElement(tagData.tagName);
  2320. if(tagData.cssText && newContTag.style){
  2321. newContTag.style.cssText = tagData.cssText;
  2322. }
  2323. if(tagData.className){
  2324. newContTag.className = tagData.className;
  2325. }
  2326. if(tagData.tagName === "FONT"){
  2327. if(tagData.color){
  2328. newContTag.color = tagData.color;
  2329. }
  2330. if(tagData.face){
  2331. newContTag.face = tagData.face;
  2332. }
  2333. if(tagData.size){
  2334. newContTag.size = tagData.size;
  2335. }
  2336. }
  2337. domConstruct.place(newContTag, newblock, "before");
  2338. while(tagList.length){
  2339. tagData = tagList.pop();
  2340. var newTgNode = doc.createElement(tagData.tagName);
  2341. if(tagData.cssText && newTgNode.style){
  2342. newTgNode.style.cssText = tagData.cssText;
  2343. }
  2344. if(tagData.className){
  2345. newTgNode.className = tagData.className;
  2346. }
  2347. if(tagData.tagName === "FONT"){
  2348. if(tagData.color){
  2349. newTgNode.color = tagData.color;
  2350. }
  2351. if(tagData.face){
  2352. newTgNode.face = tagData.face;
  2353. }
  2354. if(tagData.size){
  2355. newTgNode.size = tagData.size;
  2356. }
  2357. }
  2358. newContTag.appendChild(newTgNode);
  2359. newContTag = newTgNode;
  2360. }
  2361. // Okay, everything is theoretically split apart and removed from the content
  2362. // so insert the dummy text to select, select it, then
  2363. // clear to position cursor.
  2364. sNode = doc.createTextNode(".");
  2365. breaker.appendChild(sNode);
  2366. newContTag.appendChild(sNode);
  2367. win.withGlobal(this.window, lang.hitch(this, function(){
  2368. var newrange = rangeapi.create();
  2369. newrange.setStart(sNode, 0);
  2370. newrange.setEnd(sNode, sNode.length);
  2371. selection.removeAllRanges();
  2372. selection.addRange(newrange);
  2373. selectionapi.collapse(false);
  2374. sNode.parentNode.innerHTML = "";
  2375. }));
  2376. }else{
  2377. // No extra tags, so we have to insert a breaker point and rely
  2378. // on filters to remove it later.
  2379. breaker = doc.createElement("span");
  2380. breaker.className="ieFormatBreakerSpan";
  2381. sNode = doc.createTextNode(".");
  2382. breaker.appendChild(sNode);
  2383. domConstruct.place(breaker, newblock, "before");
  2384. win.withGlobal(this.window, lang.hitch(this, function(){
  2385. var newrange = rangeapi.create();
  2386. newrange.setStart(sNode, 0);
  2387. newrange.setEnd(sNode, sNode.length);
  2388. selection.removeAllRanges();
  2389. selection.addRange(newrange);
  2390. selectionapi.collapse(false);
  2391. sNode.parentNode.innerHTML = "";
  2392. }));
  2393. }
  2394. if(!newblock.firstChild){
  2395. // Empty, we don't need it. Split was at end or similar
  2396. // So, remove it.
  2397. domConstruct.destroy(newblock);
  2398. }
  2399. return true;
  2400. }
  2401. }
  2402. return false;
  2403. }else{
  2404. range = selection.getRangeAt(0);
  2405. rs = range.startContainer;
  2406. if(rs && rs.nodeType === 3){
  2407. // Text node, we have to split it.
  2408. win.withGlobal(this.window, lang.hitch(this, function(){
  2409. var offset = range.startOffset;
  2410. if(rs.length < offset){
  2411. //We are not splitting the right node, try to locate the correct one
  2412. ret = this._adjustNodeAndOffset(rs, offset);
  2413. rs = ret.node;
  2414. offset = ret.offset;
  2415. }
  2416. txt = rs.nodeValue;
  2417. startNode = doc.createTextNode(txt.substring(0, offset));
  2418. var endText = txt.substring(offset);
  2419. if(endText !== ""){
  2420. endNode = doc.createTextNode(txt.substring(offset));
  2421. }
  2422. // Create a space, we'll select and bold it, so
  2423. // the whole word doesn't get bolded
  2424. breaker = doc.createElement("span");
  2425. sNode = doc.createTextNode(".");
  2426. breaker.appendChild(sNode);
  2427. if(startNode.length){
  2428. domConstruct.place(startNode, rs, "after");
  2429. }else{
  2430. startNode = rs;
  2431. }
  2432. domConstruct.place(breaker, startNode, "after");
  2433. if(endNode){
  2434. domConstruct.place(endNode, breaker, "after");
  2435. }
  2436. domConstruct.destroy(rs);
  2437. var newrange = rangeapi.create();
  2438. newrange.setStart(sNode, 0);
  2439. newrange.setEnd(sNode, sNode.length);
  2440. selection.removeAllRanges();
  2441. selection.addRange(newrange);
  2442. doc.execCommand(command);
  2443. domConstruct.place(breaker.firstChild, breaker, "before");
  2444. domConstruct.destroy(breaker);
  2445. newrange.setStart(sNode, 0);
  2446. newrange.setEnd(sNode, sNode.length);
  2447. selection.removeAllRanges();
  2448. selection.addRange(newrange);
  2449. selectionapi.collapse(false);
  2450. sNode.parentNode.innerHTML = "";
  2451. }));
  2452. return true;
  2453. }
  2454. }
  2455. }else{
  2456. return false;
  2457. }
  2458. },
  2459. _adaptIEList: function(command /*===== , argument =====*/){
  2460. // summary:
  2461. // This function handles normalizing the IE list behavior as
  2462. // much as possible.
  2463. // command:
  2464. // The list command to execute.
  2465. // argument:
  2466. // Any additional argument.
  2467. // tags:
  2468. // private
  2469. var selection = rangeapi.getSelection(this.window);
  2470. if(selection.isCollapsed){
  2471. // In the case of no selection, lets commonize the behavior and
  2472. // make sure that it indents if needed.
  2473. if(selection.rangeCount && !this.queryCommandValue(command)){
  2474. var range = selection.getRangeAt(0);
  2475. var sc = range.startContainer;
  2476. if(sc && sc.nodeType == 3){
  2477. // text node. Lets see if there is a node before it that isn't
  2478. // some sort of breaker.
  2479. if(!range.startOffset){
  2480. // We're at the beginning of a text area. It may have been br split
  2481. // Who knows? In any event, we must create the list manually
  2482. // or IE may shove too much into the list element. It seems to
  2483. // grab content before the text node too if it's br split.
  2484. // Why can't IE work like everyone else?
  2485. win.withGlobal(this.window, lang.hitch(this, function(){
  2486. // Create a space, we'll select and bold it, so
  2487. // the whole word doesn't get bolded
  2488. var lType = "ul";
  2489. if(command === "insertorderedlist"){
  2490. lType = "ol";
  2491. }
  2492. var list = domConstruct.create(lType);
  2493. var li = domConstruct.create("li", null, list);
  2494. domConstruct.place(list, sc, "before");
  2495. // Move in the text node as part of the li.
  2496. li.appendChild(sc);
  2497. // We need a br after it or the enter key handler
  2498. // sometimes throws errors.
  2499. domConstruct.create("br", null, list, "after");
  2500. // Okay, now lets move our cursor to the beginning.
  2501. var newrange = rangeapi.create();
  2502. newrange.setStart(sc, 0);
  2503. newrange.setEnd(sc, sc.length);
  2504. selection.removeAllRanges();
  2505. selection.addRange(newrange);
  2506. selectionapi.collapse(true);
  2507. }));
  2508. return true;
  2509. }
  2510. }
  2511. }
  2512. }
  2513. return false;
  2514. },
  2515. _handleTextColorOrProperties: function(command, argument){
  2516. // summary:
  2517. // This function handles appplying text color as best it is
  2518. // able to do so when the selection is collapsed, making the
  2519. // behavior cross-browser consistent. It also handles the name
  2520. // and size for IE.
  2521. // command:
  2522. // The command.
  2523. // argument:
  2524. // Any additional arguments.
  2525. // tags:
  2526. // private
  2527. var selection = rangeapi.getSelection(this.window);
  2528. var doc = this.document;
  2529. var rs, ret, range, txt, startNode, endNode, breaker, sNode;
  2530. argument = argument || null;
  2531. if(command && selection && selection.isCollapsed){
  2532. if(selection.rangeCount){
  2533. range = selection.getRangeAt(0);
  2534. rs = range.startContainer;
  2535. if(rs && rs.nodeType === 3){
  2536. // Text node, we have to split it.
  2537. win.withGlobal(this.window, lang.hitch(this, function(){
  2538. var offset = range.startOffset;
  2539. if(rs.length < offset){
  2540. //We are not splitting the right node, try to locate the correct one
  2541. ret = this._adjustNodeAndOffset(rs, offset);
  2542. rs = ret.node;
  2543. offset = ret.offset;
  2544. }
  2545. txt = rs.nodeValue;
  2546. startNode = doc.createTextNode(txt.substring(0, offset));
  2547. var endText = txt.substring(offset);
  2548. if(endText !== ""){
  2549. endNode = doc.createTextNode(txt.substring(offset));
  2550. }
  2551. // Create a space, we'll select and bold it, so
  2552. // the whole word doesn't get bolded
  2553. breaker = domConstruct.create("span");
  2554. sNode = doc.createTextNode(".");
  2555. breaker.appendChild(sNode);
  2556. // Create a junk node to avoid it trying to stlye the breaker.
  2557. // This will get destroyed later.
  2558. var extraSpan = domConstruct.create("span");
  2559. breaker.appendChild(extraSpan);
  2560. if(startNode.length){
  2561. domConstruct.place(startNode, rs, "after");
  2562. }else{
  2563. startNode = rs;
  2564. }
  2565. domConstruct.place(breaker, startNode, "after");
  2566. if(endNode){
  2567. domConstruct.place(endNode, breaker, "after");
  2568. }
  2569. domConstruct.destroy(rs);
  2570. var newrange = rangeapi.create();
  2571. newrange.setStart(sNode, 0);
  2572. newrange.setEnd(sNode, sNode.length);
  2573. selection.removeAllRanges();
  2574. selection.addRange(newrange);
  2575. if(has("webkit")){
  2576. // WebKit is frustrating with positioning the cursor.
  2577. // It stinks to have a selected space, but there really
  2578. // isn't much choice here.
  2579. var style = "color";
  2580. if(command === "hilitecolor" || command === "backcolor"){
  2581. style = "backgroundColor";
  2582. }
  2583. domStyle.set(breaker, style, argument);
  2584. selectionapi.remove();
  2585. domConstruct.destroy(extraSpan);
  2586. breaker.innerHTML = "&#160;"; // &nbsp;
  2587. selectionapi.selectElement(breaker);
  2588. this.focus();
  2589. }else{
  2590. this.execCommand(command, argument);
  2591. domConstruct.place(breaker.firstChild, breaker, "before");
  2592. domConstruct.destroy(breaker);
  2593. newrange.setStart(sNode, 0);
  2594. newrange.setEnd(sNode, sNode.length);
  2595. selection.removeAllRanges();
  2596. selection.addRange(newrange);
  2597. selectionapi.collapse(false);
  2598. sNode.parentNode.removeChild(sNode);
  2599. }
  2600. }));
  2601. return true;
  2602. }
  2603. }
  2604. }
  2605. return false;
  2606. },
  2607. _adjustNodeAndOffset: function(/*DomNode*/node, /*Int*/offset){
  2608. // summary:
  2609. // In the case there are multiple text nodes in a row the offset may not be within the node.
  2610. // If the offset is larger than the node length, it will attempt to find
  2611. // the next text sibling until it locates the text node in which the offset refers to
  2612. // node:
  2613. // The node to check.
  2614. // offset:
  2615. // The position to find within the text node
  2616. // tags:
  2617. // private.
  2618. while(node.length < offset && node.nextSibling && node.nextSibling.nodeType === 3){
  2619. //Adjust the offset and node in the case of multiple text nodes in a row
  2620. offset = offset - node.length;
  2621. node = node.nextSibling;
  2622. }
  2623. return {"node": node, "offset": offset};
  2624. },
  2625. _tagNamesForCommand: function(command){
  2626. // summary:
  2627. // Function to return the tab names that are associated
  2628. // with a particular style.
  2629. // command: String
  2630. // The command to return tags for.
  2631. // tags:
  2632. // private
  2633. if(command === "bold"){
  2634. return ["b", "strong"];
  2635. }else if(command === "italic"){
  2636. return ["i","em"];
  2637. }else if(command === "strikethrough"){
  2638. return ["s", "strike"];
  2639. }else if(command === "superscript"){
  2640. return ["sup"];
  2641. }else if(command === "subscript"){
  2642. return ["sub"];
  2643. }else if(command === "underline"){
  2644. return ["u"];
  2645. }
  2646. return [];
  2647. },
  2648. _stripBreakerNodes: function(node){
  2649. // summary:
  2650. // Function for stripping out the breaker spans inserted by the formatting command.
  2651. // Registered as a filter for IE, handles the breaker spans needed to fix up
  2652. // How bold/italic/etc, work when selection is collapsed (single cursor).
  2653. win.withGlobal(this.window, lang.hitch(this, function(){
  2654. var breakers = query(".ieFormatBreakerSpan", node);
  2655. var i;
  2656. for(i = 0; i < breakers.length; i++){
  2657. var b = breakers[i];
  2658. while(b.firstChild){
  2659. domConstruct.place(b.firstChild, b, "before");
  2660. }
  2661. domConstruct.destroy(b);
  2662. }
  2663. }));
  2664. return node;
  2665. }
  2666. });
  2667. return BuxRichText;
  2668. });