How to measure word/caret position in Google Docs? - javascript

For those who haven't worked with the Google Docs editor here's a short explanation of how it works:
Google Docs has no visible editable textarea or contentEditable elements.
Google Docs listens for keydown/press/up in a separate iFrame where they place the OS cursor for event listening.
When the iFrame catches an event Google handles it by performing the equivalent operations on the visible document.
The "caret" in Google Docs is a DIV that is styled and scripted to look and act like an OS cursor.
With that out of the way, here's my request:
I'm working on a plugin that interacts with the Google Doc and I need to be able to do two things:
Highlight words with an opaque overlay DIV.
Determine cursor position inside a word.
I've been exhausting a lot of ideas about just how to handle this, but so far I've only manage to get a buggy solution for the latter problem (I perform a backspace, determine where the text changed and undo the backspace).
I'm looking for all the best ideas you can come up with to solve these problems. They don't need to be cross browser, but they do need to be able to be turned into something robust that will also handle things such as font size changed mid line.
A little bit of extra info explaining what a Google Doc looks like in HTML:
<wrapper> // Simplified wrapper containing margins, pagination and similar
<div class="kix-paragraphrenderer"> // single DIV per page wrapping all content
// Multiple paragraphs separated by linebreak created by Enter key:
<div class="kix-paragraphrendeder">...</div>
<div class="kix-paragraphrendeder">...</div>
<div class="kix-paragraphrendeder">
// Multiple wrapper divs created by Google's word wrapping:
<div class="kix-lineview">...</div>
<div class="kix-lineview">...</div>
<div class="kix-lineview">
// Single inner wrapper, still full width of first wrapper paragraph:
<div class="kix-lineview-content">
// Single wrapper SPAN containing full text of the line, but not display:block
<span class="kix-lineview-text-block">
// Multiple spans, one per new font change such as normal/bold text,
// change in font size, indentation and similar:
<span>This is normal text</span>
<span style="font-size:40px; padding-left:4px;">This larger text.</span>
<span style="font-weight:bold; padding-left:10px;">This is bold text</span>
<span style="padding-left:4px;">More normal text</span>
</span>
</div>
</div>
</div>
</div>
</wrapper>

After more tinkering I came to the conclusion that it is extremely troublesome - if not impossible - to try and programmatically determine cursor position with regard to a letter inside a <span>, simply because the <span> is the smallest element that is measurable (correct me if I am wrong).
So how to solve the problem? Here's what I ended up doing:
I create an offscreen positioned <div>
I get the text of the current paragraph (<div class="kix-paragraphrenderer">) - I could get the entire text, but wanted to limit the computational load.
I extract each single character of the paragraph by looping through its children in the following way:
Loop through linveviews of the paragraph (<div class="kix-lineview">)
Get the lineview content (<div class="kix-lineview-content">)
Loop through text blocks of the lineview content (<span class="kix-lineview-text-block">)
Loop through <span>'s of the text block
Loop through innerText of the <span>
I append each character in my offscreen <div> with the currently applied style extracted from style.cssText of the current <span>
For each character appended I measure the width of the <div> and save this in an array. I now have a position of each single character.
I measure the position of the cursor relative to my widths and voila - I know where the cursor is positioned in the text.
This is obviously a bit simplied (I left out details about margins and paddings of the different elements), but it covers the idea behind how it's possible to get the cursor position.
It works quite well, but there are many pitfalls and a lot of measuring required. On top of that it's also required to post-parse the text if you want to use it for anything, since tabs, spaces and linebreaks aren't always included in innerText (depending on where these are in the text, Google may or may not make them through positioning of new elements).

I made something like Kix two years ago Google Docs. And for any HTML design and yes, for IE6 too :-) How? All we need is to compute letter absolute position. How? Replace textNode with inline element without layout, that's important, and then use Element.getClientRects I remember I also needed wrap just letter and compute its position via fast and reliable https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect
The trick how to detect lines and wraps for home and end keys was based on some vertical heuristic letter position change. Something like if base line is different, than stop caret walking. It was pretty fast and with any markup and without any caching. Holy grail :)
The only not resolvable problem was justified text, because letters were distributed randomly and spaces between them was not computable.
That project is dead http://webeena.com now. Bad management killed it (and me almost too).

Related

Contenteditable get position including HTML

I'm working on a way to create annotations in HTML text using JavaScript. The way it works, the user clicks on a contenteditable <div> and I get the position of the cursor based on where they click. Then, when I'm placing the annotation in the text, I go to that character position to insert a footnote indicator. The problem is that the position of the cursor in the contenteditable <div> doesn't take HTML tags into account. So for example, if the <div> contained the following:
AB<b>CD</b>EF
And I placed the cursor between "C" and "D", the position is 3 because it ignores the <b>. Is there a way to get the cursor position including HTML tags so that it is consistent when I go back and place my markers? The <div> doesn't necessarily need to be contenteditable if that opens up other solutions.
(I'm currently using this solution to get the position: Get a range's start and end offset's relative to its parent container)
I think it's because <b> is no container and <div> is a container.
I didn't try the code but try to assign "display: inline-block" to <b> so it gets recognised as a container. Maybe it works.

Repositioning after changing font size in UIWebView

I have an UIWebView with a huge book in it. I'm changing it's font size via javascript, using "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='150%';
Html-page gets larger, but the scroll position remains the same, causing text to shift out of a users sight.
The only idea that I have, is really weird and inefficient:
Wrap every word in <span> tags;
find the first onscreen <span> and remember it's id;
resize font;
scroll to span, that I've found in step 2.
Is there a better way to preserve the position, that user was reading?
Finally I've found an acceptable way:
Before changing font size I use a little javascript to find and store a position of a first letter on a page:
var range = document.caretRangeFromPoint(0,0); // get a range of a first onscreen letter
var textContainer = range.startContainer.parentNode;// get an element to which it belongs
var path = getElementXPath(textContainer); // get an XPath for that element (this function is not biult in, but you can find it in some other question)
path+='|'+range.startOffset; // stick XPath and index of the letter together
After that I change the font size, find needed element by XPath, insert invisible <a> right before my letter, scroll to that invisible <a>, don't forget to remove it.
Done. That is not a stragihtforward idea, but at least it works and does not consume to much of CPU or RAM, like the idea that I'have explained in original question.
Here is the place to get getElementXPath() function

Keep caret position in contenteditable after editing the content via jscript

I've got a contenteditable div which's content is being edited via javascript after each textchange(textchange.js) like adding html tags(only span-tags for changing the color of some words) and adding or removing some whitespaces here and there, but my problem is since the content is being changed while the user is editing it, the caret changes its position after every keypress, which makes it nearly impossible to write a single word.
I'm at the moment searching for a way to prevent this jumping around, I've already thought of adding a special char which wouldn't be used anyway as a kind of marker at the position of the caret before editing it, removing it when finished and putting the caret back to this position, but since I'm using regex a lot(currently about 25 times after each textchange) this special character would ruin nearly every single one of them and I would have to add something like \x40? every where, which would not look nice and clear at all:
/\s<span class="b0">hello\sworld</span>/g
to:
/\s\x40?<span class="b0">\x40?h\x40?e\x40?l\x40?l\x40?o\x40?\s\x40?w\x40?o\x40?r\x40?l\x40?d\x40?</span>/g
I don't know if it helps but here is an example on how the content is changed(after each keypress):
foo +++ <span class="c3">bar</span> - baz -<span class="c0">qux</span>
to:
<span class="c1">foo</span> + <span class="c3">bar</span> - <span class="c1">baz</span> * <span class="c0">qux</span>
I'd be grateful for every advice, tip or hint on how to solve this problem, or a better way to do this marker-thing.
Thank you :)
Probably this is not the best solution, but I created 2 divs, one to display the text and other to edit it, the second one above the first one and with some level of transparency.
Like this:
<div style="position: absolute; left: 48px; top: 16px;" unselectable="on" onselectstart="return false">
<code id="code_show">Type here.</code>
</div>
<div style="position: absolute; left: 48px; top: 16px;">
<span style="opacity:0.33"><code id="code_area" contenteditable="true" onkeyup="colorize();">Type here.</code></span>
</div>
The onselectstart="return false" and the unselectable="on" ensure that the first one is not selectable. The onkeyup="colorize();" ensures that the javascript function colorize is called everytime that the user presses a key to edit the text.
So you have to define it to update the contents of code_show to match the ones in code_area but with colors.
For instance, this code paints every word RED in red color:
<script>
function colorize(){
var code_area= document.getElementById('code_area');
var code_show= document.getElementById('code_show');
var inner= code_area.innerHTML;
inner= inner.replace(/RED/g,"<span style=\"color: red\">RED</span>");
code_show.innerHTML= inner;
}
</script>
You could create an abstraction that would add special characters to otherwise readable regexes. It's simple and presumably requires minimal changes to your current code.
There might be a more elegant solution though. If you never replace text that has a caret in a middle of it - as in repl|aced - you could split the original string into two and apply the filter on both parts. The caret position remains at the division, that is at length of the first gist.
If you do however need to replace even at caret position, you could start with the split anyway. After that, run the filters again, one by one. If the filter matched, move caret to a predetermined relative position - say to replaced ++|+ (pipe denoting the caret) with ××. You could even let it be, which would result in ××|. Or have a hardcoded relative caret move for each filter, which is great if you do code replacements, because you can move the caret to proper location (say you replace <|i></i> with <em>|<em>; notice the caret conveniently moved to where the user would probably want it).

TinyMCE limit text on scrollerActived

I have a textarea in my rails application to collect content from user in a database. The rails application is further feeding that text to an XML-driven flex application.
The flex application has number of fixed sized containers which wraps the text inside (from the XML created by Rails app on-the-fly), but truncates the text if it exceeds the container's height. Problem is; there is no way to present the large text in XML, so it gets adjusted automatically in the compiled flex application. And the fact is; the web-based rails app and front-tier flex app are entirely disconnected in terms of having awareness of their internal events. (like in this case; rails app has no knowledge of the overflow event for flex internal containers and relying on font-size and character/line count doesn't work in this scenario!)
Therefore, I wrote a JS function to watch and rescue the textarea's overflow situation and while setting its attributes (viz; line-height, font-size, font-family, width, height... yada yada) matching that of the flex control. The complex form in rails did the trick to have dynamic number of such textarea's control being observed by the JS function.
Here is the Prototype code to handle the overflow event with the corresponding rescue code for cleanup:
var timeout;
document.observe('dom:loaded', attach_obr);
function attach_obr() {
$$('.active_text').each (function(text_element){
text_element.observe('keyup', function(e){
check_limits(text_element.id);
});
text_element.observe('change', function(e){
check_limits(text_element.id);
});
});
}
function check_limits(eyeD) {
if($(eyeD).scrollHeight > $(eyeD).offsetHeight){
// overflow occured, now the rescue code here
timeout = window.setTimeout(function() {
$("error_notice").hide();
}, 4000);
$("error_notice").show().update('There is no space left in this box, please use a new box to continue adding content');
// truncate text till the scrollbar disappears
while($(eyeD).scrollHeight > $(eyeD).offsetHeight){
$(eyeD).value = $(eyeD).value.slice(0, -1);
}
}
else {
if($("error_notice").innerHTML!=""){
$("error_notice").hide().update("");
clearTime(timeout);
}
}
}
[Note: It works with a minor flaw of truncating few more characters than expected in the last line. User can retype these letters till the end of that line. I guess this is because somehow the change in width of textarea due to the appearance of scroll-bar is effecting either the scrollHeight or offsetHeight during the process & there should be something more to the loop's condition ($(eyeD).scrollHeight > $(eyeD).offsetHeight)]
The while loop makes things bit slower, but at least it is serving the purpose. WYSIWYG is achieved. (I would love to hear any suggestion from the viewers to improve that inelegant code :O )
WYSIWYG is not achieved, in terms of rich/formatted text..
Incorporating Rich Text:
Rather than expecting from user to place tags inside the area , in the next phase, I am planning to deploy tinyMCE in my app. Now, to make the above function work with tinyMCE, I have the following code:
tinyMCE.init({
theme_advanced_buttons1 : "bold, italic, underline, strikethrough, separator, justifyleft, justifycenter, justifyright, justifyfull, separator, forecolor, backcolor",
theme:"advanced",
mode:"textareas",
plugins : "safari",
width: '360px',
height: '198px',
setup : function(ed) {
ed.onChange.add(function(ed, i) {
check_limits(ed.id);
});
}
});
The binding and firing of events is working alright. Unfortunately, the aim to control the text overflow is not working. Reason being;
a) ed.id is the id of my textarea not the interactive panel created by tinyMCE. So, the attributes like scrollHeight are offsetHeight are not getting changed for the hidden textarea control.
b) The value of textarea in this case also contains HTML code rather than the actual text. So, it is very implicit to tell what is the actual text without markup (which in our case is required when truncating the overflowed text).
My questions:
Is there a way to get the scrollHeight and offsetHeight of the control created by tinyMCE?
Is there a way to get the only-text version (without markup) of inner content of tinyMCE control?
(So, when I truncate the text in check_limits function, it doesn't effect/breaks the markup/DOM created by tinyMCE for the formatted text. In other words, I would be simulating the user action of pressing backspace on tinyMCE control in the while loop.)
Elegant way to do this whole exercise with & without tinyMCE?
Any suggestions are greatly appreciated!
First you need to know that tinymce creates a contenteditable iframe to let users edit html contents; contents from that iframe get written back to the textarea onSave. The textarea gets hidden in the rtinymce intiatilization process. The editor id is equal to the textarea id.
Here some suggestions:
1. Relevant code
var frameid = editor.id+'_ifr';
var currentiframe = document.getElementById(frameid);
var offsetHeight = currentiframe .contentDocument.body.offsetHeight;
var scrollHeight = currentfr.Document.body.scrollHeight
2. code for this (using jQuery)
var plain_text = $(editor.getBody()).text();
3. The only more efficient way to handle the while loop in the "without tinymce" case will be to slice off some more characters and follow a logarithmic approach. You slice off a bigger part of the string and then get to the final value in half-part paces. Example: You slice of 20 characters, but it fits. Then you slice off 10 characters of the original string. If it does not fit you try 15 characters and so on... this is more effectife then the while approach, but more complicated to develop.
EDIT:
It seems almost impossible to get the line number from the caret position. Problem here is that you do not know where the a text line breaks. Though it is easy to find out in which paragraph the cursor is located at (tinymce uses paragraphs to wrap text nodes).
There is a way to limit insertion in tinymce based on characters (i.e. limit can be set to 100 characters), but i guess this won't work for your use case unless you use a monospace font.
Another approach could be to set the tinymce css to set the editor window to the exact same width as your flex boxes (set the widht to the iframes body element should be sufficient). In this case it sould be easier to use the scrollHeigth approach - you would only need to find out if the heigth did change after insertion of text and then you could divied the heigth with the lineheigth to egt the line number. I suggest you write an own plugin to implement this. This is not that difficult. Here is a link to a tutorial for this.

CSS Page Layout w/ Breaks

I'm trying to make a webpage where it basically looks like a word document. There would be multiple boxes that would scroll down and the text would flow and page break from one page to the next.
Does anyone have any idea where I would even start? Thanks.
Edit: It should be right in the browser, looking similar to this:
(Ignore the columns)
CSS mostly applies styles to a full element due to its box model. Exceptions are pseudo elements. So to create an appropriate break after a fixed length you would have to separate your text into correctly sized different elements.
EDIT:
It would be possible using javascript. But even in the simplest case, where everything inside the pages delivered as just one text element with no sub elements (not even other text elements), the code will be a development nightmare and will run quite crappy. This is because there is no measure function in javascript. So you would be forced to do trail and error to find the correct position to break the element. Since the properties of the elements are live it means, that the viewer of the website will see a lot of flickering of your page just after loading. If you dare put other elements inside the html element to break into pages you get even more problems. More or less you get hundreds of special cases (break inside other elements, what if those elements are inside even other elements) to look out for.
Something like that sounds possible using javascript, but it depends a bit on the structure of your html and whether or not you want to break paragraphs or just move the next paragraph to the next page if it doesn´t fit
So the simplest example, not breaking paragraphs / html elements with a flat html structure (no nested divs, columns, etc) like:
<div class="document">
<h1>title</h1>
<p>texts</p>
<h2>subtitle</h2>
<p>texts</p>
...
<p>texts</p>
</div>
would be to do something like:
height = 0
loop through all direct child elements of .document
{
if ( (height + element_height) > page_height)
{
add page_break_element before current element
height = 0
}
height = height + element_height
}
I´d use jquery because it makes it easy to loop through the elements, measure heights, etc.
I guess breaking paragraphs would be possible as well, but a lot of extra work.
<p style="page-break-before: always">This would print on the next page</p>

Categories