admin管理员组文章数量:1134557
I have a basic text editor where the user can search for a term in the text and navigate to the next or previous occurrence. The issue I’m facing is that when the search term is at the very beginning of the textarea
(e.g., the first word "Love"), pressing the "Previous" button doesn’t loop back to the last occurrence. However, this works fine if the search term is not at the start of the text (e.g., searching for the term "never").
Here is the relevant code:
let currentMatchIndex = -1; // Tracks the current match index
function resetIndex() {
currentMatchIndex = -1;
}
function highlightMatch(startIndex, endIndex) {
const textarea = document.getElementById('myTextarea');
textarea.setSelectionRange(startIndex, endIndex);
textarea.focus();
}
function next() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
const startIndex = text.indexOf(findTerm, currentMatchIndex + 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.indexOf(findTerm); // Loop back to first occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
function previous() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
const startIndex = text.lastIndexOf(findTerm, currentMatchIndex - 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.lastIndexOf(findTerm); // Loop to last occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
<input type="text" id="findInput" placeholder="Find" onchange="resetIndex()">
<button onclick="next()">Next</button>
<button onclick="previous()">Previous</button>
<textarea id="myTextarea" rows="3" cols="16">Love never dies. Love never dies. Love never dies.</textarea>
I have a basic text editor where the user can search for a term in the text and navigate to the next or previous occurrence. The issue I’m facing is that when the search term is at the very beginning of the textarea
(e.g., the first word "Love"), pressing the "Previous" button doesn’t loop back to the last occurrence. However, this works fine if the search term is not at the start of the text (e.g., searching for the term "never").
Here is the relevant code:
let currentMatchIndex = -1; // Tracks the current match index
function resetIndex() {
currentMatchIndex = -1;
}
function highlightMatch(startIndex, endIndex) {
const textarea = document.getElementById('myTextarea');
textarea.setSelectionRange(startIndex, endIndex);
textarea.focus();
}
function next() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
const startIndex = text.indexOf(findTerm, currentMatchIndex + 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.indexOf(findTerm); // Loop back to first occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
function previous() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
const startIndex = text.lastIndexOf(findTerm, currentMatchIndex - 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.lastIndexOf(findTerm); // Loop to last occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
<input type="text" id="findInput" placeholder="Find" onchange="resetIndex()">
<button onclick="next()">Next</button>
<button onclick="previous()">Previous</button>
<textarea id="myTextarea" rows="3" cols="16">Love never dies. Love never dies. Love never dies.</textarea>
Issue
When the search term is the first word in the textarea
(e.g., "Love"), and I press the "Previous" button, it does not loop back to the last occurrence. However, if I search for a term like "never," it works fine and loops as expected.
Expected behavior
- When the "Next" button is pressed, it finds the next occurrence.
- When the "Previous" button is pressed, it loops to the last occurrence when it reaches the first match.
Has anyone encountered this issue before or knows how to fix it? I'm looking for a robust solution that avoids handling edge cases and exceptions with multiple conditional statements.
Share Improve this question edited Jan 3 at 6:36 Mori asked Dec 31, 2024 at 20:04 MoriMori 6,74219 gold badges68 silver badges102 bronze badges 12 | Show 7 more comments8 Answers
Reset to default 8 +500The issue is that according to mdn
'hello world hello'.lastIndexOf('hello', 0)
and'hello world hello'.lastIndexOf('hello', -5)
both return 0 — because both cause the method to only look for hello at index 0.
so const startIndex = text.lastIndexOf(findTerm, -2);
gives 0 for 'Love' and goes inside the if
you could handle with something like
const startIndex = (currentMatchIndex - 1 < 0) ? -1 : text.lastIndexOf(findTerm, currentMatchIndex - 1);
let currentMatchIndex = -1; // Tracks the current match index
function resetIndex() {
currentMatchIndex = -1;
}
function highlightMatch(startIndex, endIndex) {
const textarea = document.getElementById('myTextarea');
textarea.setSelectionRange(startIndex, endIndex);
textarea.focus();
}
function next() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
const startIndex = text.indexOf(findTerm, currentMatchIndex + 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.indexOf(findTerm); // Loop back to first occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
function previous() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
//handling
const startIndex = (currentMatchIndex - 1 < 0) ? -1 : text.lastIndexOf(findTerm, currentMatchIndex - 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.lastIndexOf(findTerm); // Loop to last occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
<input type="text" id="findInput" placeholder="Find" onchange="resetIndex()">
<button onclick="next()">Next</button>
<button onclick="previous()">Previous</button>
<textarea id="myTextarea" rows="1" cols="50">Love never dies. Love never dies. Love never dies.</textarea>
The code can be shortened and made re-usable by applying the DRY principle. The following snippet shows it being applied on two <div>
s with the same pattern search functionality:
function findpatterns(cont){
const [[srch],[txt],btns]=["input[type=text]","textarea","button"].map(s=>cont.querySelectorAll(s));
srch.addEventListener("input",_=>{
cont.res=[...txt.value.matchAll(srch.value)].map(a=>a.index);
cont.patlen=srch.value.length;
cont.idx=undefined;
});
btns.forEach((b,i)=>b.addEventListener("click",_=>{
// do nothing if there is no match
if(!cont.res?.length) return;
// increment or decrement idx within the available indices of cont.res:
const rev=["Previous","<"].includes(b.textContent);
cont.idx=(cont.idx==undefined
?rev?cont.res.length-1:0
:cont.idx+(rev?cont.res.length-1:1)
)%cont.res.length;
hilite();
}));
function hilite(){
let pos=cont.res[cont.idx];
txt.setSelectionRange(pos,pos+cont.patlen);
txt.focus();
}
}
// Now, apply the function on all .findpat-divs:
document.querySelectorAll(".findpat").forEach(findpatterns);
First example:
<div class="findpat">
<input type="text" placeholder="Find"><br>
<button>Next</button>
<button>Previous</button><br>
<textarea rows="3" cols="20">Love never dies. Love never dies. Love never dies.</textarea>
</div>
<br>
And here is another example of the same thing:
<div class="findpat">
<input type="text" placeholder="Find"><br>
<button><</button>
<button>></button><br>
<textarea rows="3" cols="35">And here is something completely different. There are some repeated character groups, but not as obvious as in the first example</textarea>
</div>
The lines
const rev=["Previous","<"].includes(b.textContent);
cont.idx=(cont.idx==undefined
?rev?cont.res.length-1:0
:cont.idx+(rev?cont.res.length-1:1)
)%cont.res.length
deserve some.further explanation:
The constant rev
is set to true if one of the reverse buttons ("Previous" or "≪") was clicked.
The index in cont.idx
is ...
- initialised (if
cont.idx==undefined
) tocont.res.length-1
in case ofres
beingtrue
- or to
0
otherwise.
In case cont.idx
already has a value then it is
- decremented by
-1
in case ofrev
beingtrue
or - incremented by
1
for all other clicked buttons within a.findpat
div.
Actually, instead of decrementing cont.idx
simply by -1
I add cont.res.length-1
to it and then calculate the modulus with respect to cont.res.length
. This way I will always get a number pointing to a valid array index of cont.res
.
First the solution, then the explanation.
Solution
function getIndicesOf(searchStr, str, caseSensitive) {
var searchStrLen = searchStr.length;
if (searchStrLen == 0) {
return [];
}
var startIndex = 0, index, indices = [];
if (!caseSensitive) {
str = str.toLowerCase();
searchStr = searchStr.toLowerCase();
}
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
var myTextFinder = function(searchContext, sourceContext, nextButton, previousButton) {
var currentIndex = undefined;
var matchIndices = [];
var search = searchContext.value;
this.initialize = function(s) {
search = s;
currentIndex = undefined;
matchIndices = getIndicesOf(search, sourceContext.value, true);
};
this.highlight = function() {
if (matchIndices.length) {
sourceContext.setSelectionRange(matchIndices[currentIndex], matchIndices[currentIndex] + search.length);
sourceContext.focus();
}
};
this.go = (direction) => {
if (currentIndex === undefined) {
currentIndex = ((direction > 0) ? 0 : (matchIndices.length - 1))
} else {
currentIndex = (currentIndex + matchIndices.length + direction) % matchIndices.length;
}
this.highlight();
};
this.next = () => {
this.go(1);
};
this.previous = () => {
this.go(-1);
};
nextButton.addEventListener("click", () => {this.next()});
previousButton.addEventListener("click", () => {this.previous()});
var init = () => {
this.initialize(searchContext.value);
};
searchContext.addEventListener("input", init);
sourceContext.addEventListener("input", init);
};
new myTextFinder(document.getElementById("findInput"), document.getElementById("myTextarea"), document.getElementById("next"), document.getElementById("previous"));
<input type="text" id="findInput" placeholder="Find">
<button id="next">Next</button>
<button id="previous">Previous</button>
<textarea id="myTextarea" rows="3" cols="16">Love never dies. Love never dies. Love never dies.</textarea>
Explanation
- I have used
getIndicesOf
as a function that finds all indices from the update of the first answer here - it is basically loops
str
until it no longer finds a match and builds an array of indices myTextFinder
is an old school instantiable function where helper methods are being defined intocurrentIndex
is the current place of the index. It'sundefined
upon any initialization, because we wantnext
to point to the first match andprevious
to point to the last one upon the first click, but on later clicks to modulo-increment or modulo-decrement, respectively the resultsinitialize
resetscurrentIndex
, builds the match array and sets the search text to the current valuehighlight
sets the selection range to the current one, starting from the current index up to the current index + search sizego
computes the newcurrentIndex
based on the rules I described earlier and callshighlight
next
andprevious
are basically callinggo
with the correct direction- we define proper event handlers both for the buttons and for the contexts (search and source) so that upon next or previous click the event triggers as well as on search or context input we reinitialize
The main problem is your useage of lastIndexOf
, because if the position
parameter gets <=0
it will always find an occurrence at index 0
You can fix this in your code very easily by setting the optional position
parameter to Number.MAX_SAFE_INTEGER
if currentMatchIndex <= 0
(ie on the very first click on previous, or if the current match is at the start of the text)
const startIndex = text.lastIndexOf(findTerm, currentMatchIndex <= 0 ? Number.MAX_SAFE_INTEGER : currentMatchIndex - 1);
Another simple approach is, creating an index of the words first and then iterating over that index. This will also prevent to search the text over and over again. Of course, the index has to be reset on every change of the search field or the textare. For explanation see comments in the code
let
indexArray = [],
currentMatchIndex = -1, //current index in the index array
search = "";
// recreates the indexarray and resets the index on changing
// either the textarea or the searchfield
function resetIndex() {
let text = document.getElementById("myTextarea").value;
search = document.getElementById("findInput").value;
currentMatchIndex = -1;
indexArray = [];
// if either text or search is empty, do matches can be found
if (!text || !search)
return;
// iterate searching over the text and push the found indexes
// into the search array
let i = text.indexOf(search);
while (i >= 0) {
indexArray.push(i);
i = text.indexOf(search, i + 1);
}
}
function highlightMatch(startIndex, endIndex) {
const textarea = document.getElementById('myTextarea');
textarea.setSelectionRange(startIndex, endIndex);
textarea.focus();
}
// with each click on one of the search buttons iterate over the
// index array in the given direction (+1 = next, -1 = previous
function find(direction) {
if (!indexArray.length)
return alert("input not found");
if (!direction)
return alert("invalid direction");
direction = Math.sign(direction); //ensure direction is -1 or +1
// adapt the current match index if it's the very first search after resetIndex
// and searching previous (so that the last occurence will be selected
if (currentMatchIndex < 0 && direction < 0) currentMatchIndex = 0;
// calculate the new index in the match array, ie add the direction (+1 or -1)
// to the current index and loop over if the new index is outside the array
currentMatchIndex = (currentMatchIndex + direction + indexArray.length) % indexArray.length;
//get start and end position for highlighting
let
start = indexArray[currentMatchIndex],
end = start + search.length;
highlightMatch(start, end);
}
resetIndex();
<input type="text" id="findInput" placeholder="Find" onchange="resetIndex()">
<button onclick="find(1)">Next</button>
<button onclick="find(-1)">Previous</button>
<textarea id="myTextarea" rows="3" cols="16" onchange="resetIndex()">Love never dies. Love never dies. Love never dies.</textarea>
When the second argument passed to lastIndexOf
is negative, it will act the same as if that argument were 0. So the search will still start at index 0 in that case, and the only possible return value is 0 (when it matches there) or -1 (otherwise).
A way to avoid this undesired behaviour when you would pass -1 as argument, and to support wrap-around is to duplicate the text to search in, and set the start index in the second half when looking for a previous occurrence (and in the first half when looking for a next occurrence). This way you never get into this situation of a negative argument, and you can apply the %
operator on the result to get what you expect.
Some other remarks:
- It is better practice to attach event handlers in code, not via HTML attributes.
- You can avoid the global variable
currentMatchIndex
by looking at the current position of the caret. This has the advantage that if the user moves the caret, the next click on a button will search from that updated position. - You can avoid code repetition by passing a parameter to
next
to support both a next and previous search. - Instead of displaying an alert, it is more user-friendly to display a non-blocking message on the page itself.
Here is how that could look:
// Search for the elements just once
const btnPrev = document.getElementById("prev");
const btnNext = document.getElementById("next");
const textarea = document.getElementById('myTextarea');
const inpFind = document.getElementById('findInput');
const divMsg = document.getElementById('msg');
// Attach event handlers in code
btnPrev.addEventListener("click", () => next(true));
btnNext.addEventListener("click", () => next(false));
inpFind.addEventListener("change", resetIndex);
function resetIndex() {
textarea.setSelectionRange(0, 0);
}
function highlightMatch(startIndex, endIndex) {
textarea.setSelectionRange(startIndex, endIndex);
textarea.focus();
}
function next(reverse) {
const findTerm = inpFind.value;
if (!findTerm) return;
const text = textarea.value;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// Determine whether to call indexOf or lastIndexOf, and from where
const method = reverse ? "lastIndexOf" : "indexOf";
const offset = reverse ? text.length - 1 : +(text.slice(start, end) === findTerm);
// Duplicate the text to deal with wrap-around
const match = (text + text)[method](findTerm, start + offset) % text.length;
if (match > -1) highlightMatch(match, match + findTerm.length);
divMsg.style.display = match > -1 ? "none" : "block";
}
#msg { display: none; color: red }
<input type="text" id="findInput" placeholder="Find">
<button id="next">Next</button>
<button id="prev">Previous</button><br>
<textarea id="myTextarea" rows="3" cols="16">Love never dies. Love never dies. Love never dies.</textarea>
<div id="msg">Find term not found.</div>
Update
This example uses indexOf()
by a function generator.
The example below can use simple regex as well as strings. I have attempted to make this example more readable and less terse by changing the variables to be more descriptive. It's compact because I used certain properties that are not used very often although they are standard and really old. Located at the end of this answer is a list of links that explain some of the things I've used.
Expected Behavior
✅ When the "Next" button is pressed, it finds the next occurrence.
✅ When the "Previous" button is pressed, it loops to the last occurrence when it reaches the first match.
✅ Has anyone encountered this issue before or knows how to fix it?
✅ I'm looking for a robust solution that avoids handling edge cases and exceptions with multiple conditional statements.
The solution I wrote covers one edge case which is if the user keys Enter/Return while focused in the <input>
. There are now only 4 if
s but that's to be expected when you implement event delegation. For the core part dealing with the search pattern there are 2 if
s. Conditions are a fundamental necessity of any programming language and that goes double if user interaction is involved wherein we need to anticipate the users needs and actions. As for being robust, I'm 99% certain it can't be broken by the user. Also, lastIndexOf()
was avoided (I don't think I ever used it before).
Details are commented in example.
let index = 0; // Counter
let direction = 0; // Direction of counter
let matchArray; // On every search this is a 2D array.
// Reference <form>
const mainForm = document.forms.mainForm;
/**
* Reference all form controls in <form>
* In this layout it is all:
* - <input>
* - <fieldset>
* - <button>
* - <output>
* - <textarea>
*/
const io = mainForm.elements;
const findText = io.findText; // Reference <input>
const showInfo = io.showInfo; // Reference <output>
const textArea = io.textArea; // Reference <textarea>
const btnGroup = io.btnGroup; // Reference <fieldset>
// An array of all 4 <button>s
const btnArray = [...btnGroup.children];
/**
* Hide the 2 <button>s with the given [name] (grpName).
* @paran {string} grpName - Either "seek" or "step"
*/
const hideBtns = (grpName) => {
btnArray.forEach((btn) => {
if (btn.name === grpName) {
btn.classList.add("hide");
} else {
btn.classList.remove("hide");
}
});
};
/**
* Highlight the match from start to end on the text of
* #textArea indicated by the current index.
* #showInfo informs user of current count and total of
* matches.
*/
const markTerm = () => {
showInfo.value = (index + 1) + " of " + matchArray.length
" matching " + findText.value + ".";
const startIdx = matchArray[index][0];
const endIdx = matchArray[index][1];
textArea.setSelectionRange(startIdx, endIdx);
textArea.focus();
};
/**
* Gets an iterator object from the function generator
* listTerm() and converts it into an array (matchArray).
* If there's no matches then #showInfo informs the user
* and then the function terminates. If there are one or
* more matches, the highlight will start at the beginning
* of the matches if #miniBtn was clicked (direction = +1)
* or the highlight will start at the end of matches if
* #maxiBtn was clicked (direction = -1).
* Finally, markTerm() is called and hideBtns("seek").
*/
const findTerm = () => {
matchArray = [...listTerm(textArea.value, findText.value)];
if (matchArray.length === 0) {
showInfo.value = `No match for "${findText.value}".`;
return;
}
index = direction === -1 ? matchArray.length - 1 : 0;
markTerm();
hideBtns("seek");
};
/**
* Delegates the "click" event to the 4 <button>s.
* direction is assigned either a -1 (#prevBtn, #maxiBtn)
* or a +1 (#nextBtn, #miniBtn).
* If a <button ... name="seek"> was clicked, this function
* terminates and findTerm() is called.
* If a <button ... name="step" was clicked, index is
* modified by direction...
* if index is less than 0 then index is at the end of
* matchArray...
* if index is equal to or greater than the index at the
* end of matchArray the index is 0...
* otherwise index is index.
* Next, markTerm() is called.
* @param {object} event - Event object
*/
const moveTerm = (event) => {
const clicked = event.target.id;
switch (clicked) {
case "prevBtn": direction = -1; break;
case "nextBtn": direction = +1; break;
case "maxiBtn": direction = -1; return findTerm();
case "miniBtn": direction = +1; return findTerm();
default: return;
}
index = index + direction;
index = index < 0 ? matchArray.length - 1 :
index >= matchArray.length ? 0 :
index;
markTerm();
};
/**
* Delegate the "input" event on either #findText or
* #textArea. If the user enters text in either #findText
* or #textArea btnGroup will be enabled and the [name="seek"]
* <button>s will be revealed.
* If either #findText or #textArea is empty, #btnGroup
* remains [disabled].
* @param {object} event - Event object
*/
const typeTerm = (event) => {
showInfo.value = "";
btnGroup.disabled = !findText.value.trim() ||
!textArea.value.trim();
if (!btnGroup.disabled) {
hideBtns("step");
}
};
/**
* Find each occurance of the given substring (term) in the
* given text (text). On each match, yield a subarray
* consisting of it's starting and ending indices. before
* return, an iterator object has been yielded which
* findTerm() will convert into a 2D array (matchArray).
* @param {string} text - Text to search through
* @param {string} term - Search term to find
* @yield {object} - An iterator
*/
const listTerm = function*(text, term) {
let i = 0;
while (true) {
const startIdx = text.indexOf(term, i);
if (startIdx !== -1) {
const endIdx = startIdx + term.length;
yield [startIdx, endIdx];
i = startIdx + 1;
} else return;
}
};
/**
* Register <fieldset id="btnGroup"> to listen an delegate
* the "click" event for the 4 <button>s by calling
* moveTerm(event).
*/
btnGroup.addEventListener("click", moveTerm);
/**
* Register <form id="mainForm"> to listen and delegate the
* "input" event on either #findText or #textArea by
* calling typeTerm(event).
*/
mainForm.addEventListener("input", typeTerm);
/**
* Register <form id="mainForm"> to listen for the "submit"
* event and stop the default redirection of the page if the
* user keys the Return/Enter key while focused in the
* <input>.
*/
mainForm.addEventListener("submit", (event) => {
event.preventDefault();
return;
});
:root {
font: 2ch/1.5 "Segoe UI";
}
form {
max-width: 23rem;
}
input,
button,
textarea {
display: inline-block;
font: inherit;
padding: 5px;
}
button {
width: 3rem;
margin-bottom: 8px;
cursor: pointer;
}
#findText {
width: 13.25rem;
margin-bottom: 8px;
}
#btnGroup {
display: inline-block;
margin: 0;
margin-left: 3px;
padding: 0;
border: 0;
}
#btnGroup[disabled] button {
opacity: 0.4;
cursor: not-allowed;
}
[name="seek"] {
color: lime;
}
[name="step"] {
color: cyan;
}
.hide {
display: none;
}
#showInfo {
display: block;
margin: 0 auto 5px;
color: blue;
}
#showInfo::after {
content: "\00feff";
}
#textArea {
width: 19rem;
}
<!-- <search> is just for semantics -->
<search>
<form id="mainForm">
<fieldset>
<legend>Find Text</legend>
<!--
[type="search"] is a [type="text] with a delete button: X
-->
<input id="findText" type="search" placeholder="Find">
<!--
This <fieldset> is has it's default styles removed.
It's the parent of 4 <button>s.
Access to these <button>s is blocked when it is
[disabled].
-->
<fieldset id="btnGroup" disabled>
<!--
The 1 set of 2 <button>s are hidden (.hide) while
the other set of 2 buttons are rendered in the DOM.
Which are hidden and visible is determined by user
actions: If user types anything in #findText or #textArea
the <button ... name="seek"> are visible. If the user
clicks a [name="seek"] and has one or more matches
[name="step"] <button>s appear.
<!--
These 2 <button>s move the highlight on the
text of #textArea forward and back.
They both have [name="step"].
-->
<button id="prevBtn" name="step" class="hide" type="button">◀</button>
<button id="nextBtn" name="step" class="hide" type="button">▶</button>
<!--
These 2 <button>s start the highlight the text on
either the beginning or end of the matches in the text
of #textArea.
They both have [name="seek"]
-->
<button id="maxiBtn" name="seek" type="button">◀</button>
<button id="miniBtn" name="seek" type="button">▶</button>
</fieldset>
<!--
This <output> displays messages ex:
'No match for "term".'
'3 of 6 matching "term".'
-->
<output id="showInfo"></output>
<!--
This <textarea> contains the text user will search.
-->
<textarea id="textArea" rows="3" cols="16">
Love never dies.
Love never dies.
Love never dies.
</textarea>
</fieldset>
</form>
</search>
Some Stuff Used
HTMLFormElement.elements
Terse and succinct syntax to access [form controls]:2. Assuming the layout has an
<input>
, a<textarea>
,<select>
, and multiple<button>
s:const form = document.forms.formID; const fc = form.elements; const input = fc.inputID; const textarea = fc.textareaID; const select = fc.selectID; const btnArray = Array.from(fc.btnName);
Equivalent to:
const form = document.getElementById("formID"); const fc = form.querySelectorAll("input, textarea, select, button"); const input = form.querySelector("#inputID"); const textarea = form.querySelector("#textareaID"); const select = form.querySelector("#selectID"); const btnArray = [...form.querySelectorAll("[name=btnName]")];
Ternary Operator
Terse and succinct syntax alternative flow control. Assuming
checkbox
is already defined.let state = checkbox.checked ? 1 : 0;
Equivalent to:
let state; if (checkbox.checked) state = 1; else state = 0;
the issue arises because when your search term is at the very beginning of the textarea (index 0) the calculation for the lastIndexOf in the previous() function doesn't correctly loop back to the last occurrence, it happens because currentMatchIndex - 1 evaluates to -1, and text.lastIndexOf(findTerm, -1) will not find any valid index, resulting in currentMatchIndex remaining -1.
To resolve this issue, you can adjust your previous() function logic slightly by explicitly checking whether the lastIndexOf returned a valid index and ensuring proper looping. Here's an updated version of your code:
let currentMatchIndex = -1; // Tracks the current match index
function resetIndex() {
currentMatchIndex = -1;
}
function highlightMatch(startIndex, endIndex) {
const textarea = document.getElementById('myTextarea');
textarea.setSelectionRange(startIndex, endIndex);
textarea.focus();
}
function next() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
const startIndex = text.indexOf(findTerm, currentMatchIndex + 1);
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.indexOf(findTerm); // Loop back to first occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
function previous() {
const findTerm = document.getElementById('findInput').value;
const textarea = document.getElementById('myTextarea');
const text = textarea.value;
if (findTerm) {
let startIndex;
if (currentMatchIndex > 0) {
startIndex = text.lastIndexOf(findTerm, currentMatchIndex - 1);
} else {
startIndex = text.lastIndexOf(findTerm); // Loop to last occurrence
}
if (startIndex !== -1) {
currentMatchIndex = startIndex;
} else {
currentMatchIndex = text.lastIndexOf(findTerm); // Loop to last occurrence
}
if (currentMatchIndex !== -1) {
highlightMatch(currentMatchIndex, currentMatchIndex + findTerm.length);
} else {
alert("Find term not found.");
}
}
}
<input type="text" id="findInput" placeholder="Find" onchange="resetIndex()">
<button onclick="next()">Next</button>
<button onclick="previous()">Previous</button>
<textarea id="myTextarea" rows="3" cols="16">Love never dies. Love never dies. Love never dies.</textarea>
When currentMatchIndex is 0 (the search term is the first word), subtracting 1 makes the starting point negative. JavaScript resets that to 0, so it just keeps finding the same match instead of looping back.
You can check if currentMatchIndex is 0 and, if so, start searching from the end of the text instead. Something like this:
if (currentMatchIndex === 0) {
startIndex = text.length; // wrap around to the end
} else {
startIndex = currentMatchIndex - 1;
}
This way the search loops properly
本文标签:
版权声明:本文标题:javascript - Why doesn't the previous() function loop when the find term is the first word in a textarea? - Stack Overfl 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736879605a1955978.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
position
is less than 0, the behavior is the same as for 0 — that is, the method looks for the specified substring only at index 0." – derpirscher Commented Dec 31, 2024 at 21:22lastIndexOf
? – Mori Commented Jan 2 at 21:28