Javascript html editor cursor focus following mouse - javascript

I use a dhtml (midas) editor as a html editor in my web application, what I want to do is get a focused cursor in this html editor follow the mouse, is there a way to do that?
Added Example:
I want cursor in textarea follow the mouse so if you have a big text in your textarea and you are going over it with mouse, cursor (text cursor) should follow the mouse, like this:
"This is an ex|ample text" - if mouse is over "example" word and between x and a, text cursor (|) should be focused there but when I move mouse on for example "text" cursor | should be between letters where mouse is currently located.

Ok I found the solution using Ext.util.TextMetrics, first I get position of every character in editor, then I compare that to mouse cursor position and then update midas selection based on given character from charNum array
htmlEditor.getEl().on('mousemove', function(e)
{
var charNum = {},
text = htmlEditor.getValue(),
fWidth = htmlEditor.getWidth();
var textMetrics = new Ext.util.TextMetrics(htmlEditor.getEl(), htmlEditor.getWidth());
for(var n=0;n<text.length;n++)
{
var dat = text.substring(0, n)
var width = textMetrics.getWidth(dat);
var height = textMetrics.getHeight(dat);
if(width > fWidth)
{
var mult = Math.ceil(width/fWidth)
var width = width % fWidth;
height = height*mult;
}
charNum[n] = [width, height];
}
//console.log("charNum: "+charNum.toSource());
var mX = e.getX();
var mY = e.getY();
var cXY = htmlEditor.getEl().getXY();
var cX = cXY[0];
var cY = cXY[1];
var x = mX-cX-20;
var y = mY-cY;
//console.log("fin xy: "+x+' '+y);
var n = -1;
var z = 0;
for(key in charNum)
{
if(charNum[key][0] > x && charNum[key][1] > y)
{
n = key-1;
break;
}
n++;
z++;
}
if(x < 0 && y < 14) n = -1;
if(n == (z-1) && n != -1)
{
n++;
}
var selection = htmlEditor.win.getSelection();
range = selection.getRangeAt(0);
range.selectNodeContents(htmlEditor.getEditorBody());
range.collapse(true);
for(var x=0;x<n;x++)
{
selection.modify("move", "forward", "character");
}
});

Try Activates ExtJs HtmlEditor textarea when it is loaded and Sencha Docs, the official docs says:
Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
supported by this editor.

I haven't tried the solution of #dfilkovi, but albeit it can be correct, bear in mind that any solution binding an event to mousemove will mostly certain cause a huge overhead on cpu.
To alleviate this symptom, you could unbind the listener first thing at the handler and then set a timeout to bind it after a few millisecs; something like:
// assume HandleOriginal as the original function declared by #dfilkovi
// attach the listener
startListener();
// functions
function startListener() {
htmlEditor.getEl().on('mousemove', HandleAndWait);
}
function stopListener() {
// maybe this is not the right syntax
htmlEditor.getEl().on('mousemove', null);
}
function HandleAndWait(e) {
var C_SLEEP = 50;
stopListener();
try { HandleOriginal(e); }
finally { window.setTimeout(startListener, C_SLEEP); }
}
You can, then, fine tune the value of C_SLEEP to the best user experience.

Related

Can I divide a text box by paragraph in Google Slides using Apps Script?

I am trying to design some code in Apps Script that can be put on any Google Slides presentation and split every text box by paragraphs so every paragraph has its own text box.
I started out using var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); to make the new text boxes like google describes to use in most of its tutorials but it 'couldn't identify the TEXT_BOX type' so I found .insertTextBox and that seems to work better but I've found other problems.
I can use .getParagraphs to find the number of paragraphs in a text box but I can't tell if it doesn't include the contents of each paragraph or if I'm just not using the correct command to get the text from the paragraph. I have also tried to find an alternative to find the beginning of each paragraph and divide the text from there but I can't find a command for that either. Maybe would I have to use .indexOf to find each /n or /r, or is there a simpler way?
I'm also having a problem where my equations to divide up the text box size are giving me undefined answers and I've tried declaring the variables as numbers but it just makes things worse.
function myFunction() { // get slides in the presentation and establish 'for' variables
var slide = SlidesApp.getActivePresentation().getSlides();
var i;
var j;
var k;
for (i = 0; i < slide.length; i++) { // get the text boxes on each slide
var text = slide[i].getShapes();
for (j = 0; j < text.length; j++) { // get the location of and the paragraphs in each textbox (locations don't work)
var top = text[j].getTop;
var left = text[j].getLeft;
var width = text[j].getWidth;
var height = text[j].getHeight;
var paragraph = text[j].getText().getParagraphs();
for (k = 0; k < paragraph.length; k++){ // make a textbox for each paragraph distributed vertically over the original textbox
var content = text[j].getRange(paragraph[k]); //I was hoping this would fill with the contents of current paragraph
var shapeheight = height / paragraph.length; //NaN and I don't know why
var shapetop = height * k + top; //also doesn't work these should all be numbers
slide[i].insertTextBox(content, left, shapetop, width, shapeheight);
}
text[j].remove(); //delete original textbox on slide
}
}
}
Here are pictures of what I'm trying to do:
Slide before intended changes
Approximate slide after intended changes
I believe your goal as follows.
You want to split each paragraph in a text box as each text box on Google Slides.
You want to achieve this using Google Apps Script.
Modification points:
In your script,
getTop, getLeft, getWidth and getHeight are the method. So please add ().
About var content = text[j].getRange(paragraph[k]), getRange has no arguments.
About var shapeheight = height / paragraph.length, in this case, this can be put outof the for loop.
About var shapetop = height * k + top, in this case, that might be var shapetop = shapeheight * k + top.
When above points are reflected to your script, it becomes as follows.
Modified script:
function myFunction() {
var slide = SlidesApp.getActivePresentation().getSlides();
var i;
var j;
var k;
for (i = 0; i < slide.length; i++) {
var text = slide[i].getShapes();
for (j = 0; j < text.length; j++) {
var top = text[j].getTop(); // Modified
var left = text[j].getLeft(); // Modified
var width = text[j].getWidth(); // Modified
var height = text[j].getHeight(); // Modified
var paragraph = text[j].getText().getParagraphs();
var shapeheight = height / paragraph.length; // Modified
for (k = 0; k < paragraph.length; k++) {
var content = paragraph[k].getRange().asString(); // Modified
var shapetop = shapeheight * k + top; // Modified
slide[i].insertTextBox(content, left, shapetop, width, shapeheight);
}
text[j].remove();
}
}
}
Note:
In the current stage, it seems that AutoFit cannot be set. By this, when slide[i].insertTextBox(content, left, shapetop, width, shapeheight) is used, the text deviates a little from the box. So in this case, how about not using shapeheight? In this case, please modify slide[i].insertTextBox(content, left, shapetop, width, shapeheight); as follows.
var t = slide[i].insertTextBox(content);
t.setLeft(left);
t.setTop(shapetop);
t.setWidth(width);
References:
getTop()
getLeft()
getWidth()
getHeight()
getRange()
insertTextBox(text)

textRange MoveToPoint() IE

Have a problem with moveToPoint() method of textRange IE11;
Seems like it dosen't work if pointed node wasn't in first screen;
document.addEventListener( "click", function(e) {
var x = e.clientX;
var y = e.clientY;
var range = document.body.createTextRange();
range.moveToPoint(x, y);
range.expand('word');
console.log(range);
console.log(range.text);
});
This code grab words from click point, but it wokrs normal only if we clicking in node's that were on first scroll.
If we scroll little bit down to the node that wasnt in first scroll, we will catch the exeception.
Does anybody know how to handle this situation correctly?
You can use offsetX, offsetY properties.
Or you can add scroll position to x and y vars, using scrollLeft and scrollTop properties of parent element.
I can confirm that such a bug exists in IE11. You can find details here in comments: http://generatedcontent.org/post/69213745095/ie11review-part1
Possible solution (after you create a range):
range.moveToElementText(e.target);
range.collapse(true)
range.expand("word")
Now you have the first word selected. You have to check now if the selected word fits your mouse click position using TextRange properties boundingHeight, boundingWidth, boundingLeft and boundingTop. If it doesn't, you move in cycle to the next word:
range.collapse(false);
range.expand("word");
Well, inspired by #dinalt and #JAYBEkster, I came up this solution. Maybe someone will need this after all.
Code below(for IE, didn't check all versons, but it works fine 11+) grab word in complex nested html.
Step bys step how it works:
Firstable we create range from e.target
We collapse it to the begining.
That will be awesome if we just could expand("word") and iterate every each word, but unfortunantly we cant. So we loop through characters split them to words and compare their bounding's with e.clientX, and Y, in the other hand by iterating charakters insted of words we get more control.
We get correct one and split target.innerText with it, to get left and right part of node's text.
After that we split each part again but this our separators (regexp with all white spaces and word separaters i could imagine) and we get two arr's.
Why we do steps 4 and 5? Cause we find some text that end's in that node, but it may not be the end or start of ther word, for some styling reasons, acent part of ther word my bein it's own node and target.node my just be the middle part of the word. So we have to jump up throug node's and find word's ending.
If u sure that ur html dosen't complain that u can skip all other steps.
Then if arr is empty we run our recursive function that runs throug node's and grab's text until find separator.
Yeah it really look like roket since for such, that we mayed think simple task.
But I actualy couldn't find a better solution, There are was a couple of options, bu all of them wasnt's as universal as I need it to be.
Benefits of this code that it really dosen't care about complexivity of html at all.
I will post here the link to my github repositiry where u can find the full code of wordGraber, that will work this Chrome, Safari, FF and IE of course.
https://github.com/6graNik/wordGrabber
Below is just a part for IE.
function getWordFromEventIE(e) {
const x = e.clientX;
const y = e.clientY;
const innerText = e.target && e.target.innerText;
const separators = /([\s&^:;,!?(){}])+/;
const IErange = global.document.body.createTextRange();
try {
IErange.moveToElementText(e.target);
IErange.collapse(true);
let wholeSentenceLength = 0;
const reqIEcharTest = () => {
do {
IErange.expand("character");
wholeSentenceLength += 1;
}
while (!separators.test(IErange.text.slice(-1)) && wholeSentenceLength <= innerText.length);
const {boundingLeft, boundingTop, boundingWidth, boundingHeight, text} = IErange;
if (boundingLeft <= x && x <= (boundingLeft + boundingWidth)
&& boundingTop <= y && y <= (boundingTop + boundingHeight)) {
if (wholeSentenceLength <= innerText.length && !separators.test(text.slice(-1)) ) {
return text;
}
return text.substr(0, text.length - 1);
}
IErange.collapse(false);
return reqIEcharTest();
};
const text = reqIEcharTest().trim();
const innerTextArr = innerText.split(text);
const innerTextLeft = innerTextArr[0].split(separators);
const innerTextRight = innerTextArr[1].split(separators);
let leftPart;
if (innerTextLeft <= 1) {
leftPart = recursionWordGet(e.target, 'left') + innerTextLeft.slice(-1)[0];
} else {
leftPart = innerTextLeft.slice(-1)[0];
}
let rightPart;
if (innerTextRight <= 1) {
rightPart = innerTextRight[0] + recursionWordGet(e.target, 'right');
} else {
rightPart = innerTextRight[0];
}
return leftPart + text + rightPart;
} catch (err) {
console.log('>>>>>>>>>>>>>>>>>> text', err);
}
}
function recursionWordGet(target, option) {
const separators = /([\s&^:;,!?(){}])+/;
const uniqString = Date.now();
target.setAttribute("data-target", uniqString);
const {parentNode} = target;
const copyNode = parentNode.cloneNode(true);
copyNode.querySelector(`[data-target="${uniqString}"]`).innerText = uniqString;
const tagName = copyNode.tagName;
const text = copyNode.innerText;
const textArr = text.split(uniqString);
const textLeftPartArr = textArr[0].split(separators);
const textRightPartArr = textArr[1].split(separators);
if (option === 'right') {
let returnText;
if (textRightPartArr.length <= 1 && tagName === 'span') {
returnText = textRightPartArr[0] + recursionWordGet(parentNode, 'right');
} else {
returnText = textRightPartArr[0];
}
return returnText;
}
if (option === 'left') {
let returnText;
if (textLeftPartArr <= 1 && tagName === 'span') {
returnText = recursionWordGet(parentNode, 'left') + textLeftPartArr.slice(-1)[0];
} else {
returnText = textLeftPartArr.slice(-1)[0];
}
return returnText;
}
return '';
}

the boundingClientRect of range not calculate properly when using range.expand('word') in inline elements

I'm working on a chrome translate extension that when holding the ctrl key for seconds,the extension get the word under the cursor,translate it,and then display the result on the top of the word.
When dealing with getting the word under the cursor,I first need to create the range of the word under the cursor.I use the following code snippet to achieve this.I reference to here.https://stackoverflow.com/a/3710561/4244369
var getRangeAtPoint = function(elem, x, y) {
if (elem.nodeType == elem.TEXT_NODE) {
var range = elem.ownerDocument.createRange();
range.selectNodeContents(elem);
var currentPos = 0;
var endPos = range.endOffset;
while (currentPos + 1 < endPos) {
range.setStart(elem, currentPos);
range.setEnd(elem, currentPos + 1);
var range_rect = range.getBoundingClientRect();
if (range_rect.left <= x && range_rect.right >= x &&
range_rect.top <= y && range_rect.bottom >= y) {
range.expand("word");
return range;
}
currentPos += 1;
}
} else {
for (var i = 0; i < elem.childNodes.length; i++) {
var range = elem.childNodes[i].ownerDocument.createRange();
range.selectNodeContents(elem.childNodes[i]);
var range_rect = range.getBoundingClientRect();
if (range_rect.left <= x && range_rect.right >= x &&
range_rect.top <= y && range_rect.bottom >= y) {
range.detach();
var computed_range = getRangeAtPoint(elem.childNodes[i], x, y);
if(computed_range){
return computed_range;
}
} else {
range.detach();
}
}
}
return (null);
};
After creating the range,I can use range.toString() to get the word and range.getBoundingClientRect() to decide the position to display the result.It works well until I met the following case:
<p>click the <a href='#'>sample words</a> here</p>
If the cursor is under the word "words",it works properly.However,when the cursor is under the word "sample",after calling range.expand('word'),the client rect is wrong,the width of client rect should be the width of "sample",however,it's the width of "sample words".
I also include a jsfiddle here.https://jsfiddle.net/sangelee/1maqmm89/
Is it the problem of range.expand('word')?How to fix it?Or instead of using range.expand('word'),are there any way to achieve this?Any help is appreciated!ps.I'm using chrome 39.
The issue is with range.expand(), as you suspected. Also, the code to get the caret position as a range can be vastly simplified. Example (WebKit only):
https://jsfiddle.net/e5knrLv8/
The console also reveals that range.expand(), which has always been a WebKit-only non-standard method, has been deprecated in favour of Selection.modify(), which unfortunately is a bit of a pain to use in practice. Here is a revised example using Selection.modify, which does fix your issue:
https://jsfiddle.net/e5knrLv8/1/

How to count the number of line boxes in a DIV or P

<div><span>aaaaaa</span> ... (many other span here) ... <span>zzzzzz</span></div>
In that case, the boxes span are placed on few line-boxes inside the div.
(The span elements can use different font-size.)
1) How can we know the number of the line-boxes ?
2) Can we know on which line-boxe an element span is placed ?
3) Can we know on which line-boxe the caret is placed (contenteditable) ?
Thank you
I'll suppose the DOM in your example is an effective example of the actual complexity of your DOM, and that a "line-boxe" is just a line of text.
1-2) For every <span> inside the <div>, you can count the number of lines they span with something like this:
var spans = div.getElementsByTagName("span"), spandata = [];
for (var i = 0; i < spans.length; i++) {
var rects = spans[i].getClientRects();
if (i > 0)
if (rects[0].bottom > obj.rects[obj.rects - 1].bottom)
var inirow = obj.lastRow + 1;
else var inirow = obj.lastRow;
var obj = {
element: spans[i],
rects: rects,
iniRow: inirow,
lastRow: inirow + rects.length - 1
};
spandata.push(obj);
}
Now spandata is a list of all the data you want about the <span> elements. I'm also supposing that each one of them may span through more than one line.
Keep in mind that getClientRects has some issues in IE<8.
3) In modern browsers, the getSelection method can help you:
var sel = window.getSelection();
if (sel.type === "Caret")
var span = sel.anchorNode.parentNode;
About the line position, I must say it's not an easy task. You can't easily get the page position of the caret. The simplest thing you can do is to place a dummy inline element in the place of the caret:
var text = sel.anchorNode.nodeValue;
sel.anchorNode.nodeValue = text.substring(0, sel.anchorOffset);
var dummy = document.createElement("i");
span.appendChild(dummy);
var pos = dummy.getBoundingClientRect();
sel.anchorNode.nodeValue = text;
span.removeChild(dummy);
pos contains the info of the position of the caret. Now you have to compare them with the rect infos about the span:
var rects = span.getClientRects();
for (var i = 0; i < rects.length; i++)
if (rects[i]].bottom === pos.bottom) break;
if (i < rects.length) {
for (var i = 0; i < spandata.length; i++) {
if (spandata[i].element === span) {
var line = spandata[i].iniRow + i;
break;
}
}
}
In the end, if line != null, it contains the line of the caret.
Man, that was complicated...
Let's say your div is in the el variable:
el.children.length; // Number of direct children
// You have one of the children in the "child" variable, to know its index:
[].indexOf.call( el.children, child ); // Index of child in el.children
I'm not mentioning the cross-browser issues there, but Array.prototype.indexOf is only available starting IE9 (so it works in all modern browsers).

Character offset in an Internet Explorer TextRange

As far as I can tell there's no simple way of retrieving a character offset from a TextRange object in Internet Explorer. The W3C Range object has a node, and the offset into the text within that node. IE seems to just have pixel offsets. There are methods to create, extend and compare ranges, so it would be possible to write an algorithm to calculate the character offset, but I feel I must be missing something.
So, what's the easiest way to calculate the character offset of the start of an Internet Explorer TextRange?
I use a method based on this caret position trick:
// Assume r is a range:
var offsetFromBody = Math.abs( r.moveEnd('character', -1000000) );
Since moveEnd returns the number of characters actually moved, offset should now be the offset from the start of the document. This works fine for testing primitive caret movement, but for expanded selections and for getting the exact node that holds the range anchor you'll need something more complex:
// where paramter r is a range:
function getRangeOffsetIE( r ) {
var end = Math.abs( r.duplicate().moveEnd('character', -1000000) );
// find the anchor element's offset
var range = r.duplicate();
r.collapse( false );
var parentElm = range.parentElement();
var children = parentElm.getElementsByTagName('*');
for (var i = children.length - 1; i >= 0; i--) {
range.moveToElementText( children[i] );
if ( range.inRange(r) ) {
parentElm = children[i];
break;
}
}
range.moveToElementText( parentElm );
return end - Math.abs( range.moveStart('character', -1000000) );
}
This should return the correct caret text offset. Of course, if you know the target node already, or are able to provide a context, then you can skip the whole looping search mess.
I'd suggest IERange, or just the TextRange-to-DOM Range algorithm from it.
Update, 9 August 2011
I'd now suggest using my own Rangy library, which is similar in idea to IERange but much more fully realized and supported.
I used a slightly simpler solution using the offset values of a textRange:
function getIECharOffset() {
var offset = 0;
// get the users selection - this handles empty selections
var userSelection = document.selection.createRange();
// get a selection from the contents of the parent element
var parentSelection = userSelection.parentElement().createTextRange();
// loop - moving the parent selection on a character at a time until the offsets match
while (!offsetEqual(parentSelection, userSelection)) {
parentSelection.move('character');
offset++;
}
// return the number of char you have moved through
return offset;
}
function offsetEqual(arg1, arg2) {
if (arg1.offsetLeft == arg2.offsetLeft && arg1.offsetTop == arg2.offsetTop) {
return true;
}
return false;
}
You can iterate through the body element's TextRange.text property using String.substring() to compare against the TextRange for which you want the character offset.
function charOffset(textRange, parentTextRange)
{ var parentTxt = parentTextRange.text;
var txt = textRange.text;
var parentLen = parentTxt.length;
for(int i=0; i < parentLen ; ++i)
{ if (parentTxt.substring(i, txt.length+i) == txt)
{ var originalPosition = textRange.getBookmark();
//moves back one and searches backwards for same text
textRange.moveStart("character",-1);
var foundOther = textRange.findText(textRange.text,-parentLen,1);
//if no others were found return offset
if (!foundOther) return i;
//returns to original position to try next offset
else textRange.moveToBookmark(originalPosition);
}
}
return -1;
}
[Reference for findText()]

Categories