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(/([^sn]+|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.