Slow performance Input event in contenteditable DIV - javascript

I have a DIV which has contenteditable attribute and I am running 70 lines of JavaScript code on input event but when I try to type fast. The JavaScript code is very slow and the results don't show/update in the UI as quickly as expected.
What I am doing in the JavaScript is in the input event:
I am getting the textcontent of the DIV with contenteditable attribute and then changing it to array with split() function and then, by using a for loop, I am comparing the array values with other array. If the value matches I create a SPAN with textContent set as the current value from that contenteditable DIV and then put it in an spanArray array. Then I'm appending all those SPANs into the contenteditable DIV and setting the caret at the end.
How to optimize the code so that the type speed / performance is not affected by the JavaScript's heavy for loops seen in the below example?
Here is the code I got so far:
const mainDiv = document.querySelector("#mainDiv");
const placeHolderDiv = document.querySelector("#placeholder");
let placeHolder = "";
let placeHolderArr;
fetch("https://type.fit/api/quotes")
.then((data) => {
return data.json();
})
.then((result) => {
for (let quote = 0; quote < 10; quote++) {
placeHolder += result[quote].text;
placeHolderArr = placeHolder.split(" ");
placeHolderDiv.textContent = placeHolder;
}
});
mainDiv.addEventListener("input", (e) => {
let spanArr = [];
const mainDivArr = mainDiv.textContent.split(" ");
let num = 0;
if (e.code !== "Space") {
for (let i of mainDivArr) {
if (placeHolderArr[num].trim() === mainDivArr[num].trim()) {
const span = document.createElement("span");
span.textContent = mainDivArr[num] + " ";
mainDiv.innerHTML = "";
spanArr.push(span);
num++;
} else if (placeHolderArr[num].trim() !== mainDivArr[num].trim()) {
const span = document.createElement("span");
span.style.color = "red";
span.textContent = mainDivArr[num] + " ";
mainDiv.innerHTML = "";
spanArr.push(span);
num++;
}
for (let spans of spanArr) {
mainDiv.innerHTML += spans.outerHTML;
// Placing Caret At The End
function placeCaretAtEnd(el) {
el.focus();
if (
typeof window.getSelection != "undefined" &&
typeof document.createRange != "undefined"
) {
var range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (typeof document.body.createTextRange != "undefined") {
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.collapse(false);
textRange.select();
}
}
placeCaretAtEnd(mainDiv);
}
}
} else {
console.log("space pressed");
}
});
body {
height: 90vh;
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
font-size: 1.5rem;
}
#content {
position: relative;
color: grey;
width: 70vw;
}
#mainDiv {
position: absolute;
top: 0;
left: 0;
color: #000;
width: 100%;
border: none;
outline: none;
}
<div id="content">
<div id="mainDiv" autofocus contenteditable></div>
<div id="placeholder"></div>
</div>

Consider another approach to the problem.
Instead of doing heavy operations over a contenteditable, disrupting such an area while the user is inserting text / typing, using two nested heavy for loops (O(n^2) complexity), and defining functions within a for loop (all really bad practices), I would suggest:
use only one DIV element
populate it with SPANs for words and characters
set it as tabindex="1" so that you can focus it and capture keyboard events
create an index variable idx so you can keep track on the progress and add classes to every character SPAN
on "keydown" event, use Event.key to determine the pressed key character. If the character matches with the current expected character, advance the index
const text = `Focus and start typing. Genius is one percent inspiration and ninety-nine percent perspiration.`;
const elText = document.querySelector("#text");
const textChars = [];
const words = text.split(/(?<=\S+\s)/g);
// Create SPAN for words and characters
elText.innerHTML = words.reduce((html, word) => {
textChars.push(...word);
const chars = [...word].map(ch => `<span class="char">${ch}</span>`).join("");
return html + `<span class="word">${chars}</span>`;
}, "");
const elsChars = elText.querySelectorAll(".char");
const totChars = textChars.length;
let totIncorrect = 0;
let isGameOver = false;
let idx = 0; // type progress index
elsChars[idx].classList.add("current");
elText.addEventListener("keydown", (ev) => {
if (isGameOver) return; // Prevent any more typing
ev.preventDefault(); // Prevent spacebar scrolling etc...
let typeChar = ev.key;
if (typeChar === "Enter") typeChar = " "; // Treat Enter as Space
if (typeChar.length > 1) return; // Do nothing on special keys
// CORRECT:
if (typeChar === textChars[idx]) {
elsChars[idx].classList.add("ok");
}
// INCORRECT:
else {
elsChars[idx].classList.add("err");
totIncorrect += 1;
}
// ADVANCE:
elsChars[idx].classList.remove("current");
idx += 1;
elsChars[idx]?.classList.add("current");
// GAME OVER:
if (idx === totChars) {
isGameOver = true;
console.log(`Well done! You had ${totIncorrect} mistakes out of ${totChars}!`);
}
});
body {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font: 1.5rem/1.3 sans-serif;
}
#text {
position: relative;
padding: 1rem;
color: grey;
width: 60vw;
color: #888;
letter-spacing: 0.15rem;
}
#text:focus {
outline: 1px solid #ddd;
}
.char {
display: inline;
white-space: pre-wrap;
}
#text:focus .current {
animation: blink 1s infinite;
}
#keyframes blink {
0% {box-shadow: -3px 0 0 #000;}
50% {box-shadow: -3px 0 0 #000;}
100% {box-shadow: -3px 0 0 transparent;}
}
.ok {
color: #000;
background: hsla(200 , 100%, 50%, 0.1);
}
.err {
color: #f00;
background: hsla(0, 100%, 50%, 0.1);
}
<div id="text" tabindex="1"></div>
With this simpler solution, now you can type as fast as it goes :)
For mobile devices you could incorporate an overlay transparent textarea in order to grab focus and prompt the onscreen keyboard.

Related

Autostyle input text in contenteditable div while typing

I am making a text editor, and I want to have a feature, such that while typing, if the user enters some keyword (e.g. happy, sad), the word is automaticly styled (e.g. color changed). How might I go about doing this?
document.getElementById('texteditor').addEventListener('keyup', function(e) {
styleCode(); //Style the text input
});
//Change the result of pressing Enter and Tab keys
document.getElementById('texteditor').addEventListener('keydown', function(e) {
switch (e.key) {
case 'Tab':
e.preventDefault();
document.execCommand('insertHTML', false, ' '); //Insert a 4-space tab
break;
case 'Enter':
e.preventDefault();
document.execCommand("insertLineBreak"); //Insert a new line
break;
}
});
function styleCode(){
//Style the code in the input box
}
#texteditor {
border: 3px solid black;
width:100%;
height: 500px;;
overflow:auto;
flex:1;
word-wrap: break-word;
word-break: break-all;
white-space:pre-wrap;
padding:5px;
font-family: Consolas,"courier new";
font-size:14px;
}
.styleA {
color:red;
}
.styleB {
color:blue;
}
<div id='texteditor' contenteditable></div>
Basically, when the user fully types "happy" (upon releasing the 'y' key) the word "happy" should turn red (using the styleA CSS class) in the editor. A similar thing should happen when the user finishes typing "sad"; the word "sad" should turn blue using the styleB CSS class.
Thanks in advance for any help.
const SpecialWords = [
"happy",
"sad"//style word
];
const WordColors = [
"styleA",
"styleB"//style class name
];
document.getElementById('texteditor').addEventListener('keyup', function(e) {
styleCode(); //Style the text input
});
//Change the result of pressing Enter and Tab keys
document.getElementById('texteditor').addEventListener('keydown', function(e) {
switch (e.key) {
case 'Tab':
e.preventDefault();
document.execCommand('insertHTML', false, ' '); //Insert a 4-space tab
break;
case 'Enter':
e.preventDefault();
document.execCommand("insertLineBreak"); //Insert a new line
break;
}
});
var oldWord = "";//initialise
function styleCode() {
//Style the code in the input box
var wordList = document.getElementById('texteditor').innerText.split(" ");
/*if old word is same as now then it means we have presed arrow key or caps or so,it do not wan't to style now as no change*/
if(!(oldWord == document.getElementById('texteditor').innerText)){
var oldPos = getCaretPosition(document.getElementById('texteditor'));//backup old position of cursor
for (let n = 0; n < SpecialWords.length; n++) {
var res = replaceAll(wordList,SpecialWords[n],`<span class="${WordColors[n]}">${SpecialWords[n]}</span>`).join(" ");//style adding
}
document.getElementById('texteditorS').innerHTML=res;
setCursor(oldPos,document.getElementById('texteditor'));//set back cursor position
}
oldWord = document.getElementById('texteditor').innerText;//old word for next time's reference
}
function replaceAll(array, find, replace) {
var arr = array;
for (let i = 0; i < arr.length; i++) {
if (arr[i] == find)
arr[i] = replace;
}
return (arr);
}
function getCaretPosition(editableDiv) {
var caretPos = 0,
sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode == editableDiv) {
caretPos = range.endOffset;
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() == editableDiv) {
var tempEl = document.createElement("span");
editableDiv.insertBefore(tempEl, editableDiv.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint("EndToEnd", range);
caretPos = tempRange.text.length;
}
}
return caretPos;
}
function setCursor(pos,editableDiv) {
if(!(pos == 0)){//if 0 it gives unwanted error
var tag = editableDiv;
// Creates range object
var setpos = document.createRange();
// Creates object for selection
var set = window.getSelection();
// Set start position of range
setpos.setStart(tag.childNodes[0], pos);
// Collapse range within its boundary points
// Returns boolean
setpos.collapse(true);
// Remove all ranges set
set.removeAllRanges();
// Add range with respect to range object.
set.addRange(setpos);
// Set cursor on focus
tag.focus();
}
}
.edit {
border: 3px solid black;
width: 100%;
height: 500px;
;
overflow: auto;
flex: 1;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
padding: 5px;
font-family: Consolas, "courier new";
font-size: 14px;
}
.styleA {
color: red;
}
.styleB {
color: blue;
}
#texteditorS{
pointer-events:none; /*click through*/
position:relative;
bottom: calc(500px + 2 * (5px + 3px));/*overlay on top of editable div,500px is height,5px is padding,3px is border*/
}
<div id='texteditor' class="edit" contenteditable></div>
<div id='texteditorS' class="edit"></div>
Features
support selection( ctrl-a ), arrow keys ...
many word,style support - just define variable
scrollable higlight
highly commented code
Idea
use another editor named texteditorS for style ,it overlaps the main editor,have click-through support for mouse to click the below/underlying editor
check whether any change in words has occured as it might be press of ctrl-a or arrow keys
sync-scroll of texteditor to texteditorS for scrolling styles
save the cursor position and after setting innerHTML set back cursor position.

Why does left-padding becomes part of span?

I am creating chinese checkers, I use span tag to create circles. Added only left padding to the top corner. I have two questions:
1) Why rows seem to have distance between them, but not columns.
2) To fix 1) I added padding-left, but instead of adding distance the padding became part of the circle, why?
Here's the link how it looks:
Here's part of code:
.player0{
height: 40px;
width: 40px;
padding-right: 5px;
background-color: transparent;
border-radius: 50%;
display: inline-block;
}
divs += "<span class='player"+fullBoardArr[fullArrIter]+" 'onclick='send()'></span>"
divs += "<div class='clear_float'> </div>" //for separation of rows
As I said in comments, you need to use margin instead of padding.
I would not use "clear_float" (I assume this is about the float CSS property). Instead wrap elements that belong in the same row, in a separate div element.
From the image you included, it seems that you have a problem in aligning the cells. You can use many ways to solve this, but as your board is symmetric horizontally (ignoring the colors), you can just use text-align: center.
I had some fun in creating JavaScript logic for the board itself. You may find some aspects interesting to reuse:
class Cell {
constructor(rowId, colId) {
this._value = 0;
this.rowId = rowId;
this.colId = colId;
this.elem = document.createElement("span");
this.elem.className = "cell";
this.selected = false;
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
this.elem.style.backgroundColor = ["", "grey", "blue", "red"][value];
}
toggleSelected() {
this.selected = !this.selected;
this.elem.classList.toggle("selected", this.selected);
}
}
class Board {
constructor() {
this._container = document.createElement("div");
this._container.className = "board";
this.elemMap = new Map;
this.grid = [[0,0,0,0,2,0,0,0,0,0,0,0,0],
[0,0,0,0,2,2,0,0,0,0,0,0,0],
[0,0,0,0,2,2,2,0,0,0,0,0,0],
[0,0,0,0,2,2,2,2,0,0,0,0,0],
[3,3,3,3,1,1,1,1,1,4,4,4,4],
[0,3,3,3,1,1,1,1,1,1,4,4,4],
[0,0,3,3,1,1,1,1,1,1,1,4,4],
[0,0,0,3,1,1,1,1,1,1,1,1,4],
[0,0,0,0,1,1,1,1,1,1,1,1,1]];
// create the data structure for the game and produce the corresponding DOM
this.grid.forEach((row, rowId) => {
let div = document.createElement("div");
row.forEach((value, colId) => {
if (!value--) return;
let cell = row[colId] = new Cell(rowId, colId);
cell.value = value;
div.appendChild(cell.elem);
this.elemMap.set(cell.elem, cell);
});
this._container.appendChild(div);
});
}
set container(elem) {
elem.appendChild(this._container);
}
getEventCell(e) {
return this.elemMap.get(e.target);
}
set selected(cell) {
if (this._selected) {
this._selected.toggleSelected();
this._selected = null;
}
if (!cell) return;
cell.toggleSelected();
this._selected = cell;
}
get selected() {
return this._selected;
}
move(cellFrom, cellTo) {
// TODO: Implement the real move rules here
if (!cellFrom.value) return; // must move a piece
if (cellTo.value) return; // capturing not allowed
cellTo.value = cellFrom.value;
cellFrom.value = 0;
board.selected = null;
}
}
let container = document.querySelector("#container");
let board = new Board();
board.container = container;
container.addEventListener("click", e => {
let cell = board.getEventCell(e);
if (!cell) return; // click was not on a cell
if (!board.selected || cell.value) {
board.selected = cell;
} else {
board.move(board.selected, cell);
}
});
.board {
text-align: center;
margin-left: auto; margin-right: auto;
}
.cell {
height: 15px;
width: 15px;
margin: 0px 2px;
border: 1px solid;
border-radius: 50%;
display: inline-block;
}
.selected {
border-color: orange;
}
<div id="container"></div>
You can click to select a piece and then click again on an empty spot to move it there.
Use margin instead of padding:
.player0{
height: 40px;
width: 40px;
margin-right: 5px;
background-color: transparent;
border-radius: 50%;
display: inline-block;
}
As an easy-to-remember quick reference, margin changes the position starting from outside the element border, padding from the inside

How to group text being typed into an array and wrap every 20 chars in a div

I am attempting to wrap every 20 chars in a new div container. I keep getting undefined I assume from the array results that do not exist yet. How can I wrap every 20 chars as the user types and reaches char 20, 40, 60,and 80 and place it into a div container with the max allowed chars being 80, and is there a better way of doing this than writing out the array groups manually by 20's.
function creaeTextField(clicked_id) {
var dropArea = document.getElementById('headarea');
var myArea = document.createElement('div');
var myAreaOuter = document.createElement('div');
myArea.className = "areaClass";
myAreaOuter.className = "areaClassOuter";
myArea.id = "areaClass";
myAreaOuter.id = "areaClassOuter";
myArea.contentEditable = "true";
if (clicked_id == 'text') {
myAreaOuter.appendChild(myArea);
dropArea.appendChild(myAreaOuter);
myArea.addEventListener("keydown", findLimitb);
myArea.addEventListener("keyup", findLimitb);
var style = window.getComputedStyle(myArea, null).getPropertyValue('font-size');
var fontSize = parseFloat(style);
function findLimitb() {
if (myArea.offsetHeight <= fontSize * 4) {
myArea.addEventListener("keydown", breaker);
} else {
if (event.keyCode === 8 || event.keyCode === 46 || event.keyCode === 37 || event.keyCode === 38 || event.keyCode === 39 || event.keyCode === 40) {
myArea.focus();
} else {
myArea.removeEventListener("keydown", breaker);
event.preventDefault();
myArea.style.height = fontSize * 4 + "px";
}
}
}
function breaker() {
var myAr = myArea.innerHTML.split("");
var divCon = document.createElement('div');
myArea.appendChild(divCon);
if (myArea.innerHTML.length > 20) {
divCon.innerHTML = myAr[0] + myAr[1] + myAr[2] + myAr[3] + myAr[4] + myAr[5] + myAr[6] + myAr[7] + myAr[8] + myAr[9] + myAr[10] + myAr[11] + myAr[12] + myAr[13] + myAr[14] + myAr[15] + myAr[16] + myAr[17] + myAr[18] + myAr[19] + myAr[20];
}
}
}
}
.headarea {
width: 100%;
height: 130px;
float: left;
}
.buttonStyle {
width: 60px;
height: 25px;
float: left;
}
.areaClassOuter {
float: left;
padding: 10px;
position: relative;
z-index: 29;
border: 1px #000000 solid;
}
.areaClass {
min-width: 100px;
max-width: 310px;
min-height: 60px;
max-height: 100px;
float: left;
padding: 7px;
position: relative;
z-index: 30;
border: 1px #000000 solid;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 24pt;
text-align: center;
}
<div class="headarea" id="headarea"></div>
<button class="buttonStyle" id="text" type="button" onclick="creaeTextField(this.id)"></button>
You could just listen for the keydown-event like you did in your question and then work with substring to get and remove the first part of the string. The first part can then be appended to an container and the rest of the string (in most cases only 1 char) can be set as content of the input-element.
you should also consider using textContent instead of innerHtml, so that the user is able input safely characters that are used by html.
let textOutput = document.getElementById('textOutput');
let textInput = document.getElementById('textInput');
let maxInputLength = 20;
let maxOutputElement = 4; // 4*20 = 80 Chars
textInput.addEventListener('keydown', function(event) {
if (textOutput.children.length >= maxOutputElement) {
event.preventDefault();
textInput.textContent = '';
}
let textChanged = false;
let text = textInput.textContent;
while (text.length >= maxInputLength) {
let firstPart = text.substring(0, maxInputLength);
let textElement = document.createElement('div');
textElement.textContent = firstPart;
textElement.contentEditable = true;
textElement.addEventListener('keyup', function(event) {
let editText = event.currentTarget.textContent;
if (editText.length >= maxInputLength) {
event.currentTarget.textContent = editText.substring(0, maxInputLength);
}
});
textOutput.appendChild(textElement);
text = text.substring(maxInputLength);
textChanged = true;
}
if (textChanged) textInput.textContent = text;
});
#textOutput div {
display: inline-block;
margin: 5px;
background-color: #EEE;
}
#textInput {
display: block;
width: 160px;
height: 20px;
background: #EEE;
border: 1px dashed #CCC;
}
<div id="textOutput"></div>
<div id="textInput" contenteditable="true"></div>
You could split the string into spaces and use a modulus to see when 20 items have been reached.
This makes the code independent and allows you to change the number of words allowed in each string.
We split on the space and then iterate over the words, we reconstruct the string but push the results into an array when we reach the 'paragraphlength'. You then end up with an array of strings that are 20 words long.
There are some edge cases that aren't covered here and may not be an issue depending on your use case. For example, what if a user just puts illegible characters in, ect
//The wording as a string
const words = "this is a really long set of words that need to be broken down into a paragraphs array that has lots of words that need to be broken down into a nicer structure for users to easier consume. We split on the space and then iterate over the words, we reconstruct the string but push the results into an array when we reach the 'paragraphlength'";
const getParagraphs = (words, paragraphLength) => {
let paragraphs =[];
words.split(" ").reduce((memo, word, index) => {
if(index > 0 && index % paragraphLength === 0)
{
paragraphs.push(memo);
memo = "";
}
return memo + " " + word;
}, "");
return paragraphs
}
const paragraphs = getParagraphs(words, 20);
console.log("original string ===>", words);
console.log("paragraphs array ===>", paragraphs);
//below is for display purposes only
let results = document.getElementById('results');
paragraphs.forEach(paragraph => {
const para = document.createElement("p");
const node = document.createTextNode(paragraph);
para.appendChild(node);
results.appendChild(para);
});
<div id="results">
</div>

How can you intelligently break a block of text in to paragraphs?

The following fiddle allows text to be imported into a <textarea> and dynamically generated into equal paragraphs. Is it possible to break the text in to paragraphs without breaking the text in the middle of a sentence? I want the length of each paragraph to be at or near the ChunkSize or user-adjusted limit, with each paragraph's element on the page being the same height.
If an updated fiddle could please be provided, would be extremely helpful, as I am still new to coding.
Thank You!
Fiddle
$(function() {
$('select').on('change', function() {
//Lets target the parent element, instead of P. P will inherit it's font size (css)
var targets = $('#content'),
property = this.dataset.property;
targets.css(property, this.value);
sameheight('#content p');
}).prop('selectedIndex', 0);
});
var btn = document.getElementById('go'),
textarea = document.getElementById('textarea1'),
content = document.getElementById('content');
chunkSize = 100;
btn.addEventListener('click', initialDistribute);
content.addEventListener('keyup', handleKey);
content.addEventListener('paste', handlePaste);
function initialDistribute() {
custom = parseInt(document.getElementById("custom").value);
chunkSize = (custom > 0) ? custom : chunkSize;
var text = textarea.value;
while (content.hasChildNodes()) {
content.removeChild(content.lastChild);
}
rearrange(text);
}
function rearrange(text) {
var chunks = splitText(text, false);
chunks.forEach(function(str, idx) {
para = document.createElement('P');
para.classList.add("Paragraph_CSS");
para.setAttribute('contenteditable', true);
para.textContent = str;
content.appendChild(para);
});
sameheight('#content p');
}
function handleKey(e) {
var para = e.target,
position,
key, fragment, overflow, remainingText;
key = e.which || e.keyCode || 0;
if (para.tagName != 'P') {
return;
}
if (key != 13 && key != 8) {
redistributeAuto(para);
return;
}
position = window.getSelection().getRangeAt(0).startOffset;
if (key == 13) {
fragment = para.lastChild;
overflow = fragment.textContent;
fragment.parentNode.removeChild(fragment);
remainingText = overflow + removeSiblings(para, false);
rearrange(remainingText);
}
if (key == 8 && para.previousElementSibling && position == 0) {
fragment = para.previousElementSibling;
remainingText = removeSiblings(fragment, true);
rearrange(remainingText);
}
}
function handlePaste(e) {
if (e.target.tagName != 'P') {
return;
}
overflow = e.target.textContent + removeSiblings(fragment, true);
rearrange(remainingText);
}
function redistributeAuto(para) {
var text = para.textContent,
fullText;
if (text.length > chunkSize) {
fullText = removeSiblings(para, true);
}
rearrange(fullText);
}
function removeSiblings(elem, includeCurrent) {
var text = '',
next;
if (includeCurrent && !elem.previousElementSibling) {
parent = elem.parentNode;
text = parent.textContent;
while (parent.hasChildNodes()) {
parent.removeChild(parent.lastChild);
}
} else {
elem = includeCurrent ? elem.previousElementSibling : elem;
while (next = elem.nextSibling) {
text += next.textContent;
elem.parentNode.removeChild(next);
}
}
return text;
}
function splitText(text, useRegex) {
var chunks = [],
i, textSize, boundary = 0;
if (useRegex) {
var regex = new RegExp('.{1,' + chunkSize + '}\\b', 'g');
chunks = text.match(regex) || [];
} else {
for (i = 0, textSize = text.length; i < textSize; i = boundary) {
boundary = i + chunkSize;
if (boundary <= textSize && text.charAt(boundary) == ' ') {
chunks.push(text.substring(i, boundary));
} else {
while (boundary <= textSize && text.charAt(boundary) != ' ') {
boundary++;
}
chunks.push(text.substring(i, boundary));
}
}
}
return chunks;
}
#text_land {
border: 1px solid #ccc;
padding: 25px;
margin-bottom: 30px;
}
textarea {
width: 95%;
}
label {
display: block;
width: 50%;
clear: both;
margin: 0 0 .5em;
}
label select {
width: 50%;
float: right;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
font-family: monospace;
font-size: 1em;
}
h3 {
margin: 1.2em 0;
}
div {
margin: 1.2em;
}
textarea {
width: 100%;
}
button {
padding: .5em;
}
p {
/*Here the sliles for OTHER paragraphs*/
}
#content p {
font-size: inherit;
/*So it gets the font size set on the #content div*/
padding: 1.2em .5em;
margin: 1.4em 0;
border: 1px dashed #aaa;
overflow: hidden;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<h3>Import Text below, then press the button</h3>
<textarea id="textarea1" placeholder="Type text here, then press the button below." rows="5">
</textarea>
<input style="width:200px;" id="custom" placeholder="Custom Characters per box">
<br>
<button style="width:200px;" id="go">Divide Text into Paragraphs</button>
</div>
<div>
<h3 align="right">Divided Text Will Appear Below:</h3>
<hr>
<div id="content"></div>
</div>
You can take the approach of splitting the text in to sentences, and then adding sentences to the paragraphs until you reach the desired length (chunkSize in your code).
function splitText (text) {
var paragraph = "",
paragraphs = [],
sentenceRegex = /[^\.!\?]+([\.!\?]+|\s*$)/g,
sentences = text.match(sentenceRegex);
sentences.forEach(function createParagraphs (sentence, index) {
paragraph += sentence;
if (paragraph.length >= chunkSize || index === sentences.length - 1) {
paragraphs.push(paragraph);
paragraph = "";
}
});
return paragraphs.length === 0 ? [text] : paragraphs;
}
https://jsfiddle.net/DirectCtrl/95kuyw4g/4/ (Tried to keep the rest of the code as similar to what it was as possible).
This doesn't deal with margins (meaning you could potentially get much longer paragraphs if you have sentences which end near the boundaries or go well beyond the boundary limit), though those kinds of problems are very likely to appear regardless on edge cases (e.g. with a chunkSize of 100 characters, what do you do when the first sentence is 40 characters and the second is 160 characters?). Tweaking this to use a margin should be pretty trivial, though, if that is a requirement. As the number of characters per paragraph increases, this would become less of an issue.

Show a caret in a custom textarea without displaying its text

I have a custom textarea. In this example, it makes the letters red or green, randomly.
var mydiv = document.getElementById('mydiv'),
myta = document.getElementById('myta');
function updateDiv() {
var fc;
while (fc = mydiv.firstChild) mydiv.removeChild(fc);
for (var i = 0; i < myta.value.length; i++) {
var span = document.createElement('span');
span.className = Math.random() < 0.5 ? 'green' : 'red';
span.appendChild(document.createTextNode(myta.value[i]));
mydiv.appendChild(span);
}
};
myta.addEventListener('input', updateDiv);
body { position: relative }
div, textarea {
-webkit-text-size-adjust: none;
width: 100%;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
font: 1rem sans-serif;
padding: 2px;
margin: 0;
border-radius: 0;
border: 1px solid #000;
resize: none;
}
textarea {
position: absolute;
top: 0;
color: transparent;
background: transparent;
}
.red { color: #f00 }
.green { color: #0f0 }
<div id="mydiv"></div>
<textarea id="myta" autofocus=""></textarea>
There's an output div with a textarea over it. So the textarea doesn't cover up any of the colorful things below it, its color and background are set to transparent. Everything works here, except that the caret (the flashing cursor provided by the user agent) is transparent.
Is there a way to show the caret without making the textarea's text visible?
If I make the div above the textarea instead and give it pointer-events: none, the textarea is still visible underneath. This arrangements also makes smooth scrolling difficult, so it doesn't work for me.
Just insert your own caret!
function blink() {
document.getElementById('caret').hidden ^= 1;
blinkTimeout = setTimeout(blink, 500);
}
var mydiv = document.getElementById('mydiv'),
myta = document.getElementById('myta'),
blinkTimeout = setTimeout(blink, 500),
lastSelectionStart = 0,
lastSelectionEnd = 0,
whichSelection = true;
function updateDiv() {
var fc;
while (fc = mydiv.firstChild) mydiv.removeChild(fc);
if (myta.selectionStart != lastSelectionStart) {
lastSelectionStart = myta.selectionStart;
whichSelection = false;
}
if (myta.selectionEnd != lastSelectionEnd) {
lastSelectionEnd = myta.selectionEnd;
whichSelection = true;
}
var cursorPos = whichSelection ? myta.selectionEnd : myta.selectionStart;
for (var i = 0; i < myta.value.length; i++) {
if (i == cursorPos) {
var caret = document.createElement('span');
caret.id = 'caret';
caret.appendChild(document.createTextNode('\xA0'));
mydiv.appendChild(caret);
clearTimeout(blinkTimeout);
blinkTimeout = setTimeout(blink, 500);
}
var span = document.createElement('span');
span.className = Math.random() < 0.5 ? 'green' : 'red';
span.appendChild(document.createTextNode(myta.value[i]));
mydiv.appendChild(span);
}
if (myta.value.length == cursorPos) {
var caret = document.createElement('span');
caret.id = 'caret';
caret.appendChild(document.createTextNode('\xA0'));
mydiv.appendChild(caret);
clearTimeout(blinkTimeout);
blinkTimeout = setTimeout(blink, 500);
}
};
myta.addEventListener('input', updateDiv);
myta.addEventListener('focus', updateDiv);
myta.addEventListener('mousedown', function() {
setTimeout(updateDiv, 0);
});
myta.addEventListener('keydown', function() {
setTimeout(updateDiv, 0);
});
myta.addEventListener('blur', function() {
document.getElementById('caret').hidden = true;
clearTimeout(blinkTimeout);
});
body { position: relative }
div, textarea {
-webkit-text-size-adjust: none;
width: 100%;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
font: 1rem sans-serif;
padding: 2px;
margin: 0;
border-radius: 0;
border: 1px solid #000;
resize: none;
}
textarea {
position: absolute;
top: 0;
color: transparent;
background: transparent;
}
.red { color: #f00 }
.green { color: #0f0 }
#caret {
display: inline-block;
position: absolute;
width: 1px;
background: #000;
}
#caret[hidden] { display: none }
<div id="mydiv"><span id="caret"> </span></div>
<textarea id="myta" autofocus=""></textarea>
I have here a <span> #caret inserted into the div which blinks every 500ms by toggling its hidden attribute using JS. To replicate browser behavior, I had to detect whether it was the selectionStart or the selectionEnd which the caret was actually at, and make it remain solid while text was being input.
This is a bit harder to achieve when the spans aren't of fixed length or are nested, but it's easier than fiddling with contentEditable with a more complex highlighter. This function will insert the caret in the right spot:
function insertNodeAtPosition(node, refNode, pos) {
if (typeof(refNode.nodeValue) == 'string') refNode.parentNode.insertBefore(node, refNode.splitText(pos));
else {
for (var i = 0; i < refNode.childNodes.length; i++) {
var chNode = refNode.childNodes[i];
if (chNode.textContent.length <= pos && i != refNode.childNodes.length - 1) pos -= chNode.textContent.length;
else return insertNodeAtPosition(node, chNode, pos);
}
}
}
Usage (where i is the position to insert it):
var caret = document.createElement('span');
caret.id = 'caret';
caret.appendChild(document.createTextNode('\xA0'));
insertNodeAtPosition(caret, mydiv, i);
clearTimeout(blinkTimeout);
blinkTimeout = setTimeout(blink, 500);
Why not simply use a <div contenteditable="true"></div> instead <textarea></textarea>?. With this you don't need the extra textarea. See a demo here.
HTML:
<div id="myta" autofocus="" contenteditable="true"></div>
JavaScript:
var myta = document.getElementById('myta');
function updateDiv() {
var fc;
var text = myta.innerText || myta.textContent;
while (fc = myta.firstChild) myta.removeChild(fc);
for (var i = 0; i < text.length; i++) {
var span = document.createElement('span');
span.className = Math.random() < 0.5 ? 'green' : 'red';
span.appendChild(document.createTextNode(text[i]));
myta.appendChild(span);
}
placeCaretAtEnd(myta);
};
myta.addEventListener('input', updateDiv);
Also, to move the caret at the end when you put the new text inside the div I used that function from this answer:
function placeCaretAtEnd(el) {
el.focus();
if (typeof window.getSelection != "undefined"
&& typeof document.createRange != "undefined") {
var range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (typeof document.body.createTextRange != "undefined") {
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.collapse(false);
textRange.select();
}
}

Categories

Resources