Annotation of kupuMPIWG/common/kupueditor.js, revision 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>