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