big-moving.ru/api/soft/ajaxorg/lib/ace/bidihandler.js

390 lines
15 KiB
JavaScript
Raw Normal View History

2022-06-24 15:29:23 +05:00
/* ***** 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;
});