blob: 0bd5b27ffcc5287122260a087bb689bb008635b3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2011 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0
* (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
* License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
*
* Contributors:
* Felipe Heidrich (IBM Corporation) - initial API and implementation
* Silenio Quarti (IBM Corporation) - initial API and implementation
* Mihai Sucan (Mozilla Foundation) - fix for Bugs 334583, 348471
******************************************************************************/
/*global window document navigator setTimeout clearTimeout XMLHttpRequest define */
/**
* @namespace The global container for Orion APIs.
*/
var orion = orion || {};
orion.textview = orion.textview || {};
/**
* Constructs a new text view.
*
* @param options the view options.
* @param {String|DOMElement} options.parent the parent element for the view, it can be either a DOM element or an ID for a DOM element.
* @param {orion.textview.TextModel} [options.model] the text model for the view. If this options is not set the view creates an empty {@link orion.textview.TextModel}.
* @param {Boolean} [options.readonly=false] whether or not the view is read-only.
* @param {Boolean} [options.fullSelection=true] whether or not the view is in full selection mode.
* @param {String|String[]} [options.stylesheet] one or more stylesheet URIs for the view.
* @param {Number} [options.tabSize] The number of spaces in a tab.
*
* @class A TextView is a user interface for editing text.
* @name orion.textview.TextView
*/
orion.textview.TextView = (function() {
/** @private */
function addHandler(node, type, handler, capture) {
if (typeof node.addEventListener === "function") {
node.addEventListener(type, handler, capture === true);
} else {
node.attachEvent("on" + type, handler);
}
}
/** @private */
function removeHandler(node, type, handler, capture) {
if (typeof node.removeEventListener === "function") {
node.removeEventListener(type, handler, capture === true);
} else {
node.detachEvent("on" + type, handler);
}
}
var isIE = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent) ? document.documentMode : undefined;
var isFirefox = parseFloat(navigator.userAgent.split("Firefox/")[1] || navigator.userAgent.split("Minefield/")[1]) || undefined;
var isOpera = navigator.userAgent.indexOf("Opera") !== -1;
var isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
var isSafari = navigator.userAgent.indexOf("Safari") !== -1;
var isWebkit = navigator.userAgent.indexOf("WebKit") !== -1;
var isPad = navigator.userAgent.indexOf("iPad") !== -1;
var isMac = navigator.platform.indexOf("Mac") !== -1;
var isWindows = navigator.platform.indexOf("Win") !== -1;
var isLinux = navigator.platform.indexOf("Linux") !== -1;
var isW3CEvents = typeof window.document.documentElement.addEventListener === "function";
var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function";
var platformDelimiter = isWindows ? "\r\n" : "\n";
/**
* Constructs a new Selection object.
*
* @class A Selection represents a range of selected text in the view.
* @name orion.textview.Selection
*/
var Selection = (function() {
/** @private */
function Selection (start, end, caret) {
/**
* The selection start offset.
*
* @name orion.textview.Selection#start
*/
this.start = start;
/**
* The selection end offset.
*
* @name orion.textview.Selection#end
*/
this.end = end;
/** @private */
this.caret = caret; //true if the start, false if the caret is at end
}
Selection.prototype = /** @lends orion.textview.Selection.prototype */ {
/** @private */
clone: function() {
return new Selection(this.start, this.end, this.caret);
},
/** @private */
collapse: function() {
if (this.caret) {
this.end = this.start;
} else {
this.start = this.end;
}
},
/** @private */
extend: function (offset) {
if (this.caret) {
this.start = offset;
} else {
this.end = offset;
}
if (this.start > this.end) {
var tmp = this.start;
this.start = this.end;
this.end = tmp;
this.caret = !this.caret;
}
},
/** @private */
setCaret: function(offset) {
this.start = offset;
this.end = offset;
this.caret = false;
},
/** @private */
getCaret: function() {
return this.caret ? this.start : this.end;
},
/** @private */
toString: function() {
return "start=" + this.start + " end=" + this.end + (this.caret ? " caret is at start" : " caret is at end");
},
/** @private */
isEmpty: function() {
return this.start === this.end;
},
/** @private */
equals: function(object) {
return this.caret === object.caret && this.start === object.start && this.end === object.end;
}
};
return Selection;
}());
/**
* Constructs a new EventTable object.
*
* @class
* @name orion.textview.EventTable
* @private
*/
var EventTable = (function() {
/** @private */
function EventTable(){
this._listeners = {};
}
EventTable.prototype = /** @lends EventTable.prototype */ {
/** @private */
addEventListener: function(type, context, func, data) {
if (!this._listeners[type]) {
this._listeners[type] = [];
}
var listener = {
context: context,
func: func,
data: data
};
this._listeners[type].push(listener);
},
/** @private */
sendEvent: function(type, event) {
var listeners = this._listeners[type];
if (listeners) {
for (var i=0, len=listeners.length; i < len; i++){
var l = listeners[i];
if (l && l.context && l.func) {
l.func.call(l.context, event, l.data);
}
}
}
},
/** @private */
removeEventListener: function(type, context, func, data){
var listeners = this._listeners[type];
if (listeners) {
for (var i=0, len=listeners.length; i < len; i++){
var l = listeners[i];
if (l.context === context && l.func === func && l.data === data) {
listeners.splice(i, 1);
break;
}
}
}
}
};
return EventTable;
}());
/** @private */
function TextView (options) {
this._init(options);
}
TextView.prototype = /** @lends orion.textview.TextView.prototype */ {
/**
* Adds an event listener to the text view.
*
* @param {String} type the event type. The supported events are:
* <ul>
* <li>"Modify" See {@link #onModify} </li>
* <li>"Selection" See {@link #onSelection} </li>
* <li>"Scroll" See {@link #onScroll} </li>
* <li>"Verify" See {@link #onVerify} </li>
* <li>"Destroy" See {@link #onDestroy} </li>
* <li>"LineStyle" See {@link #onLineStyle} </li>
* <li>"ModelChanging" See {@link #onModelChanging} </li>
* <li>"ModelChanged" See {@link #onModelChanged} </li>
* </ul>
* @param {Object} context the context of the function.
* @param {Function} func the function that will be executed when the event happens.
* The function should take an event as the first parameter and optional data as the second parameter.
* @param {Object} [data] optional data passed to the function.
*
* @see #removeEventListener
*/
addEventListener: function(type, context, func, data) {
this._eventTable.addEventListener(type, context, func, data);
},
/**
* @class This interface represents a ruler for the text view.
* <p>
* A Ruler is a graphical element that is placed either on the left or on the right side of
* the view. It can be used to provide the view with per line decoration such as line numbering,
* bookmarks, breakpoints, folding disclosures, etc.
* </p><p>
* There are two types of rulers: page and document. A page ruler only shows the content for the lines that are
* visible, while a document ruler always shows the whole content.
* </p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#addRuler}
* </p>
* @name orion.textview.Ruler
*
*/
/**
* Returns the ruler overview type.
*
* @name getOverview
* @methodOf orion.textview.Ruler#
* @returns {String} the overview type, which is either "page" or "document".
*
* @see #getLocation
*/
/**
* Returns the ruler location.
*
* @name getLocation
* @methodOf orion.textview.Ruler#
* @returns {String} the ruler location, which is either "left" or "right".
*/
/**
* Returns the HTML content for the decoration of a given line.
* <p>
* If the line index is <code>-1</code>, the HTML content for the decoration
* that determines the width of the ruler should be returned.
* </p>
*
* @name getHTML
* @methodOf orion.textview.Ruler#
* @param {Number} lineIndex
* @returns {String} the HTML content for a given line, or generic line.
*
* @see #getStyle
*/
/**
* Returns the CSS styling information for the decoration of a given line.
* <p>
* If the line index is <code>-1</code>, the CSS styling information for the decoration
* that determines the width of the ruler should be returned. If the line is
* <code>undefined</code>, the ruler styling information should be returned.
* </p>
*
* @name getStyle
* @methodOf orion.textview.Ruler#
* @param {Number} lineIndex
* @returns {orion.textview.Style} the CSS styling for ruler, given line, or generic line.
*
* @see #getHTML
*/
/**
* Returns the indices of the lines that have decoration.
* <p>
* This function is only called for rulers with "document" overview type.
* </p>
* @name getAnnotations
* @methodOf orion.textview.Ruler#
* @returns {Number[]} an array of line indices.
*/
/**
* This event is sent when the user clicks a line decoration.
*
* @name onClick
* @event
* @methodOf orion.textview.Ruler#
* @param {Number} lineIndex the line index of the clicked decoration
* @param {DOMEvent} e the click event
*/
/**
* This event is sent when the user double clicks a line decoration.
*
* @name onDblClick
* @event
* @methodOf orion.textview.Ruler#
* @param {Number} lineIndex the line index of the double clicked decoration
* @param {DOMEvent} e the double click event
*/
/**
* Adds a ruler to the text view.
*
* @param {orion.textview.Ruler} ruler the ruler.
*/
addRuler: function (ruler) {
var document = this._frameDocument;
var body = document.body;
var side = ruler.getLocation();
var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
if (!rulerParent) {
rulerParent = document.createElement("DIV");
rulerParent.style.overflow = "hidden";
rulerParent.style.MozUserSelect = "none";
rulerParent.style.WebkitUserSelect = "none";
if (isIE) {
rulerParent.attachEvent("onselectstart", function() {return false;});
}
rulerParent.style.position = "absolute";
rulerParent.style.top = "0px";
rulerParent.style.cursor = "default";
body.appendChild(rulerParent);
if (side === "left") {
this._leftDiv = rulerParent;
rulerParent.className = "viewLeftRuler";
} else {
this._rightDiv = rulerParent;
rulerParent.className = "viewRightRuler";
}
var table = document.createElement("TABLE");
rulerParent.appendChild(table);
table.cellPadding = "0px";
table.cellSpacing = "0px";
table.border = "0px";
table.insertRow(0);
var self = this;
addHandler(rulerParent, "click", function(e) { self._handleRulerEvent(e); });
addHandler(rulerParent, "dblclick", function(e) { self._handleRulerEvent(e); });
}
var div = document.createElement("DIV");
div._ruler = ruler;
div.rulerChanged = true;
div.style.position = "relative";
var row = rulerParent.firstChild.rows[0];
var index = row.cells.length;
var cell = row.insertCell(index);
cell.vAlign = "top";
cell.appendChild(div);
ruler.setView(this);
this._updatePage();
},
/**
* Converts the given rectangle from one coordinate spaces to another.
* <p>The supported coordinate spaces are:
* <ul>
* <li>"document" - relative to document, the origin is the top-left corner of first line</li>
* <li>"page" - relative to html page that contains the text view</li>
* <li>"view" - relative to text view, the origin is the top-left corner of the view container</li>
* </ul>
* </p>
* <p>All methods in the view that take or return a position are in the document coordinate space.</p>
*
* @param rect the rectangle to convert.
* @param rect.x the x of the rectangle.
* @param rect.y the y of the rectangle.
* @param rect.width the width of the rectangle.
* @param rect.height the height of the rectangle.
* @param {String} from the source coordinate space.
* @param {String} to the destination coordinate space.
*
* @see #getLocationAtOffset
* @see #getOffsetAtLocation
* @see #getTopPixel
* @see #setTopPixel
*/
convert: function(rect, from, to) {
var scroll = this._getScroll();
var viewPad = this._getViewPadding();
var frame = this._frame.getBoundingClientRect();
var viewRect = this._viewDiv.getBoundingClientRect();
switch(from) {
case "document":
if (rect.x !== undefined) {
rect.x += - scroll.x + viewRect.left + viewPad.left;
}
if (rect.y !== undefined) {
rect.y += - scroll.y + viewRect.top + viewPad.top;
}
break;
case "page":
if (rect.x !== undefined) {
rect.x += - frame.left;
}
if (rect.y !== undefined) {
rect.y += - frame.top;
}
break;
}
//At this point rect is in the widget coordinate space
switch (to) {
case "document":
if (rect.x !== undefined) {
rect.x += scroll.x - viewRect.left - viewPad.left;
}
if (rect.y !== undefined) {
rect.y += scroll.y - viewRect.top - viewPad.top;
}
break;
case "page":
if (rect.x !== undefined) {
rect.x += frame.left;
}
if (rect.y !== undefined) {
rect.y += frame.top;
}
break;
}
},
/**
* Destroys the text view.
* <p>
* Removes the view from the page and frees all resources created by the view.
* Calling this function causes the "Destroy" event to be fire so that all components
* attached to view can release their references.
* </p>
*
* @see #onDestroy
*/
destroy: function() {
this._setGrab(null);
this._unhookEvents();
/* Destroy rulers*/
var destroyRulers = function(rulerDiv) {
if (!rulerDiv) {
return;
}
var cells = rulerDiv.firstChild.rows[0].cells;
for (var i = 0; i < cells.length; i++) {
var div = cells[i].firstChild;
div._ruler.setView(null);
}
};
destroyRulers (this._leftDiv);
destroyRulers (this._rightDiv);
/* Destroy timers */
if (this._autoScrollTimerID) {
clearTimeout(this._autoScrollTimerID);
this._autoScrollTimerID = null;
}
if (this._updateTimer) {
clearTimeout(this._updateTimer);
this._updateTimer = null;
}
/* Destroy DOM */
var parent = this._parent;
var frame = this._frame;
parent.removeChild(frame);
if (isPad) {
parent.removeChild(this._touchDiv);
this._touchDiv = null;
this._selDiv1 = null;
this._selDiv2 = null;
this._selDiv3 = null;
this._textArea = null;
}
var e = {};
this.onDestroy(e);
this._parent = null;
this._parentDocument = null;
this._model = null;
this._selection = null;
this._doubleClickSelection = null;
this._eventTable = null;
this._frame = null;
this._frameDocument = null;
this._frameWindow = null;
this._scrollDiv = null;
this._viewDiv = null;
this._clientDiv = null;
this._overlayDiv = null;
this._keyBindings = null;
this._actions = null;
},
/**
* Gives focus to the text view.
*/
focus: function() {
/*
* Feature in Chrome. When focus is called in the clientDiv without
* setting selection the browser will set the selection to the first dom
* element, which can be above the client area. When this happen the
* browser also scrolls the window to show that element.
* The fix is to call _updateDOMSelection() before calling focus().
*/
this._updateDOMSelection();
if (isPad) {
this._textArea.focus();
} else {
if (isOpera) { this._clientDiv.blur(); }
this._clientDiv.focus();
}
/*
* Feature in Safari. When focus is called the browser selects the clientDiv
* itself. The fix is to call _updateDOMSelection() after calling focus().
*/
this._updateDOMSelection();
},
/**
* Returns all action names defined in the text view.
* <p>
* There are two types of actions, the predefined actions of the view
* and the actions added by application code.
* </p>
* <p>
* The predefined actions are:
* <ul>
* <li>Navigation actions. These actions move the caret collapsing the selection.</li>
* <ul>
* <li>"lineUp" - moves the caret up by one line</li>
* <li>"lineDown" - moves the caret down by one line</li>
* <li>"lineStart" - moves the caret to beginning of the current line</li>
* <li>"lineEnd" - moves the caret to end of the current line </li>
* <li>"charPrevious" - moves the caret to the previous character</li>
* <li>"charNext" - moves the caret to the next character</li>
* <li>"pageUp" - moves the caret up by one page</li>
* <li>"pageDown" - moves the caret down by one page</li>
* <li>"wordPrevious" - moves the caret to the previous word</li>
* <li>"wordNext" - moves the caret to the next word</li>
* <li>"textStart" - moves the caret to the beginning of the document</li>
* <li>"textEnd" - moves the caret to the end of the document</li>
* </ul>
* <li>Selection actions. These actions move the caret extending the selection.</li>
* <ul>
* <li>"selectLineUp" - moves the caret up by one line</li>
* <li>"selectLineDown" - moves the caret down by one line</li>
* <li>"selectLineStart" - moves the caret to beginning of the current line</li>
* <li>"selectLineEnd" - moves the caret to end of the current line </li>
* <li>"selectCharPrevious" - moves the caret to the previous character</li>
* <li>"selectCharNext" - moves the caret to the next character</li>
* <li>"selectPageUp" - moves the caret up by one page</li>
* <li>"selectPageDown" - moves the caret down by one page</li>
* <li>"selectWordPrevious" - moves the caret to the previous word</li>
* <li>"selectWordNext" - moves the caret to the next word</li>
* <li>"selectTextStart" - moves the caret to the beginning of the document</li>
* <li>"selectTextEnd" - moves the caret to the end of the document</li>
* <li>"selectAll" - selects the entire document</li>
* </ul>
* <li>Edit actions. These actions modify the text view text</li>
* <ul>
* <li>"deletePrevious" - deletes the character preceding the caret</li>
* <li>"deleteNext" - deletes the charecter following the caret</li>
* <li>"deleteWordPrevious" - deletes the word preceding the caret</li>
* <li>"deleteWordNext" - deletes the word following the caret</li>
* <li>"tab" - inserts a tab character at the caret</li>
* <li>"enter" - inserts a line delimiter at the caret</li>
* </ul>
* <li>Clipboard actions.</li>
* <ul>
* <li>"copy" - copies the selected text to the clipboard</li>
* <li>"cut" - copies the selected text to the clipboard and deletes the selection</li>
* <li>"paste" - replaces the selected text with the clipboard contents</li>
* </ul>
* </ul>
* </p>
*
* @param {Boolean} [defaultAction=false] whether or not the predefined actions are included.
* @returns {String[]} an array of action names defined in the text view.
*
* @see #invokeAction
* @see #setAction
* @see #setKeyBinding
* @see #getKeyBindings
*/
getActions: function (defaultAction) {
var result = [];
var actions = this._actions;
for (var i = 0; i < actions.length; i++) {
if (!defaultAction && actions[i].defaultHandler) { continue; }
result.push(actions[i].name);
}
return result;
},
/**
* Returns the bottom index.
* <p>
* The bottom index is the line that is currently at the bottom of the view. This
* line may be partially visible depending on the vertical scroll of the view. The parameter
* <code>fullyVisible</code> determines whether to return only fully visible lines.
* </p>
*
* @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the last fully visible line. This
* parameter is ignored if the view is not big enough to show one line.
* @returns {Number} the index of the bottom line.
*
* @see #getTopIndex
* @see #setTopIndex
*/
getBottomIndex: function(fullyVisible) {
return this._getBottomIndex(fullyVisible);
},
/**
* Returns the bottom pixel.
* <p>
* The bottom pixel is the pixel position that is currently at
* the bottom edge of the view. This position is relative to the
* beginning of the document.
* </p>
*
* @returns {Number} the bottom pixel.
*
* @see #getTopPixel
* @see #setTopPixel
* @see #convert
*/
getBottomPixel: function() {
return this._getScroll().y + this._getClientHeight();
},
/**
* Returns the caret offset relative to the start of the document.
*
* @returns the caret offset relative to the start of the document.
*
* @see #setCaretOffset
* @see #setSelection
* @see #getSelection
*/
getCaretOffset: function () {
var s = this._getSelection();
return s.getCaret();
},
/**
* Returns the client area.
* <p>
* The client area is the portion in pixels of the document that is visible. The
* client area position is relative to the beginning of the document.
* </p>
*
* @returns the client area rectangle {x, y, width, height}.
*
* @see #getTopPixel
* @see #getBottomPixel
* @see #getHorizontalPixel
* @see #convert
*/
getClientArea: function() {
var scroll = this._getScroll();
return {x: scroll.x, y: scroll.y, width: this._getClientWidth(), height: this._getClientHeight()};
},
/**
* Returns the horizontal pixel.
* <p>
* The horizontal pixel is the pixel position that is currently at
* the left edge of the view. This position is relative to the
* beginning of the document.
* </p>
*
* @returns {Number} the horizontal pixel.
*
* @see #setHorizontalPixel
* @see #convert
*/
getHorizontalPixel: function() {
return this._getScroll().x;
},
/**
* Returns all the key bindings associated to the given action name.
*
* @param {String} name the action name.
* @returns {orion.textview.KeyBinding[]} the array of key bindings associated to the given action name.
*
* @see #setKeyBinding
* @see #setAction
*/
getKeyBindings: function (name) {
var result = [];
var keyBindings = this._keyBindings;
for (var i = 0; i < keyBindings.length; i++) {
if (keyBindings[i].name === name) {
result.push(keyBindings[i].keyBinding);
}
}
return result;
},
/**
* Returns the line height for a given line index. Returns the default line
* height if the line index is not specified.
*
* @param {Number} [lineIndex] the line index.
* @returns {Number} the height of the line in pixels.
*
* @see #getLinePixel
*/
getLineHeight: function(lineIndex) {
return this._getLineHeight();
},
/**
* Returns the top pixel position of a given line index relative to the beginning
* of the document.
* <p>
* Clamps out of range indices.
* </p>
*
* @param {Number} lineIndex the line index.
* @returns {Number} the pixel position of the line.
*
* @see #setTopPixel
* @see #convert
*/
getLinePixel: function(lineIndex) {
lineIndex = Math.min(Math.max(0, lineIndex), this._model.getLineCount());
var lineHeight = this._getLineHeight();
return lineHeight * lineIndex;
},
/**
* Returns the {x, y} pixel location of the top-left corner of the character
* bounding box at the specified offset in the document. The pixel location
* is relative to the document.
* <p>
* Clamps out of range offsets.
* </p>
*
* @param {Number} offset the character offset
* @returns the {x, y} pixel location of the given offset.
*
* @see #getOffsetAtLocation
* @see #convert
*/
getLocationAtOffset: function(offset) {
var model = this._model;
offset = Math.min(Math.max(0, offset), model.getCharCount());
var lineIndex = model.getLineAtOffset(offset);
var scroll = this._getScroll();
var viewRect = this._viewDiv.getBoundingClientRect();
var viewPad = this._getViewPadding();
var x = this._getOffsetToX(offset) + scroll.x - viewRect.left - viewPad.left;
var y = this.getLinePixel(lineIndex);
return {x: x, y: y};
},
/**
* Returns the text model of the text view.
*
* @returns {orion.textview.TextModel} the text model of the view.
*/
getModel: function() {
return this._model;
},
/**
* Returns the character offset nearest to the given pixel location. The
* pixel location is relative to the document.
*
* @param x the x of the location
* @param y the y of the location
* @returns the character offset at the given location.
*
* @see #getLocationAtOffset
*/
getOffsetAtLocation: function(x, y) {
var model = this._model;
var scroll = this._getScroll();
var viewRect = this._viewDiv.getBoundingClientRect();
var viewPad = this._getViewPadding();
var lineIndex = this._getYToLine(y - scroll.y);
x += -scroll.x + viewRect.left + viewPad.left;
var offset = this._getXToOffset(lineIndex, x);
return offset;
},
/**
* Returns the text view selection.
* <p>
* The selection is defined by a start and end character offset relative to the
* document. The character at end offset is not included in the selection.
* </p>
*
* @returns {orion.textview.Selection} the view selection
*
* @see #setSelection
*/
getSelection: function () {
var s = this._getSelection();
return {start: s.start, end: s.end};
},
/**
* Returns the text for the given range.
* <p>
* The text does not include the character at the end offset.
* </p>
*
* @param {Number} [start=0] the start offset of text range.
* @param {Number} [end=char count] the end offset of text range.
*
* @see #setText
*/
getText: function(start, end) {
var model = this._model;
return model.getText(start, end);
},
/**
* Returns the top index.
* <p>
* The top index is the line that is currently at the top of the view. This
* line may be partially visible depending on the vertical scroll of the view. The parameter
* <code>fullyVisible</code> determines whether to return only fully visible lines.
* </p>
*
* @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the first fully visible line. This
* parameter is ignored if the view is not big enough to show one line.
* @returns {Number} the index of the top line.
*
* @see #getBottomIndex
* @see #setTopIndex
*/
getTopIndex: function(fullyVisible) {
return this._getTopIndex(fullyVisible);
},
/**
* Returns the top pixel.
* <p>
* The top pixel is the pixel position that is currently at
* the top edge of the view. This position is relative to the
* beginning of the document.
* </p>
*
* @returns {Number} the top pixel.
*
* @see #getBottomPixel
* @see #setTopPixel
* @see #convert
*/
getTopPixel: function() {
return this._getScroll().y;
},
/**
* Executes the action handler associated with the given name.
* <p>
* The application defined action takes precedence over predefined actions unless
* the <code>defaultAction</code> paramater is <code>true</code>.
* </p>
* <p>
* If the application defined action returns <code>false</code>, the text view predefined
* action is executed if present.
* </p>
*
* @param {String} name the action name.
* @param {Boolean} [defaultAction] whether to always execute the predefined action.
* @returns {Boolean} <code>true</code> if the action was executed.
*
* @see #setAction
* @see #getActions
*/
invokeAction: function (name, defaultAction) {
var actions = this._actions;
for (var i = 0; i < actions.length; i++) {
var a = actions[i];
if (a.name && a.name === name) {
if (!defaultAction && a.userHandler) {
if (a.userHandler()) { return; }
}
if (a.defaultHandler) { return a.defaultHandler(); }
return false;
}
}
return false;
},
/**
* @class This is the event sent when the user right clicks or otherwise invokes the context menu of the view.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onContextMenu}
* </p>
*
* @name orion.textview.ContextMenuEvent
*
* @property {Number} x The pointer location on the x axis, relative to the document the user is editing.
* @property {Number} y The pointer location on the y axis, relative to the document the user is editing.
* @property {Number} screenX The pointer location on the x axis, relative to the screen. This is copied from the DOM contextmenu event.screenX property.
* @property {Number} screenY The pointer location on the y axis, relative to the screen. This is copied from the DOM contextmenu event.screenY property.
*/
/**
* This event is sent when the user invokes the view context menu.
*
* @event
* @param {orion.textview.ContextMenuEvent} contextMenuEvent the event
*/
onContextMenu: function(contextMenuEvent) {
this._eventTable.sendEvent("ContextMenu", contextMenuEvent);
},
/**
* @class This is the event sent when the text view is destroyed.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onDestroy}
* </p>
* @name orion.textview.DestroyEvent
*/
/**
* This event is sent when the text view has been destroyed.
*
* @event
* @param {orion.textview.DestroyEvent} destroyEvent the event
*
* @see #destroy
*/
onDestroy: function(destroyEvent) {
this._eventTable.sendEvent("Destroy", destroyEvent);
},
/**
* @class This object is used to define style information for the text view.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onLineStyle}
* </p>
* @name orion.textview.Style
*
* @property {String} styleClass A CSS class name.
* @property {Object} style An object with CSS properties.
*/
/**
* @class This object is used to style range.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onLineStyle}
* </p>
* @name orion.textview.StyleRange
*
* @property {Number} start The start character offset, relative to the document, where the style should be applied.
* @property {Number} end The end character offset (exclusive), relative to the document, where the style should be applied.
* @property {orion.textview.Style} style The style for the range.
*/
/**
* @class This is the event sent when the text view needs the style information for a line.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onLineStyle}
* </p>
* @name orion.textview.LineStyleEvent
*
* @property {Number} lineIndex The line index.
* @property {String} lineText The line text.
* @property {Number} lineStart The character offset, relative to document, of the first character in the line.
* @property {orion.textview.Style} style The style for the entire line (output argument).
* @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument).
*/
/**
* This event is sent when the text view needs the style information for a line.
*
* @event
* @param {orion.textview.LineStyleEvent} lineStyleEvent the event
*/
onLineStyle: function(lineStyleEvent) {
this._eventTable.sendEvent("LineStyle", lineStyleEvent);
},
/**
* @class This is the event sent when the text in the model has changed.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onModelChanged}<br/>
* {@link orion.textview.TextModel#onChanged}
* </p>
* @name orion.textview.ModelChangedEvent
*
* @property {Number} start The character offset in the model where the change has occurred.
* @property {Number} removedCharCount The number of characters removed from the model.
* @property {Number} addedCharCount The number of characters added to the model.
* @property {Number} removedLineCount The number of lines removed from the model.
* @property {Number} addedLineCount The number of lines added to the model.
*/
/**
* This event is sent when the text in the model has changed.
*
* @event
* @param {orion.textview.ModelChangingEvent} modelChangingEvent the event
*/
onModelChanged: function(modelChangedEvent) {
this._eventTable.sendEvent("ModelChanged", modelChangedEvent);
},
/**
* @class This is the event sent when the text in the model is about to change.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onModelChanging}<br/>
* {@link orion.textview.TextModel#onChanging}
* </p>
* @name orion.textview.ModelChangingEvent
*
* @property {String} text The text that is about to be inserted in the model.
* @property {Number} start The character offset in the model where the change will occur.
* @property {Number} removedCharCount The number of characters being removed from the model.
* @property {Number} addedCharCount The number of characters being added to the model.
* @property {Number} removedLineCount The number of lines being removed from the model.
* @property {Number} addedLineCount The number of lines being added to the model.
*/
/**
* This event is sent when the text in the model is about to change.
*
* @event
* @param {orion.textview.ModelChangingEvent} modelChangingEvent the event
*/
onModelChanging: function(modelChangingEvent) {
this._eventTable.sendEvent("ModelChanging", modelChangingEvent);
},
/**
* @class This is the event sent when the text is modified by the text view.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onModify}
* </p>
* @name orion.textview.ModifyEvent
*/
/**
* This event is sent when the text view has changed text in the model.
* <p>
* If the text is changed directly through the model API, this event
* is not sent.
* </p>
*
* @event
* @param {orion.textview.ModifyEvent} modifyEvent the event
*/
onModify: function(modifyEvent) {
this._eventTable.sendEvent("Modify", modifyEvent);
},
/**
* @class This is the event sent when the selection changes in the text view.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onSelection}
* </p>
* @name orion.textview.SelectionEvent
*
* @property {orion.textview.Selection} oldValue The old selection.
* @property {orion.textview.Selection} newValue The new selection.
*/
/**
* This event is sent when the text view selection has changed.
*
* @event
* @param {orion.textview.SelectionEvent} selectionEvent the event
*/
onSelection: function(selectionEvent) {
this._eventTable.sendEvent("Selection", selectionEvent);
},
/**
* @class This is the event sent when the text view scrolls.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onScroll}
* </p>
* @name orion.textview.ScrollEvent
*
* @property oldValue The old scroll {x,y}.
* @property newValue The new scroll {x,y}.
*/
/**
* This event is sent when the text view scrolls vertically or horizontally.
*
* @event
* @param {orion.textview.ScrollEvent} scrollEvent the event
*/
onScroll: function(scrollEvent) {
this._eventTable.sendEvent("Scroll", scrollEvent);
},
/**
* @class This is the event sent when the text is about to be modified by the text view.
* <p>
* <b>See:</b><br/>
* {@link orion.textview.TextView}<br/>
* {@link orion.textview.TextView#event:onVerify}
* </p>
* @name orion.textview.VerifyEvent
*
* @property {String} text The text being inserted.
* @property {Number} start The start offset of the text range to be replaced.
* @property {Number} end The end offset (exclusive) of the text range to be replaced.
*/
/**
* This event is sent when the text view is about to change text in the model.
* <p>
* If the text is changed directly through the model API, this event
* is not sent.
* </p>
* <p>
* Listeners are allowed to change these parameters. Setting text to null
* or undefined stops the change.
* </p>
*
* @event
* @param {orion.textview.VerifyEvent} verifyEvent the event
*/
onVerify: function(verifyEvent) {
this._eventTable.sendEvent("Verify", verifyEvent);
},
/**
* Redraws the text in the given line range.
* <p>
* The line at the end index is not redrawn.
* </p>
*
* @param {Number} [startLine=0] the start line
* @param {Number} [endLine=line count] the end line
*/
redrawLines: function(startLine, endLine, ruler) {
if (startLine === undefined) { startLine = 0; }
if (endLine === undefined) { endLine = this._model.getLineCount(); }
if (startLine === endLine) { return; }
var div = this._clientDiv;
if (ruler) {
var location = ruler.getLocation();//"left" or "right"
var divRuler = location === "left" ? this._leftDiv : this._rightDiv;
var cells = divRuler.firstChild.rows[0].cells;
for (var i = 0; i < cells.length; i++) {
if (cells[i].firstChild._ruler === ruler) {
div = cells[i].firstChild;
break;
}
}
}
if (ruler) {
div.rulerChanged = true;
}
if (!ruler || ruler.getOverview() === "page") {
var child = div.firstChild;
while (child) {
var lineIndex = child.lineIndex;
if (startLine <= lineIndex && lineIndex < endLine) {
child.lineChanged = true;
}
child = child.nextSibling;
}
}
if (!ruler) {
if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) {
this._maxLineIndex = -1;
this._maxLineWidth = 0;
}
}
this._queueUpdatePage();
},
/**
* Redraws the text in the given range.
* <p>
* The character at the end offset is not redrawn.
* </p>
*
* @param {Number} [start=0] the start offset of text range
* @param {Number} [end=char count] the end offset of text range
*/
redrawRange: function(start, end) {
var model = this._model;
if (start === undefined) { start = 0; }
if (end === undefined) { end = model.getCharCount(); }
if (start === end) { return; }
var startLine = model.getLineAtOffset(start);
var endLine = model.getLineAtOffset(Math.max(0, end - 1)) + 1;
this.redrawLines(startLine, endLine);
},
/**
* Removes an event listener from the text view.
* <p>
* All the parameters must be the same ones used to add the listener.
* </p>
*
* @param {String} type the event type.
* @param {Object} context the context of the function.
* @param {Function} func the function that will be executed when the event happens.
* @param {Object} [data] optional data passed to the function.
*
* @see #addEventListener
*/
removeEventListener: function(type, context, func, data) {
this._eventTable.removeEventListener(type, context, func, data);
},
/**
* Removes a ruler from the text view.
*
* @param {orion.textview.Ruler} ruler the ruler.
*/
removeRuler: function (ruler) {
ruler.setView(null);
var side = ruler.getLocation();
var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
var row = rulerParent.firstChild.rows[0];
var cells = row.cells;
for (var index = 0; index < cells.length; index++) {
var cell = cells[index];
if (cell.firstChild._ruler === ruler) { break; }
}
if (index === cells.length) { return; }
row.cells[index]._ruler = undefined;
row.deleteCell(index);
this._updatePage();
},
/**
* Associates an application defined handler to an action name.
* <p>
* If the action name is a predefined action, the given handler executes before
* the default action handler. If the given handler returns <code>true</code>, the
* default action handler is not called.
* </p>
*
* @param {String} name the action name.
* @param {Function} handler the action handler.
*
* @see #getActions
* @see #invokeAction
*/
setAction: function(name, handler) {
if (!name) { return; }
var actions = this._actions;
for (var i = 0; i < actions.length; i++) {
var a = actions[i];
if (a.name === name) {
a.userHandler = handler;
return;
}
}
actions.push({name: name, userHandler: handler});
},
/**
* Associates a key binding with the given action name. Any previous
* association with the specified key binding is overwriten. If the
* action name is <code>null</code>, the association is removed.
*
* @param {orion.textview.KeyBinding} keyBinding the key binding
* @param {String} name the action
*/
setKeyBinding: function(keyBinding, name) {
var keyBindings = this._keyBindings;
for (var i = 0; i < keyBindings.length; i++) {
var kb = keyBindings[i];
if (kb.keyBinding.equals(keyBinding)) {
if (name) {
kb.name = name;
} else {
if (kb.predefined) {
kb.name = null;
} else {
var oldName = kb.name;
keyBindings.splice(i, 1);
var index = 0;
while (index < keyBindings.length && oldName !== keyBindings[index].name) {
index++;
}
if (index === keyBindings.length) {
/* <p>
* Removing all the key bindings associated to an user action will cause
* the user action to be removed. TextView predefined actions are never
* removed (so they can be reinstalled in the future).
* </p>
*/
var actions = this._actions;
for (var j = 0; j < actions.length; j++) {
if (actions[j].name === oldName) {
if (!actions[j].defaultHandler) {
actions.splice(j, 1);
}
}
}
}
}
}
return;
}
}
if (name) {
keyBindings.push({keyBinding: keyBinding, name: name});
}
},
/**
* Sets the caret offset relative to the start of the document.
*
* @param {Number} caret the caret offset relative to the start of the document.
* @param {Boolean} [show=true] if <code>true</coce>, the view will scroll if needed to show the caret location.
*
* @see #getCaretOffset
* @see #setSelection
* @see #getSelection
*/
setCaretOffset: function(offset, show) {
var charCount = this._model.getCharCount();
offset = Math.max(0, Math.min (offset, charCount));
var selection = new Selection(offset, offset, false);
this._setSelection (selection, show === undefined || show);
},
/**
* Sets the horizontal pixel.
* <p>
* The horizontal pixel is the pixel position that is currently at
* the left edge of the view. This position is relative to the
* beginning of the document.
* </p>
*
* @param {Number} pixel the horizontal pixel.
*
* @see #getHorizontalPixel
* @see #convert
*/
setHorizontalPixel: function(pixel) {
pixel = Math.max(0, pixel);
this._scrollView(pixel - this._getScroll().x, 0);
},
/**
* Sets the text model of the text view.
*
* @param {orion.textview.TextModel} model the text model of the view.
*/
setModel: function(model) {
if (!model) { return; }
this._model.removeListener(this._modelListener);
var oldLineCount = this._model.getLineCount();
var oldCharCount = this._model.getCharCount();
var newLineCount = model.getLineCount();
var newCharCount = model.getCharCount();
var newText = model.getText();
var e = {
text: newText,
start: 0,
removedCharCount: oldCharCount,
addedCharCount: newCharCount,
removedLineCount: oldLineCount,
addedLineCount: newLineCount
};
this.onModelChanging(e);
this.redrawRange();
this._model = model;
e = {
start: 0,
removedCharCount: oldCharCount,
addedCharCount: newCharCount,
removedLineCount: oldLineCount,
addedLineCount: newLineCount
};
this.onModelChanged(e);
this._model.addListener(this._modelListener);
this.redrawRange();
},
/**
* Sets the text view selection.
* <p>
* The selection is defined by a start and end character offset relative to the
* document. The character at end offset is not included in the selection.
* </p>
* <p>
* The caret is always placed at the end offset. The start offset can be
* greater than the end offset to place the caret at the beginning of the
* selection.
* </p>
* <p>
* Clamps out of range offsets.
* </p>
*
* @param {Number} start the start offset of the selection
* @param {Number} end the end offset of the selection
* @param {Boolean} [show=true] if <code>true</coce>, the view will scroll if needed to show the caret location.
*
* @see #getSelection
*/
setSelection: function (start, end, show) {
var caret = start > end;
if (caret) {
var tmp = start;
start = end;
end = tmp;
}
var charCount = this._model.getCharCount();
start = Math.max(0, Math.min (start, charCount));
end = Math.max(0, Math.min (end, charCount));
var selection = new Selection(start, end, caret);
this._setSelection(selection, show === undefined || show);
},
/**
* Replaces the text in the given range with the given text.
* <p>
* The character at the end offset is not replaced.
* </p>
* <p>
* When both <code>start</code> and <code>end</code> parameters
* are not specified, the text view places the caret at the beginning
* of the document and scrolls to make it visible.
* </p>
*
* @param {String} text the new text.
* @param {Number} [start=0] the start offset of text range.
* @param {Number} [end=char count] the end offset of text range.
*
* @see #getText
*/
setText: function (text, start, end) {
var reset = start === undefined && end === undefined;
if (start === undefined) { start = 0; }
if (end === undefined) { end = this._model.getCharCount(); }
this._modifyContent({text: text, start: start, end: end, _code: true}, !reset);
if (reset) {
this._columnX = -1;
this._setSelection(new Selection (0, 0, false), true);
/*
* Bug in Firefox. For some reason, the caret does not show after the
* view is refreshed. The fix is to toggle the contentEditable state and
* force the clientDiv to loose and receive focus if the it is focused.
*/
if (isFirefox) {
var hasFocus = this._hasFocus;
var clientDiv = this._clientDiv;
if (hasFocus) { clientDiv.blur(); }
clientDiv.contentEditable = false;
clientDiv.contentEditable = true;
if (hasFocus) { clientDiv.focus(); }
}
}
},
/**
* Sets the top index.
* <p>
* The top index is the line that is currently at the top of the text view. This
* line may be partially visible depending on the vertical scroll of the view.
* </p>
*
* @param {Number} topIndex the index of the top line.
*
* @see #getBottomIndex
* @see #getTopIndex
*/
setTopIndex: function(topIndex) {
var model = this._model;
if (model.getCharCount() === 0) {
return;
}
var lineCount = model.getLineCount();
var lineHeight = this._getLineHeight();
var pageSize = Math.max(1, Math.min(lineCount, Math.floor(this._getClientHeight () / lineHeight)));
if (topIndex < 0) {
topIndex = 0;
} else if (topIndex > lineCount - pageSize) {
topIndex = lineCount - pageSize;
}
var pixel = topIndex * lineHeight - this._getScroll().y;
this._scrollView(0, pixel);
},
/**
* Sets the top pixel.
* <p>
* The top pixel is the pixel position that is currently at
* the top edge of the view. This position is relative to the
* beginning of the document.
* </p>
*
* @param {Number} pixel the top pixel.
*
* @see #getBottomPixel
* @see #getTopPixel
* @see #convert
*/
setTopPixel: function(pixel) {
var lineHeight = this._getLineHeight();
var clientHeight = this._getClientHeight();
var lineCount = this._model.getLineCount();
pixel = Math.min(Math.max(0, pixel), lineHeight * lineCount - clientHeight);
this._scrollView(0, pixel - this._getScroll().y);
},
/**
* Scrolls the selection into view if needed.
*
* @see #getSelection
* @see #setSelection
*/
showSelection: function() {
return this._showCaret();
},
/**************************************** Event handlers *********************************/
_handleBodyMouseDown: function (e) {
if (!e) { e = window.event; }
/*
* Prevent clicks outside of the view from taking focus
* away the view. Note that in Firefox and Opera clicking on the
* scrollbar also take focus from the view. Other browsers
* do not have this problem and stopping the click over the
* scrollbar for them causes mouse capture problems.
*/
var topNode = isOpera ? this._clientDiv : this._overlayDiv || this._viewDiv;
var temp = e.target ? e.target : e.srcElement;
while (temp) {
if (topNode === temp) {
return;
}
temp = temp.parentNode;
}
if (e.preventDefault) { e.preventDefault(); }
if (e.stopPropagation){ e.stopPropagation(); }
if (!isW3CEvents) {
/* In IE 8 is not possible to prevent the default handler from running
* during mouse down event using usual API. The workaround is to use
* setCapture/releaseCapture.
*/
topNode.setCapture();
setTimeout(function() { topNode.releaseCapture(); }, 0);
}
},
_handleBlur: function (e) {
if (!e) { e = window.event; }
this._hasFocus = false;
/*
* Bug in IE 8 and earlier. For some reason when text is deselected
* the overflow selection at the end of some lines does not get redrawn.
* The fix is to create a DOM element in the body to force a redraw.
*/
if (isIE < 9) {
if (!this._getSelection().isEmpty()) {
var document = this._frameDocument;
var child = document.createElement("DIV");
var body = document.body;
body.appendChild(child);
body.removeChild(child);
}
}
if (isFirefox || isIE) {
if (this._selDiv1) {
var color = isIE ? "transparent" : "#AFAFAF";
this._selDiv1.style.background = color;
this._selDiv2.style.background = color;
this._selDiv3.style.background = color;
}
}
},
_handleContextMenu: function (e) {
if (!e) { e = window.event; }
var scroll = this._getScroll();
var viewRect = this._viewDiv.getBoundingClientRect();
var viewPad = this._getViewPadding();
var x = e.clientX + scroll.x - viewRect.left - viewPad.left;
var y = e.clientY + scroll.y - viewRect.top - viewPad.top;
this.onContextMenu({x: x, y: y, screenX: e.screenX, screenY: e.screenY});
if (e.preventDefault) { e.preventDefault(); }
return false;
},
_handleCopy: function (e) {
if (this._ignoreCopy) { return; }
if (!e) { e = window.event; }
if (this._doCopy(e)) {
if (e.preventDefault) { e.preventDefault(); }
return false;
}
},
_handleCut: function (e) {
if (!e) { e = window.event; }
if (this._doCut(e)) {
if (e.preventDefault) { e.preventDefault(); }
return false;
}
},
_handleDataModified: function(e) {
this._startIME();
},
_handleDblclick: function (e) {
if (!e) { e = window.event; }
var time = e.timeStamp ? e.timeStamp : new Date().getTime();
this._lastMouseTime = time;
if (this._clickCount !== 2) {
this._clickCount = 2;
this._handleMouse(e);
}
},
_handleDragStart: function (e) {
if (!e) { e = window.event; }
if (e.preventDefault) { e.preventDefault(); }
return false;
},
_handleDragOver: function (e) {
if (!e) { e = window.event; }
e.dataTransfer.dropEffect = "none";
if (e.preventDefault) { e.preventDefault(); }
return false;
},
_handleDrop: function (e) {
if (!e) { e = window.event; }
if (e.preventDefault) { e.preventDefault(); }
return false;
},
_handleDocFocus: function (e) {
if (!e) { e = window.event; }
this._clientDiv.focus();
},
_handleFocus: function (e) {
if (!e) { e = window.event; }
this._hasFocus = true;
/*
* Feature in IE. The selection is not restored when the
* view gets focus and the caret is always placed at the
* beginning of the document. The fix is to update the DOM
* selection during the focus event.
*/
if (isIE) {
this._updateDOMSelection();
}
if (isFirefox || isIE) {
if (this._selDiv1) {
var color = this._hightlightRGB;
this._selDiv1.style.background = color;
this._selDiv2.style.background = color;
this._selDiv3.style.background = color;
}
}
},
_handleKeyDown: function (e) {
if (!e) { e = window.event; }
if (isPad) {
if (e.keyCode === 8) {
this._doBackspace({});
e.preventDefault();
}
return;
}
if (e.keyCode === 229) {
if (this.readonly) {
if (e.preventDefault) { e.preventDefault(); }
return false;
}
this._startIME();
} else {
this._commitIME();
}
/*
* Feature in Firefox. When a key is held down the browser sends
* right number of keypress events but only one keydown. This is
* unexpected and causes the view to only execute an action
* just one time. The fix is to ignore the keydown event and
* execute the actions from the keypress handler.
* Note: This only happens on the Mac and Linux (Firefox 3.6).
*
* Feature in Opera. Opera sends keypress events even for non-printable
* keys. The fix is to handle actions in keypress instead of keydown.
*/
if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
this._keyDownEvent = e;
return true;
}
if (this._doAction(e)) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.cancelBubble = true;
e.returnValue = false;
e.keyCode = 0;
}
return false;
}
},
_handleKeyPress: function (e) {
if (!e) { e = window.event; }
/*
* Feature in Embedded WebKit. Embedded WekKit on Mac runs in compatibility mode and
* generates key press events for these Unicode values (Function keys). This does not
* happen in Safari or Chrome. The fix is to ignore these key events.
*/
if (isMac && isWebkit) {
if ((0xF700 <= e.keyCode && e.keyCode <= 0xF7FF) || e.keyCode === 13 || e.keyCode === 8) {
if (e.preventDefault) { e.preventDefault(); }
return false;
}
}
if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
if (this._doAction(this._keyDownEvent)) {
if (e.preventDefault) { e.preventDefault(); }
return false;
}
}
var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
if (e.charCode !== undefined) {
if (ctrlKey) {
switch (e.charCode) {
/*
* In Firefox and Safari if ctrl+v, ctrl+c ctrl+x is canceled
* the clipboard events are not sent. The fix to allow
* the browser to handles these key events.
*/
case 99://c
case 118://v
case 120://x
return true;
}
}
}
var ignore = false;
if (isMac) {
if (e.ctrlKey || e.metaKey) { ignore = true; }
} else {
if (isFirefox) {
//Firefox clears the state mask when ALT GR generates input
if (e.ctrlKey || e.altKey) { ignore = true; }
} else {
//IE and Chrome only send ALT GR when input is generated
if (e.ctrlKey ^ e.altKey) { ignore = true; }
}
}
if (!ignore) {
var key = isOpera ? e.which : (e.charCode !== undefined ? e.charCode : e.keyCode);
if (key !== 0) {
this._doContent(String.fromCharCode (key));
if (e.preventDefault) { e.preventDefault(); }
return false;
}
}
},
_handleKeyUp: function (e) {
if (!e) { e = window.event; }
// don't commit for space (it happens during JP composition)
if (e.keyCode === 13) {
this._commitIME();
}
},
_handleMouse: function (e) {
var target = this._frameWindow;
if (isIE) { target = this._clientDiv; }
if (this._overlayDiv) {
var self = this;
setTimeout(function () {
self.focus();
}, 0);
}
if (this._clickCount === 1) {
this._setGrab(target);
this._setSelectionTo(e.clientX, e.clientY, e.shiftKey);
} else {
/*
* Feature in IE8 and older, the sequence of events in the IE8 event model
* for a doule-click is:
*
* down
* up
* up
* dblclick
*
* Given that the mouse down/up events are not balanced, it is not possible to
* grab on mouse down and ungrab on mouse up. The fix is to grab on the first
* mouse down and ungrab on mouse move when the button 1 is not set.
*/
if (isW3CEvents) { this._setGrab(target); }
this._doubleClickSelection = null;
this._setSelectionTo(e.clientX, e.clientY, e.shiftKey);
this._doubleClickSelection = this._getSelection();
}
},
_handleMouseDown: function (e) {
if (!e) { e = window.event; }
var left = e.which ? e.button === 0 : e.button === 1;
this._commitIME();
if (left) {
this._isMouseDown = true;
var deltaX = Math.abs(this._lastMouseX - e.clientX);
var deltaY = Math.abs(this._lastMouseY - e.clientY);
var time = e.timeStamp ? e.timeStamp : new Date().getTime();
if ((time - this._lastMouseTime) <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) {
this._clickCount++;
} else {
this._clickCount = 1;
}
this._lastMouseX = e.clientX;
this._lastMouseY = e.clientY;
this._lastMouseTime = time;
this._handleMouse(e);
if (isOpera) {
if (!this._hasFocus) {
this.focus();
}
e.preventDefault();
}
}
},
_handleMouseMove: function (e) {
if (!e) { e = window.event; }
/*
* Feature in IE8 and older, the sequence of events in the IE8 event model
* for a doule-click is:
*
* down
* up
* up
* dblclick
*
* Given that the mouse down/up events are not balanced, it is not possible to
* grab on mouse down and ungrab on mouse up. The fix is to grab on the first
* mouse down and ungrab on mouse move when the button 1 is not set.
*
* In order to detect double-click and drag gestures, it is necessary to send
* a mouse down event from mouse move when the button is still down and isMouseDown
* flag is not set.
*/
if (!isW3CEvents) {
if (e.button === 0) {
this._setGrab(null);
return true;
}
if (!this._isMouseDown && e.button === 1 && (this._clickCount & 1) !== 0) {
this._clickCount = 2;
return this._handleMouse(e, this._clickCount);
}
}
var x = e.clientX;
var y = e.clientY;
var viewPad = this._getViewPadding();
var viewRect = this._viewDiv.getBoundingClientRect();
var width = this._getClientWidth (), height = this._getClientHeight();
var leftEdge = viewRect.left + viewPad.left;
var topEdge = viewRect.top + viewPad.top;
var rightEdge = viewRect.left + viewPad.left + width;
var bottomEdge = viewRect.top + viewPad.top + height;
var model = this._model;
var caretLine = model.getLineAtOffset(this._getSelection().getCaret());
if (y < topEdge && caretLine !== 0) {
this._doAutoScroll("up", x, y - topEdge);
} else if (y > bottomEdge && caretLine !== model.getLineCount() - 1) {
this._doAutoScroll("down", x, y - bottomEdge);
} else if (x < leftEdge) {
this._doAutoScroll("left", x - leftEdge, y);
} else if (x > rightEdge) {
this._doAutoScroll("right", x - rightEdge, y);
} else {
this._endAutoScroll();
this._setSelectionTo(x, y, true);
/*
* Feature in IE. IE does redraw the selection background right
* away after the selection changes because of mouse move events.
* The fix is to call getBoundingClientRect() on the
* body element to force the selection to be redraw. Some how
* calling this method forces a redraw.
*/
if (isIE) {
var body = this._frameDocument.body;
body.getBoundingClientRect();
}
}
},
_handleMouseUp: function (e) {
if (!e) { e = window.event; }
this._endAutoScroll();
var left = e.which ? e.button === 0 : e.button === 1;
if (left) {
this._isMouseDown=false;
/*
* Feature in IE8 and older, the sequence of events in the IE8 event model
* for a doule-click is:
*
* down
* up
* up
* dblclick
*
* Given that the mouse down/up events are not balanced, it is not possible to
* grab on mouse down and ungrab on mouse up. The fix is to grab on the first
* mouse down and ungrab on mouse move when the button 1 is not set.
*/
if (isW3CEvents) { this._setGrab(null); }
}
},
_handleMouseWheel: function (e) {
if (!e) { e = window.event; }
var lineHeight = this._getLineHeight();
var pixelX = 0, pixelY = 0;
// Note: On the Mac the correct behaviour is to scroll by pixel.
if (isFirefox) {
var pixel;
if (isMac) {
pixel = e.detail * 3;
} else {
var limit = 256;
pixel = Math.max(-limit, Math.min(limit, e.detail)) * lineHeight;
}
if (e.axis === e.HORIZONTAL_AXIS) {
pixelX = pixel;
} else {
pixelY = pixel;
}
} else {
//Webkit
if (isMac) {
/*
* In Safari, the wheel delta is a multiple of 120. In order to
* convert delta to pixel values, it is necessary to divide delta
* by 40.
*
* In Chrome, the wheel delta depends on the type of the mouse. In
* general, it is the pixel value for Mac mice and track pads, but
* it is a multiple of 120 for other mice. There is no presise
* way to determine if it is pixel value or a multiple of 120.
*
* Note that the current approach does not calculate the correct
* pixel value for Mac mice when the delta is a multiple of 120.
*/
var denominatorX = 40, denominatorY = 40;
if (isChrome) {
if (e.wheelDeltaX % 120 !== 0) { denominatorX = 1; }
if (e.wheelDeltaY % 120 !== 0) { denominatorY = 1; }
}
pixelX = -e.wheelDeltaX / denominatorX;
if (-1 < pixelX && pixelX < 0) { pixelX = -1; }
if (0 < pixelX && pixelX < 1) { pixelX = 1; }
pixelY = -e.wheelDeltaY / denominatorY;
if (-1 < pixelY && pixelY < 0) { pixelY = -1; }
if (0 < pixelY && pixelY < 1) { pixelY = 1; }
} else {
pixelX = -e.wheelDeltaX;
var linesToScroll = 8;
pixelY = (-e.wheelDeltaY / 120 * linesToScroll) * lineHeight;
}
}
/*
* Feature in Safari. If the event target is removed from the DOM
* safari stops smooth scrolling. The fix is keep the element target
* in the DOM and remove it on a later time.
*
* Note: Using a timer is not a solution, because the timeout needs to
* be at least as long as the gesture (which is too long).
*/
if (isSafari) {
var lineDiv = e.target;
while (lineDiv && lineDiv.lineIndex === undefined) {
lineDiv = lineDiv.parentNode;
}
this._mouseWheelLine = lineDiv;
}
var oldScroll = this._getScroll();
this._scrollView(pixelX, pixelY);
var newScroll = this._getScroll();
if (isSafari) { this._mouseWheelLine = null; }
if (oldScroll.x !== newScroll.x || oldScroll.y !== newScroll.y) {
if (e.preventDefault) { e.preventDefault(); }
return false;
}
},
_handlePaste: function (e) {
if (this._ignorePaste) { return; }
if (!e) { e = window.event; }
if (this._doPaste(e)) {
if (isIE) {
/*
* Bug in IE,
*/
var self = this;
setTimeout(function() {self._updateDOMSelection();}, 0);
}
if (e.preventDefault) { e.preventDefault(); }
return false;
}
},
_handleResize: function (e) {
if (!e) { e = window.event; }
var element = this._frameDocument.documentElement;
var newWidth = element.clientWidth;
var newHeight = element.clientHeight;
if (this._frameWidth !== newWidth || this._frameHeight !== newHeight) {
this._frameWidth = newWidth;
this._frameHeight = newHeight;
this._updatePage();
}
},
_handleRulerEvent: function (e) {
if (!e) { e = window.event; }
var target = e.target ? e.target : e.srcElement;
var lineIndex = target.lineIndex;
var element = target;
while (element && !element._ruler) {
if (lineIndex === undefined && element.lineIndex !== undefined) {
lineIndex = element.lineIndex;
}
element = element.parentNode;
}
var ruler = element ? element._ruler : null;
if (isPad && lineIndex === undefined && ruler && ruler.getOverview() === "document") {
var buttonHeight = 17;
var clientHeight = this._getClientHeight ();
var lineHeight = this._getLineHeight ();
var viewPad = this._getViewPadding();
var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
var pixels = this._model.getLineCount () * lineHeight;
this.setTopPixel(Math.floor((e.clientY - buttonHeight - lineHeight) * pixels / trackHeight));
}
if (ruler) {
switch (e.type) {
case "click":
if (ruler.onClick) { ruler.onClick(lineIndex, e); }
break;
case "dblclick":
if (ruler.onDblClick) { ruler.onDblClick(lineIndex, e); }
break;
}
}
},
_handleScroll: function () {
this._doScroll(this._getScroll());
},
_handleSelectStart: function (e) {
if (!e) { e = window.event; }
if (this._ignoreSelect) {
if (e && e.preventDefault) { e.preventDefault(); }
return false;
}
},
_handleInput: function (e) {
var textArea = this._textArea;
this._doContent(textArea.value);
textArea.selectionStart = textArea.selectionEnd = 0;
textArea.value = "";
e.preventDefault();
},
_handleTextInput: function (e) {
this._doContent(e.data);
e.preventDefault();
},
_touchConvert: function (touch) {
var rect = this._frame.getBoundingClientRect();
var body = this._parentDocument.body;
return {left: touch.clientX - rect.left - body.scrollLeft, top: touch.clientY - rect.top - body.scrollTop};
},
_handleTouchStart: function (e) {
var touches = e.touches, touch, pt, sel;
this._touchMoved = false;
this._touchStartScroll = undefined;
if (touches.length === 1) {
touch = touches[0];
var pageX = touch.pageX;
var pageY = touch.pageY;
this._touchStartX = pageX;
this._touchStartY = pageY;
this._touchStartTime = e.timeStamp;
this._touchStartScroll = this._getScroll();
sel = this._getSelection();
pt = this._touchConvert(touches[0]);
this._touchGesture = "none";
if (!sel.isEmpty()) {
if (this._hitOffset(sel.end, pt.left, pt.top)) {
this._touchGesture = "extendEnd";
} else if (this._hitOffset(sel.start, pt.left, pt.top)) {
this._touchGesture = "extendStart";
}
}
if (this._touchGesture === "none") {
var textArea = this._textArea;
textArea.value = "";
textArea.style.left = "-1000px";
textArea.style.top = "-1000px";
textArea.style.width = "3000px";
textArea.style.height = "3000px";
var self = this;
var f = function() {
self._touchTimeout = null;
self._clickCount = 1;
self._setSelectionTo(pt.left, pt.top, false);
};
this._touchTimeout = setTimeout(f, 200);
}
} else if (touches.length === 2) {
this._touchGesture = "select";
if (this._touchTimeout) {
clearTimeout(this._touchTimeout);
this._touchTimeout = null;
}
pt = this._touchConvert(touches[0]);
var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
pt = this._touchConvert(touches[1]);
var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
sel = this._getSelection();
sel.setCaret(offset1);
sel.extend(offset2);
this._setSelection(sel, true, true);
}
//Cannot prevent to show maginifier
// e.preventDefault();
},
_handleTouchMove: function (e) {
this._touchMoved = true;
var touches = e.touches, pt, sel;
if (touches.length === 1) {
var touch = touches[0];
var pageX = touch.pageX;
var pageY = touch.pageY;
var deltaX = this._touchStartX - pageX;
var deltaY = this._touchStartY - pageY;
pt = this._touchConvert(touch);
sel = this._getSelection();
if (this._touchTimeout) {
clearTimeout(this._touchTimeout);
this._touchTimeout = null;
}
if (this._touchGesture === "none") {
if ((e.timeStamp - this._touchStartTime) < 200 && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
this._touchGesture = "scroll";
} else {
this._touchGesture = "caret";
}
}
if (this._touchGesture === "select") {
if (this._hitOffset(sel.end, pt.left, pt.top)) {
this._touchGesture = "extendEnd";
} else if (this._hitOffset(sel.start, pt.left, pt.top)) {
this._touchGesture = "extendStart";
} else {
this._touchGesture = "caret";
}
}
switch (this._touchGesture) {
case "scroll":
this._touchStartX = pageX;
this._touchStartY = pageY;
this._scrollView(deltaX, deltaY);
break;
case "extendStart":
case "extendEnd":
this._clickCount = 1;
var lineIndex = this._getYToLine(pt.top);
var offset = this._getXToOffset(lineIndex, pt.left);
sel.setCaret(this._touchGesture === "extendStart" ? sel.end : sel.start);
sel.extend(offset);
if (offset >= sel.end && this._touchGesture === "extendStart") {
this._touchGesture = "extendEnd";
}
if (offset <= sel.start && this._touchGesture === "extendEnd") {
this._touchGesture = "extendStart";
}
this._setSelection(sel, true, true);
break;
case "caret":
this._setSelectionTo(pt.left, pt.top, false);
break;
}
} else if (touches.length === 2) {
pt = this._touchConvert(touches[0]);
var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
pt = this._touchConvert(touches[1]);
var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
sel = this._getSelection();
sel.setCaret(offset1);
sel.extend(offset2);
this._setSelection(sel, true, true);
}
e.preventDefault();
},
_handleTouchEnd: function (e) {
if (!this._touchMoved) {
if (e.touches.length === 0 && e.changedTouches.length === 1 && this._touchTimeout) {
clearTimeout(this._touchTimeout);
this._touchTimeout = null;
var touch = e.changedTouches[0];
this._clickCount = 1;
var pt = this._touchConvert(touch);
this._setSelectionTo(pt.left, pt.top, false);
}
}
if (e.touches.length === 0) {
var self = this;
setTimeout(function() {
var selection = self._getSelection();
var text = self._model.getText(selection.start, selection.end);
var textArea = self._textArea;
textArea.value = text;
textArea.selectionStart = 0;
textArea.selectionEnd = text.length;
if (!selection.isEmpty()) {
var touchRect = self._touchDiv.getBoundingClientRect();
var bounds = self._getOffsetBounds(selection.start);
textArea.style.left = (touchRect.width / 2) + "px";
textArea.style.top = ((bounds.top > 40 ? bounds.top - 30 : bounds.top + 30)) + "px";
}
}, 0);
}
e.preventDefault();
},
/************************************ Actions ******************************************/
_doAction: function (e) {
var keyBindings = this._keyBindings;
for (var i = 0; i < keyBindings.length; i++) {
var kb = keyBindings[i];
if (kb.keyBinding.match(e)) {
if (kb.name) {
var actions = this._actions;
for (var j = 0; j < actions.length; j++) {
var a = actions[j];
if (a.name === kb.name) {
if (a.userHandler) {
if (!a.userHandler()) {
if (a.defaultHandler) {
a.defaultHandler();
} else {
return false;
}
}
} else if (a.defaultHandler) {
a.defaultHandler();
}
break;
}
}
}
return true;
}
}
return false;
},
_doBackspace: function (args) {
var selection = this._getSelection();
if (selection.isEmpty()) {
var model = this._model;
var caret = selection.getCaret();
var lineIndex = model.getLineAtOffset(caret);
if (caret === model.getLineStart(lineIndex)) {
if (lineIndex > 0) {
selection.extend(model.getLineEnd(lineIndex - 1));
}
} else {
selection.extend(this._getOffset(caret, args.unit, -1));
}
}
this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
return true;
},
_doContent: function (text) {
var selection = this._getSelection();
this._modifyContent({text: text, start: selection.start, end: selection.end, _ignoreDOMSelection: true}, true);
},
_doCopy: function (e) {
var selection = this._getSelection();
if (!selection.isEmpty()) {
var text = this._model.getText(selection.start, selection.end);
return this._setClipboardText(text, e);
}
return true;
},
_doCursorNext: function (args) {
if (!args.select) {
if (this._clearSelection("next")) { return true; }
}
var model = this._model;
var selection = this._getSelection();
var caret = selection.getCaret();
var lineIndex = model.getLineAtOffset(caret);
if (caret === model.getLineEnd(lineIndex)) {
if (lineIndex + 1 < model.getLineCount()) {
selection.extend(model.getLineStart(lineIndex + 1));
}
} else {
selection.extend(this._getOffset(caret, args.unit, 1));
}
if (!args.select) { selection.collapse(); }
this._setSelection(selection, true);
return true;
},
_doCursorPrevious: function (args) {
if (!args.select) {
if (this._clearSelection("previous")) { return true; }
}
var model = this._model;
var selection = this._getSelection();
var caret = selection.getCaret();
var lineIndex = model.getLineAtOffset(caret);
if (caret === model.getLineStart(lineIndex)) {
if (lineIndex > 0) {
selection.extend(model.getLineEnd(lineIndex - 1));
}
} else {
selection.extend(this._getOffset(caret, args.unit, -1));
}
if (!args.select) { selection.collapse(); }
this._setSelection(selection, true);
return true;
},
_doCut: function (e) {
var selection = this._getSelection();
if (!selection.isEmpty()) {
var text = this._model.getText(selection.start, selection.end);
this._doContent("");
return this._setClipboardText(text, e);
}
return true;
},
_doDelete: function (args) {
var selection = this._getSelection();
if (selection.isEmpty()) {
var model = this._model;
var caret = selection.getCaret();
var lineIndex = model.getLineAtOffset(caret);
if (caret === model.getLineEnd (lineIndex)) {
if (lineIndex + 1 < model.getLineCount()) {
selection.extend(model.getLineStart(lineIndex + 1));
}
} else {
selection.extend(this._getOffset(caret, args.unit, 1));
}
}
this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
return true;
},
_doEnd: function (args) {
var selection = this._getSelection();
var model = this._model;
if (args.ctrl) {
selection.extend(model.getCharCount());
} else {
var lineIndex = model.getLineAtOffset(selection.getCaret());
selection.extend(model.getLineEnd(lineIndex));
}
if (!args.select) { selection.collapse(); }
this._setSelection(selection, true);
return true;
},
_doEnter: function (args) {
var model = this._model;
this._doContent(model.getLineDelimiter());
return true;
},
_doHome: function (args) {
var selection = this._getSelection();
var model = this._model;
if (args.ctrl) {
selection.extend(0);
} else {
var lineIndex = model.getLineAtOffset(selection.getCaret());
selection.extend(model.getLineStart(lineIndex));
}
if (!args.select) { selection.collapse(); }
this._setSelection(selection, true);
return true;
},
_doLineDown: function (args) {
var model = this._model;
var selection = this._getSelection();
var caret = selection.getCaret();
var lineIndex = model.getLineAtOffset(caret);
if (lineIndex + 1 < model.getLineCount()) {
var x = this._columnX;
if (x === -1 || args.select) {
x = this._getOffsetToX(caret);
}
selection.extend(this._getXToOffset(lineIndex + 1, x));
if (!args.select) { selection.collapse(); }
this._setSelection(selection, true, true);
this._columnX = x;//fix x by scrolling
}
return true;
},
_doLineUp: function (args) {
var model = this._model;
var selection = this._getSelection();
var caret = selection.getCaret();
var lineIndex = model.getLineAtOffset(caret);
if (lineIndex > 0) {
var x = this._columnX;
if (x === -1 || args.select) {
x = this._getOffsetToX(caret);
}
selection.extend(this._getXToOffset(lineIndex - 1, x));
if (!args.select) { selection.collapse(); }
this._setSelection(selection, true, true);
this._columnX = x;//fix x by scrolling
}
return true;
},
_doPageDown: function (args) {
var model = this._model;
var selection = this._getSelection();
var caret = selection.getCaret();
var caretLine = model.getLineAtOffset(caret);
var lineCount = model.getLineCount();
if (caretLine < lineCount - 1) {
var clientHeight = this._getClientHeight();
var lineHeight = this._getLineHeight();
var lines = Math.floor(clientHeight / lineHeight);
var scrollLines = Math.min(lineCount - caretLine - 1, lines);
scrollLines = Math.max(1, scrollLines);
var x = this._columnX;
if (x === -1 || args.select) {
x = this._getOffsetToX(caret);
}
selection.extend(this._getXToOffset(caretLine + scrollLines, x));
if (!args.select) { selection.collapse(); }
this._setSelection(selection, false, false);
var verticalMaximum = lineCount * lineHeight;
var verticalScrollOffset = this._getScroll().y;
var scrollOffset = verticalScrollOffset + scrollLines * lineHeight;
if (scrollOffset + clientHeight > verticalMaximum) {
scrollOffset = verticalMaximum - clientHeight;
}
if (scrollOffset > verticalScrollOffset) {
this._scrollView(0, scrollOffset - verticalScrollOffset);
} else {
this._updateDOMSelection();
}
this._columnX = x;//fix x by scrolling
}
return true;
},
_doPageUp: function (args) {
var model = this._model;
var selection = this._getSelection();
var caret = selection.getCaret();
var caretLine = model.getLineAtOffset(caret);
if (caretLine > 0) {
var clientHeight = this._getClientHeight();
var lineHeight = this._getLineHeight();
var lines = Math.floor(clientHeight / lineHeight);
var scrollLines = Math.max(1, Math.min(caretLine, lines));
var x = this._columnX;
if (x === -1 || args.select) {
x = this._getOffsetToX(caret);
}
selection.extend(this._getXToOffset(caretLine - scrollLines, x));
if (!args.select) { selection.collapse(); }
this._setSelection(selection, false, false);
var verticalScrollOffset = this._getScroll().y;
var scrollOffset = Math.max(0, verticalScrollOffset - scrollLines * lineHeight);
if (scrollOffset < verticalScrollOffset) {
this._scrollView(0, scrollOffset - verticalScrollOffset);
} else {
this._updateDOMSelection();
}
this._columnX = x;//fix x by scrolling
}
return true;
},
_doPaste: function(e) {
var text = this._getClipboardText(e);
if (text) {
this._doContent(text);
}
return text !== null;
},
_doScroll: function (scroll) {
var oldX = this._hScroll;
var oldY = this._vScroll;
if (oldX !== scroll.x || oldY !== scroll.y) {
this._hScroll = scroll.x;
this._vScroll = scroll.y;
this._commitIME();
this._updatePage();
var e = {
oldValue: {x: oldX, y: oldY},
newValue: scroll
};
this.onScroll(e);
}
},
_doSelectAll: function (args) {
var model = this._model;
var selection = this._getSelection();
selection.setCaret(0);
selection.extend(model.getCharCount());
this._setSelection(selection, false);
return true;
},
_doTab: function (args) {
this._doContent("\t");
return true;
},
/************************************ Internals ******************************************/
_applyStyle: function(style, node) {
if (!style) {
return;
}
if (style.styleClass) {
node.className = style.styleClass;
}
var properties = style.style;
if (properties) {
for (var s in properties) {
if (properties.hasOwnProperty(s)) {
node.style[s] = properties[s];
}
}
}
},
_autoScroll: function () {
var selection = this._getSelection();
var line;
var x = this._autoScrollX;
if (this._autoScrollDir === "up" || this._autoScrollDir === "down") {
var scroll = this._autoScrollY / this._getLineHeight();
scroll = scroll < 0 ? Math.floor(scroll) : Math.ceil(scroll);
line = this._model.getLineAtOffset(selection.getCaret());
line = Math.max(0, Math.min(this._model.getLineCount() - 1, line + scroll));
} else if (this._autoScrollDir === "left" || this._autoScrollDir === "right") {
line = this._getYToLine(this._autoScrollY);
x += this._getOffsetToX(selection.getCaret());
}
selection.extend(this._getXToOffset(line, x));
this._setSelection(selection, true);
},
_autoScrollTimer: function () {
this._autoScroll();
var self = this;
this._autoScrollTimerID = setTimeout(function () {self._autoScrollTimer();}, this._AUTO_SCROLL_RATE);
},
_calculateLineHeight: function() {
var parent = this._clientDiv;
var document = this._frameDocument;
var c = " ";
var line = document.createElement("DIV");
line.style.position = "fixed";
line.style.left = "-1000px";
var span1 = document.createElement("SPAN");
span1.appendChild(document.createTextNode(c));
line.appendChild(span1);
var span2 = document.createElement("SPAN");
span2.style.fontStyle = "italic";
span2.appendChild(document.createTextNode(c));
line.appendChild(span2);
var span3 = document.createElement("SPAN");
span3.style.fontWeight = "bold";
span3.appendChild(document.createTextNode(c));
line.appendChild(span3);
var span4 = document.createElement("SPAN");
span4.style.fontWeight = "bold";
span4.style.fontStyle = "italic";
span4.appendChild(document.createTextNode(c));
line.appendChild(span4);
parent.appendChild(line);
var spanRect1 = span1.getBoundingClientRect();
var spanRect2 = span2.getBoundingClientRect();
var spanRect3 = span3.getBoundingClientRect();
var spanRect4 = span4.getBoundingClientRect();
var h1 = spanRect1.bottom - spanRect1.top;
var h2 = spanRect2.bottom - spanRect2.top;
var h3 = spanRect3.bottom - spanRect3.top;
var h4 = spanRect4.bottom - spanRect4.top;
var fontStyle = 0;
var lineHeight = h1;
if (h2 > h1) {
lineHeight = h2;
fontStyle = 1;
}
if (h3 > h2) {
lineHeight = h3;
fontStyle = 2;
}
if (h4 > h3) {
lineHeight = h4;
fontStyle = 3;
}
this._largestFontStyle = fontStyle;
parent.removeChild(line);
return lineHeight;
},
_calculatePadding: function() {
var document = this._frameDocument;
var parent = this._clientDiv;
var pad = this._getPadding(this._viewDiv);
var div1 = document.createElement("DIV");
div1.style.position = "fixed";
div1.style.left = "-1000px";
div1.style.paddingLeft = pad.left + "px";
div1.style.paddingTop = pad.top + "px";
div1.style.paddingRight = pad.right + "px";
div1.style.paddingBottom = pad.bottom + "px";
div1.style.width = "100px";
div1.style.height = "100px";
var div2 = document.createElement("DIV");
div2.style.width = "100%";
div2.style.height = "100%";
div1.appendChild(div2);
parent.appendChild(div1);
var rect1 = div1.getBoundingClientRect();
var rect2 = div2.getBoundingClientRect();
parent.removeChild(div1);
pad = {
left: rect2.left - rect1.left,
top: rect2.top - rect1.top,
right: rect1.right - rect2.right,
bottom: rect1.bottom - rect2.bottom
};
return pad;
},
_clearSelection: function (direction) {
var selection = this._getSelection();
if (selection.isEmpty()) { return false; }
if (direction === "next") {
selection.start = selection.end;
} else {
selection.end = selection.start;
}
this._setSelection(selection, true);
return true;
},
_commitIME: function () {
if (this._imeOffset === -1) { return; }
// make the state of the IME match the state the view expects it be in
// when the view commits the text and IME also need to be committed
// this can be accomplished by changing the focus around
this._scrollDiv.focus();
this._clientDiv.focus();
var model = this._model;
var lineIndex = model.getLineAtOffset(this._imeOffset);
var lineStart = model.getLineStart(lineIndex);
var newText = this._getDOMText(lineIndex);
var oldText = model.getLine(lineIndex);
var start = this._imeOffset - lineStart;
var end = start + newText.length - oldText.length;
if (start !== end) {
var insertText = newText.substring(start, end);
this._doContent(insertText);
}
this._imeOffset = -1;
},
_convertDelimiter: function (text, addTextFunc, addDelimiterFunc) {
var cr = 0, lf = 0, index = 0, length = text.length;
while (index < length) {
if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
var start = index, end;
if (lf === -1 && cr === -1) {
addTextFunc(text.substring(index));
break;
}
if (cr !== -1 && lf !== -1) {
if (cr + 1 === lf) {
end = cr;
index = lf + 1;
} else {
end = cr < lf ? cr : lf;
index = (cr < lf ? cr : lf) + 1;
}
} else if (cr !== -1) {
end = cr;
index = cr + 1;
} else {
end = lf;
index = lf + 1;
}
addTextFunc(text.substring(start, end));
addDelimiterFunc();
}
},
_createActions: function () {
var KeyBinding = orion.textview.KeyBinding;
//no duplicate keybindings
var bindings = this._keyBindings = [];
// Cursor Navigation
bindings.push({name: "lineUp", keyBinding: new KeyBinding(38), predefined: true});
bindings.push({name: "lineDown", keyBinding: new KeyBinding(40), predefined: true});
bindings.push({name: "charPrevious", keyBinding: new KeyBinding(37), predefined: true});
bindings.push({name: "charNext", keyBinding: new KeyBinding(39), predefined: true});
bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true});
bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true});
if (isMac) {
bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, true), predefined: true});
bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, true), predefined: true});
bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, null, null, true), predefined: true});
bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, null, null, true), predefined: true});
bindings.push({name: "textStart", keyBinding: new KeyBinding(36), predefined: true});
bindings.push({name: "textEnd", keyBinding: new KeyBinding(35), predefined: true});
bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true});
bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true});
} else {
bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true});
bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true});
bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true});
bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true});
bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true});
bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true});
}
// Select Cursor Navigation
bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true});
bindings.push({name: "selectLineDown", keyBinding: new KeyBinding(40, null, true), predefined: true});
bindings.push({name: "selectCharPrevious", keyBinding: new KeyBinding(37, null, true), predefined: true});
bindings.push({name: "selectCharNext", keyBinding: new KeyBinding(39, null, true), predefined: true});
bindings.push({name: "selectPageUp", keyBinding: new KeyBinding(33, null, true), predefined: true});
bindings.push({name: "selectPageDown", keyBinding: new KeyBinding(34, null, true), predefined: true});
if (isMac) {
bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, true, true), predefined: true});
bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true});
bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true});
bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true});
bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true});
bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true});
} else {
bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true});
bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true});
bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true});
bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true});
}
//Misc
bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8), predefined: true});
bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8, null, true), predefined: true});
bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46), predefined: true});
bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true), predefined: true});
bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true, true), predefined: true});
bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, true), predefined: true});
bindings.push({name: "tab", keyBinding: new KeyBinding(9), predefined: true});
bindings.push({name: "enter", keyBinding: new KeyBinding(13), predefined: true});
bindings.push({name: "enter", keyBinding: new KeyBinding(13, null, true), predefined: true});
bindings.push({name: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true});
if (isMac) {
bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true});
bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true});
bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, null, null, true), predefined: true});
}
/*
* Feature in IE/Chrome: prevent ctrl+'u', ctrl+'i', and ctrl+'b' from applying styles to the text.
*
* Note that Chrome applies the styles on the Mac with Ctrl instead of Cmd.
*/
var isMacChrome = isMac && isChrome;
bindings.push({name: null, keyBinding: new KeyBinding('u', !isMacChrome, false, false, isMacChrome), predefined: true});
bindings.push({name: null, keyBinding: new KeyBinding('i', !isMacChrome, false, false, isMacChrome), predefined: true});
bindings.push({name: null, keyBinding: new KeyBinding('b', !isMacChrome, false, false, isMacChrome), predefined: true});
if (isFirefox) {
bindings.push({name: "copy", keyBinding: new KeyBinding(45, true), predefined: true});
bindings.push({name: "paste", keyBinding: new KeyBinding(45, null, true), predefined: true});
bindings.push({name: "cut", keyBinding: new KeyBinding(46, null, true), predefined: true});
}
//1 to 1, no duplicates
var self = this;
this._actions = [
{name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}},
{name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}},
{name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}},
{name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}},
{name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}},
{name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}},
{name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}},
{name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}},
{name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}},
{name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}},
{name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}},
{name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}},
{name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}},
{name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}},
{name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}},
{name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}},
{name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}},
{name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}},
{name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}},
{name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}},
{name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}},
{name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}},
{name: "selectTextStart", defaultHandler: function() {return self._doHome({select: true, ctrl:true});}},
{name: "selectTextEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:true});}},
{name: "deletePrevious", defaultHandler: function() {return self._doBackspace({unit:"character"});}},
{name: "deleteNext", defaultHandler: function() {return self._doDelete({unit:"character"});}},
{name: "deleteWordPrevious", defaultHandler: function() {return self._doBackspace({unit:"word"});}},
{name: "deleteWordNext", defaultHandler: function() {return self._doDelete({unit:"word"});}},
{name: "tab", defaultHandler: function() {return self._doTab();}},
{name: "enter", defaultHandler: function() {return self._doEnter();}},
{name: "selectAll", defaultHandler: function() {return self._doSelectAll();}},
{name: "copy", defaultHandler: function() {return self._doCopy();}},
{name: "cut", defaultHandler: function() {return self._doCut();}},
{name: "paste", defaultHandler: function() {return self._doPaste();}}
];
},
_createLine: function(parent, sibling, document, lineIndex, model) {
var lineText = model.getLine(lineIndex);
var lineStart = model.getLineStart(lineIndex);
var e = {lineIndex: lineIndex, lineText: lineText, lineStart: lineStart};
this.onLineStyle(e);
var child = document.createElement("DIV");
child.lineIndex = lineIndex;
this._applyStyle(e.style, child);
if (lineText.length !== 0) {
var start = 0;
var tabSize = this._tabSize;
if (tabSize && tabSize !== 8) {
var tabIndex = lineText.indexOf("\t"), ignoreChars = 0;
while (tabIndex !== -1) {
this._createRange(child, document, e.ranges, start, tabIndex, lineText, lineStart);
var spacesCount = tabSize - ((tabIndex + ignoreChars) % tabSize);
var spaces = "\u00A0";
for (var i = 1; i < spacesCount; i++) {
spaces += " ";
}
var tabSpan = document.createElement("SPAN");
tabSpan.appendChild(document.createTextNode(spaces));
tabSpan.ignoreChars = spacesCount - 1;
ignoreChars += tabSpan.ignoreChars;
if (e.ranges) {
for (var j = 0; j < e.ranges.length; j++) {
var range = e.ranges[j];
var styleStart = range.start - lineStart;
var styleEnd = range.end - lineStart;
if (styleStart > tabIndex) { break; }
if (styleStart <= tabIndex && tabIndex < styleEnd) {
this._applyStyle(range.style, tabSpan);
break;
}
}
}
child.appendChild(tabSpan);
start = tabIndex + 1;
tabIndex = lineText.indexOf("\t", start);
}
}
this._createRange(child, document, e.ranges, start, lineText.length, lineText, lineStart);
}
/*
* Firefox, Opera and IE9 do not extend the selection at the end of the line
* when the line is fully selected. The fix is to add an extra space at the end
* of the line.
*
* Note: the height of a div with only an empty span is zero. The fix is
* the add a extra zero-width non-break space to preserve the default
* height in the line div. In Chrome this character shows a glyph, so the
* zero-width non-joiner character is used instead.
*
* Note: in order to support bold and italic fonts with fixed line
* height all lines need to have at least one span with the largest
* font.
*/
var span = document.createElement("SPAN");
span.ignoreChars = 1;
if ((this._largestFontStyle & 1) !== 0) {
span.style.fontStyle = "italic";
}
if ((this._largestFontStyle & 2) !== 0) {
span.style.fontWeight = "bold";
}
var fullSelection = this._fullSelection;
var extendSelection = !fullSelection && (isFirefox || isOpera || isIE >= 9);
var c = extendSelection ? " " : (isWebkit || isFirefox ? "\u200C" : "\uFEFF");
span.appendChild(document.createTextNode(c));
child.appendChild(span);
parent.insertBefore(child, sibling);
return child;
},
_createRange: function(parent, document, ranges, start, end, text, lineStart) {
if (start >= end) { return; }
var span;
if (ranges) {
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (range.end <= lineStart + start) { continue; }
var styleStart = Math.max(lineStart + start, range.start) - lineStart;
if (styleStart >= end) { break; }
var styleEnd = Math.min(lineStart + end, range.end) - lineStart;
if (styleStart < styleEnd) {
styleStart = Math.max(start, styleStart);
styleEnd = Math.min(end, styleEnd);
if (start < styleStart) {
span = document.createElement("SPAN");
span.appendChild(document.createTextNode(text.substring(start, styleStart)));
parent.appendChild(span);
}
span = document.createElement("SPAN");
span.appendChild(document.createTextNode(text.substring(styleStart, styleEnd)));
this._applyStyle(range.style, span);
parent.appendChild(span);
start = styleEnd;
}
}
}
if (start < end) {
span = document.createElement("SPAN");
span.appendChild(document.createTextNode(text.substring(start, end)));
parent.appendChild(span);
}
},
_doAutoScroll: function (direction, x, y) {
this._autoScrollDir = direction;
this._autoScrollX = x;
this._autoScrollY = y;
if (!this._autoScrollTimerID) {
this._autoScrollTimer();
}
},
_endAutoScroll: function () {
if (this._autoScrollTimerID) { clearTimeout(this._autoScrollTimerID); }
this._autoScrollDir = undefined;
this._autoScrollTimerID = undefined;
},
_getBoundsAtOffset: function (offset) {
var model = this._model;
var document = this._frameDocument;
var clientDiv = this._clientDiv;
var lineIndex = model.getLineAtOffset(offset);
var dummy;
var child = this._getLineNode(lineIndex);
if (!child) {
child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
}
var result = null;
if (offset < model.getLineEnd(lineIndex)) {
var lineOffset = model.getLineStart(lineIndex);
var lineChild = child.firstChild;
while (lineChild) {
var textNode = lineChild.firstChild;
var nodeLength = textNode.length;
if (lineChild.ignoreChars) {
nodeLength -= lineChild.ignoreChars;
}
if (lineOffset + nodeLength > offset) {
var index = offset - lineOffset;
var range;
if (isRangeRects) {
range = document.createRange();
range.setStart(textNode, index);
range.setEnd(textNode, index + 1);
result = range.getBoundingClientRect();
} else if (isIE) {
range = document.body.createTextRange();
range.moveToElementText(lineChild);
range.collapse();
range.moveEnd("character", index + 1);
range.moveStart("character", index);
result = range.getBoundingClientRect();
} else {
var text = textNode.data;
lineChild.removeChild(textNode);
lineChild.appendChild(document.createTextNode(text.substring(0, index)));
var span = document.createElement("SPAN");
span.appendChild(document.createTextNode(text.substring(index, index + 1)));
lineChild.appendChild(span);
lineChild.appendChild(document.createTextNode(text.substring(index + 1)));
result = span.getBoundingClientRect();
lineChild.innerHTML = "";
lineChild.appendChild(textNode);
if (!dummy) {
/*
* Removing the element node that holds the selection start or end
* causes the selection to be lost. The fix is to detect this case
* and restore the selection.
*/
var s = this._getSelection();
if ((lineOffset <= s.start && s.start < lineOffset + nodeLength) || (lineOffset <= s.end && s.end < lineOffset + nodeLength)) {
this._updateDOMSelection();
}
}
}
if (isIE) {
var logicalXDPI = window.screen.logicalXDPI;
var deviceXDPI = window.screen.deviceXDPI;
result.left = result.left * logicalXDPI / deviceXDPI;
result.right = result.right * logicalXDPI / deviceXDPI;
}
break;
}
lineOffset += nodeLength;
lineChild = lineChild.nextSibling;
}
}
if (!result) {
var rect = this._getLineBoundingClientRect(child);
result = {left: rect.right, right: rect.right};
}
if (dummy) { clientDiv.removeChild(dummy); }
return result;
},
_getBottomIndex: function (fullyVisible) {
var child = this._bottomChild;
if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
var rect = child.getBoundingClientRect();
var clientRect = this._clientDiv.getBoundingClientRect();
if (rect.bottom > clientRect.bottom) {
child = this._getLinePrevious(child) || child;
}
}
return child.lineIndex;
},
_getFrameHeight: function() {
return this._frameDocument.documentElement.clientHeight;
},
_getFrameWidth: function() {
return this._frameDocument.documentElement.clientWidth;
},
_getClientHeight: function() {
var viewPad = this._getViewPadding();
return Math.max(0, this._viewDiv.clientHeight - viewPad.top - viewPad.bottom);
},
_getClientWidth: function() {
var viewPad = this._getViewPadding();
return Math.max(0, this._viewDiv.clientWidth - viewPad.left - viewPad.right);
},
_getClipboardText: function (event) {
var delimiter = this._model.getLineDelimiter();
var clipboadText, text;
if (this._frameWindow.clipboardData) {
//IE
clipboadText = [];
text = this._frameWindow.clipboardData.getData("Text");
this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
return clipboadText.join("");
}
if (isFirefox) {
var window = this._frameWindow;
var document = this._frameDocument;
var child = document.createElement("PRE");
child.style.position = "fixed";
child.style.left = "-1000px";
child.appendChild(document.createTextNode(" "));
this._clientDiv.appendChild(child);
var range = document.createRange();
range.selectNodeContents(child);
var sel = window.getSelection();
if (sel.rangeCount > 0) { sel.removeAllRanges(); }
sel.addRange(range);
var self = this;
var cleanup = function() {
self._updateDOMSelection();
self._clientDiv.removeChild(child);
};
var _getText = function() {
/*
* Use the selection anchor to determine the end of the pasted text as it is possible that
* some browsers (like Firefox) add extra elements (<BR>) after the pasted text.
*/
var endNode = null;
if (sel.anchorNode.nodeType !== child.TEXT_NODE) {
endNode = sel.anchorNode.childNodes[sel.anchorOffset];
}
var text = [];
var getNodeText = function(node) {
var nodeChild = node.firstChild;
while (nodeChild && nodeChild !== endNode) {
if (nodeChild.nodeType === child.TEXT_NODE) {
text.push(nodeChild !== sel.anchorNode ? nodeChild.data : nodeChild.data.substring(0, sel.anchorOffset));
} else if (nodeChild.tagName === "BR") {
text.push(delimiter);
} else {
getNodeText(nodeChild);
}
nodeChild = nodeChild.nextSibling;
}
};
getNodeText(child);
cleanup();
return text.join("");
};
/* Try execCommand first. Works on firefox with clipboard permission. */
var result = false;
this._ignorePaste = true;
try {
result = document.execCommand("paste", false, null);
} catch (ex) {}
this._ignorePaste = false;
if (!result) {
/*
* Try native paste in DOM, works for firefox during the paste event.
*/
if (event) {
setTimeout(function() {
var text = _getText();
if (text) { self._doContent(text); }
}, 0);
return null;
} else {
/* no event and no clipboard permission, paste can't be performed */
cleanup();
return "";
}
}
return _getText();
}
//webkit
if (event && event.clipboardData) {
/*
* Webkit (Chrome/Safari) allows getData during the paste event
* Note: setData is not allowed, not even during copy/cut event
*/
clipboadText = [];
text = event.clipboardData.getData("text/plain");
this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
return clipboadText.join("");
} else {
//TODO try paste using extension (Chrome only)
}
return "";
},
_getDOMText: function(lineIndex) {
var child = this._getLineNode(lineIndex);
var lineChild = child.firstChild;
var text = "";
while (lineChild) {
var textNode = lineChild.firstChild;
while (textNode) {
if (lineChild.ignoreChars) {
for (var i = 0; i < textNode.length; i++) {
var ch = textNode.data.substring(i, i + 1);
if (ch !== " ") {
text += ch;
}
}
} else {
text += textNode.data;
}
textNode = textNode.nextSibling;
}
lineChild = lineChild.nextSibling;
}
return text;
},
_getViewPadding: function() {
return this._viewPadding;
},
_getLineBoundingClientRect: function (child) {
var rect = child.getBoundingClientRect();
var lastChild = child.lastChild;
//Remove any artificial trailing whitespace in the line
while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) {
lastChild = lastChild.previousSibling;
}
if (!lastChild) {
return {left: rect.left, top: rect.top, right: rect.left, bottom: rect.bottom};
}
var lastRect = lastChild.getBoundingClientRect();
return {left: rect.left, top: rect.top, right: lastRect.right, bottom: rect.bottom};
},
_getLineHeight: function() {
return this._lineHeight;
},
_getLineNode: function (lineIndex) {
var clientDiv = this._clientDiv;
var child = clientDiv.firstChild;
while (child) {
if (lineIndex === child.lineIndex) {
return child;
}
child = child.nextSibling;
}
return undefined;
},
_getLineNext: function (lineNode) {
var node = lineNode ? lineNode.nextSibling : this._clientDiv.firstChild;
while (node && node.lineIndex === -1) {
node = node.nextSibling;
}
return node;
},
_getLinePrevious: function (lineNode) {
var node = lineNode ? lineNode.previousSibling : this._clientDiv.lastChild;
while (node && node.lineIndex === -1) {
node = node.previousSibling;
}
return node;
},
_getOffset: function (offset, unit, direction) {
if (unit === "wordend") {
return this._getOffset_W3C(offset, unit, direction);
}
return isIE ? this._getOffset_IE(offset, unit, direction) : this._getOffset_W3C(offset, unit, direction);
},
_getOffset_W3C: function (offset, unit, direction) {
function _isPunctuation(c) {
return (33 <= c && c <= 47) || (58 <= c && c <= 64) || (91 <= c && c <= 94) || c === 96 || (123 <= c && c <= 126);
}
function _isWhitespace(c) {
return c === 32 || c === 9;
}
if (unit === "word" || unit === "wordend") {
var model = this._model;
var lineIndex = model.getLineAtOffset(offset);
var lineText = model.getLine(lineIndex);
var lineStart = model.getLineStart(lineIndex);
var lineEnd = model.getLineEnd(lineIndex);
var lineLength = lineText.length;
var offsetInLine = offset - lineStart;
var c, previousPunctuation, previousLetterOrDigit, punctuation, letterOrDigit;
if (direction > 0) {
if (offsetInLine === lineLength) { return lineEnd; }
c = lineText.charCodeAt(offsetInLine);
previousPunctuation = _isPunctuation(c);
previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
offsetInLine++;
while (offsetInLine < lineLength) {
c = lineText.charCodeAt(offsetInLine);
punctuation = _isPunctuation(c);
if (unit === "wordend") {
if (!punctuation && previousPunctuation) { break; }
} else {
if (punctuation && !previousPunctuation) { break; }
}
letterOrDigit = !punctuation && !_isWhitespace(c);
if (unit === "wordend") {
if (!letterOrDigit && previousLetterOrDigit) { break; }
} else {
if (letterOrDigit && !previousLetterOrDigit) { break; }
}
previousLetterOrDigit = letterOrDigit;
previousPunctuation = punctuation;
offsetInLine++;
}
} else {
if (offsetInLine === 0) { return lineStart; }
offsetInLine--;
c = lineText.charCodeAt(offsetInLine);
previousPunctuation = _isPunctuation(c);
previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
while (0 < offsetInLine) {
c = lineText.charCodeAt(offsetInLine - 1);
punctuation = _isPunctuation(c);
if (unit === "wordend") {
if (punctuation && !previousPunctuation) { break; }
} else {
if (!punctuation && previousPunctuation) { break; }
}
letterOrDigit = !punctuation && !_isWhitespace(c);
if (unit === "wordend") {
if (letterOrDigit && !previousLetterOrDigit) { break; }
} else {
if (!letterOrDigit && previousLetterOrDigit) { break; }
}
previousLetterOrDigit = letterOrDigit;
previousPunctuation = punctuation;
offsetInLine--;
}
}
return lineStart + offsetInLine;
}
return offset + direction;
},
_getOffset_IE: function (offset, unit, direction) {
var document = this._frameDocument;
var model = this._model;
var lineIndex = model.getLineAtOffset(offset);
var clientDiv = this._clientDiv;
var dummy;
var child = this._getLineNode(lineIndex);
if (!child) {
child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
}
var result = 0, range, length;
var lineOffset = model.getLineStart(lineIndex);
if (offset === model.getLineEnd(lineIndex)) {
range = document.body.createTextRange();
range.moveToElementText(child.lastChild);
length = range.text.length;
range.moveEnd(unit, direction);
result = offset + range.text.length - length;
} else if (offset === lineOffset && direction < 0) {
result = lineOffset;
} else {
var lineChild = child.firstChild;
while (lineChild) {
var textNode = lineChild.firstChild;
var nodeLength = textNode.length;
if (lineChild.ignoreChars) {
nodeLength -= lineChild.ignoreChars;
}
if (lineOffset + nodeLength > offset) {
range = document.body.createTextRange();
if (offset === lineOffset && direction < 0) {
range.moveToElementText(lineChild.previousSibling);
} else {
range.moveToElementText(lineChild);
range.collapse();
range.moveEnd("character", offset - lineOffset);
}
length = range.text.length;
range.moveEnd(unit, direction);
result = offset + range.text.length - length;
break;
}
lineOffset = nodeLength + lineOffset;
lineChild = lineChild.nextSibling;
}
}
if (dummy) { clientDiv.removeChild(dummy); }
return result;
},
_getOffsetToX: function (offset) {
return this._getBoundsAtOffset(offset).left;
},
_getPadding: function (node) {
var left,top,right,bottom;
if (node.currentStyle) {
left = node.currentStyle.paddingLeft;
top = node.currentStyle.paddingTop;
right = node.currentStyle.paddingRight;
bottom = node.currentStyle.paddingBottom;
} else if (this._frameWindow.getComputedStyle) {
var style = this._frameWindow.getComputedStyle(node, null);
left = style.getPropertyValue("padding-left");
top = style.getPropertyValue("padding-top");
right = style.getPropertyValue("padding-right");
bottom = style.getPropertyValue("padding-bottom");
}
return {
left: parseInt(left, 10),
top: parseInt(top, 10),
right: parseInt(right, 10),
bottom: parseInt(bottom, 10)
};
},
_getScroll: function() {
var viewDiv = this._viewDiv;
return {x: viewDiv.scrollLeft, y: viewDiv.scrollTop};
},
_getSelection: function () {
return this._selection.clone();
},
_getTopIndex: function (fullyVisible) {
var child = this._topChild;
if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
var rect = child.getBoundingClientRect();
var viewPad = this._getViewPadding();
var viewRect = this._viewDiv.getBoundingClientRect();
if (rect.top < viewRect.top + viewPad.top) {
child = this._getLineNext(child) || child;
}
}
return child.lineIndex;
},
_getXToOffset: function (lineIndex, x) {
var model = this._model;
var lineStart = model.getLineStart(lineIndex);
var lineEnd = model.getLineEnd(lineIndex);
if (lineStart === lineEnd) {
return lineStart;
}
var document = this._frameDocument;
var clientDiv = this._clientDiv;
var dummy;
var child = this._getLineNode(lineIndex);
if (!child) {
child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
}
var lineRect = this._getLineBoundingClientRect(child);
if (x < lineRect.left) { x = lineRect.left; }
if (x > lineRect.right) { x = lineRect.right; }
/*
* Bug in IE 8 and earlier. The coordinates of getClientRects() are relative to
* the browser window. The fix is to convert to the frame window before using it.
*/
var deltaX = 0, rects;
if (isIE < 9) {
rects = child.getClientRects();
var minLeft = rects[0].left;
for (var i=1; i<rects.length; i++) {
minLeft = Math.min(rects[i].left, minLeft);
}
deltaX = minLeft - lineRect.left;
}
var scrollX = this._getScroll().x;
function _getClientRects(element) {
var rects, newRects, i, r;
if (!element._rectsCache) {
rects = element.getClientRects();
newRects = [rects.length];
for (i = 0; i<rects.length; i++) {
r = rects[i];
newRects[i] = {left: r.left - deltaX + scrollX, top: r.top, right: r.right - deltaX + scrollX, bottom: r.bottom};
}
element._rectsCache = newRects;
}
rects = element._rectsCache;
newRects = [rects.length];
for (i = 0; i<rects.length; i++) {
r = rects[i];
newRects[i] = {left: r.left - scrollX, top: r.top, right: r.right - scrollX, bottom: r.bottom};
}
return newRects;
}
var logicalXDPI = isIE ? window.screen.logicalXDPI : 1;
var deviceXDPI = isIE ? window.screen.deviceXDPI : 1;
var offset = lineStart;
var lineChild = child.firstChild;
done:
while (lineChild) {
var textNode = lineChild.firstChild;
var nodeLength = textNode.length;
if (lineChild.ignoreChars) {
nodeLength -= lineChild.ignoreChars;
}
rects = _getClientRects(lineChild);
for (var j = 0; j < rects.length; j++) {
var rect = rects[j];
if (rect.left <= x && x < rect.right) {
var range, start, end;
if (isIE || isRangeRects) {
range = isRangeRects ? document.createRange() : document.body.createTextRange();
var high = nodeLength;
var low = -1;
while ((high - low) > 1) {
var mid = Math.floor((high + low) / 2);
start = low + 1;
end = mid === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : mid + 1;
if (isRangeRects) {
range.setStart(textNode, start);
range.setEnd(textNode, end);
} else {
range.moveToElementText(lineChild);
range.move("character", start);
range.moveEnd("character", end - start);
}
rects = range.getClientRects();
var found = false;
for (var k = 0; k < rects.length; k++) {
rect = rects[k];
var rangeLeft = rect.left * logicalXDPI / deviceXDPI - deltaX;
var rangeRight = rect.right * logicalXDPI / deviceXDPI - deltaX;
if (rangeLeft <= x && x < rangeRight) {
found = true;
break;
}
}
if (found) {
high = mid;
} else {
low = mid;
}
}
offset += high;
start = high;
end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : high + 1;
if (isRangeRects) {
range.setStart(textNode, start);
range.setEnd(textNode, end);
} else {
range.moveToElementText(lineChild);
range.move("character", start);
range.moveEnd("character", end - start);
}
rect = range.getClientRects()[0];
//TODO test for character trailing (wrong for bidi)
if (x > ((rect.left * logicalXDPI / deviceXDPI - deltaX) + ((rect.right - rect.left) * logicalXDPI / deviceXDPI / 2))) {
offset++;
}
} else {
var newText = [];
for (var q = 0; q < nodeLength; q++) {
newText.push("<span>");
if (q === nodeLength - 1) {
newText.push(textNode.data.substring(q));
} else {
newText.push(textNode.data.substring(q, q + 1));
}
newText.push("</span>");
}
lineChild.innerHTML = newText.join("");
var rangeChild = lineChild.firstChild;
while (rangeChild) {
rect = rangeChild.getBoundingClientRect();
if (rect.left <= x && x < rect.right) {
//TODO test for character trailing (wrong for bidi)
if (x > rect.left + (rect.right - rect.left) / 2) {
offset++;
}
break;
}
offset++;
rangeChild = rangeChild.nextSibling;
}
if (!dummy) {
lineChild.innerHTML = "";
lineChild.appendChild(textNode);
/*
* Removing the element node that holds the selection start or end
* causes the selection to be lost. The fix is to detect this case
* and restore the selection.
*/
var s = this._getSelection();
if ((offset <= s.start && s.start < offset + nodeLength) || (offset <= s.end && s.end < offset + nodeLength)) {
this._updateDOMSelection();
}
}
}
break done;
}
}
offset += nodeLength;
lineChild = lineChild.nextSibling;
}
if (dummy) { clientDiv.removeChild(dummy); }
return Math.min(lineEnd, Math.max(lineStart, offset));
},
_getYToLine: function (y) {
var viewPad = this._getViewPadding();
var viewRect = this._viewDiv.getBoundingClientRect();
y -= viewRect.top + viewPad.top;
var lineHeight = this._getLineHeight();
var lineIndex = Math.floor((y + this._getScroll().y) / lineHeight);
var lineCount = this._model.getLineCount();
return Math.max(0, Math.min(lineCount - 1, lineIndex));
},
_getOffsetBounds: function(offset) {
var model = this._model;
var lineIndex = model.getLineAtOffset(offset);
var lineHeight = this._getLineHeight();
var scroll = this._getScroll();
var viewPad = this._getViewPadding();
var viewRect = this._viewDiv.getBoundingClientRect();
var bounds = this._getBoundsAtOffset(offset);
var left = bounds.left;
var right = bounds.right;
var top = (lineIndex * lineHeight) - scroll.y + viewRect.top + viewPad.top;
var bottom = top + lineHeight;
return {left: left, top: top, right: right, bottom: bottom};
},
_hitOffset: function (offset, x, y) {
var bounds = this._getOffsetBounds(offset);
var left = bounds.left;
var right = bounds.right;
var top = bounds.top;
var bottom = bounds.bottom;
var area = 20;
left -= area;
top -= area;
right += area;
bottom += area;
return (left <= x && x <= right && top <= y && y <= bottom);
},
_hookEvents: function() {
var self = this;
this._modelListener = {
/** @private */
onChanging: function(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
self._onModelChanging(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount);
},
/** @private */
onChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
self._onModelChanged(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount);
}
};
this._model.addListener(this._modelListener);
this._mouseMoveClosure = function(e) { return self._handleMouseMove(e);};
this._mouseUpClosure = function(e) { return self._handleMouseUp(e);};
var clientDiv = this._clientDiv;
var viewDiv = this._viewDiv;
var body = this._frameDocument.body;
var handlers = this._handlers = [];
var resizeNode = isIE < 9 ? this._frame : this._frameWindow;
var focusNode = isPad ? this._textArea : (isIE || isFirefox ? this._clientDiv: this._frameWindow);
handlers.push({target: resizeNode, type: "resize", handler: function(e) { return self._handleResize(e);}});
handlers.push({target: focusNode, type: "blur", handler: function(e) { return self._handleBlur(e);}});
handlers.push({target: focusNode, type: "focus", handler: function(e) { return self._handleFocus(e);}});
handlers.push({target: viewDiv, type: "scroll", handler: function(e) { return self._handleScroll(e);}});
if (isPad) {
var touchDiv = this._touchDiv;
var textArea = this._textArea;
handlers.push({target: textArea, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }});
handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }});
handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }});
handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }});
handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }});
} else {
var topNode = this._overlayDiv || this._clientDiv;
handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}});
handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}});
handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}});
handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}});
handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}});
handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}});
handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}});
handlers.push({target: topNode, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}});
handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}});
handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}});
handlers.push({target: topNode, type: "drop", handler: function(e) { return self._handleDrop(e);}});
if (isIE) {
handlers.push({target: this._frameDocument, type: "activate", handler: function(e) { return self._handleDocFocus(e); }});
}
if (isFirefox) {
handlers.push({target: this._frameDocument, type: "focus", handler: function(e) { return self._handleDocFocus(e); }});
}
if (!isIE && !isOpera) {
var wheelEvent = isFirefox ? "DOMMouseScroll" : "mousewheel";
handlers.push({target: this._viewDiv, type: wheelEvent, handler: function(e) { return self._handleMouseWheel(e); }});
}
if (isFirefox && !isWindows) {
handlers.push({target: this._clientDiv, type: "DOMCharacterDataModified", handler: function (e) { return self._handleDataModified(e); }});
}
if (this._overlayDiv) {
handlers.push({target: this._overlayDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e); }});
}
if (!isW3CEvents) {
handlers.push({target: this._clientDiv, type: "dblclick", handler: function(e) { return self._handleDblclick(e); }});
}
}
for (var i=0; i<handlers.length; i++) {
var h = handlers[i];
addHandler(h.target, h.type, h.handler, h.capture);
}
},
_init: function(options) {
var parent = options.parent;
if (typeof(parent) === "string") {
parent = window.document.getElementById(parent);
}
if (!parent) { throw "no parent"; }
this._parent = parent;
this._model = options.model ? options.model : new orion.textview.TextModel();
this.readonly = options.readonly === true;
this._selection = new Selection (0, 0, false);
this._eventTable = new EventTable();
this._maxLineWidth = 0;
this._maxLineIndex = -1;
this._ignoreSelect = true;
this._columnX = -1;
/* Auto scroll */
this._autoScrollX = null;
this._autoScrollY = null;
this._autoScrollTimerID = null;
this._AUTO_SCROLL_RATE = 50;
this._grabControl = null;
this._moseMoveClosure = null;
this._mouseUpClosure = null;
/* Double click */
this._lastMouseX = 0;
this._lastMouseY = 0;
this._lastMouseTime = 0;
this._clickCount = 0;
this._clickTime = 250;
this._clickDist = 5;
this._isMouseDown = false;
this._doubleClickSelection = null;
/* Scroll */
this._hScroll = 0;
this._vScroll = 0;
/* IME */
this._imeOffset = -1;
/* Create elements */
while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
var parentDocument = parent.document || parent.ownerDocument;
this._parentDocument = parentDocument;
var frame = parentDocument.createElement("IFRAME");
this._frame = frame;
frame.frameBorder = "0px";//for IE, needs to be set before the frame is added to the parent
frame.style.width = "100%";
frame.style.height = "100%";
frame.scrolling = "no";
frame.style.border = "0px";
parent.appendChild(frame);
var html = [];
html.push("<!DOCTYPE html>");
html.push("<html>");
html.push("<head>");
if (isIE < 9) {
html.push("<meta http-equiv='X-UA-Compatible' content='IE=EmulateIE7'/>");
}
html.push("<style>");
html.push(".viewContainer {font-family: monospace; font-size: 10pt;}");
html.push(".view {padding: 1px 2px;}");
html.push(".viewContent {}");
html.push("</style>");
if (options.stylesheet) {
var stylesheet = typeof(options.stylesheet) === "string" ? [options.stylesheet] : options.stylesheet;
for (var i = 0; i < stylesheet.length; i++) {
try {
//Force CSS to be loaded synchronously so lineHeight can be calculated
var objXml = new XMLHttpRequest();
if (objXml.overrideMimeType) {
objXml.overrideMimeType("text/css");
}
objXml.open("GET", stylesheet[i], false);
objXml.send(null);
html.push("<style>");
html.push(objXml.responseText);
html.push("</style>");
} catch (e) {
html.push("<link rel='stylesheet' type='text/css' href='");
html.push(stylesheet[i]);
html.push("'></link>");
}
}
}
html.push("</head>");
html.push("<body spellcheck='false'></body>");
html.push("</html>");
var frameWindow = frame.contentWindow;
this._frameWindow = frameWindow;
var document = frameWindow.document;
this._frameDocument = document;
document.open();
document.write(html.join(""));
document.close();
var body = document.body;
body.className = "viewContainer";
body.style.margin = "0px";
body.style.borderWidth = "0px";
body.style.padding = "0px";
if (isPad) {
var touchDiv = parentDocument.createElement("DIV");
this._touchDiv = touchDiv;
touchDiv.style.position = "absolute";
touchDiv.style.border = "0px";
touchDiv.style.padding = "0px";
touchDiv.style.margin = "0px";
touchDiv.style.zIndex = "2";
touchDiv.style.overflow = "hidden";
touchDiv.style.background="transparent";
touchDiv.style.WebkitUserSelect = "none";
parent.appendChild(touchDiv);
var textArea = parentDocument.createElement("TEXTAREA");
this._textArea = textArea;
textArea.style.position = "absolute";
textArea.style.whiteSpace = "pre";
textArea.style.left = "-1000px";
textArea.tabIndex = 1;
textArea.autocapitalize = false;
textArea.autocorrect = false;
textArea.className = "viewContainer";
textArea.style.background = "transparent";
textArea.style.color = "transparent";
textArea.style.border = "0px";
textArea.style.padding = "0px";
textArea.style.margin = "0px";
textArea.style.borderRadius = "0px";
textArea.style.WebkitAppearance = "none";
textArea.style.WebkitTapHighlightColor = "transparent";
touchDiv.appendChild(textArea);
}
var viewDiv = document.createElement("DIV");
viewDiv.className = "view";
this._viewDiv = viewDiv;
viewDiv.id = "viewDiv";
viewDiv.tabIndex = -1;
viewDiv.style.overflow = "auto";
viewDiv.style.position = "absolute";
viewDiv.style.top = "0px";
viewDiv.style.borderWidth = "0px";
viewDiv.style.margin = "0px";
viewDiv.style.MozOutline = "none";
viewDiv.style.outline = "none";
body.appendChild(viewDiv);
var scrollDiv = document.createElement("DIV");
this._scrollDiv = scrollDiv;
scrollDiv.id = "scrollDiv";
scrollDiv.style.margin = "0px";
scrollDiv.style.borderWidth = "0px";
scrollDiv.style.padding = "0px";
viewDiv.appendChild(scrollDiv);
this._fullSelection = options.fullSelection === undefined || options.fullSelection;
/*
* Bug in IE 8. For some reason, during scrolling IE does not reflow the elements
* that are used to compute the location for the selection divs. This causes the
* divs to be placed at the wrong location. The fix is to disabled full selection for IE8.
*/
if (isIE < 9) {
this._fullSelection = false;
}
if (isPad || (this._fullSelection && !isWebkit)) {
this._hightlightRGB = "Highlight";
var selDiv1 = document.createElement("DIV");
this._selDiv1 = selDiv1;
selDiv1.id = "selDiv1";
selDiv1.style.position = "fixed";
selDiv1.style.borderWidth = "0px";
selDiv1.style.margin = "0px";
selDiv1.style.padding = "0px";
selDiv1.style.MozOutline = "none";
selDiv1.style.outline = "none";
selDiv1.style.background = this._hightlightRGB;
selDiv1.style.width="0px";
selDiv1.style.height="0px";
scrollDiv.appendChild(selDiv1);
var selDiv2 = document.createElement("DIV");
this._selDiv2 = selDiv2;
selDiv2.id = "selDiv2";
selDiv2.style.position = "fixed";
selDiv2.style.borderWidth = "0px";
selDiv2.style.margin = "0px";
selDiv2.style.padding = "0px";
selDiv2.style.MozOutline = "none";
selDiv2.style.outline = "none";
selDiv2.style.background = this._hightlightRGB;
selDiv2.style.width="0px";
selDiv2.style.height="0px";
scrollDiv.appendChild(selDiv2);
var selDiv3 = document.createElement("DIV");
this._selDiv3 = selDiv3;
selDiv3.id = "selDiv3";
selDiv3.style.position = "fixed";
selDiv3.style.borderWidth = "0px";
selDiv3.style.margin = "0px";
selDiv3.style.padding = "0px";
selDiv3.style.MozOutline = "none";
selDiv3.style.outline = "none";
selDiv3.style.background = this._hightlightRGB;
selDiv3.style.width="0px";
selDiv3.style.height="0px";
scrollDiv.appendChild(selDiv3);
/*
* Bug in Firefox. The Highlight color is mapped to list selection
* background instead of the text selection background. The fix
* is to map known colors using a table or fallback to light blue.
*/
if (isFirefox && isMac) {
var style = frameWindow.getComputedStyle(selDiv3, null);
var rgb = style.getPropertyValue("background-color");
switch (rgb) {
case "rgb(119, 141, 168)": rgb = "rgb(199, 208, 218)"; break;
case "rgb(127, 127, 127)": rgb = "rgb(198, 198, 198)"; break;
case "rgb(255, 193, 31)": rgb = "rgb(250, 236, 115)"; break;
case "rgb(243, 70, 72)": rgb = "rgb(255, 176, 139)"; break;
case "rgb(255, 138, 34)": rgb = "rgb(255, 209, 129)"; break;
case "rgb(102, 197, 71)": rgb = "rgb(194, 249, 144)"; break;
case "rgb(140, 78, 184)": rgb = "rgb(232, 184, 255)"; break;
default: rgb = "rgb(180, 213, 255)"; break;
}
this._hightlightRGB = rgb;
selDiv1.style.background = rgb;
selDiv2.style.background = rgb;
selDiv3.style.background = rgb;
var styleSheet = document.styleSheets[0];
styleSheet.insertRule("::-moz-selection {background: " + rgb + "; }", 0);
}
}
var clientDiv = document.createElement("DIV");
clientDiv.className = "viewContent";
this._clientDiv = clientDiv;
clientDiv.id = "clientDiv";
clientDiv.style.whiteSpace = "pre";
clientDiv.style.position = "fixed";
clientDiv.style.borderWidth = "0px";
clientDiv.style.margin = "0px";
clientDiv.style.padding = "0px";
clientDiv.style.MozOutline = "none";
clientDiv.style.outline = "none";
if (isPad) {
clientDiv.style.WebkitTapHighlightColor = "transparent";
}
scrollDiv.appendChild(clientDiv);
if (isFirefox) {
var overlayDiv = document.createElement("DIV");
this._overlayDiv = overlayDiv;
overlayDiv.id = "overlayDiv";
overlayDiv.style.position = clientDiv.style.position;
overlayDiv.style.borderWidth = clientDiv.style.borderWidth;
overlayDiv.style.margin = clientDiv.style.margin;
overlayDiv.style.padding = clientDiv.style.padding;
overlayDiv.style.cursor = "text";
overlayDiv.style.zIndex = "1";
scrollDiv.appendChild(overlayDiv);
}
if (!isPad) {
clientDiv.contentEditable = "true";
}
this._lineHeight = this._calculateLineHeight();
this._viewPadding = this._calculatePadding();
if (isIE) {
body.style.lineHeight = this._lineHeight + "px";
}
if (options.tabSize) {
if (isOpera) {
clientDiv.style.OTabSize = options.tabSize+"";
} else if (isFirefox >= 4) {
clientDiv.style.MozTabSize = options.tabSize+"";
} else if (options.tabSize !== 8) {
this._tabSize = options.tabSize;
}
}
this._createActions();
this._hookEvents();
},
_modifyContent: function(e, updateCaret) {
if (this.readonly && !e._code) {
return;
}
this.onVerify(e);
if (e.text === null || e.text === undefined) { return; }
var model = this._model;
if (e._ignoreDOMSelection) { this._ignoreDOMSelection = true; }
model.setText (e.text, e.start, e.end);
if (e._ignoreDOMSelection) { this._ignoreDOMSelection = false; }
if (updateCaret) {
var selection = this._getSelection ();
selection.setCaret(e.start + e.text.length);
this._setSelection(selection, true);
}
this.onModify({});
},
_onModelChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
var e = {
start: start,
removedCharCount: removedCharCount,
addedCharCount: addedCharCount,
removedLineCount: removedLineCount,
addedLineCount: addedLineCount
};
this.onModelChanged(e);
var selection = this._getSelection();
if (selection.end > start) {
if (selection.end > start && selection.start < start + removedCharCount) {
// selection intersects replaced text. set caret behind text change
selection.setCaret(start + addedCharCount);
} else {
// move selection to keep same text selected
selection.start += addedCharCount - removedCharCount;
selection.end += addedCharCount - removedCharCount;
}
this._setSelection(selection, false, false);
}
var model = this._model;
var startLine = model.getLineAtOffset(start);
var child = this._getLineNext();
while (child) {
var lineIndex = child.lineIndex;
if (startLine <= lineIndex && lineIndex <= startLine + removedLineCount) {
child.lineChanged = true;
}
if (lineIndex > startLine + removedLineCount) {
child.lineIndex = lineIndex + addedLineCount - removedLineCount;
}
child = this._getLineNext(child);
}
if (startLine <= this._maxLineIndex && this._maxLineIndex <= startLine + removedLineCount) {
this._maxLineIndex = -1;
this._maxLineWidth = 0;
}
this._updatePage();
},
_onModelChanging: function(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
var e = {
text: newText,
start: start,
removedCharCount: removedCharCount,
addedCharCount: addedCharCount,
removedLineCount: removedLineCount,
addedLineCount: addedLineCount
};
this.onModelChanging(e);
},
_queueUpdatePage: function() {
if (this._updateTimer) { return; }
var self = this;
this._updateTimer = setTimeout(function() {
self._updateTimer = null;
self._updatePage();
}, 0);
},
_resizeTouchDiv: function() {
var viewRect = this._viewDiv.getBoundingClientRect();
var parentRect = this._frame.getBoundingClientRect();
var temp = this._frame;
while (temp) {
if (temp.style && temp.style.top) { break; }
temp = temp.parentNode;
}
var parentTop = parentRect.top;
if (temp) {
parentTop -= temp.getBoundingClientRect().top;
} else {
parentTop += this._parentDocument.body.scrollTop;
}
temp = this._frame;
while (temp) {
if (temp.style && temp.style.left) { break; }
temp = temp.parentNode;
}
var parentLeft = parentRect.left;
if (temp) {
parentLeft -= temp.getBoundingClientRect().left;
} else {
parentLeft += this._parentDocument.body.scrollLeft;
}
var touchDiv = this._touchDiv;
touchDiv.style.left = (parentLeft + viewRect.left) + "px";
touchDiv.style.top = (parentTop + viewRect.top) + "px";
touchDiv.style.width = viewRect.width + "px";
touchDiv.style.height = viewRect.height + "px";
},
_scrollView: function (pixelX, pixelY) {
/*
* Always set _ensureCaretVisible to false so that the view does not scroll
* to show the caret when scrollView is not called from showCaret().
*/
this._ensureCaretVisible = false;
/*
* Scrolling is done only by setting the scrollLeft and scrollTop fields in the
* view div. This causes an updatePage from the scroll event. In some browsers
* this event is asynchromous and forcing update page to run synchronously
* (by calling doScroll) leads to redraw problems. On Chrome 11, the view
* stops redrawing at times when holding PageDown/PageUp key.
* On Firefox 4 for Linux, the view redraws the first page when holding
* PageDown/PageUp key, but it will not redraw again until the key is released.
*/
var viewDiv = this._viewDiv;
if (pixelX) { viewDiv.scrollLeft += pixelX; }
if (pixelY) { viewDiv.scrollTop += pixelY; }
},
_setClipboardText: function (text, event) {
var clipboardText;
if (this._frameWindow.clipboardData) {
//IE
clipboardText = [];
this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
return this._frameWindow.clipboardData.setData("Text", clipboardText.join(""));
}
/* Feature in Chrome, clipboardData.setData is no-op on Chrome even though it returns true */
if (isChrome || isFirefox || !event) {
var window = this._frameWindow;
var document = this._frameDocument;
var child = document.createElement("PRE");
child.style.position = "fixed";
child.style.left = "-1000px";
this._convertDelimiter(text,
function(t) {
child.appendChild(document.createTextNode(t));
},
function() {
child.appendChild(document.createElement("BR"));
}
);
child.appendChild(document.createTextNode(" "));
this._clientDiv.appendChild(child);
var range = document.createRange();
range.setStart(child.firstChild, 0);
range.setEndBefore(child.lastChild);
var sel = window.getSelection();
if (sel.rangeCount > 0) { sel.removeAllRanges(); }
sel.addRange(range);
var self = this;
var cleanup = function() {
self._clientDiv.removeChild(child);
self._updateDOMSelection();
};
var result = false;
/*
* Try execCommand first, it works on firefox with clipboard permission,
* chrome 5, safari 4.
*/
this._ignoreCopy = true;
try {
result = document.execCommand("copy", false, null);
} catch (e) {}
this._ignoreCopy = false;
if (!result) {
if (event) {
setTimeout(cleanup, 0);
return false;
}
}
/* no event and no permission, copy can not be done */
cleanup();
return true;
}
if (event && event.clipboardData) {
//webkit
clipboardText = [];
this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
return event.clipboardData.setData("text/plain", clipboardText.join(""));
}
},
_setDOMSelection: function (startNode, startOffset, endNode, endOffset) {
var window = this._frameWindow;
var document = this._frameDocument;
var startLineNode, startLineOffset, endLineNode, endLineOffset;
var offset = 0;
var lineChild = startNode.firstChild;
var node, nodeLength, model = this._model;
var startLineEnd = model.getLine(startNode.lineIndex).length;
while (lineChild) {
node = lineChild.firstChild;
nodeLength = node.length;
if (lineChild.ignoreChars) {
nodeLength -= lineChild.ignoreChars;
}
if (offset + nodeLength > startOffset || offset + nodeLength >= startLineEnd) {
startLineNode = node;
startLineOffset = startOffset - offset;
if (lineChild.ignoreChars && nodeLength > 0 && startLineOffset === nodeLength) {
startLineOffset += lineChild.ignoreChars;
}
break;
}
offset += nodeLength;
lineChild = lineChild.nextSibling;
}
offset = 0;
lineChild = endNode.firstChild;
var endLineEnd = this._model.getLine(endNode.lineIndex).length;
while (lineChild) {
node = lineChild.firstChild;
nodeLength = node.length;
if (lineChild.ignoreChars) {
nodeLength -= lineChild.ignoreChars;
}
if (nodeLength + offset > endOffset || offset + nodeLength >= endLineEnd) {
endLineNode = node;
endLineOffset = endOffset - offset;
if (lineChild.ignoreChars && nodeLength > 0 && endLineOffset === nodeLength) {
endLineOffset += lineChild.ignoreChars;
}
break;
}
offset += nodeLength;
lineChild = lineChild.nextSibling;
}
this._setDOMFullSelection(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd);
if (isPad) { return; }
var range;
if (window.getSelection) {
//W3C
range = document.createRange();
range.setStart(startLineNode, startLineOffset);
range.setEnd(endLineNode, endLineOffset);
var sel = window.getSelection();
this._ignoreSelect = false;
if (sel.rangeCount > 0) { sel.removeAllRanges(); }
sel.addRange(range);
this._ignoreSelect = true;
} else if (document.selection) {
//IE < 9
var body = document.body;
/*
* Bug in IE. For some reason when text is deselected the overflow
* selection at the end of some lines does not get redrawn. The
* fix is to create a DOM element in the body to force a redraw.
*/
var child = document.createElement("DIV");
body.appendChild(child);
body.removeChild(child);
range = body.createTextRange();
range.moveToElementText(startLineNode.parentNode);
range.moveStart("character", startLineOffset);
var endRange = body.createTextRange();
endRange.moveToElementText(endLineNode.parentNode);
endRange.moveStart("character", endLineOffset);
range.setEndPoint("EndToStart", endRange);
this._ignoreSelect = false;
range.select();
this._ignoreSelect = true;
}
},
_setDOMFullSelection: function(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd) {
var model = this._model;
if (this._selDiv1) {
var startLineBounds, l;
startLineBounds = this._getLineBoundingClientRect(startNode);
if (startOffset === 0) {
l = startLineBounds.left;
} else {
if (startOffset >= startLineEnd) {
l = startLineBounds.right;
} else {
this._ignoreDOMSelection = true;
l = this._getBoundsAtOffset(model.getLineStart(startNode.lineIndex) + startOffset).left;
this._ignoreDOMSelection = false;
}
}
var textArea = this._textArea;
if (textArea) {
textArea.selectionStart = textArea.selectionEnd = 0;
var rect = this._frame.getBoundingClientRect();
var touchRect = this._touchDiv.getBoundingClientRect();
var viewBounds = this._viewDiv.getBoundingClientRect();
if (!(viewBounds.left <= l && l <= viewBounds.left + viewBounds.width &&
viewBounds.top <= startLineBounds.top && startLineBounds.top <= viewBounds.top + viewBounds.height) ||
!(startNode === endNode && startOffset === endOffset))
{
textArea.style.left = "-1000px";
} else {
textArea.style.left = (l - 4 + rect.left - touchRect.left) + "px";
}
textArea.style.top = (startLineBounds.top + rect.top - touchRect.top) + "px";
textArea.style.width = "6px";
textArea.style.height = (startLineBounds.bottom - startLineBounds.top) + "px";
}
var selDiv = this._selDiv1;
selDiv.style.width = "0px";
selDiv.style.height = "0px";
selDiv = this._selDiv2;
selDiv.style.width = "0px";
selDiv.style.height = "0px";
selDiv = this._selDiv3;
selDiv.style.width = "0px";
selDiv.style.height = "0px";
if (!(startNode === endNode && startOffset === endOffset)) {
var handleWidth = isPad ? 2 : 0;
var handleBorder = handleWidth + "px blue solid";
var viewPad = this._getViewPadding();
var clientRect = this._clientDiv.getBoundingClientRect();
var viewRect = this._viewDiv.getBoundingClientRect();
var left = viewRect.left + viewPad.left;
var right = clientRect.right;
var top = viewRect.top + viewPad.top;
var bottom = clientRect.bottom;
var r;
var endLineBounds = this._getLineBoundingClientRect(endNode);
if (endOffset === 0) {
r = endLineBounds.left;
} else {
if (endOffset >= endLineEnd) {
r = endLineBounds.right;
} else {
this._ignoreDOMSelection = true;
r = this._getBoundsAtOffset(model.getLineStart(endNode.lineIndex) + endOffset).left;
this._ignoreDOMSelection = false;
}
}
var sel1Div = this._selDiv1;
var sel1Left = Math.min(right, Math.max(left, l));
var sel1Top = Math.min(bottom, Math.max(top, startLineBounds.top));
var sel1Right = right;
var sel1Bottom = Math.min(bottom, Math.max(top, startLineBounds.bottom));
sel1Div.style.left = sel1Left + "px";
sel1Div.style.top = sel1Top + "px";
sel1Div.style.width = Math.max(0, sel1Right - sel1Left) + "px";
sel1Div.style.height = Math.max(0, sel1Bottom - sel1Top) + (isPad ? 1 : 0) + "px";
if (isPad) {
sel1Div.style.borderLeft = handleBorder;
sel1Div.style.borderRight = "0px";
}
if (startNode === endNode) {
sel1Right = Math.min(r, right);
sel1Div.style.width = Math.max(0, sel1Right - sel1Left - handleWidth * 2) + "px";
if (isPad) {
sel1Div.style.borderRight = handleBorder;
}
} else {
var sel3Left = left;
var sel3Top = Math.min(bottom, Math.max(top, endLineBounds.top));
var sel3Right = Math.min(right, Math.max(left, r));
var sel3Bottom = Math.min(bottom, Math.max(top, endLineBounds.bottom));
var sel3Div = this._selDiv3;
sel3Div.style.left = sel3Left + "px";
sel3Div.style.top = sel3Top + "px";
sel3Div.style.width = Math.max(0, sel3Right - sel3Left - handleWidth) + "px";
sel3Div.style.height = Math.max(0, sel3Bottom - sel3Top) + "px";
if (isPad) {
sel3Div.style.borderRight = handleBorder;
}
if (sel3Top - sel1Bottom > 0) {
var sel2Div = this._selDiv2;
sel2Div.style.left = left + "px";
sel2Div.style.top = sel1Bottom + "px";
sel2Div.style.width = Math.max(0, right - left) + "px";
sel2Div.style.height = Math.max(0, sel3Top - sel1Bottom) + (isPad ? 1 : 0) + "px";
}
}
}
}
},
_setGrab: function (target) {
if (target === this._grabControl) { return; }
if (target) {
addHandler(target, "mousemove", this._mouseMoveClosure);
addHandler(target, "mouseup", this._mouseUpClosure);
if (target.setCapture) { target.setCapture(); }
this._grabControl = target;
} else {
removeHandler(this._grabControl, "mousemove", this._mouseMoveClosure);
removeHandler(this._grabControl, "mouseup", this._mouseUpClosure);
if (this._grabControl.releaseCapture) { this._grabControl.releaseCapture(); }
this._grabControl = null;
}
},
_setSelection: function (selection, scroll, update) {
if (selection) {
this._columnX = -1;
if (update === undefined) { update = true; }
var oldSelection = this._selection;
if (!oldSelection.equals(selection)) {
this._selection = selection;
var e = {
oldValue: {start:oldSelection.start, end:oldSelection.end},
newValue: {start:selection.start, end:selection.end}
};
this.onSelection(e);
}
/*
* Always showCaret(), even when the selection is not changing, to ensure the
* caret is visible. Note that some views do not scroll to show the caret during
* keyboard navigation when the selection does not chanage. For example, line down
* when the caret is already at the last line.
*/
if (scroll) { update = !this._showCaret(); }
/*
* Sometimes the browser changes the selection
* as result of method calls or "leaked" events.
* The fix is to set the visual selection even
* when the logical selection is not changed.
*/
if (update) { this._updateDOMSelection(); }
}
},
_setSelectionTo: function (x,y,extent) {
var model = this._model, offset;
var selection = this._getSelection();
var lineIndex = this._getYToLine(y);
if (this._clickCount === 1) {
offset = this._getXToOffset(lineIndex, x);
selection.extend(offset);
if (!extent) { selection.collapse(); }
} else {
var word = (this._clickCount & 1) === 0;
var start, end;
if (word) {
offset = this._getXToOffset(lineIndex, x);
if (this._doubleClickSelection) {
if (offset >= this._doubleClickSelection.start) {
start = this._doubleClickSelection.start;
end = this._getOffset(offset, "wordend", +1);
} else {
start = this._getOffset(offset, "word", -1);
end = this._doubleClickSelection.end;
}
} else {
start = this._getOffset(offset, "word", -1);
end = this._getOffset(start, "wordend", +1);
}
} else {
if (this._doubleClickSelection) {
var doubleClickLine = model.getLineAtOffset(this._doubleClickSelection.start);
if (lineIndex >= doubleClickLine) {
start = model.getLineStart(doubleClickLine);
end = model.getLineEnd(lineIndex);
} else {
start = model.getLineStart(lineIndex);
end = model.getLineEnd(doubleClickLine);
}
} else {
start = model.getLineStart(lineIndex);
end = model.getLineEnd(lineIndex);
}
}
selection.setCaret(start);
selection.extend(end);
}
this._setSelection(selection, true, true);
},
_showCaret: function () {
var model = this._model;
var selection = this._getSelection();
var scroll = this._getScroll();
var caret = selection.getCaret();
var start = selection.start;
var end = selection.end;
var startLine = model.getLineAtOffset(start);
var endLine = model.getLineAtOffset(end);
var endInclusive = Math.max(Math.max(start, model.getLineStart(endLine)), end - 1);
var viewPad = this._getViewPadding();
var clientWidth = this._getClientWidth();
var leftEdge = viewPad.left;
var rightEdge = viewPad.left + clientWidth;
var bounds = this._getBoundsAtOffset(caret === start ? start : endInclusive);
var left = bounds.left;
var right = bounds.right;
var minScroll = clientWidth / 4;
if (!selection.isEmpty() && startLine === endLine) {
bounds = this._getBoundsAtOffset(caret === end ? start : endInclusive);
var selectionWidth = caret === start ? bounds.right - left : right - bounds.left;
if ((clientWidth - minScroll) > selectionWidth) {
if (left > bounds.left) { left = bounds.left; }
if (right < bounds.right) { right = bounds.right; }
}
}
var viewRect = this._viewDiv.getBoundingClientRect();
left -= viewRect.left;
right -= viewRect.left;
var pixelX = 0;
if (left < leftEdge) {
pixelX = Math.min(left - leftEdge, -minScroll);
}
if (right > rightEdge) {
var maxScroll = this._scrollDiv.scrollWidth - scroll.x - clientWidth;
pixelX = Math.min(maxScroll, Math.max(right - rightEdge, minScroll));
}
var pixelY = 0;
var topIndex = this._getTopIndex(true);
var bottomIndex = this._getBottomIndex(true);
var caretLine = model.getLineAtOffset(caret);
var clientHeight = this._getClientHeight();
if (!(topIndex <= caretLine && caretLine <= bottomIndex)) {
var lineHeight = this._getLineHeight();
var selectionHeight = (endLine - startLine) * lineHeight;
pixelY = caretLine * lineHeight;
pixelY -= scroll.y;
if (pixelY + lineHeight > clientHeight) {
pixelY -= clientHeight - lineHeight;
if (caret === start && start !== end) {
pixelY += Math.min(clientHeight - lineHeight, selectionHeight);
}
} else {
if (caret === end) {
pixelY -= Math.min (clientHeight - lineHeight, selectionHeight);
}
}
}
if (pixelX !== 0 || pixelY !== 0) {
this._scrollView (pixelX, pixelY);
/*
* When the view scrolls it is possible that one of the scrollbars can show over the caret.
* Depending on the browser scrolling can be synchronous (Safari), in which case the change
* can be detected before showCaret() returns. When scrolling is asynchronous (most browsers),
* the detection is done during the next update page.
*/
if (clientHeight !== this._getClientHeight() || clientWidth !== this._getClientWidth()) {
this._showCaret();
} else {
this._ensureCaretVisible = true;
}
return true;
}
return false;
},
_startIME: function () {
if (this._imeOffset !== -1) { return; }
var selection = this._getSelection();
if (!selection.isEmpty()) {
this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
}
this._imeOffset = selection.start;
},
_unhookEvents: function() {
this._model.removeListener(this._modelListener);
this._modelListener = null;
this._mouseMoveClosure = null;
this._mouseUpClosure = null;
for (var i=0; i<this._handlers.length; i++) {
var h = this._handlers[i];
removeHandler(h.target, h.type, h.handler);
}
this._handlers = null;
},
_updateDOMSelection: function () {
if (this._ignoreDOMSelection) { return; }
var selection = this._getSelection();
var model = this._model;
var startLine = model.getLineAtOffset(selection.start);
var endLine = model.getLineAtOffset(selection.end);
var firstNode = this._getLineNext();
/*
* Bug in Firefox. For some reason, after a update page sometimes the
* firstChild returns null incorrectly. The fix is to ignore show selection.
*/
if (!firstNode) { return; }
var lastNode = this._getLinePrevious();
var topNode, bottomNode, topOffset, bottomOffset;
if (startLine < firstNode.lineIndex) {
topNode = firstNode;
topOffset = 0;
} else if (startLine > lastNode.lineIndex) {
topNode = lastNode;
topOffset = 0;
} else {
topNode = this._getLineNode(startLine);
topOffset = selection.start - model.getLineStart(startLine);
}
if (endLine < firstNode.lineIndex) {
bottomNode = firstNode;
bottomOffset = 0;
} else if (endLine > lastNode.lineIndex) {
bottomNode = lastNode;
bottomOffset = 0;
} else {
bottomNode = this._getLineNode(endLine);
bottomOffset = selection.end - model.getLineStart(endLine);
}
this._setDOMSelection(topNode, topOffset, bottomNode, bottomOffset);
},
_updatePage: function() {
if (this._updateTimer) {
clearTimeout(this._updateTimer);
this._updateTimer = null;
}
var document = this._frameDocument;
var frameWidth = this._getFrameWidth();
var frameHeight = this._getFrameHeight();
document.body.style.width = frameWidth + "px";
document.body.style.height = frameHeight + "px";
var viewDiv = this._viewDiv;
var clientDiv = this._clientDiv;
var viewPad = this._getViewPadding();
/* Update view height in order to have client height computed */
viewDiv.style.height = Math.max(0, (frameHeight - viewPad.top - viewPad.bottom)) + "px";
var model = this._model;
var lineHeight = this._getLineHeight();
var scrollY = this._getScroll().y;
var firstLine = Math.max(0, scrollY) / lineHeight;
var topIndex = Math.floor(firstLine);
var lineStart = Math.max(0, topIndex - 1);
var top = Math.round((firstLine - lineStart) * lineHeight);
var lineCount = model.getLineCount();
var clientHeight = this._getClientHeight();
var partialY = Math.round((firstLine - topIndex) * lineHeight);
var linesPerPage = Math.floor((clientHeight + partialY) / lineHeight);
var bottomIndex = Math.min(topIndex + linesPerPage, lineCount - 1);
var lineEnd = Math.min(bottomIndex + 1, lineCount - 1);
this._partialY = partialY;
var lineIndex, lineWidth;
var child = clientDiv.firstChild;
while (child) {
lineIndex = child.lineIndex;
var nextChild = child.nextSibling;
if (!(lineStart <= lineIndex && lineIndex <= lineEnd) || child.lineChanged || child.lineIndex === -1) {
if (this._mouseWheelLine === child) {
child.style.display = "none";
child.lineIndex = -1;
} else {
clientDiv.removeChild(child);
}
}
child = nextChild;
}
child = this._getLineNext();
var frag = document.createDocumentFragment();
for (lineIndex=lineStart; lineIndex<=lineEnd; lineIndex++) {
if (!child || child.lineIndex > lineIndex) {
this._createLine(frag, null, document, lineIndex, model);
} else {
if (frag.firstChild) {
clientDiv.insertBefore(frag, child);
frag = document.createDocumentFragment();
}
child = this._getLineNext(child);
}
}
if (frag.firstChild) { clientDiv.insertBefore(frag, child); }
/*
* Feature in WekKit. Webkit limits the width of the lines
* computed below to the width of the client div. This causes
* the lines to be wrapped even though "pre" is set. The fix
* is to set the width of the client div to a larger number
* before computing the lines width. Note that this value is
* reset to the appropriate value further down.
*/
if (isWebkit) {
clientDiv.style.width = (0x7FFFF).toString() + "px";
}
child = this._getLineNext();
while (child) {
lineWidth = child.lineWidth;
if (lineWidth === undefined) {
var rect = this._getLineBoundingClientRect(child);
lineWidth = child.lineWidth = rect.right - rect.left;
}
if (lineWidth >= this._maxLineWidth) {
this._maxLineWidth = lineWidth;
this._maxLineIndex = child.lineIndex;
}
if (child.lineIndex === topIndex) { this._topChild = child; }
if (child.lineIndex === bottomIndex) { this._bottomChild = child; }
child = this._getLineNext(child);
}
// Update rulers
this._updateRuler(this._leftDiv, topIndex, bottomIndex);
this._updateRuler(this._rightDiv, topIndex, bottomIndex);
var leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
var rightWidth = this._rightDiv ? this._rightDiv.scrollWidth : 0;
viewDiv.style.left = leftWidth + "px";
viewDiv.style.width = Math.max(0, frameWidth - leftWidth - rightWidth - viewPad.left - viewPad.right) + "px";
if (this._rightDiv) {
this._rightDiv.style.left = (frameWidth - rightWidth) + "px";
}
var scrollDiv = this._scrollDiv;
/* Need to set the height first in order for the width to consider the vertical scrollbar */
var scrollHeight = lineCount * lineHeight;
scrollDiv.style.height = scrollHeight + "px";
var clientWidth = this._getClientWidth();
var width = Math.max(this._maxLineWidth, clientWidth);
/*
* Except by IE 8 and earlier, all other browsers are not allocating enough space for the right padding
* in the scrollbar. It is possible this a bug since all other paddings are considered.
*/
var scrollWidth = width;
if (!isIE || isIE >= 9) { width += viewPad.right; }
scrollDiv.style.width = width + "px";
// Get the left scroll after setting the width of the scrollDiv as this can change the horizontal scroll offset.
var scroll = this._getScroll();
var left = scroll.x;
var clipLeft = left;
var clipTop = top;
var clipRight = left + clientWidth;
var clipBottom = top + clientHeight;
if (clipLeft === 0) { clipLeft -= viewPad.left; }
if (clipTop === 0) { clipTop -= viewPad.top; }
if (clipRight === scrollWidth) { clipRight += viewPad.right; }
if (scroll.y + clientHeight === scrollHeight) { clipBottom += viewPad.bottom; }
clientDiv.style.clip = "rect(" + clipTop + "px," + clipRight + "px," + clipBottom + "px," + clipLeft + "px)";
clientDiv.style.left = (-left + leftWidth + viewPad.left) + "px";
clientDiv.style.top = (-top + viewPad.top) + "px";
clientDiv.style.width = (isWebkit ? scrollWidth : clientWidth + left) + "px";
clientDiv.style.height = (clientHeight + top) + "px";
var overlayDiv = this._overlayDiv;
if (overlayDiv) {
overlayDiv.style.clip = clientDiv.style.clip;
overlayDiv.style.left = clientDiv.style.left;
overlayDiv.style.top = clientDiv.style.top;
overlayDiv.style.width = clientDiv.style.width;
overlayDiv.style.height = clientDiv.style.height;
}
function _updateRulerSize(divRuler) {
if (!divRuler) { return; }
var rulerHeight = clientHeight + viewPad.top + viewPad.bottom;
var cells = divRuler.firstChild.rows[0].cells;
for (var i = 0; i < cells.length; i++) {
var div = cells[i].firstChild;
var offset = lineHeight;
if (div._ruler.getOverview() === "page") { offset += partialY; }
div.style.top = -offset + "px";
div.style.height = (rulerHeight + offset) + "px";
div = div.nextSibling;
}
divRuler.style.height = rulerHeight + "px";
}
_updateRulerSize(this._leftDiv);
_updateRulerSize(this._rightDiv);
if (isPad) {
var self = this;
setTimeout(function() {self._resizeTouchDiv();}, 0);
}
this._updateDOMSelection();
/*
* If the client height changed during the update page it means that scrollbar has either been shown or hidden.
* When this happens update page has to run again to ensure that the top and bottom lines div are correct.
*
* Note: On IE, updateDOMSelection() has to be called before getting the new client height because it
* forces the client area to be recomputed.
*/
var ensureCaretVisible = this._ensureCaretVisible;
this._ensureCaretVisible = false;
if (clientHeight !== this._getClientHeight()) {
this._updatePage();
if (ensureCaretVisible) {
this._showCaret();
}
}
},
_updateRuler: function (divRuler, topIndex, bottomIndex) {
if (!divRuler) { return; }
var cells = divRuler.firstChild.rows[0].cells;
var lineHeight = this._getLineHeight();
var parentDocument = this._frameDocument;
var viewPad = this._getViewPadding();
for (var i = 0; i < cells.length; i++) {
var div = cells[i].firstChild;
var ruler = div._ruler, style;
if (div.rulerChanged) {
this._applyStyle(ruler.getStyle(), div);
}
var widthDiv;
var child = div.firstChild;
if (child) {
widthDiv = child;
child = child.nextSibling;
} else {
widthDiv = parentDocument.createElement("DIV");
widthDiv.style.visibility = "hidden";
div.appendChild(widthDiv);
}
var lineIndex;
if (div.rulerChanged) {
if (widthDiv) {
lineIndex = -1;
this._applyStyle(ruler.getStyle(lineIndex), widthDiv);
widthDiv.innerHTML = ruler.getHTML(lineIndex);
widthDiv.lineIndex = lineIndex;
widthDiv.style.height = (lineHeight + viewPad.top) + "px";
}
}
var overview = ruler.getOverview(), lineDiv, frag;
if (overview === "page") {
while (child) {
lineIndex = child.lineIndex;
var nextChild = child.nextSibling;
if (!(topIndex <= lineIndex && lineIndex <= bottomIndex) || child.lineChanged) {
div.removeChild(child);
}
child = nextChild;
}
child = div.firstChild.nextSibling;
frag = document.createDocumentFragment();
for (lineIndex=topIndex; lineIndex<=bottomIndex; lineIndex++) {
if (!child || child.lineIndex > lineIndex) {
lineDiv = parentDocument.createElement("DIV");
this._applyStyle(ruler.getStyle(lineIndex), lineDiv);
lineDiv.innerHTML = ruler.getHTML(lineIndex);
lineDiv.lineIndex = lineIndex;
lineDiv.style.height = lineHeight + "px";
frag.appendChild(lineDiv);
} else {
if (frag.firstChild) {
div.insertBefore(frag, child);
frag = document.createDocumentFragment();
}
if (child) {
child = child.nextSibling;
}
}
}
if (frag.firstChild) { div.insertBefore(frag, child); }
} else {
var buttonHeight = 17;
var clientHeight = this._getClientHeight ();
var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
var lineCount = this._model.getLineCount ();
var divHeight = trackHeight / lineCount;
if (div.rulerChanged) {
var count = div.childNodes.length;
while (count > 1) {
div.removeChild(div.lastChild);
count--;
}
var lines = ruler.getAnnotations ();
frag = document.createDocumentFragment();
for (var j = 0; j < lines.length; j++) {
lineIndex = lines[j];
lineDiv = parentDocument.createElement("DIV");
this._applyStyle(ruler.getStyle(lineIndex), lineDiv);
lineDiv.style.position = "absolute";
lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineIndex * divHeight) + "px";
lineDiv.innerHTML = ruler.getHTML(lineIndex);
lineDiv.lineIndex = lineIndex;
frag.appendChild(lineDiv);
}
div.appendChild(frag);
} else if (div._oldTrackHeight !== trackHeight) {
lineDiv = div.firstChild ? div.firstChild.nextSibling : null;
while (lineDiv) {
lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineDiv.lineIndex * divHeight) + "px";
lineDiv = lineDiv.nextSibling;
}
}
div._oldTrackHeight = trackHeight;
}
div.rulerChanged = false;
div = div.nextSibling;
}
}
};//end prototype
return TextView;
}());
if (typeof window !== "undefined" && typeof window.define !== "undefined") {
define(['orion/textview/textModel', 'orion/textview/keyBinding'], function() {
return orion.textview;
});
}