Annotation of kupu/common/kupueditor.js, revision 1.1.1.1
1.1 dwinter 1: /*****************************************************************************
2: *
3: * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
4: *
5: * This software is distributed under the terms of the Kupu
6: * License. See LICENSE.txt for license text. For a list of Kupu
7: * Contributors see CREDITS.txt.
8: *
9: *****************************************************************************/
10:
11: // $Id: kupueditor.js 14851 2005-07-21 11:39:15Z duncan $
12:
13: //----------------------------------------------------------------------------
14: // Main classes
15: //----------------------------------------------------------------------------
16:
17: /* KupuDocument
18:
19: This essentially wraps the iframe.
20: XXX Is this overkill?
21:
22: */
23:
24: function KupuDocument(iframe) {
25: /* Model */
26:
27: // attrs
28: this.editable = iframe; // the iframe
29: this.window = this.editable.contentWindow;
30: this.document = this.window.document;
31:
32: this._browser = _SARISSA_IS_IE ? 'IE' : 'Mozilla';
33:
34: // methods
35: this.execCommand = function(command, arg) {
36: /* delegate execCommand */
37: if (arg === undefined) arg = null;
38: this.document.execCommand(command, false, arg);
39: };
40:
41: this.reloadSource = function() {
42: /* reload the source */
43:
44: // XXX To temporarily work around problems with resetting the
45: // state after a reload, currently the whole page is reloaded.
46: // XXX Nasty workaround!! to solve refresh problems...
47: document.location = document.location;
48: };
49:
50: this.getDocument = function() {
51: /* returns a reference to the window.document object of the iframe */
52: return this.document;
53: };
54:
55: this.getWindow = function() {
56: /* returns a reference to the window object of the iframe */
57: return this.window;
58: };
59:
60: this.getSelection = function() {
61: if (this._browser == 'Mozilla') {
62: return new MozillaSelection(this);
63: } else {
64: return new IESelection(this);
65: };
66: };
67:
68: this.getEditable = function() {
69: return this.editable;
70: };
71:
72: };
73:
74: /* KupuEditor
75:
76: This controls the document, should be used from the UI.
77:
78: */
79:
80: function KupuEditor(document, config, logger) {
81: /* Controller */
82:
83: // attrs
84: this.document = document; // the model
85: this.config = config; // an object that holds the config values
86: this.log = logger; // simple logger object
87: this.tools = {}; // mapping id->tool
88: this.filters = new Array(); // contentfilters
89:
90: this._designModeSetAttempts = 0;
91: this._initialized = false;
92:
93: // some properties to save the selection, required for IE to remember where
94: // in the iframe the selection was
95: this._previous_range = null;
96:
97: // this property is true if the content is changed, false if no changes are made yet
98: this.content_changed = false;
99:
100: // methods
101: this.initialize = function() {
102: /* Should be called on iframe.onload, will initialize the editor */
103: //DOM2Event.initRegistration();
104: this._initializeEventHandlers();
105: if (this.getBrowserName() == "IE") {
106: var body = this.getInnerDocument().getElementsByTagName('body')[0];
107: body.setAttribute('contentEditable', 'true');
108: // provide an 'afterInit' method on KupuEditor.prototype
109: // for additional bootstrapping (after editor init)
110: this._initialized = true;
111: if (this.afterInit) {
112: this.afterInit();
113: };
114: this._saveSelection();
115: } else {
116: this._setDesignModeWhenReady();
117: };
118: this.logMessage(_('Editor initialized'));
119: };
120:
121: this.setContextMenu = function(menu) {
122: /* initialize the contextmenu */
123: menu.initialize(this);
124: };
125:
126: this.registerTool = function(id, tool) {
127: /* register a tool */
128: this.tools[id] = tool;
129: tool.initialize(this);
130: };
131:
132: this.getTool = function(id) {
133: /* get a tool by id */
134: return this.tools[id];
135: };
136:
137: this.registerFilter = function(filter) {
138: /* register a content filter method
139:
140: the method will be called together with any other registered
141: filters before the content is saved to the server, the methods
142: can be used to filter any trash out of the content. they are
143: called with 1 argument, which is a reference to the rootnode
144: of the content tree (the html node)
145: */
146: this.filters.push(filter);
147: filter.initialize(this);
148: };
149:
150: this.updateStateHandler = function(event) {
151: /* check whether the event is interesting enough to trigger the
152: updateState machinery and act accordingly */
153: var interesting_codes = new Array(8, 13, 37, 38, 39, 40, 46);
154: // unfortunately it's not possible to do this on blur, since that's
155: // too late. also (some versions of?) IE 5.5 doesn't support the
156: // onbeforedeactivate event, which would be ideal here...
157: this._saveSelection();
158:
159: if (event.type == 'click' || event.type=='mouseup' ||
160: (event.type == 'keyup' &&
161: interesting_codes.contains(event.keyCode))) {
162: // Filthy trick to make the updateState method get called *after*
163: // the event has been resolved. This way the updateState methods can
164: // react to the situation *after* any actions have been performed (so
165: // can actually stay up to date).
166: this.updateState(event);
167: }
168: };
169:
170: this.updateState = function(event) {
171: /* let each tool change state if required */
172: // first see if the event is interesting enough to trigger
173: // the whole updateState machinery
174: var selNode = this.getSelectedNode();
175: for (var id in this.tools) {
176: try {
177: this.tools[id].updateState(selNode, event);
178: } catch (e) {
179: if (e == UpdateStateCancelBubble) {
180: this.updateState(event);
181: break;
182: } else {
183: this.logMessage(
184: _('Exception while processing updateState on ' +
185: '${id}: ${msg}', {'id': id, 'msg': e}), 2);
186: };
187: };
188: };
189: };
190:
191: this.saveDocument = function(redirect, synchronous) {
192: /* save the document, redirect if te arg is provided and the save is successful
193:
194: the (optional) redirect argument can be used to make the client jump to
195: another URL when the save action was successful.
196: */
197:
198: // if no dst is available, bail out
199: if (!this.config.dst) {
200: this.logMessage(_('No destination URL available!'), 2);
201: return;
202: }
203: var sourcetool = this.getTool('sourceedittool');
204: if (sourcetool) {sourcetool.cancelSourceMode();};
205:
206: // make sure people can't edit or save during saving
207: if (!this._initialized) {
208: return;
209: }
210: this._initialized = false;
211:
212: // set the window status so people can see we're actually saving
213: window.status= _("Please wait while saving document...");
214:
215: // call (optional) beforeSave() method on all tools
216: for (var id in this.tools) {
217: var tool = this.tools[id];
218: if (tool.beforeSave) {
219: try {
220: tool.beforeSave();
221: } catch(e) {
222: alert(e);
223: this._initialized = true;
224: return;
225: };
226: };
227: };
228:
229: // pass the content through the filters
230: this.logMessage(_("Starting HTML cleanup"));
231: var transform = this._filterContent(this.getInnerDocument().documentElement);
232:
233: // serialize to a string
234: var contents = this._serializeOutputToString(transform);
235:
236: this.logMessage(_("Cleanup done, sending document to server"));
237: var request = new XMLHttpRequest();
238:
239: if (!synchronous) {
240: request.onreadystatechange = (new ContextFixer(this._saveCallback,
241: this, request, redirect)).execute;
242: request.open("PUT", this.config.dst, true);
243: request.setRequestHeader("Content-type", this.config.content_type);
244: request.send(contents);
245: this.logMessage(_("Request sent to server"));
246: } else {
247: this.logMessage(_('Sending request to server'));
248: request.open("PUT", this.config.dst, false);
249: request.setRequestHeader("Content-type", this.config.content_type);
250: request.send(contents);
251: this.handleSaveResponse(request,redirect)
252: };
253: };
254:
255: this.prepareForm = function(form, id) {
256: /* add a field to the form and place the contents in it
257:
258: can be used for simple POST support where Kupu is part of a
259: form
260: */
261: var sourcetool = this.getTool('sourceedittool');
262: if (sourcetool) {sourcetool.cancelSourceMode();};
263:
264: // make sure people can't edit or save during saving
265: if (!this._initialized) {
266: return;
267: }
268: this._initialized = false;
269:
270: // set the window status so people can see we're actually saving
271: window.status= _("Please wait while saving document...");
272:
273: // call (optional) beforeSave() method on all tools
274: for (var tid in this.tools) {
275: var tool = this.tools[tid];
276: if (tool.beforeSave) {
277: try {
278: tool.beforeSave();
279: } catch(e) {
280: alert(e);
281: this._initialized = true;
282: return;
283: };
284: };
285: };
286:
287: // set a default id
288: if (!id) {
289: id = 'kupu';
290: };
291:
292: // pass the content through the filters
293: this.logMessage(_("Starting HTML cleanup"));
294: var transform = this._filterContent(this.getInnerDocument().documentElement);
295:
296: // XXX need to fix this. Sometimes a spurious "\n\n" text
297: // node appears in the transform, which breaks the Moz
298: // serializer on .xml
299: var contents = this._serializeOutputToString(transform);
300:
301: this.logMessage(_("Cleanup done, sending document to server"));
302:
303: // now create the form input, since IE 5.5 doesn't support the
304: // ownerDocument property we use window.document as a fallback (which
305: // will almost by definition be correct).
306: var document = form.ownerDocument ? form.ownerDocument : window.document;
307: var ta = document.createElement('textarea');
308: ta.style.visibility = 'hidden';
309: var text = document.createTextNode(contents);
310: ta.appendChild(text);
311: ta.setAttribute('name', id);
312:
313: // and add it to the form
314: form.appendChild(ta);
315:
316: // let the calling code know we have added the textarea
317: return true;
318: };
319:
320: this.execCommand = function(command, param) {
321: /* general stuff like making current selection bold, italics etc.
322: and adding basic elements such as lists
323: */
324: if (!this._initialized) {
325: this.logMessage(_('Editor not initialized yet!'));
326: return;
327: };
328: if (this.getBrowserName() == "IE") {
329: this._restoreSelection();
330: } else {
331: this.focusDocument();
332: if (command != 'useCSS') {
333: this.content_changed = true;
334: // note the negation: the argument doesn't work as
335: // expected...
336: // Done here otherwise it doesn't always work or gets lost
337: // after some commands
338: this.getDocument().execCommand('useCSS', !this.config.use_css);
339: };
340: };
341: this.getDocument().execCommand(command, param);
342: var message = _('Command ${command} executed', {'command': command});
343: if (param) {
344: message = _('Command ${command} executed with parameter ${param}',
345: {'command': command, 'param': param});
346: }
347: this.updateState();
348: this.logMessage(message);
349: };
350:
351: this.getSelection = function() {
352: /* returns a Selection object wrapping the current selection */
353: this._restoreSelection();
354: return this.getDocument().getSelection();
355: };
356:
357: this.getSelectedNode = function() {
358: /* returns the selected node (read: parent) or none */
359: return this.getSelection().parentElement();
360: };
361:
362: this.getNearestParentOfType = function(node, type) {
363: /* well the title says it all ;) */
364: var type = type.toLowerCase();
365: while (node) {
366: if (node.nodeName.toLowerCase() == type) {
367: return node
368: }
369: var node = node.parentNode;
370: }
371: return false;
372: };
373:
374: this.removeNearestParentOfType = function(node, type) {
375: var nearest = this.getNearestParentOfType(node, type);
376: if (!nearest) {
377: return false;
378: };
379: var parent = nearest.parentNode;
380: while (nearest.childNodes.length) {
381: var child = nearest.firstChild;
382: child = nearest.removeChild(child);
383: parent.insertBefore(child, nearest);
384: };
385: parent.removeChild(nearest);
386: };
387:
388: this.getDocument = function() {
389: /* returns a reference to the document object that wraps the iframe */
390: return this.document;
391: };
392:
393: this.getInnerDocument = function() {
394: /* returns a reference to the window.document object of the iframe */
395: return this.getDocument().getDocument();
396: };
397:
398: this.insertNodeAtSelection = function(insertNode, selectNode) {
399: /* insert a newly created node into the document */
400: if (!this._initialized) {
401: this.logMessage(_('Editor not initialized yet!'));
402: return;
403: };
404:
405: this.content_changed = true;
406:
407: var browser = this.getBrowserName();
408: if (browser != "IE") {
409: this.focusDocument();
410: };
411:
412: var ret = this.getSelection().replaceWithNode(insertNode, selectNode);
413: this._saveSelection();
414:
415: return ret;
416: };
417:
418: this.focusDocument = function() {
419: this.getDocument().getWindow().focus();
420: }
421:
422: this.logMessage = function(message, severity) {
423: /* log a message using the logger, severity can be 0 (message, default), 1 (warning) or 2 (error) */
424: this.log.log(message, severity);
425: };
426:
427: this.registerContentChanger = function(element) {
428: /* set this.content_changed to true (marking the content changed) when the
429: element's onchange is called
430: */
431: addEventHandler(element, 'change', function() {this.content_changed = true;}, this);
432: };
433:
434: // helper methods
435: this.getBrowserName = function() {
436: /* returns either 'Mozilla' (for Mozilla, Firebird, Netscape etc.) or 'IE' */
437: if (_SARISSA_IS_MOZ) {
438: return "Mozilla";
439: } else if (_SARISSA_IS_IE) {
440: return "IE";
441: } else {
442: throw _("Browser not supported!");
443: }
444: };
445:
446: this.handleSaveResponse = function(request, redirect) {
447: // mind the 1223 status, somehow IE gives that sometimes (on 204?)
448: // at first we didn't want to add it here, since it's a specific IE
449: // bug, but too many users had trouble with it...
450: if (request.status != '200' && request.status != '204' &&
451: request.status != '1223') {
452: var msg = _('Error saving your data.\nResponse status: ' +
453: '${status}.\nCheck your server log for more ' +
454: 'information.', {'status': request.status});
455: alert(msg);
456: window.status = _("Error saving document");
457: } else if (redirect) { // && (!request.status || request.status == '200' || request.status == '204'))
458: window.document.location = redirect;
459: this.content_changed = false;
460: } else {
461: // clear content_changed before reloadSrc so saveOnPart is not triggered
462: this.content_changed = false;
463: if (this.config.reload_after_save) {
464: this.reloadSrc();
465: };
466: // we're done so we can start editing again
467: window.status= _("Document saved");
468: };
469: this._initialized = true;
470: };
471:
472: // private methods
473: this._addEventHandler = addEventHandler;
474:
475: this._saveCallback = function(request, redirect) {
476: /* callback for Sarissa */
477: if (request.readyState == 4) {
478: this.handleSaveResponse(request, redirect)
479: };
480: };
481:
482: this.reloadSrc = function() {
483: /* reload the src, called after a save when reload_src is set to true */
484: // XXX Broken!!!
485: /*
486: if (this.getBrowserName() == "Mozilla") {
487: this.getInnerDocument().designMode = "Off";
488: }
489: */
490: // XXX call reloadSrc() which has a workaround, reloads the full page
491: // instead of just the iframe...
492: this.getDocument().reloadSource();
493: if (this.getBrowserName() == "Mozilla") {
494: this.getInnerDocument().designMode = "On";
495: };
496: /*
497: var selNode = this.getSelectedNode();
498: this.updateState(selNode);
499: */
500: };
501:
502: this._initializeEventHandlers = function() {
503: /* attache the event handlers to the iframe */
504: // Initialize DOM2Event compatibility
505: // XXX should come back and change to passing in an element
506: this._addEventHandler(this.getInnerDocument(), "click", this.updateStateHandler, this);
507: this._addEventHandler(this.getInnerDocument(), "dblclick", this.updateStateHandler, this);
508: this._addEventHandler(this.getInnerDocument(), "keyup", this.updateStateHandler, this);
509: this._addEventHandler(this.getInnerDocument(), "keyup", function() {this.content_changed = true}, this);
510: this._addEventHandler(this.getInnerDocument(), "mouseup", this.updateStateHandler, this);
511: };
512:
513: this._setDesignModeWhenReady = function() {
514: /* Rather dirty polling loop to see if Mozilla is done doing it's
515: initialization thing so design mode can be set.
516: */
517: this._designModeSetAttempts++;
518: if (this._designModeSetAttempts > 25) {
519: alert(_('Couldn\'t set design mode. Kupu will not work on this browser.'));
520: return;
521: };
522: var success = false;
523: try {
524: this._setDesignMode();
525: success = true;
526: } catch (e) {
527: // register a function to the timer_instance because
528: // window.setTimeout can't refer to 'this'...
529: timer_instance.registerFunction(this, this._setDesignModeWhenReady, 100);
530: };
531: if (success) {
532: // provide an 'afterInit' method on KupuEditor.prototype
533: // for additional bootstrapping (after editor init)
534: if (this.afterInit) {
535: this.afterInit();
536: };
537: };
538: };
539:
540: this._setDesignMode = function() {
541: this.getInnerDocument().designMode = "On";
542: this.execCommand("undo");
543: // note the negation: the argument doesn't work as expected...
544: this._initialized = true;
545: };
546:
547: this._saveSelection = function() {
548: /* Save the selection, works around a problem with IE where the
549: selection in the iframe gets lost. We only save if the current
550: selection in the document */
551: if (this._isDocumentSelected()) {
552: var currange = this.getInnerDocument().selection.createRange();
553: this._previous_range = currange;
554: };
555: };
556:
557: this._restoreSelection = function() {
558: /* re-selects the previous selection in IE. We only restore if the
559: current selection is not in the document.*/
560: if (this._previous_range && !this._isDocumentSelected()) {
561: try {
562: this._previous_range.select();
563: } catch (e) {
564: alert("Error placing back selection");
565: this.logMessage(_('Error placing back selection'));
566: };
567: };
568: };
569:
570: if (this.getBrowserName() != "IE") {
571: this._saveSelection = function() {};
572: this._restoreSelection = function() {};
573: }
574:
575: this._isDocumentSelected = function() {
576: var editable_body = this.getInnerDocument().getElementsByTagName('body')[0];
577: try {
578: var selrange = this.getInnerDocument().selection.createRange();
579: } catch(e) {
580: return false;
581: }
582: var someelement = selrange.parentElement ? selrange.parentElement() : selrange.item(0);
583:
584: while (someelement.nodeName.toLowerCase() != 'body') {
585: someelement = someelement.parentNode;
586: };
587:
588: return someelement == editable_body;
589: };
590:
591: this._clearSelection = function() {
592: /* clear the last stored selection */
593: this._previous_range = null;
594: };
595:
596: this._filterContent = function(documentElement) {
597: /* pass the content through all the filters */
598: // first copy all nodes to a Sarissa document so it's usable
599: var xhtmldoc = Sarissa.getDomDocument();
600: var doc = this._convertToSarissaNode(xhtmldoc, documentElement);
601: // now pass it through all filters
602: for (var i=0; i < this.filters.length; i++) {
603: var doc = this.filters[i].filter(xhtmldoc, doc);
604: };
605: // fix some possible structural problems, such as an empty or missing head, title
606: // or script or textarea tags without closing tag...
607: this._fixXML(doc, xhtmldoc);
608: return doc;
609: };
610:
611: this.getXMLBody = function(transform) {
612: var bodies = transform.getElementsByTagName('body');
613: var data = '';
614: for (var i = 0; i < bodies.length; i++) {
615: data += Sarissa.serialize(bodies[i]);
616: }
617: return this.escapeEntities(data);
618: };
619:
620: this.getHTMLBody = function() {
621: var doc = this.getInnerDocument();
622: var docel = doc.documentElement;
623: var bodies = docel.getElementsByTagName('body');
624: var data = '';
625: for (var i = 0; i < bodies.length; i++) {
626: data += bodies[i].innerHTML;
627: }
628: return this.escapeEntities(data);
629: };
630:
631: // If we have multiple bodies this needs to remove the extras.
632: this.setHTMLBody = function(text) {
633: var bodies = this.getInnerDocument().documentElement.getElementsByTagName('body');
634: for (var i = 0; i < bodies.length-1; i++) {
635: bodies[i].parentNode.removeChild(bodies[i]);
636: }
637: bodies[bodies.length-1].innerHTML = text;
638: };
639:
640: this._fixXML = function(doc, document) {
641: /* fix some structural problems in the XML that make it invalid XTHML */
642: // find if we have a head and title, and if not add them
643: var heads = doc.getElementsByTagName('head');
644: var titles = doc.getElementsByTagName('title');
645: if (!heads.length) {
646: // assume we have a body, guess Kupu won't work without one anyway ;)
647: var body = doc.getElementsByTagName('body')[0];
648: var head = document.createElement('head');
649: body.parentNode.insertBefore(head, body);
650: var title = document.createElement('title');
651: var titletext = document.createTextNode('');
652: head.appendChild(title);
653: title.appendChild(titletext);
654: } else if (!titles.length) {
655: var head = heads[0];
656: var title = document.createElement('title');
657: var titletext = document.createTextNode('');
658: head.appendChild(title);
659: title.appendChild(titletext);
660: };
661: // create a closing element for all elements that require one in XHTML
662: var dualtons = new Array('a', 'abbr', 'acronym', 'address', 'applet',
663: 'b', 'bdo', 'big', 'blink', 'blockquote',
664: 'button', 'caption', 'center', 'cite',
665: 'comment', 'del', 'dfn', 'dir', 'div',
666: 'dl', 'dt', 'em', 'embed', 'fieldset',
667: 'font', 'form', 'frameset', 'h1', 'h2',
668: 'h3', 'h4', 'h5', 'h6', 'i', 'iframe',
669: 'ins', 'kbd', 'label', 'legend', 'li',
670: 'listing', 'map', 'marquee', 'menu',
671: 'multicol', 'nobr', 'noembed', 'noframes',
672: 'noscript', 'object', 'ol', 'optgroup',
673: 'option', 'p', 'pre', 'q', 's', 'script',
674: 'select', 'small', 'span', 'strike',
675: 'strong', 'style', 'sub', 'sup', 'table',
676: 'tbody', 'td', 'textarea', 'tfoot',
677: 'th', 'thead', 'title', 'tr', 'tt', 'u',
678: 'ul', 'xmp');
679: // XXX I reckon this is *way* slow, can we use XPath instead or
680: // something to speed this up?
681: for (var i=0; i < dualtons.length; i++) {
682: var elname = dualtons[i];
683: var els = doc.getElementsByTagName(elname);
684: for (var j=0; j < els.length; j++) {
685: var el = els[j];
686: if (!el.hasChildNodes()) {
687: var child = document.createTextNode('');
688: el.appendChild(child);
689: };
690: };
691: };
692: };
693:
694: this.xhtmlvalid = new XhtmlValidation(this);
695:
696: this._convertToSarissaNode = function(ownerdoc, htmlnode) {
697: /* Given a string of non-well-formed HTML, return a string of
698: well-formed XHTML.
699:
700: This function works by leveraging the already-excellent HTML
701: parser inside the browser, which generally can turn a pile
702: of crap into a DOM. We iterate over the HTML DOM, appending
703: new nodes (elements and attributes) into a node.
704:
705: The primary problems this tries to solve for crappy HTML: mixed
706: element names, elements that open but don't close,
707: and attributes that aren't in quotes. This can also be adapted
708: to filter out tags that you don't want and clean up inline styles.
709:
710: Inspired by Guido, adapted by Paul from something in usenet.
711: Tag and attribute tables added by Duncan
712: */
713: return this.xhtmlvalid._convertToSarissaNode(ownerdoc, htmlnode);
714: };
715:
716: this._fixupSingletons = function(xml) {
717: return xml.replace(/<([^>]+)\/>/g, "<$1 />");
718: }
719: this._serializeOutputToString = function(transform) {
720: // XXX need to fix this. Sometimes a spurious "\n\n" text
721: // node appears in the transform, which breaks the Moz
722: // serializer on .xml
723:
724: if (this.config.strict_output) {
725: var contents = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' +
726: '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' +
727: '<html xmlns="http://www.w3.org/1999/xhtml">' +
728: Sarissa.serialize(transform.getElementsByTagName("head")[0]) +
729: Sarissa.serialize(transform.getElementsByTagName("body")[0]) +
730: '</html>';
731: } else {
732: var contents = '<html>' +
733: Sarissa.serialize(transform.getElementsByTagName("head")[0]) +
734: Sarissa.serialize(transform.getElementsByTagName("body")[0]) +
735: '</html>';
736: };
737:
738: contents = this.escapeEntities(contents);
739:
740: if (this.config.compatible_singletons) {
741: contents = this._fixupSingletons(contents);
742: };
743:
744: return contents;
745: };
746: this.escapeEntities = function(xml) {
747: // Escape non-ascii characters as entities.
748: return xml.replace(/[^\r\n -\177]/g,
749: function(c) {
750: return '&#'+c.charCodeAt(0)+';';
751: });
752: }
753:
754: this.getFullEditor = function() {
755: var fulleditor = this.getDocument().getEditable();
756: while (!/kupu-fulleditor/.test(fulleditor.className)) {
757: fulleditor = fulleditor.parentNode;
758: }
759: return fulleditor;
760: }
761: // Control the className and hence the style for the whole editor.
762: this.setClass = function(name) {
763: this.getFullEditor().className += ' '+name;
764: }
765:
766: this.clearClass = function(name) {
767: var fulleditor = this.getFullEditor();
768: fulleditor.className = fulleditor.className.replace(' '+name, '');
769: }
770:
771: this.suspendEditing = function() {
772: this._previous_range = this.getSelection().getRange();
773: this.setClass('kupu-modal');
774: for (var id in this.tools) {
775: this.tools[id].disable();
776: }
777: if (this.getBrowserName() == "IE") {
778: var body = this.getInnerDocument().getElementsByTagName('body')[0];
779: body.setAttribute('contentEditable', 'false');
780: } else {
781:
782: this.getInnerDocument().designMode = "Off";
783: var iframe = this.getDocument().getEditable();
784: iframe.style.position = iframe.style.position?"":"relative"; // Changing this disables designMode!
785: }
786: this.suspended = true;
787: }
788:
789: this.resumeEditing = function() {
790: if (!this.suspended) {
791: return;
792: }
793: this.suspended = false;
794: this.clearClass('kupu-modal');
795: for (var id in this.tools) {
796: this.tools[id].enable();
797: }
798: if (this.getBrowserName() == "IE") {
799: this._restoreSelection();
800: var body = this.getInnerDocument().getElementsByTagName('body')[0];
801: body.setAttribute('contentEditable', 'true');
802: } else {
803: var doc = this.getInnerDocument();
804: doc.designMode = "On";
805: this.getSelection().restoreRange(this._previous_range);
806: }
807: }
808: }
809:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>