JavaScript and css: highlight multiple words all with same background - javascript

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>

Related

HTML contenteditable: Keep Caret Position When Inner HTML Changes

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.

how do you deselect a word when clicking on it again?

How to deselect a word after making it clickable
What I want to do is add (I'm assuming) another JS so when they click on the word again, it'll appear back to normal without the black background?
Rather than setting inline values for your code, just create a CSS class to handle this, then toggle as necessary.
element.onclick = () => {
element.classList.toggle('highlighted');
}
And the class ...
.highlighted {
background-color: black;
color: white;
}
A messier solution would be to add an if/else statement to your onclick function.
element.onclick = () => {
if (element.style.background == '#000') {
element.style.background = '#fff';
}
else {
element.style.background = '#000';
}
}
The .split() function actually removes your spaces, so just make sure you fix that when appending everything back together as well.
inputValue.split(" ").forEach(word => {
const element = document.createElement("span");
element.innerHTML = word + ' ';
...
I've mocked up the class solution at codepen.io.

Css display dynamically added child with position absolute over parent with overflow hidden

So this is a bit of a mix-up.
I want to highlight certain words and add a help-text to them when you hover them. My current code searches for keywords, let's say the word "Mammal". When it finds the word "Mammal" it adds a span element around the word with a class that styles it so that you get an underline on the word. It also adds a child span element on the word that is hidden and contains the help-text I want to show.
The child's position is set to position:absolute and placed directly underneath the underlined word.
I have no control over the parent elements of the word or where on the page the word is, so the words parent/grandparent/etc might have overflow:hidden (and/or position:relative) which will partially hide the word. I want it to always show up (without taking any space on the page) when you hover the marked word, but I just can't think of a good way to solve this.
I've thought about putting the hover display text, not as a child of the hover element, but to have it further up above the overflow:hidden container, but I can't think of a good way how I would get that to work and how the child element would then target a grandparent sibling element to be displayed, etc.
Here's an element that shows how it looks and behaves:
.tooltip {
border-bottom: 1px solid orange;
}
.tooltip .tooltipText {
visibility: hidden;
width: 100px;
background-color: green;
position: absolute;
margin: 20px 0px 0px -80px;
}
.tooltip:hover .tooltipText {
visibility: visible;
}
<html>
<body>
<div style="overflow: hidden; width:100px;">
<div style="position:relative;">
<div>text <span class="tooltip">containing<span class="tooltipText">Hover text here</span></span> the word i'm looking for</div>
</div>
</div>
</body>
First i get all the nodes, then i go through them updating the words and adding the required elements:
nodes.forEach(function(node){
let nextNode = node.nextSibling,
parent = node.parentNode,
content = node.textContent,
newNodes = [];
node.parentNode.removeChild(node);
content.split(wordToHighlight).forEach(function(part, i, arr){
newNodes.push(document.createTextNode(part));
if(i < arr.length - 1){
let highlight = document.createElement("span");
highlight.innerHTML = wordToHighlight;
highlight.classList.add('tooltip');
let label = document.createElement('span');
label.classList.add('tooltipText');
label.innerHTML = "Hover text here";
highlight.appendChild(label);
newNodes.push(highlight);
}
});
newNodes.forEach(function(n){
if(nextNode)
parent.insertBefore(n, nextNode);
else
parent.appendChild(n);
});
});
This logic works for the most part, but when a parent/grandparent/etc contains either position:relative or overflow:hidden then the text gets cut and I've tried fiddling with CSS for hours now to see if I could find a way to make it work without much success. I would also like a better way to center it directly underneath the word it highlights, but move it so that it won't go out of the viewport to the right/left/top/bottom depending on where the word appears if that's possible.
I can add more details/code or such if needed, thanks for any help!
just insert the tooltip outside the element which has overflow: hidden;.
That is the only solution here, though you will need to position accordingly in that case.
The comment from #pilchard about using the library Popper worked perfectly for this use case and it was easy and fast to set up.
function show(selector, popperInstance) {
const tooltip = document.querySelector(selector);
tooltip.setAttribute('data-show', '');
popperInstance.setOptions({
modifiers: [{ name: 'eventListeners', enabled: true }],
});
popperInstance.update();
}
function hide(selector, popperInstance) {
const tooltip = document.querySelector(selector);
tooltip.removeAttribute('data-show');
popperInstance.setOptions({
modifiers: [{ name: 'eventListeners', enabled: false }],
});
}
function underlineWords()
{
const showEvents = ['mouseenter', 'focus'];
const hideEvents = ['mouseleave', 'blur'];
let nodeNumber = 1;
nodes.forEach(function(node){
let nextNode = node.nextSibling,
parent = node.parentNode,
content = node.textContent,
newNodes = [],
selectorIds = [],
poppers = [],
textIds = [];
node.parentNode.removeChild(node);
content.split(itemName).forEach(function(part, i, arr){
newNodes.push(document.createTextNode(part));
if(i < arr.length - 1){
let highlight = document.createElement('span');
highlight.innerHTML = itemName;
highlight.classList.add('tooltipHover');
highlight.id = itemName + nodeNumber + '-' + i + 'Hover';
highlight.setAttribute('aria-describedby', 'tooltip');
selectorIds.push('#' + highlight.id);
let tooltipText = document.createElement('span');
tooltipText.innerHTML = "Hover text here";
tooltipText.classList.add('tooltip');
tooltipText.id = nodeNumber + '-' + i + 'Show';
textIds.push('#' + tooltipText.id);
highlight.appendChild(tooltipText);
newNodes.push(highlight);
const popper = Popper.createPopper(highlight, tooltipText, {
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
});
poppers.push(popper);
}
});
newNodes.forEach(function(n){
if(nextNode)
parent.insertBefore(n, nextNode);
else
parent.appendChild(n);
});
for(let i = 0; i < selectorIds.length; i++){
const popper = poppers[i];
const selectorId = selectorIds[i];
const textId = textIds[i];
const selector = document.querySelector(selectorId);
showEvents.forEach(event => {
selector.addEventListener(event, function(){ show(textId, popper) });
});
hideEvents.forEach(event => {
selector.addEventListener(event, function(){ hide(textId, popper) });
});
}
nodeNumber += 1;
});
}

How to build a Smart Compose like Gmail? Possible in a textarea?

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

How to search text from page with next and previous functionality in jquery

I am implementing the Text search functionality on page .I found lot of links .But i need more functionality .
Here is good example
http://jsfiddle.net/z7fjW/137/
function searchAndHighlight(searchTerm, selector) {
if(searchTerm) {
//var wholeWordOnly = new RegExp("\\g"+searchTerm+"\\g","ig"); //matches whole word only
//var anyCharacter = new RegExp("\\g["+searchTerm+"]\\g","ig"); //matches any word with any of search chars characters
var selector = selector || "body"; //use body as selector if none provided
var searchTermRegEx = new RegExp(searchTerm,"ig");
var matches = $(selector).text().match(searchTermRegEx);
if(matches) {
$('.highlighted').removeClass('highlighted'); //Remove old search highlights
$(selector).html($(selector).html()
.replace(searchTermRegEx, "<span class='highlighted'>"+searchTerm+"</span>"));
if($('.highlighted:first').length) { //if match found, scroll to where the first one appears
$(window).scrollTop($('.highlighted:first').position().top);
}
return true;
}
}
return false;
}
But I need it only search first word(present first) then using next it goes to next (go to next position).Then previous (go to previous position).as in note pad ?
Is this possible in query?
instead of directly highligt them add class "match" and work with it
$(selector).html($(selector).html()
.replace(searchTermRegEx, "<span class='match'>"+searchTerm+"</span>"));
//to highlighted specific index
$('.match:first').addClass('highlighted');
//to work with index you need you var matches to know what indexes exist
$('.match').eq(3).addClass('highlighted');
demo
I looked at http://jsfiddle.net/ruddog2003/z7fjW/141/, and it was a great starting point. I modified the logic a bit to allow both next and previous, and to be a bit more robust. Its not perfect, but here follows my code in AJAX format
HTML
> <div data-role="content">
> <div id="fulltext-search">
> <input type="text" name="search-input" value="" placeholder="Type text here..."/>
> <input type="button" name="search-action" value="Search"/>
> <button id="searchPrevious"> << </button>
> <button id="searchNext"> >> </button>
> </div>
> </div>
CSS
#document_fulltext [data-role="content"] #fulltext-search {
width: 100%;
text-align: center;
position: fixed;
top: 0px;
background-color: rgba(255,255,255, 0.8);
z-index: 10000;
border-bottom: 1px solid #000;
}
.highlighted {
color: white;
background-color: rgba(255,20,0,0.5);
padding: 3px;
border: 1px solid red;
-moz-border-radius: 15px;
border-radius: 15px;
}
JAVASCRIPT EVENT
$(document).ready(function( ) {
loadFulltext();
//Load full text into the designated div
function loadFulltext(searchString){
//reset
$("#fulltext").html('');
filePath = 'http://localhost/income_tax_act_58_of_1962.html';
$.ajax({
url: filePath,
beforeSend: function( xhr ) {
xhr.overrideMimeType( "text/html; charset=UTF-8;" );
},
cache: false,
success: function(html){
$("#fulltext").html(html);
// if search string was defined, perform a search
if ((searchString != undefined) && (searchString != '') && (searchString != null)){
if(!searchAndHighlight(searchString, '#fulltext')) {
alert("No results found");
}
}
},
fail: function(){
alert('FAIL');
}
});
}
/* ------------------------------ CLICK - REFRESH DOCUMENTS LIST --- */
$(document).on('click', 'input[name="search-action"]', function ( event ){
// load fresh copy of full text into the div and perform a search once it is successfully completed
loadFulltext($('input[name="search-input"]').val());
});
});
JAVASCRIPT FUNCTION
function searchAndHighlight(searchTerm, selector) {
if(searchTerm) {
$('.highlighted').removeClass('highlighted'); //Remove old search highlights
$('.match').removeClass('match'); //Remove old matches
//var wholeWordOnly = new RegExp("\\g"+searchTerm+"\\g","ig"); //matches whole word only
//var anyCharacter = new RegExp("\\g["+searchTerm+"]\\g","ig"); //matches any word with any of search chars characters
var selector = selector || "body"; //use body as selector if none provided
var searchTermRegEx = new RegExp(searchTerm,"ig");
var matches = $(selector).text().match(searchTermRegEx);
// count amount of matches found
if(matches) {
alert('['+matches.length+'] matches found');
// replace new matches
$(selector).html($(selector).html().replace(searchTermRegEx, "<span class='match'>"+searchTerm+"</span>"));
// add highligt to first matched class
$('.match:first').addClass('highlighted');
// keep track of next and previous. Start at one because on SEARCH the forst one was already highlightes
var matchIndex = 1;
// look out for user click on NEXT
$('#searchNext').on('click', function() {
//Re-set match index to create a wrap effect if the amount if next clicks exceeds the amount of matches found
if (matchIndex >= matches.length){
matchIndex = 0;
}
var currentMatch = $('.match');
currentMatch.removeClass('highlighted');
var nextMatch = $('.match').eq(matchIndex);
matchIndex += 1;
nextMatch.addClass('highlighted');
// scroll to the top of the next found instance -n to allow easy viewing
$(window).scrollTop(nextMatch.offset().top-30);
});
// look out for user click on PREVIOUS
$('#searchPrevious').on('click', function() {
//Re-set match index to create a wrap effect if the amount if next clicks exceeds the amount of matches found
if (matchIndex < 0){
matchIndex = matches.length-1;
}
var currentMatch = $('.match');
currentMatch.removeClass('highlighted');
var previousMatch = $('.match').eq(matchIndex-2);
matchIndex -= 1;
previousMatch.addClass('highlighted');
// scroll to the top of the next found instance -n to allow easy viewing
$(window).scrollTop(previousMatch.offset().top-30);
});
// if match found, scroll to where the first one appears
if($('.highlighted:first').length) {
$(window).scrollTop($('.highlighted:first').position().top);
}
return true;
}
}
return false;
}

Categories