|
1 /** |
|
2 * Editor.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /*jshint scripturl:true */ |
|
12 |
|
13 /** |
|
14 * Include the base event class documentation. |
|
15 * |
|
16 * @include ../../../tools/docs/tinymce.Event.js |
|
17 */ |
|
18 |
|
19 /** |
|
20 * This class contains the core logic for a TinyMCE editor. |
|
21 * |
|
22 * @class tinymce.Editor |
|
23 * @mixes tinymce.util.Observable |
|
24 * @example |
|
25 * // Add a class to all paragraphs in the editor. |
|
26 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); |
|
27 * |
|
28 * // Gets the current editors selection as text |
|
29 * tinymce.activeEditor.selection.getContent({format: 'text'}); |
|
30 * |
|
31 * // Creates a new editor instance |
|
32 * var ed = new tinymce.Editor('textareaid', { |
|
33 * some_setting: 1 |
|
34 * }, tinymce.EditorManager); |
|
35 * |
|
36 * // Select each item the user clicks on |
|
37 * ed.on('click', function(e) { |
|
38 * ed.selection.select(e.target); |
|
39 * }); |
|
40 * |
|
41 * ed.render(); |
|
42 */ |
|
43 define("tinymce/Editor", [ |
|
44 "tinymce/dom/DOMUtils", |
|
45 "tinymce/dom/DomQuery", |
|
46 "tinymce/AddOnManager", |
|
47 "tinymce/NodeChange", |
|
48 "tinymce/html/Node", |
|
49 "tinymce/dom/Serializer", |
|
50 "tinymce/html/Serializer", |
|
51 "tinymce/dom/Selection", |
|
52 "tinymce/Formatter", |
|
53 "tinymce/UndoManager", |
|
54 "tinymce/EnterKey", |
|
55 "tinymce/ForceBlocks", |
|
56 "tinymce/EditorCommands", |
|
57 "tinymce/util/URI", |
|
58 "tinymce/dom/ScriptLoader", |
|
59 "tinymce/dom/EventUtils", |
|
60 "tinymce/WindowManager", |
|
61 "tinymce/html/Schema", |
|
62 "tinymce/html/DomParser", |
|
63 "tinymce/util/Quirks", |
|
64 "tinymce/Env", |
|
65 "tinymce/util/Tools", |
|
66 "tinymce/EditorObservable", |
|
67 "tinymce/Shortcuts" |
|
68 ], function( |
|
69 DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer, |
|
70 Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands, |
|
71 URI, ScriptLoader, EventUtils, WindowManager, |
|
72 Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts |
|
73 ) { |
|
74 // Shorten these names |
|
75 var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager; |
|
76 var extend = Tools.extend, each = Tools.each, explode = Tools.explode; |
|
77 var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve; |
|
78 var Event = EventUtils.Event; |
|
79 var isGecko = Env.gecko, ie = Env.ie; |
|
80 |
|
81 /** |
|
82 * Include documentation for all the events. |
|
83 * |
|
84 * @include ../../../tools/docs/tinymce.Editor.js |
|
85 */ |
|
86 |
|
87 /** |
|
88 * Constructs a editor instance by id. |
|
89 * |
|
90 * @constructor |
|
91 * @method Editor |
|
92 * @param {String} id Unique id for the editor. |
|
93 * @param {Object} settings Settings for the editor. |
|
94 * @param {tinymce.EditorManager} editorManager EditorManager instance. |
|
95 * @author Moxiecode |
|
96 */ |
|
97 function Editor(id, settings, editorManager) { |
|
98 var self = this, documentBaseUrl, baseUri; |
|
99 |
|
100 documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; |
|
101 baseUri = editorManager.baseURI; |
|
102 |
|
103 /** |
|
104 * Name/value collection with editor settings. |
|
105 * |
|
106 * @property settings |
|
107 * @type Object |
|
108 * @example |
|
109 * // Get the value of the theme setting |
|
110 * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); |
|
111 */ |
|
112 self.settings = settings = extend({ |
|
113 id: id, |
|
114 theme: 'modern', |
|
115 delta_width: 0, |
|
116 delta_height: 0, |
|
117 popup_css: '', |
|
118 plugins: '', |
|
119 document_base_url: documentBaseUrl, |
|
120 add_form_submit_trigger: true, |
|
121 submit_patch: true, |
|
122 add_unload_trigger: true, |
|
123 convert_urls: true, |
|
124 relative_urls: true, |
|
125 remove_script_host: true, |
|
126 object_resizing: true, |
|
127 doctype: '<!DOCTYPE html>', |
|
128 visual: true, |
|
129 font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large', |
|
130 |
|
131 // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size |
|
132 font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%', |
|
133 forced_root_block: 'p', |
|
134 hidden_input: true, |
|
135 padd_empty_editor: true, |
|
136 render_ui: true, |
|
137 indentation: '30px', |
|
138 inline_styles: true, |
|
139 convert_fonts_to_spans: true, |
|
140 indent: 'simple', |
|
141 indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + |
|
142 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', |
|
143 indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + |
|
144 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', |
|
145 validate: true, |
|
146 entity_encoding: 'named', |
|
147 url_converter: self.convertURL, |
|
148 url_converter_scope: self, |
|
149 ie7_compat: true |
|
150 }, settings); |
|
151 |
|
152 AddOnManager.language = settings.language || 'en'; |
|
153 AddOnManager.languageLoad = settings.language_load; |
|
154 |
|
155 AddOnManager.baseURL = editorManager.baseURL; |
|
156 |
|
157 /** |
|
158 * Editor instance id, normally the same as the div/textarea that was replaced. |
|
159 * |
|
160 * @property id |
|
161 * @type String |
|
162 */ |
|
163 self.id = settings.id = id; |
|
164 |
|
165 /** |
|
166 * State to force the editor to return false on a isDirty call. |
|
167 * |
|
168 * @property isNotDirty |
|
169 * @type Boolean |
|
170 * @example |
|
171 * function ajaxSave() { |
|
172 * var ed = tinymce.get('elm1'); |
|
173 * |
|
174 * // Save contents using some XHR call |
|
175 * alert(ed.getContent()); |
|
176 * |
|
177 * ed.isNotDirty = true; // Force not dirty state |
|
178 * } |
|
179 */ |
|
180 self.isNotDirty = true; |
|
181 |
|
182 /** |
|
183 * Name/Value object containting plugin instances. |
|
184 * |
|
185 * @property plugins |
|
186 * @type Object |
|
187 * @example |
|
188 * // Execute a method inside a plugin directly |
|
189 * tinymce.activeEditor.plugins.someplugin.someMethod(); |
|
190 */ |
|
191 self.plugins = {}; |
|
192 |
|
193 /** |
|
194 * URI object to document configured for the TinyMCE instance. |
|
195 * |
|
196 * @property documentBaseURI |
|
197 * @type tinymce.util.URI |
|
198 * @example |
|
199 * // Get relative URL from the location of document_base_url |
|
200 * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); |
|
201 * |
|
202 * // Get absolute URL from the location of document_base_url |
|
203 * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); |
|
204 */ |
|
205 self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, { |
|
206 base_uri: baseUri |
|
207 }); |
|
208 |
|
209 /** |
|
210 * URI object to current document that holds the TinyMCE editor instance. |
|
211 * |
|
212 * @property baseURI |
|
213 * @type tinymce.util.URI |
|
214 * @example |
|
215 * // Get relative URL from the location of the API |
|
216 * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); |
|
217 * |
|
218 * // Get absolute URL from the location of the API |
|
219 * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); |
|
220 */ |
|
221 self.baseURI = baseUri; |
|
222 |
|
223 /** |
|
224 * Array with CSS files to load into the iframe. |
|
225 * |
|
226 * @property contentCSS |
|
227 * @type Array |
|
228 */ |
|
229 self.contentCSS = []; |
|
230 |
|
231 /** |
|
232 * Array of CSS styles to add to head of document when the editor loads. |
|
233 * |
|
234 * @property contentStyles |
|
235 * @type Array |
|
236 */ |
|
237 self.contentStyles = []; |
|
238 |
|
239 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic |
|
240 self.shortcuts = new Shortcuts(self); |
|
241 self.loadedCSS = {}; |
|
242 self.editorCommands = new EditorCommands(self); |
|
243 |
|
244 if (settings.target) { |
|
245 self.targetElm = settings.target; |
|
246 } |
|
247 |
|
248 self.suffix = editorManager.suffix; |
|
249 self.editorManager = editorManager; |
|
250 self.inline = settings.inline; |
|
251 |
|
252 if (settings.cache_suffix) { |
|
253 Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, ''); |
|
254 } |
|
255 |
|
256 // Call setup |
|
257 editorManager.fire('SetupEditor', self); |
|
258 self.execCallback('setup', self); |
|
259 |
|
260 /** |
|
261 * Dom query instance with default scope to the editor document and default element is the body of the editor. |
|
262 * |
|
263 * @property $ |
|
264 * @type tinymce.dom.DomQuery |
|
265 * @example |
|
266 * tinymce.activeEditor.$('p').css('color', 'red'); |
|
267 * tinymce.activeEditor.$().append('<p>new</p>'); |
|
268 */ |
|
269 self.$ = DomQuery.overrideDefaults(function() { |
|
270 return { |
|
271 context: self.inline ? self.getBody() : self.getDoc(), |
|
272 element: self.getBody() |
|
273 }; |
|
274 }); |
|
275 } |
|
276 |
|
277 Editor.prototype = { |
|
278 /** |
|
279 * Renderes the editor/adds it to the page. |
|
280 * |
|
281 * @method render |
|
282 */ |
|
283 render: function() { |
|
284 var self = this, settings = self.settings, id = self.id, suffix = self.suffix; |
|
285 |
|
286 function readyHandler() { |
|
287 DOM.unbind(window, 'ready', readyHandler); |
|
288 self.render(); |
|
289 } |
|
290 |
|
291 // Page is not loaded yet, wait for it |
|
292 if (!Event.domLoaded) { |
|
293 DOM.bind(window, 'ready', readyHandler); |
|
294 return; |
|
295 } |
|
296 |
|
297 // Element not found, then skip initialization |
|
298 if (!self.getElement()) { |
|
299 return; |
|
300 } |
|
301 |
|
302 // No editable support old iOS versions etc |
|
303 if (!Env.contentEditable) { |
|
304 return; |
|
305 } |
|
306 |
|
307 // Hide target element early to prevent content flashing |
|
308 if (!settings.inline) { |
|
309 self.orgVisibility = self.getElement().style.visibility; |
|
310 self.getElement().style.visibility = 'hidden'; |
|
311 } else { |
|
312 self.inline = true; |
|
313 } |
|
314 |
|
315 var form = self.getElement().form || DOM.getParent(id, 'form'); |
|
316 if (form) { |
|
317 self.formElement = form; |
|
318 |
|
319 // Add hidden input for non input elements inside form elements |
|
320 if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) { |
|
321 DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id); |
|
322 self.hasHiddenInput = true; |
|
323 } |
|
324 |
|
325 // Pass submit/reset from form to editor instance |
|
326 self.formEventDelegate = function(e) { |
|
327 self.fire(e.type, e); |
|
328 }; |
|
329 |
|
330 DOM.bind(form, 'submit reset', self.formEventDelegate); |
|
331 |
|
332 // Reset contents in editor when the form is reset |
|
333 self.on('reset', function() { |
|
334 self.setContent(self.startContent, {format: 'raw'}); |
|
335 }); |
|
336 |
|
337 // Check page uses id="submit" or name="submit" for it's submit button |
|
338 if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { |
|
339 form._mceOldSubmit = form.submit; |
|
340 form.submit = function() { |
|
341 self.editorManager.triggerSave(); |
|
342 self.isNotDirty = true; |
|
343 |
|
344 return form._mceOldSubmit(form); |
|
345 }; |
|
346 } |
|
347 } |
|
348 |
|
349 /** |
|
350 * Window manager reference, use this to open new windows and dialogs. |
|
351 * |
|
352 * @property windowManager |
|
353 * @type tinymce.WindowManager |
|
354 * @example |
|
355 * // Shows an alert message |
|
356 * tinymce.activeEditor.windowManager.alert('Hello world!'); |
|
357 * |
|
358 * // Opens a new dialog with the file.htm file and the size 320x240 |
|
359 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. |
|
360 * tinymce.activeEditor.windowManager.open({ |
|
361 * url: 'file.htm', |
|
362 * width: 320, |
|
363 * height: 240 |
|
364 * }, { |
|
365 * custom_param: 1 |
|
366 * }); |
|
367 */ |
|
368 self.windowManager = new WindowManager(self); |
|
369 |
|
370 if (settings.encoding == 'xml') { |
|
371 self.on('GetContent', function(e) { |
|
372 if (e.save) { |
|
373 e.content = DOM.encode(e.content); |
|
374 } |
|
375 }); |
|
376 } |
|
377 |
|
378 if (settings.add_form_submit_trigger) { |
|
379 self.on('submit', function() { |
|
380 if (self.initialized) { |
|
381 self.save(); |
|
382 } |
|
383 }); |
|
384 } |
|
385 |
|
386 if (settings.add_unload_trigger) { |
|
387 self._beforeUnload = function() { |
|
388 if (self.initialized && !self.destroyed && !self.isHidden()) { |
|
389 self.save({format: 'raw', no_events: true, set_dirty: false}); |
|
390 } |
|
391 }; |
|
392 |
|
393 self.editorManager.on('BeforeUnload', self._beforeUnload); |
|
394 } |
|
395 |
|
396 // Load scripts |
|
397 function loadScripts() { |
|
398 var scriptLoader = ScriptLoader.ScriptLoader; |
|
399 |
|
400 if (settings.language && settings.language != 'en' && !settings.language_url) { |
|
401 settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js'; |
|
402 } |
|
403 |
|
404 if (settings.language_url) { |
|
405 scriptLoader.add(settings.language_url); |
|
406 } |
|
407 |
|
408 if (settings.theme && typeof settings.theme != "function" && |
|
409 settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) { |
|
410 var themeUrl = settings.theme_url; |
|
411 |
|
412 if (themeUrl) { |
|
413 themeUrl = self.documentBaseURI.toAbsolute(themeUrl); |
|
414 } else { |
|
415 themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js'; |
|
416 } |
|
417 |
|
418 ThemeManager.load(settings.theme, themeUrl); |
|
419 } |
|
420 |
|
421 if (Tools.isArray(settings.plugins)) { |
|
422 settings.plugins = settings.plugins.join(' '); |
|
423 } |
|
424 |
|
425 each(settings.external_plugins, function(url, name) { |
|
426 PluginManager.load(name, url); |
|
427 settings.plugins += ' ' + name; |
|
428 }); |
|
429 |
|
430 each(settings.plugins.split(/[ ,]/), function(plugin) { |
|
431 plugin = trim(plugin); |
|
432 |
|
433 if (plugin && !PluginManager.urls[plugin]) { |
|
434 if (plugin.charAt(0) == '-') { |
|
435 plugin = plugin.substr(1, plugin.length); |
|
436 |
|
437 var dependencies = PluginManager.dependencies(plugin); |
|
438 |
|
439 each(dependencies, function(dep) { |
|
440 var defaultSettings = { |
|
441 prefix: 'plugins/', |
|
442 resource: dep, |
|
443 suffix: '/plugin' + suffix + '.js' |
|
444 }; |
|
445 |
|
446 dep = PluginManager.createUrl(defaultSettings, dep); |
|
447 PluginManager.load(dep.resource, dep); |
|
448 }); |
|
449 } else { |
|
450 PluginManager.load(plugin, { |
|
451 prefix: 'plugins/', |
|
452 resource: plugin, |
|
453 suffix: '/plugin' + suffix + '.js' |
|
454 }); |
|
455 } |
|
456 } |
|
457 }); |
|
458 |
|
459 scriptLoader.loadQueue(function() { |
|
460 if (!self.removed) { |
|
461 self.init(); |
|
462 } |
|
463 }); |
|
464 } |
|
465 |
|
466 loadScripts(); |
|
467 }, |
|
468 |
|
469 /** |
|
470 * Initializes the editor this will be called automatically when |
|
471 * all plugins/themes and language packs are loaded by the rendered method. |
|
472 * This method will setup the iframe and create the theme and plugin instances. |
|
473 * |
|
474 * @method init |
|
475 */ |
|
476 init: function() { |
|
477 var self = this, settings = self.settings, elm = self.getElement(); |
|
478 var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = []; |
|
479 |
|
480 this.editorManager.i18n.setCode(settings.language); |
|
481 self.rtl = this.editorManager.i18n.rtl; |
|
482 self.editorManager.add(self); |
|
483 |
|
484 settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area')); |
|
485 |
|
486 /** |
|
487 * Reference to the theme instance that was used to generate the UI. |
|
488 * |
|
489 * @property theme |
|
490 * @type tinymce.Theme |
|
491 * @example |
|
492 * // Executes a method on the theme directly |
|
493 * tinymce.activeEditor.theme.someMethod(); |
|
494 */ |
|
495 if (settings.theme) { |
|
496 if (typeof settings.theme != "function") { |
|
497 settings.theme = settings.theme.replace(/-/, ''); |
|
498 Theme = ThemeManager.get(settings.theme); |
|
499 self.theme = new Theme(self, ThemeManager.urls[settings.theme]); |
|
500 |
|
501 if (self.theme.init) { |
|
502 self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$); |
|
503 } |
|
504 } else { |
|
505 self.theme = settings.theme; |
|
506 } |
|
507 } |
|
508 |
|
509 function initPlugin(plugin) { |
|
510 var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance; |
|
511 |
|
512 pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, ''); |
|
513 plugin = trim(plugin); |
|
514 if (Plugin && inArray(initializedPlugins, plugin) === -1) { |
|
515 each(PluginManager.dependencies(plugin), function(dep) { |
|
516 initPlugin(dep); |
|
517 }); |
|
518 |
|
519 pluginInstance = new Plugin(self, pluginUrl, self.$); |
|
520 |
|
521 self.plugins[plugin] = pluginInstance; |
|
522 |
|
523 if (pluginInstance.init) { |
|
524 pluginInstance.init(self, pluginUrl); |
|
525 initializedPlugins.push(plugin); |
|
526 } |
|
527 } |
|
528 } |
|
529 |
|
530 // Create all plugins |
|
531 each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin); |
|
532 |
|
533 // Measure box |
|
534 if (settings.render_ui && self.theme) { |
|
535 self.orgDisplay = elm.style.display; |
|
536 |
|
537 if (typeof settings.theme != "function") { |
|
538 w = settings.width || elm.style.width || elm.offsetWidth; |
|
539 h = settings.height || elm.style.height || elm.offsetHeight; |
|
540 minHeight = settings.min_height || 100; |
|
541 re = /^[0-9\.]+(|px)$/i; |
|
542 |
|
543 if (re.test('' + w)) { |
|
544 w = Math.max(parseInt(w, 10), 100); |
|
545 } |
|
546 |
|
547 if (re.test('' + h)) { |
|
548 h = Math.max(parseInt(h, 10), minHeight); |
|
549 } |
|
550 |
|
551 // Render UI |
|
552 o = self.theme.renderUI({ |
|
553 targetNode: elm, |
|
554 width: w, |
|
555 height: h, |
|
556 deltaWidth: settings.delta_width, |
|
557 deltaHeight: settings.delta_height |
|
558 }); |
|
559 |
|
560 // Resize editor |
|
561 if (!settings.content_editable) { |
|
562 h = (o.iframeHeight || h) + (typeof h == 'number' ? (o.deltaHeight || 0) : ''); |
|
563 if (h < minHeight) { |
|
564 h = minHeight; |
|
565 } |
|
566 } |
|
567 } else { |
|
568 o = settings.theme(self, elm); |
|
569 |
|
570 // Convert element type to id:s |
|
571 if (o.editorContainer.nodeType) { |
|
572 o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent"; |
|
573 } |
|
574 |
|
575 // Convert element type to id:s |
|
576 if (o.iframeContainer.nodeType) { |
|
577 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer"; |
|
578 } |
|
579 |
|
580 // Use specified iframe height or the targets offsetHeight |
|
581 h = o.iframeHeight || elm.offsetHeight; |
|
582 } |
|
583 |
|
584 self.editorContainer = o.editorContainer; |
|
585 } |
|
586 |
|
587 // Load specified content CSS last |
|
588 if (settings.content_css) { |
|
589 each(explode(settings.content_css), function(u) { |
|
590 self.contentCSS.push(self.documentBaseURI.toAbsolute(u)); |
|
591 }); |
|
592 } |
|
593 |
|
594 // Load specified content CSS last |
|
595 if (settings.content_style) { |
|
596 self.contentStyles.push(settings.content_style); |
|
597 } |
|
598 |
|
599 // Content editable mode ends here |
|
600 if (settings.content_editable) { |
|
601 elm = n = o = null; // Fix IE leak |
|
602 return self.initContentBody(); |
|
603 } |
|
604 |
|
605 self.iframeHTML = settings.doctype + '<html><head>'; |
|
606 |
|
607 // We only need to override paths if we have to |
|
608 // IE has a bug where it remove site absolute urls to relative ones if this is specified |
|
609 if (settings.document_base_url != self.documentBaseUrl) { |
|
610 self.iframeHTML += '<base href="' + self.documentBaseURI.getURI() + '" />'; |
|
611 } |
|
612 |
|
613 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. |
|
614 if (!Env.caretAfter && settings.ie7_compat) { |
|
615 self.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; |
|
616 } |
|
617 |
|
618 self.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; |
|
619 |
|
620 // Load the CSS by injecting them into the HTML this will reduce "flicker" |
|
621 for (i = 0; i < self.contentCSS.length; i++) { |
|
622 var cssUrl = self.contentCSS[i]; |
|
623 self.iframeHTML += ( |
|
624 '<link type="text/css" ' + |
|
625 'rel="stylesheet" ' + |
|
626 'href="' + Tools._addCacheSuffix(cssUrl) + '" />' |
|
627 ); |
|
628 self.loadedCSS[cssUrl] = true; |
|
629 } |
|
630 |
|
631 bodyId = settings.body_id || 'tinymce'; |
|
632 if (bodyId.indexOf('=') != -1) { |
|
633 bodyId = self.getParam('body_id', '', 'hash'); |
|
634 bodyId = bodyId[self.id] || bodyId; |
|
635 } |
|
636 |
|
637 bodyClass = settings.body_class || ''; |
|
638 if (bodyClass.indexOf('=') != -1) { |
|
639 bodyClass = self.getParam('body_class', '', 'hash'); |
|
640 bodyClass = bodyClass[self.id] || ''; |
|
641 } |
|
642 |
|
643 if (settings.content_security_policy) { |
|
644 self.iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + settings.content_security_policy + '" />'; |
|
645 } |
|
646 |
|
647 self.iframeHTML += '</head><body id="' + bodyId + |
|
648 '" class="mce-content-body ' + bodyClass + |
|
649 '" data-id="' + self.id + '"><br></body></html>'; |
|
650 |
|
651 /*eslint no-script-url:0 */ |
|
652 var domainRelaxUrl = 'javascript:(function(){' + |
|
653 'document.open();document.domain="' + document.domain + '";' + |
|
654 'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' + |
|
655 'document.close();ed.initContentBody(true);})()'; |
|
656 |
|
657 // Domain relaxing is required since the user has messed around with document.domain |
|
658 if (document.domain != location.hostname) { |
|
659 url = domainRelaxUrl; |
|
660 } |
|
661 |
|
662 // Create iframe |
|
663 // TODO: ACC add the appropriate description on this. |
|
664 var ifr = DOM.create('iframe', { |
|
665 id: self.id + "_ifr", |
|
666 //src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7 |
|
667 frameBorder: '0', |
|
668 allowTransparency: "true", |
|
669 title: self.editorManager.translate( |
|
670 "Rich Text Area. Press ALT-F9 for menu. " + |
|
671 "Press ALT-F10 for toolbar. Press ALT-0 for help" |
|
672 ), |
|
673 style: { |
|
674 width: '100%', |
|
675 height: h, |
|
676 display: 'block' // Important for Gecko to render the iframe correctly |
|
677 } |
|
678 }); |
|
679 |
|
680 ifr.onload = function() { |
|
681 ifr.onload = null; |
|
682 self.fire("load"); |
|
683 }; |
|
684 |
|
685 DOM.setAttrib(ifr, "src", url || 'javascript:""'); |
|
686 |
|
687 self.contentAreaContainer = o.iframeContainer; |
|
688 self.iframeElement = ifr; |
|
689 |
|
690 n = DOM.add(o.iframeContainer, ifr); |
|
691 |
|
692 // Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname |
|
693 // Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!! |
|
694 if (ie) { |
|
695 try { |
|
696 self.getDoc(); |
|
697 } catch (e) { |
|
698 n.src = url = domainRelaxUrl; |
|
699 } |
|
700 } |
|
701 |
|
702 if (o.editorContainer) { |
|
703 DOM.get(o.editorContainer).style.display = self.orgDisplay; |
|
704 self.hidden = DOM.isHidden(o.editorContainer); |
|
705 } |
|
706 |
|
707 self.getElement().style.display = 'none'; |
|
708 DOM.setAttrib(self.id, 'aria-hidden', true); |
|
709 |
|
710 if (!url) { |
|
711 self.initContentBody(); |
|
712 } |
|
713 |
|
714 elm = n = o = null; // Cleanup |
|
715 }, |
|
716 |
|
717 /** |
|
718 * This method get called by the init method ones the iframe is loaded. |
|
719 * It will fill the iframe with contents, setups DOM and selection objects for the iframe. |
|
720 * |
|
721 * @method initContentBody |
|
722 * @private |
|
723 */ |
|
724 initContentBody: function(skipWrite) { |
|
725 var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText; |
|
726 |
|
727 // Restore visibility on target element |
|
728 if (!settings.inline) { |
|
729 self.getElement().style.visibility = self.orgVisibility; |
|
730 } |
|
731 |
|
732 // Setup iframe body |
|
733 if (!skipWrite && !settings.content_editable) { |
|
734 doc.open(); |
|
735 doc.write(self.iframeHTML); |
|
736 doc.close(); |
|
737 } |
|
738 |
|
739 if (settings.content_editable) { |
|
740 self.on('remove', function() { |
|
741 var bodyEl = this.getBody(); |
|
742 |
|
743 DOM.removeClass(bodyEl, 'mce-content-body'); |
|
744 DOM.removeClass(bodyEl, 'mce-edit-focus'); |
|
745 DOM.setAttrib(bodyEl, 'contentEditable', null); |
|
746 }); |
|
747 |
|
748 DOM.addClass(targetElm, 'mce-content-body'); |
|
749 self.contentDocument = doc = settings.content_document || document; |
|
750 self.contentWindow = settings.content_window || window; |
|
751 self.bodyElement = targetElm; |
|
752 |
|
753 // Prevent leak in IE |
|
754 settings.content_document = settings.content_window = null; |
|
755 |
|
756 // TODO: Fix this |
|
757 settings.root_name = targetElm.nodeName.toLowerCase(); |
|
758 } |
|
759 |
|
760 // It will not steal focus while setting contentEditable |
|
761 body = self.getBody(); |
|
762 body.disabled = true; |
|
763 |
|
764 if (!settings.readonly) { |
|
765 if (self.inline && DOM.getStyle(body, 'position', true) == 'static') { |
|
766 body.style.position = 'relative'; |
|
767 } |
|
768 |
|
769 body.contentEditable = self.getParam('content_editable_state', true); |
|
770 } |
|
771 |
|
772 body.disabled = false; |
|
773 |
|
774 /** |
|
775 * Schema instance, enables you to validate elements and it's children. |
|
776 * |
|
777 * @property schema |
|
778 * @type tinymce.html.Schema |
|
779 */ |
|
780 self.schema = new Schema(settings); |
|
781 |
|
782 /** |
|
783 * DOM instance for the editor. |
|
784 * |
|
785 * @property dom |
|
786 * @type tinymce.dom.DOMUtils |
|
787 * @example |
|
788 * // Adds a class to all paragraphs within the editor |
|
789 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); |
|
790 */ |
|
791 self.dom = new DOMUtils(doc, { |
|
792 keep_values: true, |
|
793 url_converter: self.convertURL, |
|
794 url_converter_scope: self, |
|
795 hex_colors: settings.force_hex_style_colors, |
|
796 class_filter: settings.class_filter, |
|
797 update_styles: true, |
|
798 root_element: self.inline ? self.getBody() : null, |
|
799 collect: settings.content_editable, |
|
800 schema: self.schema, |
|
801 onSetAttrib: function(e) { |
|
802 self.fire('SetAttrib', e); |
|
803 } |
|
804 }); |
|
805 |
|
806 /** |
|
807 * HTML parser will be used when contents is inserted into the editor. |
|
808 * |
|
809 * @property parser |
|
810 * @type tinymce.html.DomParser |
|
811 */ |
|
812 self.parser = new DomParser(settings, self.schema); |
|
813 |
|
814 // Convert src and href into data-mce-src, data-mce-href and data-mce-style |
|
815 self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) { |
|
816 var i = nodes.length, node, dom = self.dom, value, internalName; |
|
817 |
|
818 while (i--) { |
|
819 node = nodes[i]; |
|
820 value = node.attr(name); |
|
821 internalName = 'data-mce-' + name; |
|
822 |
|
823 // Add internal attribute if we need to we don't on a refresh of the document |
|
824 if (!node.attributes.map[internalName]) { |
|
825 if (name === "style") { |
|
826 value = dom.serializeStyle(dom.parseStyle(value), node.name); |
|
827 |
|
828 if (!value.length) { |
|
829 value = null; |
|
830 } |
|
831 |
|
832 node.attr(internalName, value); |
|
833 node.attr(name, value); |
|
834 } else if (name === "tabindex") { |
|
835 node.attr(internalName, value); |
|
836 node.attr(name, null); |
|
837 } else { |
|
838 node.attr(internalName, self.convertURL(value, name, node.name)); |
|
839 } |
|
840 } |
|
841 } |
|
842 }); |
|
843 |
|
844 // Keep scripts from executing |
|
845 self.parser.addNodeFilter('script', function(nodes) { |
|
846 var i = nodes.length, node; |
|
847 |
|
848 while (i--) { |
|
849 node = nodes[i]; |
|
850 node.attr('type', 'mce-' + (node.attr('type') || 'no/type')); |
|
851 } |
|
852 }); |
|
853 |
|
854 self.parser.addNodeFilter('#cdata', function(nodes) { |
|
855 var i = nodes.length, node; |
|
856 |
|
857 while (i--) { |
|
858 node = nodes[i]; |
|
859 node.type = 8; |
|
860 node.name = '#comment'; |
|
861 node.value = '[CDATA[' + node.value + ']]'; |
|
862 } |
|
863 }); |
|
864 |
|
865 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) { |
|
866 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); |
|
867 |
|
868 while (i--) { |
|
869 node = nodes[i]; |
|
870 |
|
871 if (node.isEmpty(nonEmptyElements)) { |
|
872 node.append(new Node('br', 1)).shortEnded = true; |
|
873 } |
|
874 } |
|
875 }); |
|
876 |
|
877 /** |
|
878 * DOM serializer for the editor. Will be used when contents is extracted from the editor. |
|
879 * |
|
880 * @property serializer |
|
881 * @type tinymce.dom.Serializer |
|
882 * @example |
|
883 * // Serializes the first paragraph in the editor into a string |
|
884 * tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]); |
|
885 */ |
|
886 self.serializer = new DomSerializer(settings, self); |
|
887 |
|
888 /** |
|
889 * Selection instance for the editor. |
|
890 * |
|
891 * @property selection |
|
892 * @type tinymce.dom.Selection |
|
893 * @example |
|
894 * // Sets some contents to the current selection in the editor |
|
895 * tinymce.activeEditor.selection.setContent('Some contents'); |
|
896 * |
|
897 * // Gets the current selection |
|
898 * alert(tinymce.activeEditor.selection.getContent()); |
|
899 * |
|
900 * // Selects the first paragraph found |
|
901 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); |
|
902 */ |
|
903 self.selection = new Selection(self.dom, self.getWin(), self.serializer, self); |
|
904 |
|
905 /** |
|
906 * Formatter instance. |
|
907 * |
|
908 * @property formatter |
|
909 * @type tinymce.Formatter |
|
910 */ |
|
911 self.formatter = new Formatter(self); |
|
912 |
|
913 /** |
|
914 * Undo manager instance, responsible for handling undo levels. |
|
915 * |
|
916 * @property undoManager |
|
917 * @type tinymce.UndoManager |
|
918 * @example |
|
919 * // Undoes the last modification to the editor |
|
920 * tinymce.activeEditor.undoManager.undo(); |
|
921 */ |
|
922 self.undoManager = new UndoManager(self); |
|
923 |
|
924 self.forceBlocks = new ForceBlocks(self); |
|
925 self.enterKey = new EnterKey(self); |
|
926 self._nodeChangeDispatcher = new NodeChange(self); |
|
927 |
|
928 self.fire('PreInit'); |
|
929 |
|
930 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { |
|
931 doc.body.spellcheck = false; // Gecko |
|
932 DOM.setAttrib(body, "spellcheck", "false"); |
|
933 } |
|
934 |
|
935 self.fire('PostRender'); |
|
936 |
|
937 self.quirks = new Quirks(self); |
|
938 |
|
939 if (settings.directionality) { |
|
940 body.dir = settings.directionality; |
|
941 } |
|
942 |
|
943 if (settings.nowrap) { |
|
944 body.style.whiteSpace = "nowrap"; |
|
945 } |
|
946 |
|
947 if (settings.protect) { |
|
948 self.on('BeforeSetContent', function(e) { |
|
949 each(settings.protect, function(pattern) { |
|
950 e.content = e.content.replace(pattern, function(str) { |
|
951 return '<!--mce:protected ' + escape(str) + '-->'; |
|
952 }); |
|
953 }); |
|
954 }); |
|
955 } |
|
956 |
|
957 self.on('SetContent', function() { |
|
958 self.addVisual(self.getBody()); |
|
959 }); |
|
960 |
|
961 // Remove empty contents |
|
962 if (settings.padd_empty_editor) { |
|
963 self.on('PostProcess', function(e) { |
|
964 e.content = e.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); |
|
965 }); |
|
966 } |
|
967 |
|
968 self.load({initial: true, format: 'html'}); |
|
969 self.startContent = self.getContent({format: 'raw'}); |
|
970 |
|
971 /** |
|
972 * Is set to true after the editor instance has been initialized |
|
973 * |
|
974 * @property initialized |
|
975 * @type Boolean |
|
976 * @example |
|
977 * function isEditorInitialized(editor) { |
|
978 * return editor && editor.initialized; |
|
979 * } |
|
980 */ |
|
981 self.initialized = true; |
|
982 self.bindPendingEventDelegates(); |
|
983 |
|
984 self.fire('init'); |
|
985 self.focus(true); |
|
986 self.nodeChanged({initial: true}); |
|
987 self.execCallback('init_instance_callback', self); |
|
988 |
|
989 // Add editor specific CSS styles |
|
990 if (self.contentStyles.length > 0) { |
|
991 contentCssText = ''; |
|
992 |
|
993 each(self.contentStyles, function(style) { |
|
994 contentCssText += style + "\r\n"; |
|
995 }); |
|
996 |
|
997 self.dom.addStyle(contentCssText); |
|
998 } |
|
999 |
|
1000 // Load specified content CSS last |
|
1001 each(self.contentCSS, function(cssUrl) { |
|
1002 if (!self.loadedCSS[cssUrl]) { |
|
1003 self.dom.loadCSS(cssUrl); |
|
1004 self.loadedCSS[cssUrl] = true; |
|
1005 } |
|
1006 }); |
|
1007 |
|
1008 // Handle auto focus |
|
1009 if (settings.auto_focus) { |
|
1010 setTimeout(function() { |
|
1011 var editor; |
|
1012 |
|
1013 if (settings.auto_focus === true) { |
|
1014 editor = self; |
|
1015 } else { |
|
1016 editor = self.editorManager.get(settings.auto_focus); |
|
1017 } |
|
1018 |
|
1019 if (!editor.destroyed) { |
|
1020 editor.focus(); |
|
1021 } |
|
1022 }, 100); |
|
1023 } |
|
1024 |
|
1025 // Clean up references for IE |
|
1026 targetElm = doc = body = null; |
|
1027 }, |
|
1028 |
|
1029 /** |
|
1030 * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection |
|
1031 * it will also place DOM focus inside the editor. |
|
1032 * |
|
1033 * @method focus |
|
1034 * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor. |
|
1035 */ |
|
1036 focus: function(skipFocus) { |
|
1037 var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng; |
|
1038 var controlElm, doc = self.getDoc(), body; |
|
1039 |
|
1040 if (!skipFocus) { |
|
1041 // Get selected control element |
|
1042 rng = selection.getRng(); |
|
1043 if (rng.item) { |
|
1044 controlElm = rng.item(0); |
|
1045 } |
|
1046 |
|
1047 self._refreshContentEditable(); |
|
1048 |
|
1049 // Focus the window iframe |
|
1050 if (!contentEditable) { |
|
1051 // WebKit needs this call to fire focusin event properly see #5948 |
|
1052 // But Opera pre Blink engine will produce an empty selection so skip Opera |
|
1053 if (!Env.opera) { |
|
1054 self.getBody().focus(); |
|
1055 } |
|
1056 |
|
1057 self.getWin().focus(); |
|
1058 } |
|
1059 |
|
1060 // Focus the body as well since it's contentEditable |
|
1061 if (isGecko || contentEditable) { |
|
1062 body = self.getBody(); |
|
1063 |
|
1064 // Check for setActive since it doesn't scroll to the element |
|
1065 if (body.setActive) { |
|
1066 // IE 11 sometimes throws "Invalid function" then fallback to focus |
|
1067 try { |
|
1068 body.setActive(); |
|
1069 } catch (ex) { |
|
1070 body.focus(); |
|
1071 } |
|
1072 } else { |
|
1073 body.focus(); |
|
1074 } |
|
1075 |
|
1076 if (contentEditable) { |
|
1077 selection.normalize(); |
|
1078 } |
|
1079 } |
|
1080 |
|
1081 // Restore selected control element |
|
1082 // This is needed when for example an image is selected within a |
|
1083 // layer a call to focus will then remove the control selection |
|
1084 if (controlElm && controlElm.ownerDocument == doc) { |
|
1085 rng = doc.body.createControlRange(); |
|
1086 rng.addElement(controlElm); |
|
1087 rng.select(); |
|
1088 } |
|
1089 } |
|
1090 |
|
1091 self.editorManager.setActive(self); |
|
1092 }, |
|
1093 |
|
1094 /** |
|
1095 * Executes a legacy callback. This method is useful to call old 2.x option callbacks. |
|
1096 * There new event model is a better way to add callback so this method might be removed in the future. |
|
1097 * |
|
1098 * @method execCallback |
|
1099 * @param {String} name Name of the callback to execute. |
|
1100 * @return {Object} Return value passed from callback function. |
|
1101 */ |
|
1102 execCallback: function(name) { |
|
1103 var self = this, callback = self.settings[name], scope; |
|
1104 |
|
1105 if (!callback) { |
|
1106 return; |
|
1107 } |
|
1108 |
|
1109 // Look through lookup |
|
1110 if (self.callbackLookup && (scope = self.callbackLookup[name])) { |
|
1111 callback = scope.func; |
|
1112 scope = scope.scope; |
|
1113 } |
|
1114 |
|
1115 if (typeof callback === 'string') { |
|
1116 scope = callback.replace(/\.\w+$/, ''); |
|
1117 scope = scope ? resolve(scope) : 0; |
|
1118 callback = resolve(callback); |
|
1119 self.callbackLookup = self.callbackLookup || {}; |
|
1120 self.callbackLookup[name] = {func: callback, scope: scope}; |
|
1121 } |
|
1122 |
|
1123 return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1)); |
|
1124 }, |
|
1125 |
|
1126 /** |
|
1127 * Translates the specified string by replacing variables with language pack items it will also check if there is |
|
1128 * a key mathcin the input. |
|
1129 * |
|
1130 * @method translate |
|
1131 * @param {String} text String to translate by the language pack data. |
|
1132 * @return {String} Translated string. |
|
1133 */ |
|
1134 translate: function(text) { |
|
1135 var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; |
|
1136 |
|
1137 if (!text) { |
|
1138 return ''; |
|
1139 } |
|
1140 |
|
1141 return i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) { |
|
1142 return i18n.data[lang + '.' + b] || '{#' + b + '}'; |
|
1143 }); |
|
1144 }, |
|
1145 |
|
1146 /** |
|
1147 * Returns a language pack item by name/key. |
|
1148 * |
|
1149 * @method getLang |
|
1150 * @param {String} name Name/key to get from the language pack. |
|
1151 * @param {String} defaultVal Optional default value to retrive. |
|
1152 */ |
|
1153 getLang: function(name, defaultVal) { |
|
1154 return ( |
|
1155 this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] || |
|
1156 (defaultVal !== undefined ? defaultVal : '{#' + name + '}') |
|
1157 ); |
|
1158 }, |
|
1159 |
|
1160 /** |
|
1161 * Returns a configuration parameter by name. |
|
1162 * |
|
1163 * @method getParam |
|
1164 * @param {String} name Configruation parameter to retrive. |
|
1165 * @param {String} defaultVal Optional default value to return. |
|
1166 * @param {String} type Optional type parameter. |
|
1167 * @return {String} Configuration parameter value or default value. |
|
1168 * @example |
|
1169 * // Returns a specific config value from the currently active editor |
|
1170 * var someval = tinymce.activeEditor.getParam('myvalue'); |
|
1171 * |
|
1172 * // Returns a specific config value from a specific editor instance by id |
|
1173 * var someval2 = tinymce.get('my_editor').getParam('myvalue'); |
|
1174 */ |
|
1175 getParam: function(name, defaultVal, type) { |
|
1176 var value = name in this.settings ? this.settings[name] : defaultVal, output; |
|
1177 |
|
1178 if (type === 'hash') { |
|
1179 output = {}; |
|
1180 |
|
1181 if (typeof value === 'string') { |
|
1182 each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) { |
|
1183 value = value.split('='); |
|
1184 |
|
1185 if (value.length > 1) { |
|
1186 output[trim(value[0])] = trim(value[1]); |
|
1187 } else { |
|
1188 output[trim(value[0])] = trim(value); |
|
1189 } |
|
1190 }); |
|
1191 } else { |
|
1192 output = value; |
|
1193 } |
|
1194 |
|
1195 return output; |
|
1196 } |
|
1197 |
|
1198 return value; |
|
1199 }, |
|
1200 |
|
1201 /** |
|
1202 * Distpaches out a onNodeChange event to all observers. This method should be called when you |
|
1203 * need to update the UI states or element path etc. |
|
1204 * |
|
1205 * @method nodeChanged |
|
1206 * @param {Object} args Optional args to pass to NodeChange event handlers. |
|
1207 */ |
|
1208 nodeChanged: function(args) { |
|
1209 this._nodeChangeDispatcher.nodeChanged(args); |
|
1210 }, |
|
1211 |
|
1212 /** |
|
1213 * Adds a button that later gets created by the theme in the editors toolbars. |
|
1214 * |
|
1215 * @method addButton |
|
1216 * @param {String} name Button name to add. |
|
1217 * @param {Object} settings Settings object with title, cmd etc. |
|
1218 * @example |
|
1219 * // Adds a custom button to the editor that inserts contents when clicked |
|
1220 * tinymce.init({ |
|
1221 * ... |
|
1222 * |
|
1223 * toolbar: 'example' |
|
1224 * |
|
1225 * setup: function(ed) { |
|
1226 * ed.addButton('example', { |
|
1227 * title: 'My title', |
|
1228 * image: '../js/tinymce/plugins/example/img/example.gif', |
|
1229 * onclick: function() { |
|
1230 * ed.insertContent('Hello world!!'); |
|
1231 * } |
|
1232 * }); |
|
1233 * } |
|
1234 * }); |
|
1235 */ |
|
1236 addButton: function(name, settings) { |
|
1237 var self = this; |
|
1238 |
|
1239 if (settings.cmd) { |
|
1240 settings.onclick = function() { |
|
1241 self.execCommand(settings.cmd); |
|
1242 }; |
|
1243 } |
|
1244 |
|
1245 if (!settings.text && !settings.icon) { |
|
1246 settings.icon = name; |
|
1247 } |
|
1248 |
|
1249 self.buttons = self.buttons || {}; |
|
1250 settings.tooltip = settings.tooltip || settings.title; |
|
1251 self.buttons[name] = settings; |
|
1252 }, |
|
1253 |
|
1254 /** |
|
1255 * Adds a menu item to be used in the menus of the theme. There might be multiple instances |
|
1256 * of this menu item for example it might be used in the main menus of the theme but also in |
|
1257 * the context menu so make sure that it's self contained and supports multiple instances. |
|
1258 * |
|
1259 * @method addMenuItem |
|
1260 * @param {String} name Menu item name to add. |
|
1261 * @param {Object} settings Settings object with title, cmd etc. |
|
1262 * @example |
|
1263 * // Adds a custom menu item to the editor that inserts contents when clicked |
|
1264 * // The context option allows you to add the menu item to an existing default menu |
|
1265 * tinymce.init({ |
|
1266 * ... |
|
1267 * |
|
1268 * setup: function(ed) { |
|
1269 * ed.addMenuItem('example', { |
|
1270 * text: 'My menu item', |
|
1271 * context: 'tools', |
|
1272 * onclick: function() { |
|
1273 * ed.insertContent('Hello world!!'); |
|
1274 * } |
|
1275 * }); |
|
1276 * } |
|
1277 * }); |
|
1278 */ |
|
1279 addMenuItem: function(name, settings) { |
|
1280 var self = this; |
|
1281 |
|
1282 if (settings.cmd) { |
|
1283 settings.onclick = function() { |
|
1284 self.execCommand(settings.cmd); |
|
1285 }; |
|
1286 } |
|
1287 |
|
1288 self.menuItems = self.menuItems || {}; |
|
1289 self.menuItems[name] = settings; |
|
1290 }, |
|
1291 |
|
1292 /** |
|
1293 * Adds a custom command to the editor, you can also override existing commands with this method. |
|
1294 * The command that you add can be executed with execCommand. |
|
1295 * |
|
1296 * @method addCommand |
|
1297 * @param {String} name Command name to add/override. |
|
1298 * @param {addCommandCallback} callback Function to execute when the command occurs. |
|
1299 * @param {Object} scope Optional scope to execute the function in. |
|
1300 * @example |
|
1301 * // Adds a custom command that later can be executed using execCommand |
|
1302 * tinymce.init({ |
|
1303 * ... |
|
1304 * |
|
1305 * setup: function(ed) { |
|
1306 * // Register example command |
|
1307 * ed.addCommand('mycommand', function(ui, v) { |
|
1308 * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'})); |
|
1309 * }); |
|
1310 * } |
|
1311 * }); |
|
1312 */ |
|
1313 addCommand: function(name, callback, scope) { |
|
1314 /** |
|
1315 * Callback function that gets called when a command is executed. |
|
1316 * |
|
1317 * @callback addCommandCallback |
|
1318 * @param {Boolean} ui Display UI state true/false. |
|
1319 * @param {Object} value Optional value for command. |
|
1320 * @return {Boolean} True/false state if the command was handled or not. |
|
1321 */ |
|
1322 this.editorCommands.addCommand(name, callback, scope); |
|
1323 }, |
|
1324 |
|
1325 /** |
|
1326 * Adds a custom query state command to the editor, you can also override existing commands with this method. |
|
1327 * The command that you add can be executed with queryCommandState function. |
|
1328 * |
|
1329 * @method addQueryStateHandler |
|
1330 * @param {String} name Command name to add/override. |
|
1331 * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrival occurs. |
|
1332 * @param {Object} scope Optional scope to execute the function in. |
|
1333 */ |
|
1334 addQueryStateHandler: function(name, callback, scope) { |
|
1335 /** |
|
1336 * Callback function that gets called when a queryCommandState is executed. |
|
1337 * |
|
1338 * @callback addQueryStateHandlerCallback |
|
1339 * @return {Boolean} True/false state if the command is enabled or not like is it bold. |
|
1340 */ |
|
1341 this.editorCommands.addQueryStateHandler(name, callback, scope); |
|
1342 }, |
|
1343 |
|
1344 /** |
|
1345 * Adds a custom query value command to the editor, you can also override existing commands with this method. |
|
1346 * The command that you add can be executed with queryCommandValue function. |
|
1347 * |
|
1348 * @method addQueryValueHandler |
|
1349 * @param {String} name Command name to add/override. |
|
1350 * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrival occurs. |
|
1351 * @param {Object} scope Optional scope to execute the function in. |
|
1352 */ |
|
1353 addQueryValueHandler: function(name, callback, scope) { |
|
1354 /** |
|
1355 * Callback function that gets called when a queryCommandValue is executed. |
|
1356 * |
|
1357 * @callback addQueryValueHandlerCallback |
|
1358 * @return {Object} Value of the command or undefined. |
|
1359 */ |
|
1360 this.editorCommands.addQueryValueHandler(name, callback, scope); |
|
1361 }, |
|
1362 |
|
1363 /** |
|
1364 * Adds a keyboard shortcut for some command or function. |
|
1365 * |
|
1366 * @method addShortcut |
|
1367 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. |
|
1368 * @param {String} desc Text description for the command. |
|
1369 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. |
|
1370 * @param {Object} sc Optional scope to execute the function in. |
|
1371 * @return {Boolean} true/false state if the shortcut was added or not. |
|
1372 */ |
|
1373 addShortcut: function(pattern, desc, cmdFunc, scope) { |
|
1374 this.shortcuts.add(pattern, desc, cmdFunc, scope); |
|
1375 }, |
|
1376 |
|
1377 /** |
|
1378 * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or |
|
1379 * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org. |
|
1380 * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these |
|
1381 * return true it will handle the command as a internal browser command. |
|
1382 * |
|
1383 * @method execCommand |
|
1384 * @param {String} cmd Command name to execute, for example mceLink or Bold. |
|
1385 * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not. |
|
1386 * @param {mixed} value Optional command value, this can be anything. |
|
1387 * @param {Object} args Optional arguments object. |
|
1388 */ |
|
1389 execCommand: function(cmd, ui, value, args) { |
|
1390 return this.editorCommands.execCommand(cmd, ui, value, args); |
|
1391 }, |
|
1392 |
|
1393 /** |
|
1394 * Returns a command specific state, for example if bold is enabled or not. |
|
1395 * |
|
1396 * @method queryCommandState |
|
1397 * @param {string} cmd Command to query state from. |
|
1398 * @return {Boolean} Command specific state, for example if bold is enabled or not. |
|
1399 */ |
|
1400 queryCommandState: function(cmd) { |
|
1401 return this.editorCommands.queryCommandState(cmd); |
|
1402 }, |
|
1403 |
|
1404 /** |
|
1405 * Returns a command specific value, for example the current font size. |
|
1406 * |
|
1407 * @method queryCommandValue |
|
1408 * @param {string} cmd Command to query value from. |
|
1409 * @return {Object} Command specific value, for example the current font size. |
|
1410 */ |
|
1411 queryCommandValue: function(cmd) { |
|
1412 return this.editorCommands.queryCommandValue(cmd); |
|
1413 }, |
|
1414 |
|
1415 /** |
|
1416 * Returns true/false if the command is supported or not. |
|
1417 * |
|
1418 * @method queryCommandSupported |
|
1419 * @param {String} cmd Command that we check support for. |
|
1420 * @return {Boolean} true/false if the command is supported or not. |
|
1421 */ |
|
1422 queryCommandSupported: function(cmd) { |
|
1423 return this.editorCommands.queryCommandSupported(cmd); |
|
1424 }, |
|
1425 |
|
1426 /** |
|
1427 * Shows the editor and hides any textarea/div that the editor is supposed to replace. |
|
1428 * |
|
1429 * @method show |
|
1430 */ |
|
1431 show: function() { |
|
1432 var self = this; |
|
1433 |
|
1434 if (self.hidden) { |
|
1435 self.hidden = false; |
|
1436 |
|
1437 if (self.inline) { |
|
1438 self.getBody().contentEditable = true; |
|
1439 } else { |
|
1440 DOM.show(self.getContainer()); |
|
1441 DOM.hide(self.id); |
|
1442 } |
|
1443 |
|
1444 self.load(); |
|
1445 self.fire('show'); |
|
1446 } |
|
1447 }, |
|
1448 |
|
1449 /** |
|
1450 * Hides the editor and shows any textarea/div that the editor is supposed to replace. |
|
1451 * |
|
1452 * @method hide |
|
1453 */ |
|
1454 hide: function() { |
|
1455 var self = this, doc = self.getDoc(); |
|
1456 |
|
1457 if (!self.hidden) { |
|
1458 // Fixed bug where IE has a blinking cursor left from the editor |
|
1459 if (ie && doc && !self.inline) { |
|
1460 doc.execCommand('SelectAll'); |
|
1461 } |
|
1462 |
|
1463 // We must save before we hide so Safari doesn't crash |
|
1464 self.save(); |
|
1465 |
|
1466 if (self.inline) { |
|
1467 self.getBody().contentEditable = false; |
|
1468 |
|
1469 // Make sure the editor gets blurred |
|
1470 if (self == self.editorManager.focusedEditor) { |
|
1471 self.editorManager.focusedEditor = null; |
|
1472 } |
|
1473 } else { |
|
1474 DOM.hide(self.getContainer()); |
|
1475 DOM.setStyle(self.id, 'display', self.orgDisplay); |
|
1476 } |
|
1477 |
|
1478 self.hidden = true; |
|
1479 self.fire('hide'); |
|
1480 } |
|
1481 }, |
|
1482 |
|
1483 /** |
|
1484 * Returns true/false if the editor is hidden or not. |
|
1485 * |
|
1486 * @method isHidden |
|
1487 * @return {Boolean} True/false if the editor is hidden or not. |
|
1488 */ |
|
1489 isHidden: function() { |
|
1490 return !!this.hidden; |
|
1491 }, |
|
1492 |
|
1493 /** |
|
1494 * Sets the progress state, this will display a throbber/progess for the editor. |
|
1495 * This is ideal for asycronous operations like an AJAX save call. |
|
1496 * |
|
1497 * @method setProgressState |
|
1498 * @param {Boolean} state Boolean state if the progress should be shown or hidden. |
|
1499 * @param {Number} time Optional time to wait before the progress gets shown. |
|
1500 * @return {Boolean} Same as the input state. |
|
1501 * @example |
|
1502 * // Show progress for the active editor |
|
1503 * tinymce.activeEditor.setProgressState(true); |
|
1504 * |
|
1505 * // Hide progress for the active editor |
|
1506 * tinymce.activeEditor.setProgressState(false); |
|
1507 * |
|
1508 * // Show progress after 3 seconds |
|
1509 * tinymce.activeEditor.setProgressState(true, 3000); |
|
1510 */ |
|
1511 setProgressState: function(state, time) { |
|
1512 this.fire('ProgressState', {state: state, time: time}); |
|
1513 }, |
|
1514 |
|
1515 /** |
|
1516 * Loads contents from the textarea or div element that got converted into an editor instance. |
|
1517 * This method will move the contents from that textarea or div into the editor by using setContent |
|
1518 * so all events etc that method has will get dispatched as well. |
|
1519 * |
|
1520 * @method load |
|
1521 * @param {Object} args Optional content object, this gets passed around through the whole load process. |
|
1522 * @return {String} HTML string that got set into the editor. |
|
1523 */ |
|
1524 load: function(args) { |
|
1525 var self = this, elm = self.getElement(), html; |
|
1526 |
|
1527 if (elm) { |
|
1528 args = args || {}; |
|
1529 args.load = true; |
|
1530 |
|
1531 html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args); |
|
1532 args.element = elm; |
|
1533 |
|
1534 if (!args.no_events) { |
|
1535 self.fire('LoadContent', args); |
|
1536 } |
|
1537 |
|
1538 args.element = elm = null; |
|
1539 |
|
1540 return html; |
|
1541 } |
|
1542 }, |
|
1543 |
|
1544 /** |
|
1545 * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance. |
|
1546 * This method will move the HTML contents from the editor into that textarea or div by getContent |
|
1547 * so all events etc that method has will get dispatched as well. |
|
1548 * |
|
1549 * @method save |
|
1550 * @param {Object} args Optional content object, this gets passed around through the whole save process. |
|
1551 * @return {String} HTML string that got set into the textarea/div. |
|
1552 */ |
|
1553 save: function(args) { |
|
1554 var self = this, elm = self.getElement(), html, form; |
|
1555 |
|
1556 if (!elm || !self.initialized) { |
|
1557 return; |
|
1558 } |
|
1559 |
|
1560 args = args || {}; |
|
1561 args.save = true; |
|
1562 |
|
1563 args.element = elm; |
|
1564 html = args.content = self.getContent(args); |
|
1565 |
|
1566 if (!args.no_events) { |
|
1567 self.fire('SaveContent', args); |
|
1568 } |
|
1569 |
|
1570 html = args.content; |
|
1571 |
|
1572 if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) { |
|
1573 // Update DIV element when not in inline mode |
|
1574 if (!self.inline) { |
|
1575 elm.innerHTML = html; |
|
1576 } |
|
1577 |
|
1578 // Update hidden form element |
|
1579 if ((form = DOM.getParent(self.id, 'form'))) { |
|
1580 each(form.elements, function(elm) { |
|
1581 if (elm.name == self.id) { |
|
1582 elm.value = html; |
|
1583 return false; |
|
1584 } |
|
1585 }); |
|
1586 } |
|
1587 } else { |
|
1588 elm.value = html; |
|
1589 } |
|
1590 |
|
1591 args.element = elm = null; |
|
1592 |
|
1593 if (args.set_dirty !== false) { |
|
1594 self.isNotDirty = true; |
|
1595 } |
|
1596 |
|
1597 return html; |
|
1598 }, |
|
1599 |
|
1600 /** |
|
1601 * Sets the specified content to the editor instance, this will cleanup the content before it gets set using |
|
1602 * the different cleanup rules options. |
|
1603 * |
|
1604 * @method setContent |
|
1605 * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well. |
|
1606 * @param {Object} args Optional content object, this gets passed around through the whole set process. |
|
1607 * @return {String} HTML string that got set into the editor. |
|
1608 * @example |
|
1609 * // Sets the HTML contents of the activeEditor editor |
|
1610 * tinymce.activeEditor.setContent('<span>some</span> html'); |
|
1611 * |
|
1612 * // Sets the raw contents of the activeEditor editor |
|
1613 * tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'}); |
|
1614 * |
|
1615 * // Sets the content of a specific editor (my_editor in this example) |
|
1616 * tinymce.get('my_editor').setContent(data); |
|
1617 * |
|
1618 * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added |
|
1619 * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'}); |
|
1620 */ |
|
1621 setContent: function(content, args) { |
|
1622 var self = this, body = self.getBody(), forcedRootBlockName; |
|
1623 |
|
1624 // Setup args object |
|
1625 args = args || {}; |
|
1626 args.format = args.format || 'html'; |
|
1627 args.set = true; |
|
1628 args.content = content; |
|
1629 |
|
1630 // Do preprocessing |
|
1631 if (!args.no_events) { |
|
1632 self.fire('BeforeSetContent', args); |
|
1633 } |
|
1634 |
|
1635 content = args.content; |
|
1636 |
|
1637 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content |
|
1638 // It will also be impossible to place the caret in the editor unless there is a BR element present |
|
1639 if (content.length === 0 || /^\s+$/.test(content)) { |
|
1640 forcedRootBlockName = self.settings.forced_root_block; |
|
1641 |
|
1642 // Check if forcedRootBlock is configured and that the block is a valid child of the body |
|
1643 if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { |
|
1644 // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly |
|
1645 content = ie && ie < 11 ? '' : '<br data-mce-bogus="1">'; |
|
1646 content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content); |
|
1647 } else if (!ie) { |
|
1648 // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret |
|
1649 content = '<br data-mce-bogus="1">'; |
|
1650 } |
|
1651 |
|
1652 self.dom.setHTML(body, content); |
|
1653 |
|
1654 self.fire('SetContent', args); |
|
1655 } else { |
|
1656 // Parse and serialize the html |
|
1657 if (args.format !== 'raw') { |
|
1658 content = new Serializer({}, self.schema).serialize( |
|
1659 self.parser.parse(content, {isRootContent: true}) |
|
1660 ); |
|
1661 } |
|
1662 |
|
1663 // Set the new cleaned contents to the editor |
|
1664 args.content = trim(content); |
|
1665 self.dom.setHTML(body, args.content); |
|
1666 |
|
1667 // Do post processing |
|
1668 if (!args.no_events) { |
|
1669 self.fire('SetContent', args); |
|
1670 } |
|
1671 |
|
1672 // Don't normalize selection if the focused element isn't the body in |
|
1673 // content editable mode since it will steal focus otherwise |
|
1674 /*if (!self.settings.content_editable || document.activeElement === self.getBody()) { |
|
1675 self.selection.normalize(); |
|
1676 }*/ |
|
1677 } |
|
1678 |
|
1679 return args.content; |
|
1680 }, |
|
1681 |
|
1682 /** |
|
1683 * Gets the content from the editor instance, this will cleanup the content before it gets returned using |
|
1684 * the different cleanup rules options. |
|
1685 * |
|
1686 * @method getContent |
|
1687 * @param {Object} args Optional content object, this gets passed around through the whole get process. |
|
1688 * @return {String} Cleaned content string, normally HTML contents. |
|
1689 * @example |
|
1690 * // Get the HTML contents of the currently active editor |
|
1691 * console.debug(tinymce.activeEditor.getContent()); |
|
1692 * |
|
1693 * // Get the raw contents of the currently active editor |
|
1694 * tinymce.activeEditor.getContent({format: 'raw'}); |
|
1695 * |
|
1696 * // Get content of a specific editor: |
|
1697 * tinymce.get('content id').getContent() |
|
1698 */ |
|
1699 getContent: function(args) { |
|
1700 var self = this, content, body = self.getBody(); |
|
1701 |
|
1702 // Setup args object |
|
1703 args = args || {}; |
|
1704 args.format = args.format || 'html'; |
|
1705 args.get = true; |
|
1706 args.getInner = true; |
|
1707 |
|
1708 // Do preprocessing |
|
1709 if (!args.no_events) { |
|
1710 self.fire('BeforeGetContent', args); |
|
1711 } |
|
1712 |
|
1713 // Get raw contents or by default the cleaned contents |
|
1714 if (args.format == 'raw') { |
|
1715 content = body.innerHTML; |
|
1716 } else if (args.format == 'text') { |
|
1717 content = body.innerText || body.textContent; |
|
1718 } else { |
|
1719 content = self.serializer.serialize(body, args); |
|
1720 } |
|
1721 |
|
1722 // Trim whitespace in beginning/end of HTML |
|
1723 if (args.format != 'text') { |
|
1724 args.content = trim(content); |
|
1725 } else { |
|
1726 args.content = content; |
|
1727 } |
|
1728 |
|
1729 // Do post processing |
|
1730 if (!args.no_events) { |
|
1731 self.fire('GetContent', args); |
|
1732 } |
|
1733 |
|
1734 return args.content; |
|
1735 }, |
|
1736 |
|
1737 /** |
|
1738 * Inserts content at caret position. |
|
1739 * |
|
1740 * @method insertContent |
|
1741 * @param {String} content Content to insert. |
|
1742 * @param {Object} args Optional args to pass to insert call. |
|
1743 */ |
|
1744 insertContent: function(content, args) { |
|
1745 if (args) { |
|
1746 content = extend({content: content}, args); |
|
1747 } |
|
1748 |
|
1749 this.execCommand('mceInsertContent', false, content); |
|
1750 }, |
|
1751 |
|
1752 /** |
|
1753 * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. |
|
1754 * |
|
1755 * @method isDirty |
|
1756 * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. |
|
1757 * @example |
|
1758 * if (tinymce.activeEditor.isDirty()) |
|
1759 * alert("You must save your contents."); |
|
1760 */ |
|
1761 isDirty: function() { |
|
1762 return !this.isNotDirty; |
|
1763 }, |
|
1764 |
|
1765 /** |
|
1766 * Returns the editors container element. The container element wrappes in |
|
1767 * all the elements added to the page for the editor. Such as UI, iframe etc. |
|
1768 * |
|
1769 * @method getContainer |
|
1770 * @return {Element} HTML DOM element for the editor container. |
|
1771 */ |
|
1772 getContainer: function() { |
|
1773 var self = this; |
|
1774 |
|
1775 if (!self.container) { |
|
1776 self.container = DOM.get(self.editorContainer || self.id + '_parent'); |
|
1777 } |
|
1778 |
|
1779 return self.container; |
|
1780 }, |
|
1781 |
|
1782 /** |
|
1783 * Returns the editors content area container element. The this element is the one who |
|
1784 * holds the iframe or the editable element. |
|
1785 * |
|
1786 * @method getContentAreaContainer |
|
1787 * @return {Element} HTML DOM element for the editor area container. |
|
1788 */ |
|
1789 getContentAreaContainer: function() { |
|
1790 return this.contentAreaContainer; |
|
1791 }, |
|
1792 |
|
1793 /** |
|
1794 * Returns the target element/textarea that got replaced with a TinyMCE editor instance. |
|
1795 * |
|
1796 * @method getElement |
|
1797 * @return {Element} HTML DOM element for the replaced element. |
|
1798 */ |
|
1799 getElement: function() { |
|
1800 if (!this.targetElm) { |
|
1801 this.targetElm = DOM.get(this.id); |
|
1802 } |
|
1803 |
|
1804 return this.targetElm; |
|
1805 }, |
|
1806 |
|
1807 /** |
|
1808 * Returns the iframes window object. |
|
1809 * |
|
1810 * @method getWin |
|
1811 * @return {Window} Iframe DOM window object. |
|
1812 */ |
|
1813 getWin: function() { |
|
1814 var self = this, elm; |
|
1815 |
|
1816 if (!self.contentWindow) { |
|
1817 elm = self.iframeElement; |
|
1818 |
|
1819 if (elm) { |
|
1820 self.contentWindow = elm.contentWindow; |
|
1821 } |
|
1822 } |
|
1823 |
|
1824 return self.contentWindow; |
|
1825 }, |
|
1826 |
|
1827 /** |
|
1828 * Returns the iframes document object. |
|
1829 * |
|
1830 * @method getDoc |
|
1831 * @return {Document} Iframe DOM document object. |
|
1832 */ |
|
1833 getDoc: function() { |
|
1834 var self = this, win; |
|
1835 |
|
1836 if (!self.contentDocument) { |
|
1837 win = self.getWin(); |
|
1838 |
|
1839 if (win) { |
|
1840 self.contentDocument = win.document; |
|
1841 } |
|
1842 } |
|
1843 |
|
1844 return self.contentDocument; |
|
1845 }, |
|
1846 |
|
1847 /** |
|
1848 * Returns the root element of the editable area. |
|
1849 * For a non-inline iframe-based editor, returns the iframe's body element. |
|
1850 * |
|
1851 * @method getBody |
|
1852 * @return {Element} The root element of the editable area. |
|
1853 */ |
|
1854 getBody: function() { |
|
1855 return this.bodyElement || this.getDoc().body; |
|
1856 }, |
|
1857 |
|
1858 /** |
|
1859 * URL converter function this gets executed each time a user adds an img, a or |
|
1860 * any other element that has a URL in it. This will be called both by the DOM and HTML |
|
1861 * manipulation functions. |
|
1862 * |
|
1863 * @method convertURL |
|
1864 * @param {string} url URL to convert. |
|
1865 * @param {string} name Attribute name src, href etc. |
|
1866 * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert. |
|
1867 * @return {string} Converted URL string. |
|
1868 */ |
|
1869 convertURL: function(url, name, elm) { |
|
1870 var self = this, settings = self.settings; |
|
1871 |
|
1872 // Use callback instead |
|
1873 if (settings.urlconverter_callback) { |
|
1874 return self.execCallback('urlconverter_callback', url, elm, true, name); |
|
1875 } |
|
1876 |
|
1877 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs |
|
1878 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) { |
|
1879 return url; |
|
1880 } |
|
1881 |
|
1882 // Convert to relative |
|
1883 if (settings.relative_urls) { |
|
1884 return self.documentBaseURI.toRelative(url); |
|
1885 } |
|
1886 |
|
1887 // Convert to absolute |
|
1888 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); |
|
1889 |
|
1890 return url; |
|
1891 }, |
|
1892 |
|
1893 /** |
|
1894 * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor. |
|
1895 * |
|
1896 * @method addVisual |
|
1897 * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid. |
|
1898 */ |
|
1899 addVisual: function(elm) { |
|
1900 var self = this, settings = self.settings, dom = self.dom, cls; |
|
1901 |
|
1902 elm = elm || self.getBody(); |
|
1903 |
|
1904 if (self.hasVisual === undefined) { |
|
1905 self.hasVisual = settings.visual; |
|
1906 } |
|
1907 |
|
1908 each(dom.select('table,a', elm), function(elm) { |
|
1909 var value; |
|
1910 |
|
1911 switch (elm.nodeName) { |
|
1912 case 'TABLE': |
|
1913 cls = settings.visual_table_class || 'mce-item-table'; |
|
1914 value = dom.getAttrib(elm, 'border'); |
|
1915 |
|
1916 if ((!value || value == '0') && self.hasVisual) { |
|
1917 dom.addClass(elm, cls); |
|
1918 } else { |
|
1919 dom.removeClass(elm, cls); |
|
1920 } |
|
1921 |
|
1922 return; |
|
1923 |
|
1924 case 'A': |
|
1925 if (!dom.getAttrib(elm, 'href', false)) { |
|
1926 value = dom.getAttrib(elm, 'name') || elm.id; |
|
1927 cls = settings.visual_anchor_class || 'mce-item-anchor'; |
|
1928 |
|
1929 if (value && self.hasVisual) { |
|
1930 dom.addClass(elm, cls); |
|
1931 } else { |
|
1932 dom.removeClass(elm, cls); |
|
1933 } |
|
1934 } |
|
1935 |
|
1936 return; |
|
1937 } |
|
1938 }); |
|
1939 |
|
1940 self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual}); |
|
1941 }, |
|
1942 |
|
1943 /** |
|
1944 * Removes the editor from the dom and tinymce collection. |
|
1945 * |
|
1946 * @method remove |
|
1947 */ |
|
1948 remove: function() { |
|
1949 var self = this; |
|
1950 |
|
1951 if (!self.removed) { |
|
1952 self.save(); |
|
1953 self.removed = 1; |
|
1954 self.unbindAllNativeEvents(); |
|
1955 |
|
1956 // Remove any hidden input |
|
1957 if (self.hasHiddenInput) { |
|
1958 DOM.remove(self.getElement().nextSibling); |
|
1959 } |
|
1960 |
|
1961 if (!self.inline) { |
|
1962 // IE 9 has a bug where the selection stops working if you place the |
|
1963 // caret inside the editor then remove the iframe |
|
1964 if (ie && ie < 10) { |
|
1965 self.getDoc().execCommand('SelectAll', false, null); |
|
1966 } |
|
1967 |
|
1968 DOM.setStyle(self.id, 'display', self.orgDisplay); |
|
1969 self.getBody().onload = null; // Prevent #6816 |
|
1970 } |
|
1971 |
|
1972 self.fire('remove'); |
|
1973 |
|
1974 self.editorManager.remove(self); |
|
1975 DOM.remove(self.getContainer()); |
|
1976 self.destroy(); |
|
1977 } |
|
1978 }, |
|
1979 |
|
1980 /** |
|
1981 * Destroys the editor instance by removing all events, element references or other resources |
|
1982 * that could leak memory. This method will be called automatically when the page is unloaded |
|
1983 * but you can also call it directly if you know what you are doing. |
|
1984 * |
|
1985 * @method destroy |
|
1986 * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one. |
|
1987 */ |
|
1988 destroy: function(automatic) { |
|
1989 var self = this, form; |
|
1990 |
|
1991 // One time is enough |
|
1992 if (self.destroyed) { |
|
1993 return; |
|
1994 } |
|
1995 |
|
1996 // If user manually calls destroy and not remove |
|
1997 // Users seems to have logic that calls destroy instead of remove |
|
1998 if (!automatic && !self.removed) { |
|
1999 self.remove(); |
|
2000 return; |
|
2001 } |
|
2002 |
|
2003 if (!automatic) { |
|
2004 self.editorManager.off('beforeunload', self._beforeUnload); |
|
2005 |
|
2006 // Manual destroy |
|
2007 if (self.theme && self.theme.destroy) { |
|
2008 self.theme.destroy(); |
|
2009 } |
|
2010 |
|
2011 // Destroy controls, selection and dom |
|
2012 self.selection.destroy(); |
|
2013 self.dom.destroy(); |
|
2014 } |
|
2015 |
|
2016 form = self.formElement; |
|
2017 if (form) { |
|
2018 if (form._mceOldSubmit) { |
|
2019 form.submit = form._mceOldSubmit; |
|
2020 form._mceOldSubmit = null; |
|
2021 } |
|
2022 |
|
2023 DOM.unbind(form, 'submit reset', self.formEventDelegate); |
|
2024 } |
|
2025 |
|
2026 self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null; |
|
2027 self.bodyElement = self.contentDocument = self.contentWindow = null; |
|
2028 self.iframeElement = self.targetElm = null; |
|
2029 |
|
2030 if (self.selection) { |
|
2031 self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null; |
|
2032 } |
|
2033 |
|
2034 self.destroyed = 1; |
|
2035 }, |
|
2036 |
|
2037 // Internal functions |
|
2038 |
|
2039 _refreshContentEditable: function() { |
|
2040 var self = this, body, parent; |
|
2041 |
|
2042 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again |
|
2043 if (self._isHidden()) { |
|
2044 body = self.getBody(); |
|
2045 parent = body.parentNode; |
|
2046 |
|
2047 parent.removeChild(body); |
|
2048 parent.appendChild(body); |
|
2049 |
|
2050 body.focus(); |
|
2051 } |
|
2052 }, |
|
2053 |
|
2054 _isHidden: function() { |
|
2055 var sel; |
|
2056 |
|
2057 if (!isGecko) { |
|
2058 return 0; |
|
2059 } |
|
2060 |
|
2061 // Weird, wheres that cursor selection? |
|
2062 sel = this.selection.getSel(); |
|
2063 return (!sel || !sel.rangeCount || sel.rangeCount === 0); |
|
2064 } |
|
2065 }; |
|
2066 |
|
2067 extend(Editor.prototype, EditorObservable); |
|
2068 |
|
2069 return Editor; |
|
2070 }); |