Get not overflowing text length dynamically - javascript

Is there a way to get the length of visible part of an overflown text (or the size of the overflown part, to calculate the rest, for the case) with CSS or JavaScript?
And if so, could it be calculated dynamically (i.e. on window resize?)
The idea is to make a read more button which span button always sticks at the end of the last visible line, it doesn't matter the screen size.

Something like this involves a lot of factors, for instance, your "text" could be a mixture of text and other elements (<b>, <i>, <img>). Assuming it's just straight text, the following works by splitting the string at various halves, with successively smaller halves, to eventually arrive at the text that gives the same height as your source element with hidden overflow.
function getVisibleText(source)
{
let yardstick = document.createElement(source.nodeName);
let sourceRectangle = source.getBoundingClientRect();
let text = source.textContent;
yardstick.style.width = sourceRectangle.width + "px";
yardstick.style.position = "absolute";
yardstick.style.top = "-999px";
yardstick.style.left = "-999px";
yardstick.textContent = text;
source.parentNode.appendChild(yardstick);
let size = text.length;
let difference = size;
let yardstickRectangle = yardstick.getBoundingClientRect();
let result = text;
while((difference > 1 || yardstickRectangle.height > sourceRectangle.height) && size > 0)
{
difference = Math.round(difference / 2);
if(yardstickRectangle.height > sourceRectangle.height)
size -= difference;
else
size += difference;
result = text.substring(0, size);
yardstick.textContent = result;
yardstickRectangle = yardstick.getBoundingClientRect();
}
yardstick.parentNode.removeChild(yardstick);
// Trim to the last whole word
let match = (new RegExp("\\s+\\S*?$", "g")).exec(result)[0];
if(match)
result = result.substring(0, result.length - match.length);
return(result);
}

Related

ScriptUI - Integer number in slider percentage

I have a script that shows a dialog window with a slider bar, next to a static text.
The text shows the percentage of the slider bar (0 to 100).
The problem is that the percentage is shown in decimals, while I would need it to show them in integers, as the next part of the code takes the number from the text and I don't need decimals.
The code is :
var Dial = new Window ("dialog");
Dial.alignChildren = ["right","center"];
var Group1 = Dial.add ("group");
var Slide = Group1.add ("slider");
Slide.minvalue = 0;
Slide.maxvalue = 100;
Slide.value = 50;
Slide.preferredSize.width = 300;
Slide.onChanging = function () {
Label.text = Slide.value;
}
var Label = Group1.add ("statictext");
Label.preferredSize.width = 30;
Label.text = Slide.value;
var Button1 = Dial.add ("button");
Button1.text = "OK";
Button1.onClick = function () {
Dial.close()
}
Dial.show()
Someone has any idea how to do it?
I've tried with Math.round() on Label.text and on Slide.value, but I don't know if I can't use it correctly or if it's not the right code for this case.
I'm sure the line like this is the absolute correct way to get the round numbers in this case:
Label.text = Math.round(Slide.value);
If somewhere down-river you will need a string you can get the string via var myString = Label.text.toString(); or even var myString = '' + Label.text;. But usually JavaScript/Extendscript doesn't care either the value is a number or a string. It deals with them indiscriminately.

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 '';
}

jQuery ellipsis based on available space

I want to be able to ellipsis text based on how much space is available. Currently I have to provide the maximum number of characters I want to my ellipsis function but it would be far better if it did it based on available space.
How can I achieve this?
function ellipsisText(object, maxLength, ellipsistooltip) {
var grace = 3;
var text = object.text();
if (text.length - grace > maxLength) {
var etext = text.substring(0, maxLength);
etext += "...";
object.text(etext);
if (ellipsistooltip) {
object.addClass("tooltip");
object.attr("tooltiptitle", text);
}
}
}

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).

Using javascript substring() to create a read more link

I'm developing a Classic ASP page that pulls some content from a database and creates a Read more link after the first 100 characters as follows;
<div class="contentdetail"><%=StripHTML(rspropertyresults.Fields.Item("ContentDetails").Value)%></div>
<script type="text/javascript">
$(function() {
var cutoff = 200;
var text = $('div.contentdetail').text();
var rest = $('div.contentdetail').text().substring(cutoff);
if (text.length > 200) {
var period = rest.indexOf('.');
var space = rest.indexOf(' ');
cutoff += Math.max(Math.min(period, space), 0);
}
var visibleText = $('div.contentdetail').text().substring(0, cutoff);
$('div.contentdetail')
.html(visibleText + ('<span>' + rest + '</span>'))
.append('<a title="Read More" style="font-weight:bold;display: block; cursor: pointer;">Read Moreā€¦</a>')
.click(function() {
$(this).find('span').toggle();
$(this).find('a:last').hide();
});
$('div.contentdetail span').hide();
});
</script>
However, the script obviously just cuts the text off after 100 characters. Preferably I would like it to keep on writing text until the first period or space, for example. Is this possible to do?
Thank you.
var cutoff = 100;
var text = $('div.contentdetail').text();
var rest = text.substring(cutoff);
if (text.length > cutoff) {
var period = rest.indexOf('.');
var space = rest.indexOf(' ');
cutoff += Math.max(Math.min(period, space), 0);
}
// Assign the rest again, because we recalculated the cutoff
rest = text.substring(cutoff);
var visibleText = $('div.contentdetail').text().substring(0, cutoff);
EDIT: shortened it a bit.
EDIT: Fixed a bug
EDIT: QoL improvement
How about:
var text= $('div.contentdetail').text();
var match= text.match( /^(.{100}([^ .]{0,20}[ .])?)(.{20,})$/ );
if (match!==null) {
var visibleText = match[1];
var textToHide = match[3];
...do replacement...
}
The {0,20} will look forward for a space or period for up to 20 characters before giving up and breaking at exactly 100 characters. This stops an extremely long word from breaking out of the length limitation. The {20,} at the end stops a match being made when it would only hide a pointlessly small amount of content.
As for the replacement code, don't do this:
.html(visibleText + ('<span>' + textToHide + '</span>'))
This is inserting plain-text into an HTML context without any escaping. If visibleText or textToHide contains any < or & characters you will be mangling them, perhaps causing a XSS security problem in the process.
Instead create the set the text() of the div and the span separately, since that's the way you read the text in the first place.
Here is a fairly simple approach to getting endings at the word level, and shooting for about your given limit in characters.
var limit = 100,
text = $('div.contentdetail').text().split(/\s+/),
word,
letter_count = 0,
trunc = '',
i = 0;
while (i < text.length && letter_count < limit) {
word = text[i++];
trunc += word+' ';
letter_count = trunc.length-1;
}
trunc = $.trim(trunc)+'...';
console.log(trunc);

Categories