com.smartclient.debug.public.sc.client.widgets.RichTextCanvas.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of smartgwt Show documentation
Show all versions of smartgwt Show documentation
SmartGWT - GWT API's for SmartClient
The newest version!
/*
* Isomorphic SmartClient
* Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
* Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
* "SmartClient" is a trademark of Isomorphic Software, Inc.
*
* [email protected]
*
* http://smartclient.com/license
*/
//> @class RichTextCanvas
//
// Canvas to be used for Rich Text Editing
//
//<
isc.ClassFactory.defineClass("RichTextCanvas","Canvas");
isc.RichTextCanvas.addClassProperties({
// enumerated Justification types
//CENTER:"center",
//LEFT:"left",
//RIGHT:"right",
FULL:"full",
//>@classAttr RichTextCanvas.unsupportedErrorMessage (string : "Rich text editing not supported in this browser" : [IRW])
// Message to display to the user if they attempt to access the page in a browser which
// does not support rich-text-editing
//<
unsupportedErrorMessage : "Rich text editing not supported in this browser"
});
isc.RichTextCanvas.addProperties({
editable:true,
// Override 'canSelectText': to allow for most editing actions the user must be able to
// select the content from this widget.
canSelectText:true,
// RTC's are focusable
canFocus : true,
// Don't write out a focusProxy for RichTextCanvases - we don't want native keyboard
// focus to go to a hidden element
// Instead - in design mode we apply tabIndex directly to the content frame
// otherwise we rely on native tabIndex to allow focus in the widget handle at
// the correct times.
_useFocusProxy:false,
overflow:isc.Canvas.AUTO,
showCustomScrollbars:false,
// If a syntax rehilite of the entire contents is required, do it this number of
// milliseconds after the last keystroke (resets to this number every time the user hits a key)
fullSyntaxHiliteDelay: 3000,
// Don't show a non-breaking space by default
contents : ""
// even when hidden the rich text area picks up the bitmap of the area behind it and drags it
// along as it is scrolled.
//
// XXX not a viable workaround - when hidden in this manner, it fails to show()
// hideUsingDisplayNone: isc.Browser.isMoz
});
isc.RichTextCanvas.addClassMethods({
//> @classMethod RichTextCanvas.supportsRichTextEditing()
// Does this browser support rich text editing, using the isc.RichTextCanvas class?
//
// @return (boolean) true if supported by this browser.
//<
supportsRichTextEditing : function () {
var supported = ((isc.Browser.isSafari && isc.Browser.safariVersion >= 312) ||
(isc.Browser.isIE) ||
// Tested Moz (>=1.4 on Linux / Mac / Windows)
// Firefox (>=1.0 on Linux / Mac / Windows)
// Doesn't work on latest camino as of May 17 05 (Version 0.8.4)
(isc.Browser.isMoz && !isc.Browser.isCamino) ||
isc.Browser.isOpera
);
return supported;
}
});
//!>Deferred
isc.RichTextCanvas.addMethods({
// On init, verify that we're in a supported browser, and that the overflow is "auto"
initWidget : function () {
if (!isc.RichTextCanvas.supportsRichTextEditing()) {
var errorMessage = isc.RichTextCanvas.unsupportedErrorMessage;
this.logError(errorMessage);
}
if (this.overflow != isc.Canvas.AUTO) {
this.logWarn('RichTextCanvas class currently only supports an overflow property of "auto"');
this.overflow = isc.Canvas.AUTO;
}
// In "design mode" - where we write out an iframe with editable body content,
// turn off native tab index on the handle. We'll instead apply the tabIndex directly
// to the iframe
if (this._useDesignMode()) {
this._useNativeTabIndex = false;
}
this.Super("initWidget", arguments);
},
// Override getHandleOverflow - we always have an overflow of "auto" specified on the widget
// but if we're writing out an editable IFRAME, any scrollbars will show up on the inner
// content frame rather than the handle, so we never want to show scrollbars on the handle
_getHandleOverflow : function () {
if (this._useDesignMode()) {
var overflow;
if (this._useMozScrollbarsNone) {
overflow = "-moz-scrollbars-none";
this._useMozScrollSize = true;
} else {
overflow = this._$hidden;
}
return overflow;
} else return this.Super("_getHandleOverflow", arguments);
},
// getInnerHTML() overridden to write out an editable area.
getInnerHTML : function () {
// If we're writing out an IFrame with designMode:"On", return the appropriate HTML
if (this._useDesignMode() && !this.isPrinting) {
return this.getIFrameHTML();
}
// Otherwise we'll just be setting contentEditable on the standard widget handle.
//
// Note: we used to call Super here, but the Canvas implementation calls getContents()
// with no args which in this case returns the un-marked-up source, resulting in
// hilighting breaking on redraw. In this particular case, return the marked up
// contents since we'll be assigning to innerHTML
return this.getContents(true);
},
// _useDesignMode: Should we achieve our rich text canvas via an IFrame with DesignMode "On",
// or via a contentEdtiable DIV.
_useDesignMode : function () {
return isc.Browser.isMoz || isc.Browser.isSafari;
},
// ---------- Design Mode / IFRAME handling ------------------
getIFrameHTML : function () {
var isSafari = isc.Browser.isSafari,
URL = isSafari ? isc.Page.getBlankFrameURL() : null,
width = this.getContentFrameWidth() + isc.px,
height = this.getContentFrameHeight() + isc.px,
srcArray= [
""
];
//this.logWarn(srcArray.join(""));
return srcArray.join(isc.emptyString);
},
_setHandleTabIndex : function (index) {
if (this._useDesignMode()) {
var frame = this.getContentFrame();
if (frame != null) frame.tabIndex = index;
} else {
return this.Super("_setHandleTabIndex", arguments);
}
},
// getBrowserSpellCheck - function to determine if we want to use native browser spellcheck
// functionality where present.
getBrowserSpellCheck : function () {
return true;
},
// _frameLoaded - helper method to notify us that the IFRAME has loaded, so we can
// set up its contents / editability.
_frameLoaded : function () {
if (!this._drawingFrame) return;
delete this._drawingFrame;
if (!this.isDrawn()) return;
this._setupEditArea();
},
// Get the ID for the frame in the DOM
getIFrameID : function () {
return this.getID() + "_iframe";
},
// Get a pointer to the IFRAME content document
getContentDocument : function () {
if (isc.Browser.isIE) return document;
var win = this.getContentWindow(),
doc = win ? win.document : null;
if (doc == null) {
// This can happen validly as the document is not always available immediately
// after drawing.
this.logDebug("Unable to get pointer to content document. Content may not be written out");
}
return doc;
},
// Get a pointer to the document body
getContentBody : function () {
var doc = this.getContentDocument();
if (doc) return doc.body;
return null;
},
// Get a pointer to the IFRAME window object.
getContentWindow : function () {
var element = this.getContentFrame();
return element ? element.contentWindow : null;
},
// get a pointer to the IFRAME element in the DOM
getContentFrame : function () {
if (!this._useDesignMode() || !this.isDrawn()) return null;
return isc.Element.get(this.getIFrameID());
},
// Scrolling / Overflow:
// Override setOverflow() to be a no-op. We've already guaranteed the overflow will be
// 'auto' when the RichTextCanvas is initialized in initWidget().
setOverflow : function () {
},
// getScrollHandle() Returns a pointer to the element that gets natively scrolled by
// calls to scrollTo().
// - Overridden to point to the content body if _useDesignMode() is true.
getScrollHandle : function () {
if (this._useDesignMode()) return this.getContentBody();
return this.Super("getScrollHandle", arguments);
},
// Override the internal adjustOverflow method.
// If we're showing an IFrame, the default implementation will not reliably calculate
// whether scrollbars are visible.
__adjustOverflow : function () {
// always call the standard 'adjustOverflow' method to ensure we setHandleRect etc as
// appropriate.
this.Super("__adjustOverflow", arguments);
// If we're not writing out an IFrame we can just do normal overflow adjustment
// Overflows other than "auto" are not really supported - in this case just return too.
if (!this._useDesignMode() || this.overflow != isc.Canvas.AUTO) return;
// Update hscrollOn/ vscrollOn - not reliably set by the standard adjustOverflow logic.
var scrollHeight = this.getScrollHeight(),
scrollWidth = this.getScrollWidth(),
height = this.getHeight(), width = this.getWidth(),
scrollbarSize = this.getScrollbarSize(),
hscrollOn = false, vscrollOn = false;
if (scrollHeight > height) vscrollOn = true;
if (hscrollOn) width -= scrollbarSize;
if (scrollWidth > width) hscrollOn = true;
if (hscrollOn && !vscrollOn && (scrollHeight > height - scrollbarSize)) vscrollOn = true;
this.hscrollOn = hscrollOn;
this.vscrollOn = vscrollOn;
},
// methods to return the size for the content frame if we're using design mode.
getContentFrameWidth : function () {
return this.getWidth() - this.getHMarginBorderPad();
},
getContentFrameHeight : function () {
return this.getHeight() - this.getHMarginBorderPad();
},
// Override _setHandleRect() to always size the IFRAME to match the size of the
// handle.
_setHandleRect : function (left, top, width, height) {
this.Super("_setHandleRect", arguments);
if (this._useDesignMode()) {
var cf = this.getContentFrame();
if (cf != null) {
var innerWidth = this.getContentFrameWidth(), innerHeight = this.getContentFrameHeight();
cf.style.width = innerWidth + "px";
cf.style.height = innerHeight + "px";
}
}
},
// Override getScrollHeight() / width to look at the IFRAME body scroll height, since the
// IFRAME will always be sized to 100%, making the scroll size of the widget handle always
// equal to the specified size.
getScrollWidth : function (calculateNewValue) {
if ((this._scrollWidth && !calculateNewValue) || !this._useDesignMode())
return this.Super("getScrollWidth", arguments);
var cb = this.getContentBody();
if (!cb) return this.Super("getScrollWidth", arguments);
// cache the scrollWidth for next time this method is called.
this._scrollWidth = isc.Element.getScrollWidth(cb);
return this._scrollWidth;
},
getScrollHeight: function (calculateNewValue) {
if ((this._scrollHeight && !calculateNewValue) || !this._useDesignMode())
return this.Super("getScrollHeight", arguments);
var cb = this.getContentBody();
if (!cb) return this.Super("getScrollHeight", arguments);
this._scrollHeight = isc.Element.getScrollHeight(cb);
return this._scrollHeight;
},
// --------------------------------------
// _rememberSelection() - saves out the current selection position, so we can re-set it
// when this element regains focus. Only used in IE.
_rememberSelection : function () {
if (!isc.Browser.isIE) return;
// Check whether we currently have selection before proceeding - otherwise we could
// remember some text range outside our handle.
if (!this._hasSelection()) return;
this._savedSelection = document.selection.createRange();
// Also remember the content of the selection. If the content changes, we don't
// want a call to '_resetSelection' to select the new text.
this._oldSelectionText = this._savedSelection.text;
// this.logWarn("just saved selection :"+ Log.echo(this._savedSelection));
},
// _hasSelection() - Is the current document selection within the RichTextCanvas?
// Used by _rememberSelection() [IE only]
_hasSelection : function () {
if (!this.isDrawn()) return false
if (!isc.Browser.isIE) return;
if (this._useDesignMode()) {
return (this.getActiveElement() == this.getContentFrame());
}
var handle = this.getHandle();
if (!handle) return false;
var selElement = isc.Element._getElementFromSelection();
if (!selElement) return false;
return (handle == selElement || handle.contains(selElement));
},
// Remember the selection every time it changes, so we can reset the selection on focus
// or execCommand)
selectionChange : function () {
if (!this._focussing) this._rememberSelection();
},
// _resetSelection: resets selection to whatever it was last time this RTC had focus.
_resetSelection : function () {
if (!this.editable || !this.isDrawn() || !this.isVisible()) return;
if (isc.Browser.isIE) {
// If no previous selection, just bail
if (!this._savedSelection) return;
// If the content of the range has changed since it was selected, avoid selecting
// the modified text
if (this._oldSelectionText != this._savedSelection.text) {
this._savedSelection.collapse(false);
}
isc.EH._allowTextSelection = true;
this._savedSelection.select();
delete isc.EH._allowTextSelection;
//} else { //Currently only supported on IE
}
},
// Override setFocus() - when focussing in this widget we want the selection to be
// whatever it was before the widget was blurred, and for the editable text to have
// keyboard focus.
setFocus : function (hasFocus) {
// Call the Superclass implementation.
this._focussing = true;
this.Super("setFocus", arguments);
this._focussing = false;
// If we're using an IFRAME ensure it has focus natively
if (this._useDesignMode()) {
var win = this.getContentWindow();
if (!win) return;
if (hasFocus) win.focus()
else window.focus();
// Making this widget's handle contentEditable.
} else {
if (hasFocus) {
this._resetSelection();
}
//else this._rememberSelection();
}
},
// ------------------- Editor init ------------------
// Override draw to ensure we make the HTML editable when it's done drawing.
draw : function () {
this.Super("draw", arguments);
// In Moz / IE, if we're writing out an IFRAME we need to show an event mask
// for this canvas.
if (!isc.Browser.isSafari && this._useDesignMode())
isc.EventHandler.registerMaskableItem(this, true);
// Initialize the contents via _setupEditArea();
if (this._useDesignMode()) {
this._drawingFrame = true;
} else {
this._setupEditArea();
}
},
redraw : function () {
var reinitRequired = this._useDesignMode();
if (reinitRequired) this._rememberContents();
this.Super("redraw", arguments);
if (reinitRequired) this._drawingFrame = true;
},
// _setupEditArea: Fired when the RichTextCanvas is written into the DOM.
// This will ensure the appropriate contents and edit state are applied to this widget.
_setupEditArea : function () {
// Update the HTML to ensure that this is actually editable.
var designMode = this._useDesignMode();
// When using an IFRAME written out in design mode we need to add some custom event
// handlers.
if (designMode) {
// Capture keypresses directly on the IFRAME window so we can fire our
// keypress handler.
// Also capture scrolling on the IFRAME directly to update our scroll position,
// since we're showing native scrollbars on that element.
if (!this._editKeyPressHandler) {
this._editKeyPressHandler = new Function(
"event",
"var returnValue=" + this.getID() + "._iFrameKeyPress(event);" +
"if(returnValue==false && event.preventDefault)event.preventDefault()"
);
}
if (!this._editKeyDownHandler) {
this._editKeyDownHandler = new Function(
"event",
"var returnValue=" + this.getID() + "._iFrameKeyDown(event);" +
"if(returnValue==false && event.preventDefault)event.preventDefault()"
);
}
if (!this._editKeyUpHandler) {
this._editKeyUpHandler = new Function(
"event",
"var returnValue=" + this.getID() + "._iFrameKeyUp(event);" +
"if(returnValue==false && event.preventDefault)event.preventDefault()"
);
}
if (!this._editScrollHandler) {
this._editScrollHandler = new Function(
"event",
"var returnValue=" + this.getID() + "._iFrameScroll(event);" +
"if(returnValue==false && event.preventDefault)event.preventDefault()"
);
}
if (!this._editFocusHandler) {
this._editFocusHandler = new Function(
"event",
this.getID() + "._iFrameOnFocus();"
);
}
if (!this._editBlurHandler) {
this._editBlurHandler = new Function(
"event",
this.getID() + "._iFrameOnBlur();"
);
}
var win = this.getContentWindow();
win.addEventListener("keypress", this._editKeyPressHandler,
false);
win.addEventListener("keydown", this._editKeyDownHandler,
false);
win.addEventListener("keyup", this._editKeyUpHandler,
false);
win.addEventListener("scroll", this._editScrollHandler,
false);
win.addEventListener("focus", this._editFocusHandler, false);
win.addEventListener("blur", this._editBlurHandler, false);
var bodyStyle = this.getContentBody().style;
// Suppress the default margin
bodyStyle.margin = "0px";
// Apply text-properties from our specified CSS class to the content of the
// IFRAME.
var classStyle = isc.Element.getStyleDeclaration(this.className);
if (classStyle != null) {
var textStyleAttrs = isc.Canvas.textStyleAttributes;
for (var i = 0; i < textStyleAttrs.length; i++) {
var attr = textStyleAttrs[i];
bodyStyle[attr] = classStyle[attr];
}
}
}
// In moz, if we want native spell-check behavior enable it here (otherwise
// explicitly disable it).
if (isc.Browser.isMoz) {
this.getContentBody().spellcheck = (!!this.getBrowserSpellCheck())
}
var editable = (this.editable && !this.isDisabled());
// Actually make the handle editable
if (!designMode) this._setHandleEditable(editable);
else {
this.delayCall("_setHandleEditable", [editable,true], 0);
}
// set up our initial contents
//
// we're calling _setContents() which means we're bypassing hiliting - this is what we
// want most of the time because this method is called on redraws when the user may
// have just resized the browser, so there's no reason to recolorize. But if this is
// the first time we're drawing, we need to hilite if we have a syntaxHiliter because a
// contents may have been provided as an init parameter and if we don't do this it
// won't get syntax hilited.
//
// Note: important to do this after all of the above - to make sure the correcty
// styling is applied the first time - otherwise we get a partially styled rendering
// which snaps to the fully styled rendering after about a second - even on fast systems.
if (this.syntaxHiliter && !this.formattedOnce) {
this.formattedOnce = true;
this.contents = this.hiliteAndCount(this.contents);
}
this._setContents(this.contents);
},
// ----------------- Event handling ----------------------
// If using designMode, we need a handler for the native keypress event on our IFRAME
_iFrameKeyPress : function (event) {
// apply the properties (keyName, etc.) to EH.lastEvent
isc.EH.getKeyEventProperties(event);
// Fall through to standard handling, making sure this widget is logged as the
// keyTarget
return isc.EH.handleKeyPress(event, {keyTarget:this});
},
_iFrameKeyDown : function (event) {
// apply the properties (keyName, etc.) to EH.lastEvent
isc.EH.getKeyEventProperties(event);
return isc.EH.handleKeyDown(event, {keyTarget:this});
},
_iFrameKeyUp : function (event) {
// apply the properties (keyName, etc.) to EH.lastEvent
isc.EH.getKeyEventProperties(event);
return isc.EH.handleKeyUp(event, {keyTarget:this});
},
// If using designMode, we need a handler for the native scroll event on our IFRAME
// to update the stored scroll position of the handle on scroll.
// The standard handleCSSScroll method will handle scrolling (as it will check the
// scroll position of this.getScrollHandle() - which points at the IFRAME).
_iFrameScroll : function (event) {
return this._handleCSSScroll(event);
},
_iFrameOnFocus : function () {
if (this.destroyed) return;
isc.EH.focusInCanvas(this, true);
return true;
},
_iFrameOnBlur : function () {
if (this.destroyed) return;
isc.EH.blurFocusCanvas(this, true);
return true;
},
// Adjust overflow on keypress - updates recorded scroll width/height
_$br:"
",
_$Enter:"Enter",
// set of keys that are ignored by handleKeyPress because they can't modify the contents of
// the editable area. This isn't exhaustive - the main reason to have these is to
// eliminate gratuitous syntax hilighting while e.g. the user is using arrow keys to
// navigate around the document.
ignoreKeys : ["Arrow_Up", "Arrow_Down", "Arrow_Left", "Arrow_Right", "Ctrl", "Alt"],
handleKeyPress : function (event, eventInfo) {
var key = isc.EH.getKey();
if (this.ignoreKeys.contains(key)) return isc.EH.STOP_BUBBLING;
// figure out the start line number of the current selection before the key stroke so
// we can extract the modified line(s) later.
if (this.countLines) this.rememberSelectionStartLine();
this._queueContentsChanged();
var returnVal = this.Super("handleKeyPress", arguments);
// in IE, we set a timer onpaste to do syntax hiliting - this enalbes us to respond to
// a paste command initiated via the context menu or "Edit" menu, but when a user hits
// a keyboard shortcut to paste, we also get a key press. So if we're responding to a
// keypress and there's a paste timer, we delete it so we don't process the paste twice.
if (isc.Browser.isIE && this._pasteTimer) {
isc.Timer.clearTimeout(this._pasteTimer);
delete this._pasteTimer;
}
if (returnVal != false && isc.Browser.isIE && key == this._$Enter) {
this._rememberSelection();
this._savedSelection.pasteHTML(this._$br);
this._savedSelection.collapse(true);
this._savedSelection.select();
returnVal = false;
}
return returnVal;
},
_queueContentsChanged : function () {
if (!this._dirtyContent) {
this._dirtyContent = true;
if (!this._changedHandlerName) this._changedHandlerName = "_contentsChanged";
isc.Page.setEvent(isc.EH.IDLE, this, isc.Page.FIRE_ONCE, this._changedHandlerName);
}
},
//_contentsChanged - fired when the contents is edited.
// Not fired in response to explicit 'setContents' call.
_contentsChanged : function () {
delete this._dirtyContent;
var oldVal = this.contents,
newVal = this.getContents();
if (oldVal == newVal) return;
// if we're counting lines, then call doLinesChanged(). We're also checking for
// selectionIsCollapsed() here because Ctrl-A, which causes all contents to be selected
// should not fire doLinesChanged() - and that's the only known way to get a multichar
// selection after a keystroke/paste event
if (this.countLines && this.selectionIsCollapsed()) this.doLinesChanged(oldVal, newVal);
// AdjustOverflow - our scroll-size is likely to have changed
this.adjustOverflow("edited");
// Fire this.changed, if present
if (this.changed != null) this.changed(oldVal, newVal);
this.contents = newVal;
},
// ------------------------------------------------------------------------------------
// lineChanged / Synax Hiliting support
// ------------------------------------------------------------------------------------
//
// We want to detect any change made to the editable area so we can re-format the view.
// Change can be effected in several ways:
// - keypress
// - paste action using the browser "Edit" menu
// - programmatic update (currently only via setContents())
//
// At the time of the change, the user may have an insertion cursor or a block of selected
// text.
//
// We want to format the newly added data, and potentially some data around the new data.
// To that end we want to:
// - mark the location of the current insertion point, so we can restore it after making
// changes
// - determine the start and end index of the changed text
// - expand the above indexes to fully envelop the start and end line of the changed
// text.
// - we want this because recolorization will happen on a line-by-line basis (for
// performance reasons) except for some cases where we'll want to recolorize the
// whole document
//
// In IE and FF we can insert arbitrary HTML at the insertion cursor. This allows us to
// get the current location of the cursor.
//
// General issues
//---------------
// - In FF, we can't detect what was pasted unless we compare the original contents
// and the new contents - which is expensive. In fact, if the user uses the Edit
// menu paste command, then we can't tell the the editable area even changed. There's a
// DOM event called "onsubtreemodified" that's part of the w3c spec that should at least
// tell us that the contents changed - but it doesn't work at all in current versions of FF
// (and this has been corroborated by postings on the web).
// - In FF, the contents of the editable area change asynchronously with the key event. In
// other words you have to set a timeout to get the after-changed state of the editable area.
// - Detecting current selection/insertion point. In IE and FF we can detect the current
// selection/insertion point by wrapping the selection contents in a DOM node (e.g. a span)
// and then scanning the contents for it.
// - In FF we lose the insertion cursor if there's no adjoining text. Also, the cursor
// marker that we use to extract the cursor position blocks the user from using the right
// arrow on the keyboard to move to the next character beyond it.
//
// Approaches:
// -----------
// 1. Wait for the user to stop typing for a bit and then reformat the entire editable
// area. Performance is gated by the formatting algorithm, but this is very easy to
// implement and may be acceptable for some use cases.
// - benefits
// - easy to implement
// - problems
// - doesn't look as nice, because formatting is not realtime
//
// 2. Determine the edited line by scanning backwards and forwards from the current
// insertion point looking for
s.
// - benefits
// - easier than maintaining line information in the editable area
// - problems
// - scanning backwards for
s potentially not cheap
// - need to both scan backwards for
(so we can select out the HTML to pass to the
// formatter) and walk the DOM to the last
so we can efficiently insert the results.
//
// 3. Maintain line information in the editable area by inserting line spans into the
// contents provided to setContents(). Insert a span with a unique ID into the document at
// the current insertion point after a change and walk the
// DOM up from that node to find the current line for fast extraction.
// - benefits:
// - very fast line extraction
// - given a selection, can quickly determine what lines it spans.
// - after formatting the line, we can replace it effieciently via innerHTML assignment
// or equivalent.
// - As long as line-based formatting is sufficient for most keystrokes, this approach
// gives us an O(1) implementation.
// - can get the contents of any line very quickly. Given a line, can get its immediate
// surrounding lines quickly. Can get the line number quickly.
// - problems
// - These spans must be maintained as the user hits Backspace, Enter and pastes
// arbitrary content.
// - this means fragmenting multiline pastes and those created by user hitting the
// Enter key into separate lines and combining lines created by partial line pastes and
// Backspace at beginning of line.
// - In IE, copying a whole line out of the editable area also picks up its line span
// delimiters, which means we can end up with a line inside a line situation.
// - I think this can be fixed by defining a custom onbeforepaste handler on the
// line spans and filtering the line spans out of the data retrieved from the
// clipboard.
// - In Moz, the
inside the line span isn't picked up by the copy operation and
// is replaced by the paste operation if the paste is at the end of the line.
// Further, unlikes IE, the line span isn't placed inside the line that gets pasted
// into - instead the pasted-into line is fragmented into two line spans by the
// browser and the pasted line is added a peer in between those.
//
//
// line behaviors
// --------------
// - type character at beginning of line
// - IE, FF: currentLine contains original chars + new char + selection marker +
// - type character in the middle of a line
// - IE, FF: as above
// - type character at end of a line
// - IE, FF: as above
// - no special processing required for the above
//
// - hit enter at beginning of line
// - IE, FF: current line contains
origLineText
// - hit enter in the middle of a line
// - IE, FF: current line contains origLineTextBeforeEnter
origLineTextAfterEnter
// - hit enter at the end of a line
// - IE, FF: current line contains origLineText
// - split current line by
count using regexes to extract new lines
//
// - hit backspace at beginning of line
// - IE: jumps to previous line, deleting the
there.
// - FF: previous line missing
, currentLine has at start
// - hit delete at end of line
// - IE: current line now missing
(basically as IE above)
// - FF: as IE
// - hit backspace in middle of line
// - IE, FF: as typing char in middle of line, except old char deleted
// - hit backspace at end of line
// - IE, FF: as above
// - if current line is missing
, combine with next, otherwise combine previous with current
//
// - paste external chars (not from a line span) with no BR in line:
// - IE: as typing char, but selection goes to front of pasted text
// - FF: as typing char
// - paste external chars (not from a line span) with BR in line:
// - IE, FF: as above, plus a
where there's a linebreak in pasted content
// - handled by above cases
//
// - paste chars from a line span with no BR in line:
// - IE: if the copied selection touched the start of the line span, then behaves as FF,
// except that the pasted line span appears as a child of the pasted-to line span, not a peer.
// Otherwise behaves as paste of external chars
// - filter out line spans from pasted content with onbeforepaste, onpaste on line spans,
// then handled by above cases as external paste
//
// - FF: pasted-to line is fragmented into two line spans by the insertion point. pasted
// text appears as its own line in between the two. If pasted at end of a line, that
// line's BR is moved out of that line's span as the nextSibling the newly created
// line span.
// - whenever pasting into a line, that line's
is destroyed and either
// moves outside the line span (if paste was at end of line) or into a new line
// fragment (if there was text to the right of the insertion cursor before paste)
// - a paste can be multiline, introducing potentially multiple whole lines, so can't
// just look back for a missing
until we hit a normal line (since some intervening
// lines may actually be normal, because pasted wholesale from this edit area)
// - SOLUTION:
// - before paste:
// - query total number of lines
// - after paste:
// - if
is nextSibling outside currentLine (selection always at end of paste),
// pull it into currentLine.
// - now query total number of lines (after paste). This is the number of lines we
// have to walk back looking for missing
s and combining.
//
// Actual approach used:
//----------------------
// #3, except don't combine lines in realtime. Instead, provide a special change()
// notification that gives the user the pasted contents and the pasted contents out to line
// breaks, and the line number. The user can then format them and call a method to replace
// them and insert them at a given location.
//
// apply a SyntaxHiliter to the contents
setSyntaxHiliter : function (syntaxHiliter) {
if (syntaxHiliter == null) {
this.removeSyntaxHiliter();
return;
}
this.syntaxHiliter = syntaxHiliter;
this.countLines = true;
// apply syntax hiliting to the contents
var contents = this.getContents() || isc.emptyString;
this.setContents(contents);
},
removeSyntaxHiliter : function () {
// get the contents (do this before we delete this.syntaxHiliter, otherwise the
// contents will come with markup.
var contents = this.getContents() || isc.emptyString;
delete this.syntaxHiliter;
delete this.countLines;
// apply the contents, now without the markup
this.setContents(contents);
},
doLinesChanged : function (oldVal, newVal) {
// this.logWarn("doLinesChanged - newVal: " + newVal);
var startLineNum = this.getLastSelectionStartLine();
// initial setContents() only
if (startLineNum == null) return;
var startLine = this.getLine(startLineNum);
// this.logWarn("startLineNum: " + startLineNum);
// if (!startLine) this.logWarn("startLine is null");
// else this.logWarn("startLine: " + startLine.innerHTML);
var html = isc.emptyString;
var selectionId = this.markCurrentSelection();
if (isc.Browser.isIE) {
// startLine contains everything we need - in the event that lines from the editor
// got pasted back in, those lines appear as children of the current line and the
// line markers will just get stripped out by unescapeHTML()
if (!startLine) {
this.getLineContainer().innerHTML = isc.emptyString;
var line = this.createLine();
this.getLineContainer().appendChild(line);
var range = document.selection.createRange();
range.moveToElementText(line);
range.collapse();
range.select();
selectionId = this.markCurrentSelection();
startLineNum = 0;
startLine = this.getLine(0);
}
html = startLine.innerHTML;
} else {
var endLine = this.getSelectionStartLine();
var endLineNum = this.getLineNumber(endLine);
if (endLineNum < startLineNum) {
startLine = endLine;
startLineNum = endLineNum;
}
// this.logWarn("endLine: " + endLine.innerHTML);
// this.logWarn("nextSibling: " + isc.Log.echoAll(endLine.nextSibling));
var currentLine = startLine;
var numLines = 0;
while (currentLine && currentLine != endLine) {
if (currentLine.innerHTML) {
html += currentLine.innerHTML;
// this.logWarn("html is now: " + html);
}
numLines++;
currentLine = currentLine.nextSibling;
}
// repair bonus BR that gets shunted out of the original line span by FF
var nextNode = endLine.nextSibling;
if (nextNode && nextNode.tagName.toLowerCase() == "br") {
nextNode.parentNode.removeChild(nextNode);
endLine.appendChild(nextNode);
// this.logWarn("repaired br, endline is now: " + endLine.innerHTML);
}
html += endLine.innerHTML;
// if we pasted a line span into the middle of another line span, then it will be
// fragmented - pick up the trailing fragment
if (!html.replace(/\n|\r/g, isc.emptyString).match(/
$/i)) {
if (endLine.nextSibling) {
html += endLine.nextSibling.innerHTML;
numLines++;
}
}
}
// this.logWarn("linesChanged: " + html);
if (!oldVal) {
oldVal = this.contents;
newVal = this.getContents();
}
// fire linesChanged if it's defined
if (this.linesChanged) {
this.linesChanged(oldVal, newVal, startLineNum, numLines, html, selectionId);
} else if (this.syntaxHiliter) {
// currently syntaxHiliter is not compatible with linesChanged - use one or the other.
this.doSyntaxHilite(oldVal, newVal, startLineNum, numLines, html, selectionId);
}
},
doSyntaxHilite : function (oldVal, newVal, startLineNum, numLines, changedHTML, selectionId) {
// keeping a marker in the code sent to the colorizer, breaks some colorization cases -
// specifically, this happens if the marker is in the middle of something that would be
// matched by a regex. For example in XML colorization of the marker is next to the
// equal sign in this expression: foo="bar", then that expression won't colorize until
// something else is edited such that the selection marker moves.
// this.logWarn("source before html removal: " + changedHTML);
// remove markup, but keep the selectionSpan so we can extract its index for
// repositioning the selection correctly after syntax hiliting
var source = this.removeMarkup(changedHTML, true);
// this.logWarn("source after html removal: " + source);
// save off the index of the locationMarker
var selectionMarkerIndex = this.getSelectionMarkerIndex(source);
if (selectionMarkerIndex == -1) {
// marker has been wiped out by a select-all, re-hilite everything
this.doFullSyntaxHilite();
return;
}
// remove the selectionMarker from the source
source = this.removeMarkup(changedHTML);
// if the modified source contains a token that requires us to reformat queue a full
// hilite, but still do the partial update immediately
// if (this.syntaxHiliter.containsMultilineToken(source)) this.queueFullHilite();
// apply the syntax hiliting to just the lines passed in
var newLines = this.syntaxHiliter.hilite(source, true, selectionMarkerIndex,
this._getSelectionSpanHTML(selectionId));
this.overwriteLines(startLineNum, numLines, newLines);
this.moveSelectionToMarker(selectionId);
},
doFullSyntaxHilite : function () {
// this.logWarn("full syntax hilite running");
var selectionId = this.markCurrentSelection();
var contents = this._getContents();
var source = this.removeMarkup(contents, true);
var selectionMarkerIndex = this.getSelectionMarkerIndex(source);
if (selectionMarkerIndex == -1) {
selectionMarkerIndex = contents.length;
}
// remove the selectionMarker from the source
source = this.removeMarkup(contents);
this.setContents(source, true, selectionMarkerIndex, this._getSelectionSpanHTML(selectionId));
this.moveSelectionToMarker(selectionId);
delete this.fullHiliteTimer;
},
queueFullHilite : function () {
// this.logWarn("(re)queueing full hilite");
if (this.fullHiliteTimer) isc.Timer.clearTimeout(this.fullHiliteTimer);
this.fullHiliteTimer = this.delayCall("doFullSyntaxHilite", [], this.fullSyntaxHiliteDelay);
},
selectionIsCollapsed : function () {
if (isc.Browser.isIE) {
var range = document.selection.createRange();
return range.text.length == 0;
} else if (isc.Browser.isMoz) {
var selection = this.getContentWindow().getSelection();
return selection.isCollapsed;
}
},
rememberSelectionStartLine : function () {
this.startLineNum = this.getLineNumber(this.getSelectionStartLine());
},
getLastSelectionStartLine : function () {
return this.startLineNum;
},
_setPasteTimer : function () {
this._pasteTimer = this.delayCall("doLinesChanged", [], 0);
},
// onpaste/onbeforepaste for IE - caching theset strings
_getOnBeforePaste : function () {
if (!this._onBeforePaste)
this._onBeforePaste = this.getID()+".rememberSelectionStartLine();event.returnValue=true";
return this._onBeforePaste;
},
_getOnPaste : function () {
if (!this._onPaste) this._onPaste = this.getID()+"._setPasteTimer();event.returnValue=true"
return this._onPaste;
},
// line span HTML caching
_getLineSpanHTML : function () {
if (!this._lineSpanHTML) {
this._lineSpanHTML = "$1";
}
return this._lineSpanHTML;
},
createLine : function (contents) {
var doc = this.getContentDocument();
var line = doc.createElement("span");
line.setAttribute("isLine", "true");
// prevent the "bouncing line" effect where adding a space on a line that's clipped
// causes it to be broken into two lines by the browser (since that space is the first
// available place to wrap it). The rendering then snaps back into a single line when
// the syntaxHiliter converts the space into an
if (this.syntaxHiliter && !this.syntaxHiliter.autoWrap)
line.setAttribute("style", "white-space:nowrap");
if (isc.Browser.isIE) {
line.setAttribute("onbeforepaste", this._getOnBeforePaste());
line.setAttribute("onpaste", this._getOnPaste());
}
line.innerHTML = contents ? contents : "
";
return line;
},
// returns a string containing an incrementing counter usable unique identifier for the
// selection span.
_getNextSelectionId : function () {
if (!this.selectionIdSequence) this.selectionIdSequence = 0;
return this.getID()+"_selection_"+this.selectionIdSequence++;
},
// returns the DOM node that is the line span containing the start of the current selection.
getSelectionStartLine : function () {
var doc = this.getContentDocument();
var line;
if (isc.Browser.isIE) {
var selectionId = this._getNextSelectionId();
var range = doc.selection.createRange();
// collapse the selection range to the start of the current selection
range.collapse();
range.pasteHTML("");
var selNode = doc.getElementById(selectionId);
line = selNode.parentNode;
// this.logWarn("startLine: " + line.outerHTML);
line.removeChild(selNode);
} else if (isc.Browser.isMoz) {
var selection = this.getContentWindow().getSelection();
var line = selection.anchorNode;
}
// this.logWarn("anchorNode: " + Log.echo(line));
// IE will paste the isLine spans into the current line, so we need to find the
// top-most element that is actually a line
var lastLine = line;
while(line.parentNode != null) {
if (line.getAttribute && line.getAttribute("isLine") != null) lastLine = line;
line = line.parentNode;
}
return lastLine;
},
_getSelectionSpanHTML : function (selectionId) {
return "";
},
// inserts an HTML marker at the start of the current selection. In IE, the marker will be
// inserted immediately before the current selection. In FF, immediately after. FF can be
// made to work as IE for collapsed selections, but for multichar selections it may be
// impossible. See the notes below for enabling IE-style behavior in FF for collapsed
// selections.
markCurrentSelection : function () {
var selectionId = this._getNextSelectionId();
var doc = this.getContentDocument();
if (isc.Browser.isIE) {
var range = doc.selection.createRange();
// collapse the selection range to the start of the current selection
range.collapse();
// insert our marker span
range.pasteHTML(this._getSelectionSpanHTML(selectionId));
} else if (isc.Browser.isMoz) {
// create the selection span
var selectionNode = doc.createElement("span");
selectionNode.setAttribute('isSelectionSpan', "true");
selectionNode.setAttribute('id', selectionId);
// grab the current selection range
var selection = this.getContentWindow().getSelection();
var range = selection.getRangeAt(0);
if (selection.isCollapsed) {
range.insertNode(selectionNode);
} else {
// create a new range whose start and end match the selection range start boundary
var collapsedRange = range.cloneRange();
// collapse the new range to the end of the selection
collapsedRange.collapse(false);
// insert the selection node at the collapsed range
collapsedRange.insertNode(selectionNode);
collapsedRange.detach();
/*
// The code below will insert the selection marker immediately before the start
// of the current selection, but it only works for a collapsed selection. This
// is because range.insertNode() puts the new node right after the start of the
// selection which means that we need to jump the start of the selection over
// the newly inserted node. This works fine for a collapsed selection, but a
// multicharacter selection gets destroyed by insertNode() - at least when the
// start and end of the multichar selection are in one text node that is
// fragmented by insertNode(). After insertNode(), the selection reflects
// the character immediately before the previous start of the selection!
// Weird FF bug - we need to clear the selection range and then re-add it after
// we're done manipulating our clone selection - because if we don't it expands
// to the parentNode for no apparent reason.
selection.removeAllRanges();
// create a new range whose start and end match the selection range start boundary
var collapsedRange = doc.createRange();
collapsedRange.setStart(range.startContainer, range.startOffset);
collapsedRange.setEnd(range.startContainer, range.startOffset);
// insert the selection node at the collapsed range
collapsedRange.insertNode(selectionNode);
collapsedRange.detach();
// the above should have been all, but unfortunately the above insertion happened
// AFTER the start of the current selection range, so now we need to move the
// current selection range start to after the node we inserted
if(range.startContainer.nodeType == 3) {
// if it's a text node, then the above insertion fragmented the text node into
// a two, so just jump over the selectionNode we just inserted.
range.setEnd(range.startContainer.nextSibling.nextSibling, 0);
range.setStart(range.startContainer.nextSibling.nextSibling, 0);
} else {
range.setEndAfter(selectionNode);
range.setStartAfter(selectionNode);
}
// add the modified range back so the cursor shows up.
selection.addRange(range);
*/
}
}
return selectionId;
},
overwriteLines : function (lineNum, numLines, newLines) {
if (!isc.isAn.Array(newLines)) newLines = [newLines];
var line = this.getLine(lineNum);
while (lineNum >= 0 && (!line || !line.getAttribute || !line.getAttribute("isLine"))) {
line = this.getLine(lineNum);
lineNum--
}
if (lineNum < 0) {
// this.logWarn("wiping lineContainer");
this.getLineContainer().innerHTML = isc.emptyString;
line = this.createLine();
this.getLineContainer().appendChild(line);
if (isc.Browser.isMoz) lineNum++;
}
var container = line.parentNode;
// this.logWarn("looking for lineNum: " + lineNum + " - got: " + isc.Log.echoAll(line));
line.innerHTML = newLines[0];
// this.logWarn("replaced line: " + lineNum + " with: " + newLines[0]);
// remove the numLines to replace
// this.logWarn("removing: " + numLines + " lines");
while (numLines != null && numLines-- > 0) {
var removeLine = this.getLine(lineNum+1);
if (removeLine) {
// this.logWarn("removing line: " + removeLine.innerHTML);
// this.logWarn("next line is: " + (removeLine.nextSibling ? removeLine.nextSibling.innerHTML:"null"));
container.removeChild(removeLine);
}
}
// add new lines
for (var i = 1; i < newLines.length; i++) {
if (newLines[i] != -1) this.addLineAfter(lineNum+i-1, newLines[i]);
}
},
addLineAfter : function (lineNum, line) {
// this.logWarn("addAfter: " + lineNum + " line: " + line);
var afterLine = this.getLine(lineNum);
var nextLine = this.getNextLine(afterLine);
line = this.createLine(line);
if (nextLine) {
nextLine.parentNode.insertBefore(line, nextLine);
} else {
afterLine.parentNode.appendChild(line);
}
},
escapeSelection : function (str, escapeValue) {
if (escapeValue == null) escapeValue = isc.emptyString;
return str.replace(/]*isSelectionSpan[^>]*><\/span>/gi, escapeValue);
// var r = new RegExp("]*id=\"?"+selectionId+"[^>]*><\/span>", "gi");
// return str.replace(r, escapeValue);
},
getSelectionMarkerIndex : function (s) {
var regex = new RegExp("]*isSelectionSpan[^>]*>", "i");
var result = regex.exec(s);
if (result) return result.index;
return -1;
},
getLineNumber : function (line) {
var peers = line.parentNode.childNodes;
for (var i = 0; i < peers.length; i++)
if (peers[i] == line) return i;
},
getPreviousLine : function (line) {
return line.previousSibling;
},
getNextLine : function (line) {
return line.nextSibling;
},
getLineContainer : function () {
return isc.Browser.isIE ? this.getHandle() : this.getContentBody();
},
getLine : function (lineNum) {
return this.getLineContainer().childNodes[lineNum];
},
getLineHTML : function (line) {
return line.innerHTML;
},
getLineContents : function (line) {
return this.removeMarkup(this.getLineHTML(line));
},
removeMarkup : function (str, preserveSelectionSpan) {
// FF actually inserts \n or \r in addition to
on the ENTER key, so first remove
// any literal newlines.
//
// IE actually inserts tags into the contents in some cases -
// it appears to be tracking the currently applied style, so need to remove that HTML
// markup here.
if (preserveSelectionSpan) {
// remove all tag except
and the selectionSpan
str = str.replace(/\n|\r|(<\/?(?!br|BR|([^>]*isSelectionSpan)).*?>)/gi, isc.emptyString);
} else {
// remove all tag except
str = str.replace(/\n|\r|(<\/?(?!br|BR).*?>)/gi, isc.emptyString);
}
str = str.unescapeHTML();
if (isc.Browser.isOpera) {
var nbsp = new RegExp(String.fromCharCode(160), "g");
str = str.replace(nbsp, " ");
}
return str;
},
// given the selectionId of a selection span in the contents, move the selection cursor to
// that span. After selection is moved to the span, the selectionMarker is destroyed.
moveSelectionToMarker : function (selectionId) {
var doc = this.getContentDocument();
var selectionNode = doc.getElementById(selectionId);
if (isc.Browser.isIE) {
var range = doc.selection.createRange();
range.moveToElementText(selectionNode);
range.collapse();
range.select();
} else if (isc.Browser.isMoz) {
var selection = this.getContentWindow().getSelection();
selection.removeAllRanges();
var range = doc.createRange();
range.setStartBefore(selectionNode);
range.setEndBefore(selectionNode);
selection.addRange(range);
}
this.destroySelectionMarker(selectionId);
},
// destroyes the selectionMarker in the contents
destroySelectionMarker : function (selectionId) {
var doc = this.getContentDocument();
var selectionNode = doc.getElementById(selectionId);
if (selectionNode) selectionNode.parentNode.removeChild(selectionNode);
},
// ------------ Public Runtime API --------------
//>@method setEditable ()
// Enable / disable editing of the rich text canvas.
// @param editable (boolean) True if we are enabling editing
//<
setEditable : function (editable) {
//this.logWarn("setEditable" + editable);
if (editable == this.editable) return;
this.editable = editable;
this._setHandleEditable(editable);
},
// Actually set the handle to be editable or not.
_setHandleEditable : function (editable, initialPass) {
if (this._useDesignMode()) {
var cDoc = this.getContentDocument();
if (cDoc) {
if (editable || initialPass) cDoc.designMode = "on";
// Call execCommand directly rather than using our _execCommand method as
// we may have 'this.editable' set to false already.
if (isc.Browser.isMoz) cDoc.execCommand("readonly", false, editable);
if (!editable) cDoc.designMode = "off";
}
} else {
var handle = this.getHandle();
if (handle != null) {
handle.contentEditable = (editable ? true : "inherit");
if (isc.Browser.isIE) {
if (!this.isVisible() && this._hasSelection())
this._emptySelectionForHide();
else if (isc.Browser.version < 6)
this._rememberSelection();
}
}
}
},
parentVisibilityChanged : function (vis) {
if (!this._useDesignMode() && isc.Browser.isIE && (vis == isc.Canvas.HIDDEN) &&
this._hasSelection())
{
this._emptySelectionForHide();
}
return this.Super("parentVisibilityChanged", arguments);
},
// Helper method for IE to ensure that our selection is empty.
_emptySelectionForHide : function () {
document.body.focus();
var focusCanvas = isc.EH.getFocusCanvas();
if (focusCanvas != this && focusCanvas != null) {
focusCanvas.focus();
}
},
// Override disableKeyboardEvents - when disabled we always want to be non-editable
disableKeyboardEvents : function (disabled) {
this.Super("disableKeyboardEvents", arguments);
// If we're editable (when enabled) update the handle to be non editable when
// disabled (or make it editable again when enabled)
if (this.editable) this._setHandleEditable(disabled ? false : true);
},
// ---------- Contents management ---------------
// We need APIs for the developer to both set and retrieve the HTML contained in the
// editable area.
// _rememberContents - stores the contents of the editable area under this.contents.
// Note: getContents() should be used rather than checking this.contents directly.
_rememberContents : function () {
if (!this.isDrawn() || this._drawingFrame) return;
var contents = this._getContents();
if (contents != null) this.contents = contents;
},
_getContents : function () {
var contents;
if (this._useDesignMode()) {
var c_body = this.getContentBody();
if (!c_body) return;
contents = c_body.innerHTML;
} else {
var handle = this.getHandle();
if (handle) contents = handle.innerHTML;
}
return contents;
},
//> @method RichTextCanvas.getContents() ([])
// Returns current the HTML contents of the RichTextCanvas.
// @return (string) (possibly edited) contents
// @see RichTextCanvas.setContents()
//<
getContents : function (dontRemoveMarkup) {
this._rememberContents();
// if a syntaxHiliter or line counting is applied, remove line and hiliting information
// before returning the contents.
if ((this.syntaxHiliter || this.countLines) && !dontRemoveMarkup) {
return this.removeMarkup(this.contents);
} else {
return this.contents;
}
},
//> @method RichTextCanvas.setContents() ([])
// Changes the contents of a widget to newContents, an HTML string.
// @param newContents (string) an HTML string to be set as the contents of this widget
// @see RichTextCanvas.getContents()
//<
setContents : function (contents, force, selectionMarkerIndex, selectionMarkerHTML) {
// setContents in effect gets called twice in FF because the iframe takes a while
// to load, so an end user calling setContents() directly effectively ends up
// setting this.contents which is then picked up via _setupEditArea()
if (contents == this.contents && !force) return;
// don't hilite if we're not drawn since in that case _setContents() would just return
this.contents = contents;
if (!this.isDrawn() || this._drawingFrame) return;
this._setContents(this.hiliteAndCount(contents, selectionMarkerIndex, selectionMarkerHTML));
},
_setContents : function (contents) {
this.contents = contents;
if (!this.isDrawn()) return;
if (this._useDesignMode()) {
var c_body = this.getContentBody();
if (!c_body) return;
c_body.innerHTML = contents;
} else {
var handle = this.getHandle();
if (handle) handle.innerHTML = contents;
}
// contents have changed, so get updated scrollHeight, check for scrollbars, etc
this.adjustOverflow();
},
hiliteAndCount : function (contents, selectionMarkerIndex, selectionMarkerHTML) {
if (this.syntaxHiliter) {
// if a syntaxHiliter is applied, pass the new contents through it
contents = this.syntaxHiliter.hilite(contents, false, selectionMarkerIndex, selectionMarkerHTML);
}
if (this.countLines) {
// if we're initializing an empty richTextEditor and we're going to be doing
// realtime hiliting, we must have one of our special lines in there - otherwise
// all related logic will break. If the contents is blank, insert a
because
// the regex immediately below relies on it and can't be efficiently changed to
// capture this case.
if (contents == isc.emptyString) contents = "
";
contents = contents.replace(/((?:.*?
)|(?:.+$))/gi, this._getLineSpanHTML());
}
return contents;
},
// adds the specified contents to the innerHTML
appendContents : function (contents, selectionMarkerIndex, selectionMarkerHTML) {
contents = this.hiliteAndCount(contents, selectionMarkerIndex, selectionMarkerHTML);
var handle = this._useDesignMode() ? this.getContentBody() : this.getHandle();
handle.innerHTML += contents;
// contents have changed, so get updated scrollHeight, check for scrollbars, etc
this.adjustOverflow();
},
// --------------- Rich Text Editing Commands -------------------
// _execCommand() Fires the standard 'execCommand()' method to modify the editable
// content.
// We currently use the native 'document.execCommand()' method to perform most of our
// actions on the text of the RTC, so most of our Rich Text APIs fall through to this
// wrapper method.
// Will return explicitly return 'false' if the command is not supported.
_execCommand : function (command, valueString) {
if (!this.isDrawn() || !this.editable) return;
// We could use 'queryCommandEnabled()' here to determine whether the command is valid
// given the current selection, etc.
if (!isc.Page.isLoaded()) {
this.logWarn("Unsupported attempt to manipulate RichTextCanvas content style " +
"before page load: postponed until the page has done loading.");
isc.Page.setEvent(
"Load",
this.getID() + "._execCommand('"
+ command + "','" + valueString + "');"
);
return;
}
// Ensure we have focus and are selected.
this.focus();
var designMode = this._useDesignMode(),
doc = designMode ? this.getContentDocument() : document;
if (!doc) return;
// If the command is unsupported, return false to notify callers.
if (!this._commandEnabled(command)) return false;
try {
doc.execCommand(command, false, valueString);
} catch (e) {
return false;
}
if (designMode) {
// put focus into the window so the user can continue typing and have
// an effect.
var cw = this.getContentWindow();
cw.focus();
} else {
// text manipulation is likely to have changed the text insertion point.
this._rememberSelection();
}
// Fire _contentsChanged() - it is likely that the execCommand changed the content
// of the RTC.
this._contentsChanged();
},
// Helper method to test for command being enabled
_commandEnabled : function (command) {
try {
var doc = this._useDesignMode() ? this.getContentDocument() : document;
if (!doc) return false;
if (!doc.queryCommandEnabled(command)) return false;
} catch (e) {
return false;
}
return true;
},
//>@method RichTextCanvas.boldSelection
// Toggle whether the current text selection is bold or not bold
//<
boldSelection : function () {
this._execCommand("bold")
},
//>@method RichTextCanvas.italicSelection
// Toggle whether the current text selection is italic or not
//<
italicSelection : function () {
this._execCommand("italic");
},
//>@method RichTextCanvas.underlineSelection
// Toggle whether the current text selection is underlined or not
//<
underlineSelection : function () {
this._execCommand("underline");
},
//>@method RichTextCanvas.strikethroughSelection
// Toggle whether the current text selection is strikethrough or not
//<
strikethroughSelection : function () {
this._execCommand("strikethrough");
},
//>@method RichTextCanvas.showClipboardDisabledError
// For some browsers the clipboard functions (cut/copy/paste) are disabled by default.
// We catch these cases from cutSelection() / copySelection() / pasteOverSelection() and
// call this method to warn the user. Default behavior shows a warn dialog with the text
// "Your browser does not allow web pages to access the clipboard programmatically."
// May be overridden to change this behavior.
// @visibility editor_clipboard
//<
// Cut/Copy/Paste all disabled by default in Moz.
// In Safari Cut/Copy work, but Paste always fails.
// In IE all three methods are enabled and work by default.
showClipboardDisabledError : function () {
var errorMessage = "Your browser does not allow web pages to access the clipboard programmatically.";
// Mozilla allows you to turn on clipboard access for scripts but it's somewhat complex
// and you can only turn it on for specific URLs
// There's a note on this at mozilla.org: http://www.mozilla.org/editor/midasdemo/securityprefs.html
// NOTE: the freeware HTMLArea application available at:
// http://www.dynarch.com/projects/htmlarea/ or
// http://www.postnukepro.com/modules/pagesetter/guppy/HTMLArea30beta/
// points the user directly to this page. We currently don't because:
// - the steps to enable the script access are complex
// - the explanation specifically refers to the Moz "midas" demo app
// - we don't know of any way to turn on "Paste" access in Safari.
isc.warn(errorMessage);
},
//>@method RichTextCanvas.copySelection
// Copy the current text selection to the clipboard.
// @visibility editing_clipboard
//<
copySelection : function () {
if (this._execCommand("copy") == false) this.showClipboardDisabledError();
},
//>@method RichTextCanvas.cutSelection
// Copy the current text selection to the clipboard, and remove it from the edit area.
// @visibility editing_clipboard
//<
cutSelection : function () {
if (this._execCommand("cut") == false) this.showClipboardDisabledError();;
},
//>@method RichTextCanvas.pasteOverSelection
// Paste the current clipboard text over the selection
// @visibility editing_clipboard
//<
pasteOverSelection : function () {
if (this._execCommand("paste") == false) this.showClipboardDisabledError();
},
//>@method RichTextCanvas.deleteSelection
// Delete the currently selected text
//<
deleteSelection : function () {
this._execCommand("delete");
},
//>@method RichTextCanvas.indentSelection
// increase the indent for the currently selected paragraph
//<
indentSelection : function () {
this._execCommand("indent");
},
//>@method RichTextCanvas.outdentSelection
// decrease the indent for the currently selected paragraph
//<
outdentSelection : function () {
this._execCommand("outdent");
},
//>@method RichTextCanvas.justifySelection
// Applies the alignment / justification passed in to the selected paragraph.
// Options are "right", "left", "center" (for alignment), and "full" for fully justified
// text.
// @param justification (string) What justification should be applied to the text.
//<
justifySelection : function (justification) {
if (justification == isc.RichTextCanvas.CENTER) {
this._execCommand("justifycenter");
} else if (justification == isc.RichTextCanvas.FULL) {
this._execCommand("justifyfull");
} else if (justification == isc.RichTextCanvas.RIGHT) {
this._execCommand("justifyright");
} else if (justification == isc.RichTextCanvas.LEFT) {
this._execCommand("justifyleft");
/*
} else {
this._execCommand("justifynone");
*/
}
},
//>@method RichTextCanvas.setSelectionColor
// Set the font color for the selected text. Takes the desired color as a parameter.
// @param color (string) Color to apply to the text.
//<
setSelectionColor : function (color) {
this._execCommand("forecolor", color);
},
//>@method RichTextCanvas.setSelectionBackgroundColor
// Set the background color for the selected text. Takes the desired color as a parameter.
// @param color (string) Color to apply to the text background.
//<
setSelectionBackgroundColor : function (color) {
// In Moz "backcolor" will style the entire containing IFRAME - while 'hilitecolor'
// will set the background color for just the selected text.
var command = isc.Browser.isMoz ? "hilitecolor" : "backcolor";
this._execCommand(command, color);
},
//>@method RichTextCanvas.setSelectionFont
// Set the font for the selected text. Takes the name of a font as a parameter.
// @param font (string) Font to apply to the selection
//<
setSelectionFont : function (font) {
this._execCommand("fontname", font);
},
//>@method RichTextCanvas.setSelectionFontSize
// Set the size of the font for the selected text. Takes a number between 1 and 7.
// @param size (number) Desired font size - a value between 1 and 7.
//<
setSelectionFontSize : function (size) {
this._execCommand("fontsize", size);
},
createLink : function (url) {
this._execCommand("CreateLink", url);
}
});
isc.RichTextCanvas.registerStringMethods({
changed : "oldValue,newValue"
});
//!