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>