javascript - Three Contenteditable Divs that connect as one larger text box with maxChars of 274 - Stack Overflow
I'm building a tool to help me with multi-part tweets when I need to separate the character limitations into separate tweets. I want it built so that if I copy a large body of text or freely type into the first div (Tweet1), it will split it up as needed.
I've attempted some script using AI but I believe that is not allowed on this page so I did not share it below. After countless attempts and tweaks, I cannot get this to flow very well.
When I use my current code, the backspace acts all wonky and adds more lines of spaces below instead of deleting it. The first div will only allow one character at a time when I type before moving down a row. If I paste the text into the first div, it will overflow below, but it adds large blank lines. If I try to delete or edit, it adds more lines or deletes the end of that div instead of where the carrot is
Style:
.Tweet {
height: 25%;
padding: 10px;
font-size: 14px;
overflow: auto;
word-wrap: break-word;
white-space: pre-wrap;
border: 1px solid black;
margin: 10px;
}
Code:
Tweet 1
<div id='Tweet1' class='Tweet BlueBorder' contenteditable="true" oninput="countText1()"></div>
Tweet 2
<div id='Tweet2' class='Tweet BlueBorder' contenteditable="true" oninput="countText2()"></div>
Tweet 3
<div id='Tweet3' class='Tweet BlueBorder' contenteditable="true" oninput="countText3()"></div>
Script:
<script>
const Tweet1 = document.getElementById("Tweet1");
const Tweet2 = document.getElementById("Tweet2");
const Tweet3 = document.getElementById("Tweet3");
const maxChars = 274;
const urlCharCount = 23;
const tweets = [Tweet1, Tweet2, Tweet3];
tweets.forEach((div, index) => {
div.addEventListener("input", () => handleInput(index));
div.addEventListener("keydown", (e) => handleBackspace(e, index));
div.addEventListener("paste", handlePaste);
});
function handleInput(index) {
redistributeText();
}
function handleBackspace(event, index) {
const currentDiv = tweets[index];
if (event.key === "Backspace" && currentDiv.innerText.trim() === "" && index > 0) {
event.preventDefault();
const previousDiv = tweets[index - 1];
previousDiv.focus();
moveCaretToEnd(previousDiv);
redistributeText();
}
}
function handlePaste(event) {
event.preventDefault();
const text = (event.clipboardData || window.clipboardData).getData("text/plain");
const targetDiv = event.target;
// Insert pasted text and redistribute
const selection = window.getSelection();
if (selection.rangeCount) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
redistributeText();
}
}
function redistributeText() {
const allText = tweets.map(div => div.innerText).join("\n");
const words = splitTextIntoWordsAndNewLines(allText);
let remainingWords = [...words];
tweets.forEach((div, index) => {
if (index < tweets.length - 1) {
const [visibleWords, remaining] = fitWordsWithUrlHandling(remainingWords, maxChars);
div.innerText = visibleWords.join("");
remainingWords = remaining;
} else {
div.innerText = remainingWords.join("");
}
});
// Restore caret position if redistribution affected typing
restoreCaret();
}
function splitTextIntoWordsAndNewLines(text) {
const wordsAndLines = text.match(/([^\s\n]+|\s+|\n)/g) || [];
return wordsAndLines;
}
function fitWordsWithUrlHandling(words, limit) {
let visibleWords = [];
let charCount = 0;
for (const word of words) {
const isUrl = isValidUrl(word.trim());
const wordLength = word.trim() === "\n" ? 1 : isUrl ? urlCharCount : word.length;
if (charCount + wordLength <= limit) {
visibleWords.push(word);
charCount += wordLength;
} else {
break;
}
}
const remainingWords = words.slice(visibleWords.length);
return [visibleWords, remainingWords];
}
function isValidUrl(word) {
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(\/[^\s]*)?$/;
return urlRegex.test(word);
}
function moveCaretToEnd(element) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(element);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
function restoreCaret() {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const focusNode = selection.focusNode;
const focusOffset = selection.focusOffset;
tweets.forEach(div => {
const range = document.createRange();
range.selectNodeContents(div);
range.setStart(focusNode, focusOffset);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
});
}
// Initialize divs
tweets.forEach(div => {
div.innerText = "";
});
</script>
Screenshot of layout
I can either paste a large paragraph into or free-type text into and split the text into three separate Contenteditable Divs so that Tweet1 and Tweet2 will not allow any more than 274 characters before spilling down to the next div below. I want it so that it won't cut off words either so it uses a break-word to keep it moving down. I want it so that the three divs flow seamlessly between them so if I delete or add more text to any of the three sections it pushes or pulls text in or out of another div as needed.
I'm building a tool to help me with multi-part tweets when I need to separate the character limitations into separate tweets. I want it built so that if I copy a large body of text or freely type into the first div (Tweet1), it will split it up as needed.
I've attempted some script using AI but I believe that is not allowed on this page so I did not share it below. After countless attempts and tweaks, I cannot get this to flow very well.
When I use my current code, the backspace acts all wonky and adds more lines of spaces below instead of deleting it. The first div will only allow one character at a time when I type before moving down a row. If I paste the text into the first div, it will overflow below, but it adds large blank lines. If I try to delete or edit, it adds more lines or deletes the end of that div instead of where the carrot is
Style:
.Tweet {
height: 25%;
padding: 10px;
font-size: 14px;
overflow: auto;
word-wrap: break-word;
white-space: pre-wrap;
border: 1px solid black;
margin: 10px;
}
Code:
Tweet 1
<div id='Tweet1' class='Tweet BlueBorder' contenteditable="true" oninput="countText1()"></div>
Tweet 2
<div id='Tweet2' class='Tweet BlueBorder' contenteditable="true" oninput="countText2()"></div>
Tweet 3
<div id='Tweet3' class='Tweet BlueBorder' contenteditable="true" oninput="countText3()"></div>
Script:
<script>
const Tweet1 = document.getElementById("Tweet1");
const Tweet2 = document.getElementById("Tweet2");
const Tweet3 = document.getElementById("Tweet3");
const maxChars = 274;
const urlCharCount = 23;
const tweets = [Tweet1, Tweet2, Tweet3];
tweets.forEach((div, index) => {
div.addEventListener("input", () => handleInput(index));
div.addEventListener("keydown", (e) => handleBackspace(e, index));
div.addEventListener("paste", handlePaste);
});
function handleInput(index) {
redistributeText();
}
function handleBackspace(event, index) {
const currentDiv = tweets[index];
if (event.key === "Backspace" && currentDiv.innerText.trim() === "" && index > 0) {
event.preventDefault();
const previousDiv = tweets[index - 1];
previousDiv.focus();
moveCaretToEnd(previousDiv);
redistributeText();
}
}
function handlePaste(event) {
event.preventDefault();
const text = (event.clipboardData || window.clipboardData).getData("text/plain");
const targetDiv = event.target;
// Insert pasted text and redistribute
const selection = window.getSelection();
if (selection.rangeCount) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
redistributeText();
}
}
function redistributeText() {
const allText = tweets.map(div => div.innerText).join("\n");
const words = splitTextIntoWordsAndNewLines(allText);
let remainingWords = [...words];
tweets.forEach((div, index) => {
if (index < tweets.length - 1) {
const [visibleWords, remaining] = fitWordsWithUrlHandling(remainingWords, maxChars);
div.innerText = visibleWords.join("");
remainingWords = remaining;
} else {
div.innerText = remainingWords.join("");
}
});
// Restore caret position if redistribution affected typing
restoreCaret();
}
function splitTextIntoWordsAndNewLines(text) {
const wordsAndLines = text.match(/([^\s\n]+|\s+|\n)/g) || [];
return wordsAndLines;
}
function fitWordsWithUrlHandling(words, limit) {
let visibleWords = [];
let charCount = 0;
for (const word of words) {
const isUrl = isValidUrl(word.trim());
const wordLength = word.trim() === "\n" ? 1 : isUrl ? urlCharCount : word.length;
if (charCount + wordLength <= limit) {
visibleWords.push(word);
charCount += wordLength;
} else {
break;
}
}
const remainingWords = words.slice(visibleWords.length);
return [visibleWords, remainingWords];
}
function isValidUrl(word) {
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(\/[^\s]*)?$/;
return urlRegex.test(word);
}
function moveCaretToEnd(element) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(element);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
function restoreCaret() {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const focusNode = selection.focusNode;
const focusOffset = selection.focusOffset;
tweets.forEach(div => {
const range = document.createRange();
range.selectNodeContents(div);
range.setStart(focusNode, focusOffset);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
});
}
// Initialize divs
tweets.forEach(div => {
div.innerText = "";
});
</script>
Screenshot of layout
I can either paste a large paragraph into or free-type text into and split the text into three separate Contenteditable Divs so that Tweet1 and Tweet2 will not allow any more than 274 characters before spilling down to the next div below. I want it so that it won't cut off words either so it uses a break-word to keep it moving down. I want it so that the three divs flow seamlessly between them so if I delete or add more text to any of the three sections it pushes or pulls text in or out of another div as needed.
Share Improve this question asked yesterday Jake HembreeJake Hembree 11 bronze badge New contributor Jake Hembree is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 1 |1 Answer
Reset to default 0I am not sure whether users will appreciate the editing experience if the cursor jumps between <div>
elements while they type, even taking words with it that become too long for the previous <div>
.
It would be much simpler if you separated the input control (where users type or paste their text) from the output controls (the 274-character fields). Then you can simply split the whole input into 274-character chunks after every change.
function split() {
for (let i,
j = 1,
text = document.querySelector("textarea").value.replace(/\s/g, " ") + " ";
;
j++) {
const tweet = document.getElementById("t" + j);
if (!tweet) break;
i = text.lastIndexOf(" ", 274);
if (i === -1) {
tweet.value = text.substring(0, 274);
text = text.substring(274);
} else {
tweet.value = text.substring(0, i);
text = text.substring(i + 1);
}
}
}
<textarea onkeyup="split()"></textarea>
<input id="t1" size="274" readonly><br>
<input id="t2" size="274" readonly><br>
<input id="t3" size="274" readonly>
Does this meet your requirements?
Noteworthy points:
- All whitespace in the input is converted into spaces.
- An additional space is appended to the end of the input. Without that, a short input consisting of "two words" would be split into two tweets, although it fits into one.
- If there is no space among the next 274 characters, the overlong word is cut off after 274 characters.
- The
for
loop runs over all output controls, even iftext
has already been reduced to the empty string. This clears tweets that are no longer needed because the input has become shorter.
- 2013年科技行业推出的失败产品(组图)
- 分析称苹果本届WWDC将侧重软件 不会发布iPhone 5
- 山寨平板电脑走到穷途末路:方案公司纷纷倒闭
- python - tensorflow-gpu installation failed in colab - Stack Overflow
- python - Applying SMOTE-Tomek to nested cross validation with timeseries data - Stack Overflow
- python - NaN values in Pandas are not being filled by the interpolate function when it's applied to a full dataframe - S
- Laravel Livewire Pagination is not responsive - Stack Overflow
- php - Get WooCommerce custom payment gateway values in process_payment function for Blocks checkout - Stack Overflow
- python - Bullets shooting from wrong position after player moved in small "Space Invaders" game - Stack Overfl
- python 3.x - why is sqllachemy returning a class instead of a string - Stack Overflow
- utf 8 - correct way to Import database dump in PowerShell - Stack Overflow
- smartcontracts - FailedCall() with OpenZeppelin meta transactions (ERC2771Forwarder and ERC2771Context) - Stack Overflow
- jboss - Wildfly 35.0 deploy fail for missing DefaultDataSource - Stack Overflow
- flutter - How to adjust code formatting when formatting the document - Stack Overflow
- c++ - Member of struct constructed twice in custom constructor? - Stack Overflow
- python - Using OpenCV to achieve a top-down view of an image with ArUco Markers - Stack Overflow
- macos - Image from ImagePicker in landscape - Stack Overflow
textarea
elements? – trincot Commented yesterday