This repository has been archived by the owner on Sep 18, 2021. It is now read-only.
Permalink
RTLtextarea/src/RTLText.module.js
Newer
100644
397 lines (346 sloc)
11.4 KB
1
/*
2
* RTLText
3
* Copyright 2012 Twitter and other contributors
4
* Released under the MIT license
5
*
6
* What it does:
7
*
8
* This module will set the direction of a textarea to RTL when a threshold
9
* of RTL characters has been reached (rtlThreshold). It also applies Twitter-
10
* specific RTL rules regarding the placement of @ signs, # tags, and URLs.
11
*
12
* How to use:
13
*
14
* Bind keyup and keydown to RTLText.onTextChange. If you have initial text,
15
* call RTLText.setText(textarea, initial_string) to set markers on that
16
* initial text.
17
*/
23
24
/*
25
* Right-to-left Unicode blocks for modern scripts are:
26
*
27
* Consecutive range of the main letters:
28
* U+0590 to U+05FF - Hebrew
29
* U+0600 to U+06FF - Arabic
30
* U+0700 to U+074F - Syriac
31
* U+0750 to U+077F - Arabic Supplement
32
* U+0780 to U+07BF - Thaana
33
* U+07C0 to U+07FF - N'Ko
34
* U+0800 to U+083F - Samaritan
35
*
36
* Arabic Extended:
37
* U+08A0 to U+08FF - Arabic Extended-A
38
*
39
* Consecutive presentation forms:
40
* U+FB1D to U+FB4F - Hebrew presentation forms
41
* U+FB50 to U+FDFF - Arabic presentation forms A
42
*
43
* More Arabic presentation forms:
44
* U+FE70 to U+FEFF - Arabic presentation forms B
45
*/
46
var rtlChar = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg;
47
var dirMark = /\u200e|\u200f/mg;
48
var ltrMark = "\u200e";
49
var rtlMark = "\u200f";
50
var keyConstants = {
51
BACKSPACE: 8,
52
DELETE: 46
53
};
54
var twLength = 0;
55
var DEFAULT_TCO_LENGTH = 22;
56
var tcoLength = null;
57
var isRTL = false;
58
var originalText = "";
59
var originalDir = "";
60
// Can't use trim cause of IE. Regex from here: http://stackoverflow.com/questions/2308134/trim-in-javascript-not-working-in-ie
61
var trimRegex = /^\s+|\s+$/g;
62
63
var setManually = false;
64
var heldKeyCodes = { '91': false,
65
'16': false,
66
'88': false,
67
'17': false };
68
var useCtrlKey = navigator.userAgent.indexOf('Mac') === -1;
69
70
/* Private methods */
71
72
// Caret manipulation
73
function elementHasFocus (el) {
74
// Try/catch to fix a bug in IE that will cause 'unspecified error' if another frame has focus
75
try {
76
return document.activeElement === el;
77
}
78
catch (err) {
79
return false;
80
}
81
}
82
83
function getCaretPosition (el) {
84
if (!elementHasFocus(el)) { return 0; }
85
86
var range;
87
if (typeof el.selectionStart === "number") {
88
return el.selectionStart;
89
}
90
else if (document.selection) {
91
el.focus();
92
range = document.selection.createRange();
93
range.moveStart('character', -el.value.length);
94
var length = range.text.length;
95
return length;
96
}
97
}
98
99
function setCaretPosition (el, position) {
100
if (!elementHasFocus(el)) { return; }
102
if (typeof el.selectionStart === "number") {
103
el.selectionStart = position;
104
el.selectionEnd = position;
105
}
106
else if (document.selection) {
107
var range = el.createTextRange();
108
range.collapse(true);
109
range.moveEnd('character', position);
110
range.moveStart('character', position);
111
range.select();
112
}
113
}
114
115
// End of caret methods
116
117
function replaceIndices (oldText, extractFn, replaceCb) {
118
var lastIndex = 0;
119
var newText = '';
122
for (var i = 0; i < extractedItems.length; i++) {
123
var item = extractedItems[i];
124
var type = '';
126
if (item.screenName) {
127
type = 'screenName';
128
}
129
if (item.hashtag) {
130
type = 'hashtag';
131
}
132
if (item.url) {
133
type = 'url';
134
}
139
var respObj = {
140
entityText: oldText.slice(item.indices[0], item.indices[1]),
141
entityType: type
142
};
144
newText += oldText.slice(lastIndex, item.indices[0]) + replaceCb(respObj);
145
lastIndex = item.indices[1];
146
}
147
return newText + oldText.slice(lastIndex, oldText.length);
148
}
149
150
// Handle all LTR/RTL markers for tweet features
151
function setMarkers (plainText) {
152
var matchedRtlChars = plainText.match(rtlChar);
153
var text = plainText;
154
if (matchedRtlChars || originalDir === "rtl") {
155
text = replaceIndices(text, twttr.txt.extractEntitiesWithIndices, function (itemObj) {
156
if (itemObj.entityType === "screenName") {
157
return ltrMark + itemObj.entityText + rtlMark;
158
}
159
else if (itemObj.entityType === "hashtag") {
160
return (itemObj.entityText.charAt(1).match(rtlChar)) ? itemObj.entityText : ltrMark + itemObj.entityText;
161
}
162
else if (itemObj.entityType === "url") {
163
return itemObj.entityText + ltrMark;
164
}
168
});
169
}
170
return text;
171
}
172
173
// If a user deletes a hidden marker char, it will just get rewritten during
174
// notifyTextUpdated. Special case this by continuing to delete in the same
177
var offset;
178
var textarea = (e.target) ? e.target : e.srcElement;
179
var key = (e.which) ? e.which : e.keyCode;
180
if (key === keyConstants.BACKSPACE) { // backspace
181
offset = -1;
182
} else if (key === keyConstants.DELETE) { // delete forward
183
offset = 0;
184
} else {
185
return;
186
}
188
var pos = getCaretPosition(textarea);
189
var text = textarea.value;
190
var numErased = 0;
191
var charToDelete;
192
do {
193
charToDelete = text.charAt(pos + offset) || '';
194
// Delete characters until a non-marker is removed.
195
if (charToDelete) {
196
pos += offset;
197
numErased++;
198
text = text.slice(0, pos) + text.slice(pos + 1, text.length);
199
}
200
} while (charToDelete.match(dirMark));
202
if (numErased > 1) {
203
textarea.value = text;
204
// If more than 1 needed to be removed, update the text
205
// and caret manually and stop the event.
206
setCaretPosition(textarea, pos);
207
e.preventDefault ? e.preventDefault() : e.returnValue = false;
208
}
209
}
210
211
function removeMarkers (text) {
212
return text ? text.replace(dirMark, '') : '';
213
}
214
215
function shouldBeRTL (plainText) {
216
var matchedRtlChars = plainText.match(rtlChar);
217
// Remove original placeholder text from this
218
plainText = plainText.replace(originalText, "");
219
var urlMentionsLength = 0;
220
var trimmedText = plainText.replace(trimRegex, '');
223
if (!trimmedText || !trimmedText.replace(/#/g, '')) {
224
return defaultDir === 'rtl' ? true : false; // No text, use default.
229
}
230
231
if (plainText) {
232
var mentions = twttr.txt.extractMentionsWithIndices(plainText);
233
var mentionsLength = mentions.length;
234
var i;
235
236
for (i = 0; i < mentionsLength; i++) {
237
urlMentionsLength += mentions[i].screenName.length + 1;
240
var urls = twttr.txt.extractUrlsWithIndices(plainText);
241
var urlsLength = urls.length;
247
}
248
var length = trimmedText.length - urlMentionsLength;
249
return length > 0 && matchedRtlChars.length / length > rtlThreshold;
250
}
251
252
function detectManualDirection (e) {
253
var textarea = e.target || e.srcElement;
254
if (e.type === "keydown" && (e.keyCode === 91 || e.keyCode === 16 || e.keyCode === 88 || e.keyCode === 17)) {
255
heldKeyCodes[String(e.keyCode)] = true;
256
}
257
else if (e.type === "keyup" && (e.keyCode === 91 || e.keyCode === 16 || e.keyCode === 88 || e.keyCode === 17)) {
258
heldKeyCodes[String(e.keyCode)] = false;
259
}
260
261
if (((!useCtrlKey && heldKeyCodes['91']) || (useCtrlKey && heldKeyCodes['17'])) && heldKeyCodes['16'] && heldKeyCodes['88']) {
262
setManually = true;
263
264
if (textarea.dir === 'rtl') {
265
setTextDirection('ltr', textarea);
266
}
267
else {
268
setTextDirection('rtl', textarea);
269
}
270
heldKeyCodes = { '91': false,
271
'16': false,
272
'88': false,
273
'17': false };
274
}
275
}
276
277
function setTextDirection (dir, textarea) {
278
textarea.setAttribute('dir', dir);
279
textarea.style.direction = dir;
280
textarea.style.textAlign = (dir === 'rtl' ? 'right' : 'left');
281
}
282
283
/* Public methods */
284
285
// Bind this to *both* keydown & keyup
286
that.onTextChange = function (e) {
287
var event = e || window.event;
288
289
detectManualDirection(e);
290
291
// Handle backspace through control characters:
292
if (event.type === "keydown") {
293
erasePastMarkers(event);
294
}
299
// Optionally takes a second param, with original text, to exclude from RTL/LTR calculation
300
that.setText = function (textarea) {
301
// Original directionality could be in a few places. Check them all.
302
if (!originalDir) {
303
if (textarea.style.direction) {
304
originalDir = textarea.style.direction;
305
}
306
else if (textarea.dir) {
307
originalDir = textarea.dir;
308
}
309
else if (document.body.style.direction) {
310
originalDir = document.body.style.direction;
311
}
312
else {
313
originalDir = document.body.dir;
314
}
315
}
318
originalDir = textarea.ownerDocument.documentElement.className;
319
originalText = arguments[1];
320
}
323
if (!text) {
324
return;
325
}
326
var plainText = removeMarkers(text);
327
isRTL = shouldBeRTL(plainText);
328
var newText = setMarkers(plainText);
329
var newTextDir = (isRTL ? 'rtl' : 'ltr');
330
331
if (newText !== text) {
332
var pos = getCaretPosition(textarea);
333
// Fix for Chrome for Android
334
textarea.value = "";
335
textarea.focus();
336
textarea.value = newText;
337
// Assume any recent change in text length due to markers affects the
338
// current cursor position. If more accuracy is needed, the position
339
// could be translated during replace operations inside setMarkers.
340
setCaretPosition(textarea, pos + newText.length - plainText.length);
342
if (!setManually) {
343
setTextDirection(newTextDir, textarea);
344
}
346
347
// Use this to get the length of a tweet with unicode control characters removed
348
that.textLength = function (text) {
349
var tweet = removeMarkers(text);
350
var urls = twttr.txt.extractUrls(tweet);
351
var length = tweet.length - urls.join('').length;
352
var urlsLength = urls.length;
353
var tcoLength = that.getTcoLength();
354
for (var i = 0; i < urlsLength; i++) {
355
length += tcoLength;
356
if (/^https:/.test(urls[i])) {
357
length += 1;
358
}
365
that.cleanText = function (text) {
368
369
// If markers need to be added to a string without affecting the text box, use this
370
that.addRTLMarkers = function (s) {
371
return setMarkers(s);
373
374
// For determining if text should be RTL (returns boolean)
375
that.shouldBeRTL = function (s) {
379
that.getTcoLength = function () {
380
return tcoLength || DEFAULT_TCO_LENGTH;
381
};
382
that.setTcoLength = function (length) {
383
if (length > 0) {
384
tcoLength = parseInt(length, 10);
385
} else {
386
tcoLength = null;
387
}
388
};
389
390
return that;
391
392
}();
393
394
if (typeof module !== 'undefined' && module.exports) {
395
module.exports = RTLText;
396
}