Please take a look at this fiddle:
https://jsfiddle.net/darrengates/jwx41Lkz/
In this example, I have an editable div with 3 lines. I'm attempting to get the start character position, and end character position, of a selection.
This appears to work for the first line only. If I highlight more than 1 line, the values are incorrect (see the comments for an explanation).
How do I get the start and end character position of a selection when it spans multiple lines? (i.e., the character position starting at the first letter of the entire div, so for example the "s" in "second line" would be approximately character position 11).
Here's what I have so far (runnable in the fiddle):
<div id="test" contenteditable="true" class="selecttest">
first line<br />
second line<br />
third line
</div>
.selecttest {
width: 400px;
height: 200px;
border: 1px solid black;
padding: 5px;
font-size: 20px;
}
function getSelection() {
const sel = document.getSelection() || window.getSelection();
// note: if I highlight the 2nd and 3rd lines
// start and end values are 1, 11
// they should be about 11 and 22 (or so)
// on account of the existence of the first line
let start = sel.baseOffset
let end = sel.extentOffset
// if selected from right to left, reverse...
if ( start > end ) {
const temp = start;
start = end;
end = temp;
}
console.log('start, end', start, end)
}
$("#test").on('mouseup', function() {
getSelection();
})
It seems that there are many S.O. posts relating to getting selected text, but answers relating to getting the selected character positions all fail when selecting multiple lines.
Since you want to measure position in a range, you should be using a range object rather than the selection object.
A proposal on this code pen > https://codepen.io/aSH-uncover/pen/ExEdXwy
The main trick is to address the two scenarios where the range is inside the same container or not :)
function getSelection() {
const sel = document.getSelection() || window.getSelection();
const range = sel.getRangeAt(0)
let rangeContainer = range.commonAncestorContainer
let same = false
if (rangeContainer === range.startContainer) {
same = true
rangeContainer = range.startContainer.parentElement
}
let start = -1
let end = -1
const list = rangeContainer.childNodes
let offset = 0
for (let i = 0 ; i < list.length ; i++) {
const current = list[i]
if (current === range.startContainer) {
start = offset + range.startOffset
if (same) {
end = offset + range.endOffset
break
}
} else if (current === range.endContainer) {
end = offset + range.endOffset
break
}
if (current.nodeName === '#text') {
offset += current.length
}
}
console.log('start, end', start, end)
}
Edit : if you want to support more complex text (like a div with spans) you will have to improve this function and include some recursive stuff :)
I am trying to add text on an image using the <canvas> element. First the image is drawn and on the image the text is drawn. So far so good.
But where I am facing a problem is that if the text is too long, it gets cut off in the start and end by the canvas. I don't plan to resize the canvas, but I was wondering how to wrap the long text into multiple lines so that all of it gets displayed. Can anyone point me at the right direction?
Updated version of #mizar's answer, with one severe and one minor bug fixed.
function getLines(ctx, text, maxWidth) {
var words = text.split(" ");
var lines = [];
var currentLine = words[0];
for (var i = 1; i < words.length; i++) {
var word = words[i];
var width = ctx.measureText(currentLine + " " + word).width;
if (width < maxWidth) {
currentLine += " " + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
We've been using this code for some time, but today we were trying to figure out why some text wasn't drawing, and we found a bug!
It turns out that if you give a single word (without any spaces) to the getLines() function, it will return an empty array, rather than an array with a single line.
While we were investigating that, we found another (much more subtle) bug, where lines can end up slightly longer than they should be, since the original code didn't account for spaces when measuring the length of a line.
Our updated version, which works for everything we've thrown at it, is above. Let me know if you find any bugs!
A possible method (not completely tested, but as for now it worked perfectly)
/**
* Divide an entire phrase in an array of phrases, all with the max pixel length given.
* The words are initially separated by the space char.
* #param phrase
* #param length
* #return
*/
function getLines(ctx,phrase,maxPxLength,textStyle) {
var wa=phrase.split(" "),
phraseArray=[],
lastPhrase=wa[0],
measure=0,
splitChar=" ";
if (wa.length <= 1) {
return wa
}
ctx.font = textStyle;
for (var i=1;i<wa.length;i++) {
var w=wa[i];
measure=ctx.measureText(lastPhrase+splitChar+w).width;
if (measure<maxPxLength) {
lastPhrase+=(splitChar+w);
} else {
phraseArray.push(lastPhrase);
lastPhrase=w;
}
if (i===wa.length-1) {
phraseArray.push(lastPhrase);
break;
}
}
return phraseArray;
}
Here was my spin on it... I read #mizar's answer and made some alterations to it... and with a little assistance I Was able to get this.
code removed, see fiddle.
Here is example usage. http://jsfiddle.net/9PvMU/1/ - this script can also be seen here and ended up being what I used in the end... this function assumes ctx is available in the parent scope... if not you can always pass it in.
edit
the post was old and had my version of the function that I was still tinkering with. This version seems to have met my needs thus far and I hope it can help anyone else.
edit
It was brought to my attention there was a small bug in this code. It took me some time to get around to fixing it but here it is updated. I have tested it myself and it seems to work as expected now.
function fragmentText(text, maxWidth) {
var words = text.split(' '),
lines = [],
line = "";
if (ctx.measureText(text).width < maxWidth) {
return [text];
}
while (words.length > 0) {
var split = false;
while (ctx.measureText(words[0]).width >= maxWidth) {
var tmp = words[0];
words[0] = tmp.slice(0, -1);
if (!split) {
split = true;
words.splice(1, 0, tmp.slice(-1));
} else {
words[1] = tmp.slice(-1) + words[1];
}
}
if (ctx.measureText(line + words[0]).width < maxWidth) {
line += words.shift() + " ";
} else {
lines.push(line);
line = "";
}
if (words.length === 0) {
lines.push(line);
}
}
return lines;
}
context.measureText(text).width is what you're looking for...
Try this script to wrap the text on a canvas.
<script>
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
var words = text.split(' ');
var line = '';
for(var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var metrics = ctx.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
}
else {
line = testLine;
}
}
ctx.fillText(line, x, y);
}
var canvas = document.getElementById('Canvas01');
var ctx = canvas.getContext('2d');
var maxWidth = 400;
var lineHeight = 24;
var x = (canvas.width - maxWidth) / 2;
var y = 70;
var text = 'HTML is the language for describing the structure of Web pages. HTML stands for HyperText Markup Language. Web pages consist of markup tags and plain text. HTML is written in the form of HTML elements consisting of tags enclosed in angle brackets (like <html>). HTML tags most commonly come in pairs like <h1> and </h1>, although some tags represent empty elements and so are unpaired, for example <img>..';
ctx.font = '15pt Calibri';
ctx.fillStyle = '#555555';
wrapText(ctx, text, x, y, maxWidth, lineHeight);
</script>
</body>
See demo here http://codetutorial.com/examples-canvas/canvas-examples-text-wrap.
From the script here: http://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/
I've extended to include paragraph support. Use \n for new line.
function wrapText(context, text, x, y, line_width, line_height)
{
var line = '';
var paragraphs = text.split('\n');
for (var i = 0; i < paragraphs.length; i++)
{
var words = paragraphs[i].split(' ');
for (var n = 0; n < words.length; n++)
{
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > line_width && n > 0)
{
context.fillText(line, x, y);
line = words[n] + ' ';
y += line_height;
}
else
{
line = testLine;
}
}
context.fillText(line, x, y);
y += line_height;
line = '';
}
}
Text can be formatted like so:
var text =
[
"Paragraph 1.",
"\n\n",
"Paragraph 2."
].join("");
Use:
wrapText(context, text, x, y, line_width, line_height);
in place of
context.fillText(text, x, y);
I am posting my own version used here since answers here weren't sufficient for me. The first word needed to be measured in my case, to be able to deny too long words from small canvas areas. And I needed support for 'break+space, 'space+break' or double-break/paragraph-break combos.
wrapLines: function(ctx, text, maxWidth) {
var lines = [],
words = text.replace(/\n\n/g,' ` ').replace(/(\n\s|\s\n)/g,'\r')
.replace(/\s\s/g,' ').replace('`',' ').replace(/(\r|\n)/g,' '+' ').split(' '),
space = ctx.measureText(' ').width,
width = 0,
line = '',
word = '',
len = words.length,
w = 0,
i;
for (i = 0; i < len; i++) {
word = words[i];
w = word ? ctx.measureText(word).width : 0;
if (w) {
width = width + space + w;
}
if (w > maxWidth) {
return [];
} else if (w && width < maxWidth) {
line += (i ? ' ' : '') + word;
} else {
!i || lines.push(line !== '' ? line.trim() : '');
line = word;
width = w;
}
}
if (len !== i || line !== '') {
lines.push(line);
}
return lines;
}
It supports any variants of lines breaks, or paragraph breaks, removes double spaces, as well as leading or trailing paragraph breaks. It returns either an empty array if the text doesn't fit. Or an array of lines ready to draw.
look at https://developer.mozilla.org/en/Drawing_text_using_a_canvas#measureText%28%29
If you can see the selected text, and see its wider than your canvas, you can remove words, until the text is short enough. With the removed words, you can start at the second line and do the same.
Of course, this will not be very efficient, so you can improve it by not removing one word, but multiple words if you see the text is much wider than the canvas width.
I did not research, but maybe their are even javascript libraries that do this for you
I modified it using the code from here http://miteshmaheta.blogspot.sg/2012/07/html5-wrap-text-in-canvas.html
http://jsfiddle.net/wizztjh/kDy2U/41/
This should bring the lines correctly from the textbox:-
function fragmentText(text, maxWidth) {
var lines = text.split("\n");
var fittingLines = [];
for (var i = 0; i < lines.length; i++) {
if (canvasContext.measureText(lines[i]).width <= maxWidth) {
fittingLines.push(lines[i]);
}
else {
var tmp = lines[i];
while (canvasContext.measureText(tmp).width > maxWidth) {
tmp = tmp.slice(0, tmp.length - 1);
}
if (tmp.length >= 1) {
var regex = new RegExp(".{1," + tmp.length + "}", "g");
var thisLineSplitted = lines[i].match(regex);
for (var j = 0; j < thisLineSplitted.length; j++) {
fittingLines.push(thisLineSplitted[j]);
}
}
}
}
return fittingLines;
And then get draw the fetched lines on the canvas :-
var lines = fragmentText(textBoxText, (rect.w - 10)); //rect.w = canvas width, rect.h = canvas height
for (var showLines = 0; showLines < lines.length; showLines++) { // do not show lines that go beyond the height
if ((showLines * resultFont.height) >= (rect.h - 10)) { // of the canvas
break;
}
}
for (var i = 1; i <= showLines; i++) {
canvasContext.fillText(lines[i-1], rect.clientX +5 , rect.clientY + 10 + (i * (resultFont.height))); // resultfont = get the font height using some sort of calculation
}
This is a typescript version of #JBelfort's answer.
(By the way, thanks for this brilliant code)
As he mentioned in his answer this code can simulate html element such as textarea,and also the CSS property
word-break: break-all
I added canvas location parameters (x, y and lineHeight)
function wrapText(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
x: number,
y: number,
lineHeight: number
) {
const xOffset = x;
let yOffset = y;
const lines = text.split('\n');
const fittingLines: [string, number, number][] = [];
for (let i = 0; i < lines.length; i++) {
if (ctx.measureText(lines[i]).width <= maxWidth) {
fittingLines.push([lines[i], xOffset, yOffset]);
yOffset += lineHeight;
} else {
let tmp = lines[i];
while (ctx.measureText(tmp).width > maxWidth) {
tmp = tmp.slice(0, tmp.length - 1);
}
if (tmp.length >= 1) {
const regex = new RegExp(`.{1,${tmp.length}}`, 'g');
const thisLineSplitted = lines[i].match(regex);
for (let j = 0; j < thisLineSplitted!.length; j++) {
fittingLines.push([thisLineSplitted![j], xOffset, yOffset]);
yOffset += lineHeight;
}
}
}
}
return fittingLines;
}
and you can just use this like
const wrappedText = wrapText(ctx, dialog, 200, 100, 200, 50);
wrappedText.forEach(function (text) {
ctx.fillText(...text);
});
}
I'd like to ask a question regarding arrays and HTML divs.
If I have an array of:
const exampleArray = ['Germany','Australia','Canada','Mongolia','Argentina','China'];
And let's say I have a div like so, with a max length of around 100px (sorry for the long looking table, I couldn't format it on stack overflow)
| A div |
| -------- |
I want to try fill up the div as much as possible, but if it overflows, I want it to stop and end with ', ... 5' the number being the number of array items it couldn't print
For example:
| Germany, Australia, Canada, ... 2|
| -------- |
Not:
| Germany, Australia, Canada, Mong... 2|
| --------- |
Could anyone advise what is the best way to implement this?
Assuming you want the ellipsis and the bracketed number to be within the 100px, this snippet keeps adding an array item, plus its comma, calculates what the width of the ellipsis and bracketed remaining count is and if it fits, carries on.
If it has got too big then it reverts to the previous content and displays that.
Note: 100px is quite narrow given on my example at least the ellipsis plus bracketed number take at least 30px, so this snippet will show just one country name. Try it with 200px or smaller font size to test for more.
const exampleArray = ['Germany','Australia','Canada','Mongolia','Argentina','China'];
const div = document.querySelector('#list');
const endbit = document.querySelector('#endbit');
let i, endbitLength;
let divPrev = '';
let endbitPrev = '';
for (i = 0; i< exampleArray.length; i++) {
endbitPrev = endbit.innerHTML;
endbit.innerHTML = '...(' + (exampleArray.length - i) +')';
endbitLength = endbit.offsetWidth;
div.innerHTML = divPrev + exampleArray[i] + ', ';
if (div.offsetWidth > (100 - endbitLength)) {
break;
}
else {
divPrev = divPrev + exampleArray[i] + ', ';
endbitPrev = '...' + '(' + (exampleArray.length - i) + ')';
}
}
endbit.style.display = 'none';
div.innerHTML = divPrev + endbitPrev;
div {
display: inline-block;
}
<div id="list"></div>
<div id="endbit">...</div>
If you are ok with adding a span inside the div then this would do. I know there are better ways to do it, but you can try this function for now
function addToDiv(arr) {
const div = document.querySelector("#div");
const span = div.firstElementChild;
const divWidth = div.clientWidth;
let finalIndex = 0;
let remaining = 0;
for (const [index,c] of arr.entries())
{
span.append(c)
if(span.offsetWidth >= divWidth){
finalIndex = index-1;
remaining = arr.length - index;
break;
}
if(index != arr.length -1)
span.append(', ')
}
if(!remaining) return;
span.textContent = '';
for (const [index,c] of arr.entries())
{
if(index > finalIndex) break;
span.append(c+', ')
}
span.append('...'+remaining)
}
It periodically checks if the width of the span exceeds that of the div. I have written a fiddle here
https://jsfiddle.net/10fbkmg5/4/
You can play with the fiddle by changing the width of the div to check if it fits your requirements.
You can achieve your desired result by using below example, it is just reference for your requirement, just change the code or logic according.
const exampleArray = ['Germany','Australia','Canada','Mongolia','Argentina','China'];
var width=$(".tobefilled").width();
var news=exampleArray.slice(0,-width/15);
var remaining=exampleArray.length-news.length;
$(".tobefilled").html(news+",..."+remaining);
.tobefilled{
width:50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="tobefilled"></div>
You can create a clone to measure how many items fit, here is an example:
const exampleArray = ['Germany', 'Australia', 'Canada', 'Mongolia', 'Argentina', 'China'];
const target = document.querySelector('#target');
const temp = target.cloneNode();
temp.style.whiteSpace = 'nowrap';
temp.style.visibility = 'hidden';
target.parentElement.append(temp);
const content = exampleArray.filter((v, i, arr) => {
temp.textContent = arr.slice(0, i + 1).join(', ');
if (temp.scrollWidth <= target.clientWidth) {
// Make sure that this is the last element or if it is not, that the ellipsis still fits.
if (i === (arr.length - 1)) {
return true;
} else {
temp.textContent += ` ... ${arr.length - (i + 1)}`;
if (temp.scrollWidth <= target.clientWidth) {
return true;
}
}
}
});
const restCount = exampleArray.length - content.length;
target.textContent = content.join(', ')
if (restCount) {
target.textContent += ` ... ${restCount}`;
}
#target {
width: 200px;
}
<div id="parent">
<div id="target"></div>
</div>
What this does is:
Clone the #target element.
Add the clone to the parent of this element (to maximise the changes that it will have the same styling).
Filter the strings array. The filter function adds all the strings up to the current iteration's index to the clone and checks if the clone's width is bigger than the original element, it also makes sure that all the items up to this point as well as the ellipsis text will fit.
The result of the filter operation is then used to set the original element's text and to calculate the count of the remaining element (the ones that don't fit if any).
I'm trying to figure out how The Washington Post website forces the main headline to break so nicely no matter the actual text or screen width. From what I can determine, if the h1 has a class of headline, its width is dynamically calculated to achieve the effect. I know this is done with JavaScript and imagine it's not hard-coded so how is that width actually computed to be somehow cognizant of how the headline will break (preventing highly variable line lengths, orphans, etc.). I just can't track down the JS that's controlling this functionality.
Here is an example of the page loading normally:
And an example with the headline class removed:
(You can also notice the difference right when the page loads, presumabley just before the JavaScript kicks in.)
Thanks!
This is what you want. Obviously, because some words are longer than others, not all the lines have perfectly the same length. This code below preserves the amount of line breaks that are created by the browser. Then, it adjusts the line breaks' position to ensure a more or less even line lengths.
P.S.: Try running the below code without JS and you'll see the difference
let h1 = document.querySelector('h1')
let text = h1.textContent
let splitText = text.split(' ')
let getContentBoxHeight = elem => {
let elemStyle = window.getComputedStyle(elem)
let elemHeightWithPadding = parseInt(elemStyle.getPropertyValue('height'))
let elemPadding = parseInt(elemStyle.getPropertyValue('padding-top')) + parseInt(elemStyle.getPropertyValue('padding-bottom'))
return elemHeightWithPadding - elemPadding
}
let getContentBoxWidth = elem => {
let elemStyle = window.getComputedStyle(elem)
let elemWidthWithPadding = parseInt(elemStyle.getPropertyValue('width'))
let elemPadding = parseInt(elemStyle.getPropertyValue('padding-left')) + parseInt(elemStyle.getPropertyValue('padding-right'))
return elemWidthWithPadding - elemPadding
}
// return the number of line breaks created by the browser
let breakPointAmount = (() => {
let body = document.querySelector('body')
let dummyH1 = document.createElement('h1')
let oneLineHeight
let totalLineHeight = getContentBoxHeight(h1)
dummyH1.appendChild(document.createTextNode('M'))
body.appendChild(dummyH1)
oneLineHeight = getContentBoxHeight(dummyH1)
dummyH1.remove()
return Math.round(totalLineHeight / oneLineHeight) - 1
})()
// refine the number of line breaks created by the browser
let breakPoints = (() => {
let denominator = breakPointAmount + 1
let points = []
let h1Length
h1.style.width = 'max-content'
h1Length = getContentBoxWidth(h1)
h1.style.width = ''
for (let i = 0; i < breakPointAmount; i++) {
points.push(Math.round(h1Length * (i + 1) / denominator))
}
return points
})()
// determine where that same number of break points should go in text
let indexesToBreak = Array(breakPointAmount).fill(1)
let cumulativeLength = 0
let cumulativeText = ''
for (let i = 0; i < splitText.length; i++) {
let word = splitText[i]
let calculateLength = word => {
let body = document.querySelector('body')
let dummyH1 = document.createElement('h1')
let length
dummyH1.appendChild(document.createTextNode(word))
dummyH1.style.width = 'max-content'
body.appendChild(dummyH1)
length = getContentBoxWidth(dummyH1)
dummyH1.remove()
return length
}
cumulativeText += word + ' '
cumulativeLength = calculateLength(cumulativeText)
for (let j = 0; j < indexesToBreak.length; j++) {
if (indexesToBreak[j] === 1) {
indexesToBreak[j] = {
index: i,
currentMin: Math.abs(cumulativeLength - breakPoints[j])
}
} else {
if (cumulativeLength - breakPoints[j] < indexesToBreak[j].currentMin) {
indexesToBreak[j] = {
index: i,
currentMin: Math.abs(cumulativeLength - breakPoints[j])
}
}
}
}
}
// insert break points at updated locations into text
let newText = (function() {
let final = ''
let itbIndex = 0
for (let i = 0; i < splitText.length; i++) {
final += `${splitText[i]} `
if (indexesToBreak[itbIndex] && i === indexesToBreak[itbIndex].index) {
final += '<br>'
itbIndex += 1
}
}
return final.trim()
})()
// add text with new break points to page
h1.innerHTML = newText
h1 {
text-align: center;
}
<h1>Some Long Title that Requires Line Breaking To Ensure Even Line Lengths</h1>
I am searching through a paragraph/div element for the specific character of ^ with wishes to replace the following contents inside a newly created SPAN element and rise it (making indices). All is successful except for the location at where I append my SPAN element. I wish to append it to the location that I find said character, however, I don't have any lead to find that location except the index that the character is found.
I know that it is possible to insertAfter() but that won't work in this situation as I am attempting to append the element after a certain character within a paragraph/div rather than after a certain element.
<div class="codeBlock">
<span style="font-weight: normal; font-size: 20px;">Product Rule</span><br><br><br>
f(x) = u * v<br>
f'(x) = u'v + v'u<br>
<br>
<br>
<span style="font-weight: bold; font-size: 20px">I.E.</span> // Check<br><br>
f(x) = x^3 + 9
</div>
function quill() {
var blocks = document.getElementsByClassName("codeBlock");
for (block of blocks) {
for (let i = 0; i < block.textContent.length; i++) {
var ch = block.textContent[i];
if (ch == "^") {
// Loop until space
var l = i+1;
var superscript = document.createElement("SPAN");
var indice = "";
do {
indice += block.textContent[l];
l++;
} while (block.textContent[l] != " ");
superscript.textContent = indice;
superscript.className = "superscript";
block.appendChild(superscript);
console.log(indice);
}
}
}
}
quill();
When I append the element, html would look like, for example:
<p>
f(x) = x^2 /*Found the character*/ + 6
<span>2</span> /*So it appends the element at the end*/
</p>
when I am trying to get something like this:
<p>
f(x) = x^2 /*Oh! Found the character and position,
append element here in between x^2 and + 6*/<span>2</span> + 6
</p>
I guess you should use regex for that
function quill() {
var blocks = document.getElementsByClassName("codeBlock");
for (block of blocks) {
block.innerHTML = block.innerHTML.replace(/(\^\d+)/, function(item){return '<sup>'+item+'</sup>';});
}
}
quill();