390 lines
15 KiB
JavaScript
390 lines
15 KiB
JavaScript
|
/* ***** BEGIN LICENSE BLOCK *****
|
||
|
* Distributed under the BSD license:
|
||
|
*
|
||
|
* Copyright (c) 2010, Ajax.org B.V.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* Redistribution and use in source and binary forms, with or without
|
||
|
* modification, are permitted provided that the following conditions are met:
|
||
|
* * Redistributions of source code must retain the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer.
|
||
|
* * Redistributions in binary form must reproduce the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer in the
|
||
|
* documentation and/or other materials provided with the distribution.
|
||
|
* * Neither the name of Ajax.org B.V. nor the
|
||
|
* names of its contributors may be used to endorse or promote products
|
||
|
* derived from this software without specific prior written permission.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||
|
* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
|
||
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
*
|
||
|
* ***** END LICENSE BLOCK ***** */
|
||
|
|
||
|
define(function(require, exports, module) {
|
||
|
"use strict";
|
||
|
|
||
|
var bidiUtil = require("./lib/bidiutil");
|
||
|
var lang = require("./lib/lang");
|
||
|
var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\u202B]/;
|
||
|
|
||
|
/**
|
||
|
* This object is used to ensure Bi-Directional support (for languages with text flowing from right to left, like Arabic or Hebrew)
|
||
|
* including correct caret positioning, text selection mouse and keyboard arrows functioning
|
||
|
* @class BidiHandler
|
||
|
**/
|
||
|
|
||
|
/**
|
||
|
* Creates a new `BidiHandler` object
|
||
|
* @param {EditSession} session The session to use
|
||
|
*
|
||
|
* @constructor
|
||
|
**/
|
||
|
var BidiHandler = function(session) {
|
||
|
this.session = session;
|
||
|
this.bidiMap = {};
|
||
|
/* current screen row */
|
||
|
this.currentRow = null;
|
||
|
this.bidiUtil = bidiUtil;
|
||
|
/* Arabic/Hebrew character width differs from regular character width */
|
||
|
this.charWidths = [];
|
||
|
this.EOL = "\xAC";
|
||
|
this.showInvisibles = true;
|
||
|
this.isRtlDir = false;
|
||
|
this.$isRtl = false;
|
||
|
this.line = "";
|
||
|
this.wrapIndent = 0;
|
||
|
this.EOF = "\xB6";
|
||
|
this.RLE = "\u202B";
|
||
|
this.contentWidth = 0;
|
||
|
this.fontMetrics = null;
|
||
|
this.rtlLineOffset = 0;
|
||
|
this.wrapOffset = 0;
|
||
|
this.isMoveLeftOperation = false;
|
||
|
this.seenBidi = bidiRE.test(session.getValue());
|
||
|
};
|
||
|
|
||
|
(function() {
|
||
|
/**
|
||
|
* Returns 'true' if row contains Bidi characters, in such case
|
||
|
* creates Bidi map to be used in operations related to selection
|
||
|
* (keyboard arrays, mouse click, select)
|
||
|
* @param {Number} the screen row to be checked
|
||
|
* @param {Number} the document row to be checked [optional]
|
||
|
* @param {Number} the wrapped screen line index [ optional]
|
||
|
**/
|
||
|
this.isBidiRow = function(screenRow, docRow, splitIndex) {
|
||
|
if (!this.seenBidi)
|
||
|
return false;
|
||
|
if (screenRow !== this.currentRow) {
|
||
|
this.currentRow = screenRow;
|
||
|
this.updateRowLine(docRow, splitIndex);
|
||
|
this.updateBidiMap();
|
||
|
}
|
||
|
return this.bidiMap.bidiLevels;
|
||
|
};
|
||
|
|
||
|
this.onChange = function(delta) {
|
||
|
if (!this.seenBidi) {
|
||
|
if (delta.action == "insert" && bidiRE.test(delta.lines.join("\n"))) {
|
||
|
this.seenBidi = true;
|
||
|
this.currentRow = null;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
this.currentRow = null;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.getDocumentRow = function() {
|
||
|
var docRow = 0;
|
||
|
var rowCache = this.session.$screenRowCache;
|
||
|
if (rowCache.length) {
|
||
|
var index = this.session.$getRowCacheIndex(rowCache, this.currentRow);
|
||
|
if (index >= 0)
|
||
|
docRow = this.session.$docRowCache[index];
|
||
|
}
|
||
|
|
||
|
return docRow;
|
||
|
};
|
||
|
|
||
|
this.getSplitIndex = function() {
|
||
|
var splitIndex = 0;
|
||
|
var rowCache = this.session.$screenRowCache;
|
||
|
if (rowCache.length) {
|
||
|
var currentIndex, prevIndex = this.session.$getRowCacheIndex(rowCache, this.currentRow);
|
||
|
while (this.currentRow - splitIndex > 0) {
|
||
|
currentIndex = this.session.$getRowCacheIndex(rowCache, this.currentRow - splitIndex - 1);
|
||
|
if (currentIndex !== prevIndex)
|
||
|
break;
|
||
|
|
||
|
prevIndex = currentIndex;
|
||
|
splitIndex++;
|
||
|
}
|
||
|
} else {
|
||
|
splitIndex = this.currentRow;
|
||
|
}
|
||
|
|
||
|
return splitIndex;
|
||
|
};
|
||
|
|
||
|
this.updateRowLine = function(docRow, splitIndex) {
|
||
|
if (docRow === undefined)
|
||
|
docRow = this.getDocumentRow();
|
||
|
|
||
|
var isLastRow = (docRow === this.session.getLength() - 1),
|
||
|
endOfLine = isLastRow ? this.EOF : this.EOL;
|
||
|
|
||
|
this.wrapIndent = 0;
|
||
|
this.line = this.session.getLine(docRow);
|
||
|
this.isRtlDir = this.$isRtl || this.line.charAt(0) === this.RLE;
|
||
|
if (this.session.$useWrapMode) {
|
||
|
var splits = this.session.$wrapData[docRow];
|
||
|
if (splits) {
|
||
|
if (splitIndex === undefined)
|
||
|
splitIndex = this.getSplitIndex();
|
||
|
|
||
|
if(splitIndex > 0 && splits.length) {
|
||
|
this.wrapIndent = splits.indent;
|
||
|
this.wrapOffset = this.wrapIndent * this.charWidths[bidiUtil.L];
|
||
|
this.line = (splitIndex < splits.length) ?
|
||
|
this.line.substring(splits[splitIndex - 1], splits[splitIndex]) :
|
||
|
this.line.substring(splits[splits.length - 1]);
|
||
|
} else {
|
||
|
this.line = this.line.substring(0, splits[splitIndex]);
|
||
|
}
|
||
|
}
|
||
|
if (splitIndex == splits.length)
|
||
|
this.line += (this.showInvisibles) ? endOfLine : bidiUtil.DOT;
|
||
|
} else {
|
||
|
this.line += this.showInvisibles ? endOfLine : bidiUtil.DOT;
|
||
|
}
|
||
|
|
||
|
/* replace tab and wide characters by commensurate spaces */
|
||
|
var session = this.session, shift = 0, size;
|
||
|
this.line = this.line.replace(/\t|[\u1100-\u2029, \u202F-\uFFE6]/g, function(ch, i){
|
||
|
if (ch === '\t' || session.isFullWidth(ch.charCodeAt(0))) {
|
||
|
size = (ch === '\t') ? session.getScreenTabSize(i + shift) : 2;
|
||
|
shift += size - 1;
|
||
|
return lang.stringRepeat(bidiUtil.DOT, size);
|
||
|
}
|
||
|
return ch;
|
||
|
});
|
||
|
|
||
|
if (this.isRtlDir) {
|
||
|
this.fontMetrics.$main.textContent = (this.line.charAt(this.line.length - 1) == bidiUtil.DOT) ? this.line.substr(0, this.line.length - 1) : this.line;
|
||
|
this.rtlLineOffset = this.contentWidth - this.fontMetrics.$main.getBoundingClientRect().width;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.updateBidiMap = function() {
|
||
|
var textCharTypes = [];
|
||
|
if (bidiUtil.hasBidiCharacters(this.line, textCharTypes) || this.isRtlDir) {
|
||
|
this.bidiMap = bidiUtil.doBidiReorder(this.line, textCharTypes, this.isRtlDir);
|
||
|
} else {
|
||
|
this.bidiMap = {};
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Resets stored info related to current screen row
|
||
|
**/
|
||
|
this.markAsDirty = function() {
|
||
|
this.currentRow = null;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Updates array of character widths
|
||
|
* @param {Object} font metrics
|
||
|
*
|
||
|
**/
|
||
|
this.updateCharacterWidths = function(fontMetrics) {
|
||
|
if (this.characterWidth === fontMetrics.$characterSize.width)
|
||
|
return;
|
||
|
|
||
|
this.fontMetrics = fontMetrics;
|
||
|
var characterWidth = this.characterWidth = fontMetrics.$characterSize.width;
|
||
|
var bidiCharWidth = fontMetrics.$measureCharWidth("\u05d4");
|
||
|
|
||
|
this.charWidths[bidiUtil.L] = this.charWidths[bidiUtil.EN] = this.charWidths[bidiUtil.ON_R] = characterWidth;
|
||
|
this.charWidths[bidiUtil.R] = this.charWidths[bidiUtil.AN] = bidiCharWidth;
|
||
|
this.charWidths[bidiUtil.R_H] = bidiCharWidth * 0.45;
|
||
|
this.charWidths[bidiUtil.B] = this.charWidths[bidiUtil.RLE] = 0;
|
||
|
|
||
|
this.currentRow = null;
|
||
|
};
|
||
|
|
||
|
this.setShowInvisibles = function(showInvisibles) {
|
||
|
this.showInvisibles = showInvisibles;
|
||
|
this.currentRow = null;
|
||
|
};
|
||
|
|
||
|
this.setEolChar = function(eolChar) {
|
||
|
this.EOL = eolChar;
|
||
|
};
|
||
|
|
||
|
this.setContentWidth = function(width) {
|
||
|
this.contentWidth = width;
|
||
|
};
|
||
|
|
||
|
this.isRtlLine = function(row) {
|
||
|
if (this.$isRtl) return true;
|
||
|
if (row != undefined)
|
||
|
return (this.session.getLine(row).charAt(0) == this.RLE);
|
||
|
else
|
||
|
return this.isRtlDir;
|
||
|
};
|
||
|
|
||
|
this.setRtlDirection = function(editor, isRtlDir) {
|
||
|
var cursor = editor.getCursorPosition();
|
||
|
for (var row = editor.selection.getSelectionAnchor().row; row <= cursor.row; row++) {
|
||
|
if (!isRtlDir && editor.session.getLine(row).charAt(0) === editor.session.$bidiHandler.RLE)
|
||
|
editor.session.doc.removeInLine(row, 0, 1);
|
||
|
else if (isRtlDir && editor.session.getLine(row).charAt(0) !== editor.session.$bidiHandler.RLE)
|
||
|
editor.session.doc.insert({column: 0, row: row}, editor.session.$bidiHandler.RLE);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns offset of character at position defined by column.
|
||
|
* @param {Number} the screen column position
|
||
|
*
|
||
|
* @return {int} horizontal pixel offset of given screen column
|
||
|
**/
|
||
|
this.getPosLeft = function(col) {
|
||
|
col -= this.wrapIndent;
|
||
|
var leftBoundary = (this.line.charAt(0) === this.RLE) ? 1 : 0;
|
||
|
var logicalIdx = (col > leftBoundary) ? (this.session.getOverwrite() ? col : col - 1) : leftBoundary;
|
||
|
var visualIdx = bidiUtil.getVisualFromLogicalIdx(logicalIdx, this.bidiMap),
|
||
|
levels = this.bidiMap.bidiLevels, left = 0;
|
||
|
|
||
|
if (!this.session.getOverwrite() && col <= leftBoundary && levels[visualIdx] % 2 !== 0)
|
||
|
visualIdx++;
|
||
|
|
||
|
for (var i = 0; i < visualIdx; i++) {
|
||
|
left += this.charWidths[levels[i]];
|
||
|
}
|
||
|
|
||
|
if (!this.session.getOverwrite() && (col > leftBoundary) && (levels[visualIdx] % 2 === 0))
|
||
|
left += this.charWidths[levels[visualIdx]];
|
||
|
|
||
|
if (this.wrapIndent)
|
||
|
left += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
|
||
|
|
||
|
if (this.isRtlDir)
|
||
|
left += this.rtlLineOffset;
|
||
|
|
||
|
return left;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns 'selections' - array of objects defining set of selection rectangles
|
||
|
* @param {Number} the start column position
|
||
|
* @param {Number} the end column position
|
||
|
*
|
||
|
* @return {Array of Objects} Each object contains 'left' and 'width' values defining selection rectangle.
|
||
|
**/
|
||
|
this.getSelections = function(startCol, endCol) {
|
||
|
var map = this.bidiMap, levels = map.bidiLevels, level, selections = [], offset = 0,
|
||
|
selColMin = Math.min(startCol, endCol) - this.wrapIndent, selColMax = Math.max(startCol, endCol) - this.wrapIndent,
|
||
|
isSelected = false, isSelectedPrev = false, selectionStart = 0;
|
||
|
|
||
|
if (this.wrapIndent)
|
||
|
offset += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
|
||
|
|
||
|
for (var logIdx, visIdx = 0; visIdx < levels.length; visIdx++) {
|
||
|
logIdx = map.logicalFromVisual[visIdx];
|
||
|
level = levels[visIdx];
|
||
|
isSelected = (logIdx >= selColMin) && (logIdx < selColMax);
|
||
|
if (isSelected && !isSelectedPrev) {
|
||
|
selectionStart = offset;
|
||
|
} else if (!isSelected && isSelectedPrev) {
|
||
|
selections.push({left: selectionStart, width: offset - selectionStart});
|
||
|
}
|
||
|
offset += this.charWidths[level];
|
||
|
isSelectedPrev = isSelected;
|
||
|
}
|
||
|
|
||
|
if (isSelected && (visIdx === levels.length)) {
|
||
|
selections.push({left: selectionStart, width: offset - selectionStart});
|
||
|
}
|
||
|
|
||
|
if(this.isRtlDir) {
|
||
|
for (var i = 0; i < selections.length; i++) {
|
||
|
selections[i].left += this.rtlLineOffset;
|
||
|
}
|
||
|
}
|
||
|
return selections;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Converts character coordinates on the screen to respective document column number
|
||
|
* @param {int} character horizontal offset
|
||
|
*
|
||
|
* @return {Number} screen column number corresponding to given pixel offset
|
||
|
**/
|
||
|
this.offsetToCol = function(posX) {
|
||
|
if(this.isRtlDir)
|
||
|
posX -= this.rtlLineOffset;
|
||
|
|
||
|
var logicalIdx = 0, posX = Math.max(posX, 0),
|
||
|
offset = 0, visualIdx = 0, levels = this.bidiMap.bidiLevels,
|
||
|
charWidth = this.charWidths[levels[visualIdx]];
|
||
|
|
||
|
if (this.wrapIndent)
|
||
|
posX -= this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
|
||
|
|
||
|
while(posX > offset + charWidth/2) {
|
||
|
offset += charWidth;
|
||
|
if(visualIdx === levels.length - 1) {
|
||
|
/* quit when we on the right of the last character, flag this by charWidth = 0 */
|
||
|
charWidth = 0;
|
||
|
break;
|
||
|
}
|
||
|
charWidth = this.charWidths[levels[++visualIdx]];
|
||
|
}
|
||
|
|
||
|
if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && (levels[visualIdx] % 2 === 0)){
|
||
|
/* Bidi character on the left and None Bidi character on the right */
|
||
|
if(posX < offset)
|
||
|
visualIdx--;
|
||
|
logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
|
||
|
|
||
|
} else if (visualIdx > 0 && (levels[visualIdx - 1] % 2 === 0) && (levels[visualIdx] % 2 !== 0)){
|
||
|
/* None Bidi character on the left and Bidi character on the right */
|
||
|
logicalIdx = 1 + ((posX > offset) ? this.bidiMap.logicalFromVisual[visualIdx]
|
||
|
: this.bidiMap.logicalFromVisual[visualIdx - 1]);
|
||
|
|
||
|
} else if ((this.isRtlDir && visualIdx === levels.length - 1 && charWidth === 0 && (levels[visualIdx - 1] % 2 === 0))
|
||
|
|| (!this.isRtlDir && visualIdx === 0 && (levels[visualIdx] % 2 !== 0))){
|
||
|
/* To the right of last character, which is None Bidi, in RTL direction or */
|
||
|
/* to the left of first Bidi character, in LTR direction */
|
||
|
logicalIdx = 1 + this.bidiMap.logicalFromVisual[visualIdx];
|
||
|
} else {
|
||
|
/* Tweak visual position when Bidi character on the left in order to map it to corresponding logical position */
|
||
|
if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && charWidth !== 0)
|
||
|
visualIdx--;
|
||
|
|
||
|
/* Regular case */
|
||
|
logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
|
||
|
}
|
||
|
|
||
|
if (logicalIdx === 0 && this.isRtlDir)
|
||
|
logicalIdx++;
|
||
|
|
||
|
return (logicalIdx + this.wrapIndent);
|
||
|
};
|
||
|
|
||
|
}).call(BidiHandler.prototype);
|
||
|
|
||
|
exports.BidiHandler = BidiHandler;
|
||
|
});
|