The new predictive type feature Smart Compose of Gmail is quite interesting.
Let's say we want to implement such a functionality ourselves:
User enters beginning of text, e.g. How and in gray behind it appears are you?.
User hits TAB and the word tomorrow is set.
Example:
Can a textarea with Javascript be used to achieve this?
And if not, how could this be implemented otherwise?
My previous answer got deleted, so here's a better attempt at explaining how I've somewhat replicated Smart Compose. My answer only focuses on the pertinent aspects. See https://github.com/jkhaui/predictable for the code.
We are using vanilla js and contenteditable in our solution (just like Gmail does). I bootstrap my example with create-react-app and Medium-Editor, but neither React nor Medium-Editor are necessary.
We have a database of "suggestions" which can be an array of words or phrases. For our purposes, in my example, I use a static array containing 50,000+ common English phrases. But you can easily see how this could be substituted for a dynamic data-source - such as how Gmail uses its neural network API to offer suggestions based on the current context of users' emails: https://ai.googleblog.com/2018/05/smart-compose-using-neural-networks-to.html
Smart Compose uses JavaScript to insert a <span></span> element immediately after the word you are writing when it detects a phrase to suggest. The span element contains only the characters of the suggestion that have not been typed.
E.g. Say you've written "Hi, how a" and a suggestion appears. Let's say the entire suggestion is "how are you going today". In this case, the suggestion is rendered as "re you going today" within the span. If you continue typing the characters in the placeholder - such as "Hi, how are you goi" - then the text content of the span changes dynamically - such that "ng today" is now the text within the span.
My solution works slightly differently but achieves the same visual effect. The difference is I can't figure out how to insert an inline span adjacent to the user's current text and dynamically mutate the span's content in response to the user's input.
So, Instead, I've opted for an overlay element containing the suggestion. The trick is now to position the overlay container exactly over the last word being typed (where the suggestion will be rendered). This provides the same visual effect of an inline typeahead suggestion.
We achieve correct positioning of the overlay by calculating the top + left coordinates for the last word being typed. Then, using JavaScript, we couple the top + left CSS attributes of the overlay container so that they always match the coordinates of the last word. The tricky part is getting these coordinates in the first place. The general steps are:
Call window.getSelection().anchorNode.data.length which retrieves the current text node the user is writing in and returns its length, which is necessary to calculate the offset of the last word within its parent element (explained in the following steps).
For simplicity's sake, only continue if the caret is at the end of the text.
Get the parent node of the current text node we're in. Then get the length of the parent node's text content.
The parent node's text length - the current text node's (i.e the last word's) text length = the offset position of the last text node within its contenteditable parent.
Now we have the offset of the last word, we can use the various range methods to insert a span element immediately preceding the last word: https://developer.mozilla.org/en-US/docs/Web/API/Range
Let's call this span element a shadowNode. Mentally, you can now picture the DOM as follows: we have the user's text content, and we have a shadowNode placed at the position of the last word.
Finally, we call getBoundingClientRect on the shadowNode which returns specific metadata, including the top + left coordinates we're after.
Apply the top + left coordinates to the suggestions overlay container and add the appropriate event handlers/listeners to render the suggestion when Tab is pressed.
Visit this link for documentation https://linkkaro.com/autocomplete.html .
May be you need to make few adjustment in CSS ( padding and width ).
I hope it will help.[![
$(document).ready(function(){
//dummy random output. You can use api
var example = {
1:"dummy text 1",
2:"dummy text 2"
};
function randomobj(obj) {
var objkeys = Object.keys(obj)
return objkeys[Math.floor(Math.random() * objkeys.length)]
}
var autocomplete = document.querySelectorAll("#autocomplete");
var mainInput = document.querySelectorAll("#mainInput");
var foundName = '';
var predicted = '';
var apibusy= false;
var mlresponsebusy = false;
$('#mainInput').keyup(function(e) {
//check if null value send
if (mainInput[0].value == '') {
autocomplete[0].textContent = '';
return;
}
//check if space key press
if (e.keyCode == 32) {
CallMLDataSetAPI(e);
scrolltobototm();
return;
}
//check if Backspace key press
if (e.key == 'Backspace'){
autocomplete[0].textContent = '';
predicted = '';
apibusy = true;
return;
}
//check if ArrowRight or Tab key press
if(e.key != 'ArrowRight'){
if (autocomplete[0].textContent != '' && predicted){
var first_character = predicted.charAt(0);
if(e.key == first_character){
var s1 = predicted;
var s2 = s1.substr(1);
predicted = s2;
apibusy = true;
}else{
autocomplete[0].textContent = '';
apibusy= false;
}
}else{
autocomplete[0].textContent = '';
apibusy= false;
}
return;
}else{
if(predicted){
if (apibusy == true){
apibusy= false;
}
if (apibusy== false){
mainInput[0].value = foundName;
autocomplete[0].textContent = '';
}
}else{
return;
}
}
function CallMLDataSetAPI(event) {
//call api and get response
var response = {
"predicted": example[randomobj(example)]
};
if(response.predicted != ''){
predicted = response.predicted;
var new_text = event.target.value + response.predicted;
autocomplete[0].textContent = new_text;
foundName = new_text
}else{
predicted = '';
var new_text1 = event.target.value + predicted;
autocomplete[0].textContent = new_text1;
foundName = new_text1
}
};
});
$('#mainInput').keypress(function(e) {
var sc = 0;
$('#mainInput').each(function () {
this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
this.setAttribute('style', 'height:' + (this.scrollHeight+3) + 'px;overflow-y:hidden;');
sc = this.scrollHeight;
});
$('#autocomplete').each(function () {
if (sc <=400){
this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
this.setAttribute('style', 'height:' + (sc+2) + 'px;overflow-y:hidden;');
}
}).on('input', function () {
this.style.height = 0;
this.style.height = (sc+2) + 'px';
});
});
function scrolltobototm() {
var target = document.getElementById('autocomplete');
var target1 = document.getElementById('mainInput');
setInterval(function(){
target.scrollTop = target1.scrollHeight;
}, 1000);
};
$( "#mainInput" ).keydown(function(e) {
if (e.keyCode === 9) {
e.preventDefault();
presstabkey();
}
});
function presstabkey() {
if(predicted){
if (apibusy == true){
apibusy= false;
}
if (apibusy== false){
mainInput[0].value = foundName;
autocomplete[0].textContent = '';
}
}else{
return;
}
};
});
#autocomplete { opacity: 0.6; background: transparent; position: absolute; box-sizing: border-box; cursor: text; pointer-events: none; color: black; width: 421px;border:none;} .vc_textarea{ padding: 10px; min-height: 100px; resize: none; } #mainInput{ background: transparent; color: black; opacity: 1; width: 400px; } #autocomplete{ opacity: 0.6; background: transparent;padding: 11px 11px 11px 11px; }
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<textarea id="autocomplete" type="text" class="vc_textarea"></textarea>
<textarea id="mainInput" type="text" name="comments" placeholder="Write some text" class="vc_textarea"></textarea>
]1]1
Related
I have a div that acts as a WYSIWYG editor. This acts as a text box but renders markdown syntax within it, to show live changes.
Problem: When a letter is typed, the caret position is reset to the start of the div.
const editor = document.querySelector('div');
editor.innerHTML = parse('**dlob** *cilati*');
editor.addEventListener('input', () => {
editor.innerHTML = parse(editor.innerText);
});
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}
div {
height: 100vh;
width: 100vw;
}
<div contenteditable />
Codepen: https://codepen.io/ADAMJR/pen/MWvPebK
Markdown editors like QuillJS seem to edit child elements without editing the parent element. This avoids the problem but I'm now sure how to recreate that logic with this setup.
Question: How would I get the caret position to not reset when typing?
Update:
I have managed to send the caret position to the end of the div, on each input. However, this still essentially resets the position. https://codepen.io/ADAMJR/pen/KKvGNbY
You need to get position of the cursor first then process and set the content. Then restore the cursor position.
Restoring cursor position is a tricky part when there are nested elements. Also you are creating new <strong> and <em> elements every time, old ones are being discarded.
const editor = document.querySelector(".editor");
editor.innerHTML = parse(
"For **bold** two stars.\nFor *italic* one star. Some more **bold**."
);
editor.addEventListener("input", () => {
//get current cursor position
const sel = window.getSelection();
const node = sel.focusNode;
const offset = sel.focusOffset;
const pos = getCursorPosition(editor, node, offset, { pos: 0, done: false });
if (offset === 0) pos.pos += 0.5;
editor.innerHTML = parse(editor.innerText);
// restore the position
sel.removeAllRanges();
const range = setCursorPosition(editor, document.createRange(), {
pos: pos.pos,
done: false,
});
range.collapse(true);
sel.addRange(range);
});
function parse(text) {
//use (.*?) lazy quantifiers to match content inside
return (
text
.replace(/\*{2}(.*?)\*{2}/gm, "**<strong>$1</strong>**") // bold
.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/gm, "*<em>$1</em>*") // italic
// handle special characters
.replace(/\n/gm, "<br>")
.replace(/\t/gm, " ")
);
}
// get the cursor position from .editor start
function getCursorPosition(parent, node, offset, stat) {
if (stat.done) return stat;
let currentNode = null;
if (parent.childNodes.length == 0) {
stat.pos += parent.textContent.length;
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
if (currentNode === node) {
stat.pos += offset;
stat.done = true;
return stat;
} else getCursorPosition(currentNode, node, offset, stat);
}
}
return stat;
}
//find the child node and relative position and set it on range
function setCursorPosition(parent, range, stat) {
if (stat.done) return range;
if (parent.childNodes.length == 0) {
if (parent.textContent.length >= stat.pos) {
range.setStart(parent, stat.pos);
stat.done = true;
} else {
stat.pos = stat.pos - parent.textContent.length;
}
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
setCursorPosition(currentNode, range, stat);
}
}
return range;
}
.editor {
height: 100px;
width: 400px;
border: 1px solid #888;
padding: 0.5rem;
white-space: pre;
}
em, strong{
font-size: 1.3rem;
}
<div class="editor" contenteditable ></div>
The API window.getSelection returns Node and position relative to it. Every time you are creating brand new elements so we can't restore position using old node objects. So to keep it simple and have more control, we are getting position relative to the .editor using getCursorPosition function. And, after we set innerHTML content we restore the cursor position using setCursorPosition.
Both functions work with nested elements.
Also, improved the regular expressions: used (.*?) lazy quantifiers and lookahead and behind for better matching. You can find better expressions.
Note:
I've tested the code on Chrome 97 on Windows 10.
Used recursive solution in getCursorPosition and setCursorPosition for the demo and to keep it simple.
Special characters like newline require conversion to their equivalent HTML form, e.g. <br>. Tab characters require white-space: pre set on the editable element. I've tried to handled \n, \t in the demo.
The way most rich text editors does it is by keeping their own internal state, updating it on key down events and rendering a custom visual layer. For example like this:
const $editor = document.querySelector('.editor');
const state = {
cursorPosition: 0,
contents: 'hello world'.split(''),
isFocused: false,
};
const $cursor = document.createElement('span');
$cursor.classList.add('cursor');
$cursor.innerText = ''; // Mongolian vowel separator
const renderEditor = () => {
const $contents = state.contents
.map(char => {
const $span = document.createElement('span');
$span.innerText = char;
return $span;
});
$contents.splice(state.cursorPosition, 0, $cursor);
$editor.innerHTML = '';
$contents.forEach(el => $editor.append(el));
}
document.addEventListener('click', (ev) => {
if (ev.target === $editor) {
$editor.classList.add('focus');
state.isFocused = true;
} else {
$editor.classList.remove('focus');
state.isFocused = false;
}
});
document.addEventListener('keydown', (ev) => {
if (!state.isFocused) return;
switch(ev.key) {
case 'ArrowRight':
state.cursorPosition = Math.min(
state.contents.length,
state.cursorPosition + 1
);
renderEditor();
return;
case 'ArrowLeft':
state.cursorPosition = Math.max(
0,
state.cursorPosition - 1
);
renderEditor();
return;
case 'Backspace':
if (state.cursorPosition === 0) return;
delete state.contents[state.cursorPosition-1];
state.contents = state.contents.filter(Boolean);
state.cursorPosition = Math.max(
0,
state.cursorPosition - 1
);
renderEditor();
return;
default:
// This is very naive
if (ev.key.length > 1) return;
state.contents.splice(state.cursorPosition, 0, ev.key);
state.cursorPosition += 1;
renderEditor();
return;
}
});
renderEditor();
.editor {
position: relative;
min-height: 100px;
max-height: max-content;
width: 100%;
border: black 1px solid;
}
.editor.focus {
border-color: blue;
}
.editor.focus .cursor {
position: absolute;
border: black solid 1px;
border-top: 0;
border-bottom: 0;
animation-name: blink;
animation-duration: 1s;
animation-iteration-count: infinite;
}
#keyframes blink {
from {opacity: 0;}
50% {opacity: 1;}
to {opacity: 0;}
}
<div class="editor"></div>
You need to keep the state of the position and restore it on each input. There is no other way. You can look at how content editable is handled in my project jQuery Terminal (the links point to specific lines in source code and use commit hash, current master when I've written this, so they will always point to those lines).
insert method that is used when user type something (or on copy-paste).
fix_textarea - the function didn't changed after I've added content editable. The function makes sure that textarea or contenteditable (that are hidden) have the same state as the visible cursor.
clip object (that is textarea or content editable - another not refactored name that in beginning was only for clipboard).
For position I use jQuery Caret that is the core of moving the cursor. You can easily modify this code and make it work as you want. jQuery plugin can be easily refactored into a function move_cursor.
This should give you an idea how to implement this on your own in your project.
You can use window.getSelection to get the current position and, after parsing, move the cursor to again this position with sel.modify.
const editor = document.querySelector('div')
editor.innerHTML = parse('**dlob** *cilati*')
sel = window.getSelection()
editor.addEventListener('input', () => {
sel.extend(editor, 0)
pos = sel.toString().length
editor.innerHTML = parse(editor.innerText)
while (pos-->0)
sel.modify('move', 'forward', "character")
})
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}
div {
height: 100vh;
width: 100vw;
}
<div contenteditable />
That said, note the edit history is gone (i.e. no undo), when using editor.innerHTML = ....
As other indicated, it seems better to separate editing and rendering.
I call this pseudo-contenteditable. I asked a question related to this
Pseudo contenteditable: how does codemirror works?. Still waiting for an answer.
But the basic idea might look this https://jsfiddle.net/Lfbt4c7p.
I'm trying to make a basic web application where people can highlight multiple words (i.e., click on the first word, then click on a word further on, and everything will be highlighted, even on another line).
So far I was able to wrap all of the words in tags and set a click event listener to each one which changes it's className to "highlight" which is just background-color:yellow.
The problem is that only the background of that individual word is highlighted, but i want everything in between the two (or more, even on different lines) words to be highlighted.
To complicate things a little more, i also have punctuation and maybe other stuff inbetween the words, which are not surrounded by span tags, but I want everything between the words to have a different background color/ be selected.
I was thinking of just putting the necessary words that are selected in they're own, separate span tag, but then, I'm not sure how to make it dynamically change exactly, and i also want the user to save the selections and then re-select them with a button or something, so that means that one word could be in 2 different phrases, and I'm not sure how one word could be in two different span tags....
So basically: how can I select multiple words in JavaScript including highlighting everything inbetween the two words?
EDIT
There was a request for some of the code I've tried, so I've attempted to simplify the relevant sections:
var h= eid("HebrewS");
var currentPhrase=[];
var equal=false;
var shtikles = [
];
h.innerHTML = h.innerHTML.replace(/([\u0590-\u05FF\"\']+)/g,'<span class="shtikle""">$1</span>');
var words = q("#HebrewS span");
words.forEach(function(item, idx){
shtikles[idx] = {obj:item, id:idx, heb:item.innerHTML, translation:"means "+ idx};
item.addEventListener("click", function(e) {
if(currentPhrase.length == 0) {
currentPhrase.push(idx);
currentPhrase[1]=idx;
equal=true;
}
else {
currentPhrase[1]=idx;
if(currentPhrase[1] < currentPhrase[0]) {
currentPhrase.reverse();
}
if(currentPhrase[0]==currentPhrase[1])
if(!equal) {
equal=true;
} else {
currentPhrase = new Array();
equal=false;
}
else
equal=false;
}
selectPhrase(currentPhrase);
});
function selectPhrase(p) {
for(var i =0;i<shtikles.length;i++) {
if(shtikles[i].obj)
if(p.length > 0) {
if(i < p[0] || i > p[1]) {
if(shtikles[i].obj.className != "shtikle") {
shtikles[i].obj.className ="shtikle";
}
}
} else {
shtikles[i].obj.className = "shtikle";
}
}
for(var i = p[0]; i <= p[1]; i++) {
shtikles[i].obj.className="phrasePart";
}
}
function q(a) {
return document.querySelectorAll(a);
}
function eid(id) {
return document.getElementById(id);
}
Now for the html:
<div style="" id ="HebrewS">today I will show,. you how] to read.. {Maamarim! וחזקת והיית לאיש1, הנה ידוע2 שהמאמר שאמר אדמו"ר (מהורש"ב) נ"ע ביום השביעי3 דחגיגת הבר מצוה של בנו יחידו כ"ק מו"ח אדמו"ר, י"ט תמוז4 תרנ"ג [שמאמר זה הוא סיום וחותם ההמשך תפילין דמארי עלמא5 שהתחיל לומר בי"ב תמוז, יום הבר מצוה] היתה התחלתו בפסוק זה. – השייכות דפסוק זה (וחזקת והיית לאיש) לבר מצוה בפשטות היא, ע"פ הידוע6 דזה שבן שלש עשרה (דוקא) מחוייב במצוות הוא כי אז דוקא נק' בשם איש. וצריך להבין, דמכיון שבן י"ג שנה נעשה איש (ע"פ טבע), מהי ההדגשה לומר (בחגיגת בר מצוה) וחזקת והיית לאיש. וגם צריך להבין, הרי המעלה דבן י"ג שנה היא שאז נעשה בר דעת7, דדעת הוא במוחין, ובפרט לפי המבואר בהמאמר ד"ה איתא במדרש תילים תרנ"ג [שהוא אחד המאמרים שחזר אותם כ"ק מו"ח אדמו"ר בחגיגת הבר שלו]8 שהמעלה דבן י"ג שנה היא שאז יש לו עצם המוחין9, ומהו הדיוק בבן י"ג שנה בהתואר איש שמורה10 על המדות
Now css:
<style type="text/css">
.shtikle:hover{
background-color:yellow;
}
.phrasePart{
background-color: purple;
border: 0px solid black;
}
</style>
I haven't tested the simplified version of the code, but if you try it out should work.
The basic point is :it selects each word individually, but doesn't highlight the stuff between the words (and I don't want to put all of the words in the current phrase into they're own span, because I want to save the phrase and have it selectable later, and also with multiple phrases some words might be in both)
Split everything into single nodes and then work with ranges. Simple implementation might look like below. It works on two clicks (right click removes selections) and is ready to implement click and slide (you'll have to implement mouseenter event listener with some boolean flag). So the point is that after every click id of each node is checked and either it becomes starting point of a range or closes the range with a for loop that adds class for every node in between.
You might then store id ranges somewhere there and activate them on eg. button click.
//EDIT
check edit below, this is totally messy, but I believe it should fit your needs.
const txt = 'this is some word in a wordy place where all words are kind of, well... this is some word in a wordy place where all words are kind of something so: this is some word in a wordy place where all words are kind of, and then -> this is some word in a wordy place where all words are kind of nothing';
let startIndex = null;
let lines = document.querySelector('.lines');
lines.innerHTML = txt.split(' ').map((z, i) => {
return (z.replace(new RegExp("\\w+|\\W+", "g"), (t) => { return /\w+/.test(t) ? `<span data-id="${i}" data-word>${t}</span>` : `<span data-id="${i}">${t}</span>` }));
}).join(' ');
nodes = Array.prototype.slice.call(document.querySelectorAll('.lines span'));
nodes.forEach(z => {
if(z.hasAttribute('data-word')) {
z.addEventListener('mousedown', (e) => {
const id = Number(e.target.getAttribute('data-id'));
if (startIndex === null) {
startIndex = id;
e.target.classList.add('active');
} else {
const range = id > startIndex ? [startIndex, id] : [id, startIndex];
for(let i = range[0]; i<= range[1]; i++) {
(Array.prototype.slice.call(document.querySelectorAll('span[data-id="' + i + '"]'))).forEach(e => e.classList.add('active'));
};
startIndex = null;
}
});
}
});
window.oncontextmenu = function ()
{
startIndex = null;
nodes.forEach(z => z.classList.remove('active'))
return false;
}
.lines {
user-select: none;
}
.lines span {
display: inline-block;
padding:3px;
box-decoration-break: clone;
transition:.2s;
}
.lines span.active {
background: salmon;
box-shadow: 3px 0 0 salmon, -3px 0 0 salmon;
}
[data-word] {
cursor:pointer;
}
<div class="lines"></div>
I have a spell check solution that uses a content editable div and inserts span tags around words that are misspelled. Every time the inner html of the div is updated, the cursor moves to the beginning of the div.
I know I can move the cursor to the end of the div if the user adds new words to the end of the sentence (code below).
Old Text: This is a spell checker|
New Text: This is a spell checker soluuution|
var range = document.createRange();
range.selectNodeContents(element[0]);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
However, I am unable to retain the cursor position if the user adds words in the middle of a sentence.
Old Text: This is a spell checker
New Text: This is a new spell checker|
In the above case, the cursor goes to the end of the div when it should be after "new".
How do I retain the cursor position? Since I am updating the html and adding nodes, saving the range before the update and adding it to the selection object isn't working.
Thanks in advance.
As far as I know, changing the content of the div will always have problem.
So here is the solution that I came with. Please type error word such as helloo, dudeee
This should ideally work for textarea as well.
Solution details:
Use a ghost div with same text content
Use transparent color for the ghost div
Use border-bottom for the ghost div span text
Change zIndex so that it does't appear infront
// some mock logic to identify spelling error
const errorWords = ["helloo", "dudeee"];
// Find words from string like ' Helloo world .. '
// Perhaps you could find a better library you that does this logic.
const getWords = (data) =>{
console.log("Input: ", data);
const allWords = data.split(/\b/);
console.log("Output: ", allWords)
return allWords;
}
// Simple mock logic to identify errors. Now works only for
// two words [ 'helloo', 'dudeee']
const containsSpellingError = word => {
const found = errorWords.indexOf(word) !== -1;
console.log("spell check:", word, found);
return found;
}
const processSpellCheck = text => {
const allWords = getWords(text);
console.log("Words in the string: ", allWords);
const newContent = allWords.map((word, index) => {
var text = word;
if(containsSpellingError(word.toLowerCase())) {
console.log("Error word found", word);
text = $("<span />")
.addClass("spell-error")
.text(word);
}
return text;
});
return newContent;
}
function initalizeSpellcheck(editorRef) {
var editorSize = editorRef.getBoundingClientRect();
var spellcheckContainer = $("<div />", {})
.addClass("spell-check")
.prop("spellcheck", "false");
var spellcheckSpan = $("<span />")
.addClass("spell-check-text-content")
.css({
width: editorSize.width,
height: editorSize.height,
position: "absolute",
zIndex: -1
});
var text = $(editorRef).text();
var newContent = processSpellCheck(text);
spellcheckSpan.append(newContent);
spellcheckContainer.append(spellcheckSpan);
spellcheckContainer.insertBefore(editorRef);
$(editorRef).on("input.spellcheck", function(event) {
var newText = $(event.target).text();
var newContent = processSpellCheck(newText);
$(".spell-check .spell-check-text-content").text("");
$(".spell-check .spell-check-text-content").append(newContent);
});
}
$(document).ready(function() {
var editor = document.querySelector("#editor");
initalizeSpellcheck(editor);
});
#editor {
border: 1px solid black;
height: 200px;
}
.spell-check {
color: transparent;
}
.spell-error {
border-bottom: 3px solid orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="editor" contenteditable="true" spellcheck="false">
dudeee
</div>
This answer might work from SitePoint:
Store the selection x, y:
cursorPos=document.selection.createRange().duplicate();
clickx = cursorPos.getBoundingClientRect().left;
clicky = cursorPos.getBoundingClientRect().top;
Restore the selection:
cursorPos = document.body.createTextRange();
cursorPos.moveToPoint(clickx, clicky);
cursorPos.select();
SitePoint Article: Saving/restoring caret position in a contentEditable div
Update 25.10.2019:
The solution mentioned above doesn't work anymore since functions are used that are deprecated. Does chrome supports document.selection?
I am using IE11 for this since that is my companys standard browser.
I am working on a solution to catch the paste event when pasting a screen dump into the web application. So far so good but after I have pasted the image I would like to change the size. Preferable before actually so I don't get a jumping application.
I have create a jsfiddle where you can see the entire test application: http://jsfiddle.net/e5f5gLan/3/
Do like this when running the jsfiddle:
Make a screen shot
Put the marker in the red square
Press Ctrl + v
Now the intention is that the pasted image should become 100px x 100px in size. It doesn't.
The problem is that I am not getting hold of the DOM object so I can set the style/size of the image.
The significant part is at the end of the javascript (I guess...):
var image_container = document.getElementById('pastearea');
var image = image_container.getElementsByTagName("img");
image[0].setAttribute("style", "width: 100px; height: 100px");
First of all, I imagined that the img element would become part of an array and that I should access the only img-element using image[0]. But then I get the error "Not possible to get setAttribute for a reference that is undefined or null. " (freely translated from Swedish...)
Ok, perhaps it understands it is only one element and just returns an object that isn't an array. So I changed the last row above to:
image.setAttribute("style", "width: 100px; height: 100px");
Then I get that setAttribute is not supported by the object.
If I create an HTML page with similar structure (img inside div) and just tries to change the size, then it works. Check out this one (click the button to shrink the image): http://jsfiddle.net/m4kzd7jp/3/
How can I change change the size of the image before or after I have pasted it?
Yeah I added the style and it worked to keep it 100px height/width.
#pastearea img { width: 100px; height: 100px; }
I tested your code and the filelist was always 0. If you want to handle this through code, here's what I was using in our project. basically you should be looping items not files and checking if it is a file.
function (e) {
var clip = e.clipboardData || window.clipboardData;
if (clip) {
var preInsert = getData(); // this pulled the input box text or div html
for (var i = 0; i < clip.items.length; i++) {
var item = clip.items[i];
if (item.kind == "file" &&
item.type.indexOf('image/') !== -1) {
var fr = new FileReader();
fr.onloadend = function () {
if (preInsert == getData()) // if nothing changed, we'll handle it otherwise it was already handled
setData(fr.result);
};
var data = item.getAsFile();
fr.readAsDataURL(data);
}
}
}
}
This area shows what I was doing in angular to get/set
var getSelection = function() {
if (window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
{
return window.getSelection().getRangeAt(0).cloneRange();
}
else if (document.selection)//IE 8 and lower
{
return document.selection.createRange();
}
return null;
}
var isInputTextarea = false; // I was being more generic in my angular directive
var getData = function () {
if (isInputTextarea)
return $element.val();
return $element.html();
};
var setData = function (src) {
if (isInputTextarea) {
$element.val(function (index, value) {
return value + " " + "<img src='" + src + "'>";
});
}
else {
var selection = getSelection(); // I presume we have focus at this point, since it is a paste event
if (selection) {
var img = document.createElement("img");
img.src = src;
selection.insertNode(img);
}
}
};
i need to highlight all the occurrences of a string in particular div by selecting a string,
once i select a word and click a button it need to highlight all its occurrence inside a div,
eg - if i select
cricket is game
it should highlight all the occurrences of cricket is game some may be like this cricket is game or cricket is game
You can get the browser to do the hard work for you using a TextRange in IE and window.find() in other browsers.
This answer shows how to do it. It will match text that crosses element boundaries and does the highlighting for you using document.execCommand().
Alternatively, James Padolsey recently published a script that I haven't used but looks like it could help: http://james.padolsey.com/javascript/replacing-text-in-the-dom-solved/
mark.js seems pretty good for this. Here's my 3 line fiddle to take an html 'string' and highlight the search string.
$(document).ready(function() {
var html_string = "<b>Major Tom to groundcontrol.</b> Earth is blue <span> and there's something </span> i can do";
var with_highlight = $("<div/>").html(html_string).mark("can");
$("#msg").html(with_highlight);
})
Link to jsfiddle
You can tryout this script Demo
in highlightSearchTerms function of this script var bodyText = document.body.innerHTML; get replace by your divid and than it will do the task for you..
/*
* This is the function that actually highlights a text string by
* adding HTML tags before and after all occurrences of the search
* term. You can pass your own tags if you'd like, or if the
* highlightStartTag or highlightEndTag parameters are omitted or
* are empty strings then the default <font> tags will be used.
*/
function doHighlight(bodyText, searchTerm, highlightStartTag, highlightEndTag)
{
// the highlightStartTag and highlightEndTag parameters are optional
if ((!highlightStartTag) || (!highlightEndTag)) {
highlightStartTag = "<font style='color:blue; background-color:yellow;'>";
highlightEndTag = "</font>";
}
// find all occurences of the search term in the given text,
// and add some "highlight" tags to them (we're not using a
// regular expression search, because we want to filter out
// matches that occur within HTML tags and script blocks, so
// we have to do a little extra validation)
var newText = "";
var i = -1;
var lcSearchTerm = searchTerm.toLowerCase();
var lcBodyText = bodyText.toLowerCase();
while (bodyText.length > 0) {
i = lcBodyText.indexOf(lcSearchTerm, i+1);
if (i < 0) {
newText += bodyText;
bodyText = "";
} else {
// skip anything inside an HTML tag
if (bodyText.lastIndexOf(">", i) >= bodyText.lastIndexOf("<", i)) {
// skip anything inside a <script> block
if (lcBodyText.lastIndexOf("/script>", i) >= lcBodyText.lastIndexOf("<script", i)) {
newText += bodyText.substring(0, i) + highlightStartTag + bodyText.substr(i, searchTerm.length) + highlightEndTag;
bodyText = bodyText.substr(i + searchTerm.length);
lcBodyText = bodyText.toLowerCase();
i = -1;
}
}
}
}
return newText;
}
/*
* This is sort of a wrapper function to the doHighlight function.
* It takes the searchText that you pass, optionally splits it into
* separate words, and transforms the text on the current web page.
* Only the "searchText" parameter is required; all other parameters
* are optional and can be omitted.
*/
function highlightSearchTerms(searchText, treatAsPhrase, warnOnFailure, highlightStartTag, highlightEndTag)
{
// if the treatAsPhrase parameter is true, then we should search for
// the entire phrase that was entered; otherwise, we will split the
// search string so that each word is searched for and highlighted
// individually
if (treatAsPhrase) {
searchArray = [searchText];
} else {
searchArray = searchText.split(" ");
}
if (!document.body || typeof(document.body.innerHTML) == "undefined") {
if (warnOnFailure) {
alert("Sorry, for some reason the text of this page is unavailable. Searching will not work.");
}
return false;
}
var bodyText = document.body.innerHTML;
for (var i = 0; i < searchArray.length; i++) {
bodyText = doHighlight(bodyText, searchArray[i], highlightStartTag, highlightEndTag);
}
document.body.innerHTML = bodyText;
return true;
}
/*
* This displays a dialog box that allows a user to enter their own
* search terms to highlight on the page, and then passes the search
* text or phrase to the highlightSearchTerms function. All parameters
* are optional.
*/
function searchPrompt(defaultText, treatAsPhrase, textColor, bgColor)
{
// This function prompts the user for any words that should
// be highlighted on this web page
if (!defaultText) {
defaultText = "";
}
// we can optionally use our own highlight tag values
if ((!textColor) || (!bgColor)) {
highlightStartTag = "";
highlightEndTag = "";
} else {
highlightStartTag = "<font style='color:" + textColor + "; background-color:" + bgColor + ";'>";
highlightEndTag = "</font>";
}
if (treatAsPhrase) {
promptText = "Please enter the phrase you'd like to search for:";
} else {
promptText = "Please enter the words you'd like to search for, separated by spaces:";
}
searchText = prompt(promptText, defaultText);
if (!searchText) {
alert("No search terms were entered. Exiting function.");
return false;
}
return highlightSearchTerms(searchText, treatAsPhrase, true, highlightStartTag, highlightEndTag);
}
This should get you started: http://jsfiddle.net/wDN5M/
function getSelText() {
var txt = '';
if (window.getSelection) {
txt = window.getSelection();
} else if (document.getSelection) {
txt = document.getSelection();
} else if (document.selection) {
txt = document.selection.createRange().text;
}
document.getElementById('mydiv').innerHTML = document.getElementById('mydiv').innerHTML.split(txt).join('<span class="highlight">' + txt + '</span>');
}
See: Get selected text on the page (not in a textarea) with jQuery
If you want it to work across element boundaries your code will need to be more involved than this. jQuery will make your life easier when doing the necessary DOM traversal and manipulation.
I would use jQuery to iterate over all Elements in your div (Don't know if you have other elements in the div) and then a Regular Expression and do a greedy match to find all occurrences of the selected string in your text(s) in the elements.
First you need to find needed substrings in needed text and wrap them with <span class="search-highlight">. Every time you need to highlight another strings, you just get all the .search-highlight spans and turn their outerHtml into innerHtml.
So the code will be close to:
function highLight(substring, block) {
$(block).find(".search-highlight").each(function () {
$(this).outerHtml($(this).html());
});
// now the block is free from previous highlights
$(block).html($(block).html().replace(/substring/g, '<span class="search-highlight">' + substring + '</span>'));
}
<form id=f1 name="f1" action=""
onSubmit="if(this.t1.value!=null && this.t1.value!='')
findString(this.t1.value);return false"
>
<input type="text" id=t1 name=t1size=20>
<input type="submit" name=b1 value="Find">
</form>
<script>
var TRange=null;
function findString (str) {
if (parseInt(navigator.appVersion)<4) return;
var strFound;
if (window.find) {
// CODE FOR BROWSERS THAT SUPPORT window.find
strFound=self.find(str);
if (!strFound) {
strFound=self.find(str,0,1);
while (self.find(str,0,1)) continue;
}
}
else if (navigator.appName.indexOf("Microsoft")!=-1) {
// EXPLORER-SPECIFIC CODE
if (TRange!=null) {
TRange.collapse(false);
strFound=TRange.findText(str);
if (strFound) TRange.select();
}
if (TRange==null || strFound==0) {
TRange=self.document.body.createTextRange();
strFound=TRange.findText(str);
if (strFound) TRange.select();
}
}
else if (navigator.appName=="Opera") {
alert ("Opera browsers not supported, sorry...")
return;
}
if (!strFound) alert ("String '"+str+"' not found!")
return;
}
</script>
Much better to use rather JavaScript str.replace() function then window.find() to find all occurrences of a filter value. Iterating through the whole page might be bit complicated but if you want to search within a parent div, or within a table str.replace() is just simpler.
In your example you have only one DIV, that is even simpler. Here is what I would do (having your DIV an ID: myDIV):
//Searching for "District Court"
var filter = "District Court";
//Here we create the highlight with html tag: <mark> around filter
var span = '<mark>' + filter + '</mark>';
//take the content of the DIV you want to highlight
var searchInthisDiv = document.getElementById("MyDiv");
var textInDiv = searchInthisDiv.innerHTML;
//needed this var for replace function, do the replace (the highlighting)
var highlighted = textInDiv.replace(filter,span);
textInDiv.innerHTML = highlighted;
The trick is to replace the search string with a span that is having the filter within a tag.
str.replace replaces all occurrences of the search string, so no need to bother with looping. Loop can be used to loop through DIVs or other DOM elements.