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>