I'm try to get the currently selected text in an input using window.getSelection() but I'm always getting an empty string:
expect(browser.executeScript("return window.getSelection().toString();")).toEqual("test");
Results into:
Expected '' to equal 'test'.
The complete reproducible test using angularjs.org as a target site:
describe("My test", function () {
beforeEach(function () {
browser.get("https://angularjs.org/");
});
it("should select text in an input", function () {
var query = element(by.css("input.search-query"));
query.sendKeys("test");
query.sendKeys(protractor.Key.chord(protractor.Key.COMMAND, "a"));
expect(browser.executeScript("return window.getSelection().toString();")).toEqual("test");
});
});
Note that I actually see the entered text being selected with COMMAND + "a".
What am I doing wrong?
Using protractor 2.5.1, firefox 41.
getSelection does not work for text selected in input elements, but for selections made on elements across the page.
You could use selectionStart and selectionEnd like this:
return document.activeElement.value.substring(
document.activeElement.selectionStart,
document.activeElement.selectionEnd)
You should probably create a function for this instead of this one-liner. And maybe you want to then also test whether document.activeElement is indeed the right type of element, etc. And when you are at it, you might even make it compatible for pre-IE9 browsers... (difficult though)
Simple Function
This will work also on input or textarea controls that do not have focus:
function getInputSelection(el) {
if (el.selectionStart !== undefined) {
return el.value.substring(el.selectionStart, el.selectionEnd);
}
}
// Example call:
console.log(getInputSelection(document.activeElement));
Extensive jQuery Plug-in
This provides for more cross-browser compatibility (pre-IE9), and supports not only getting, but also setting the selection range and text, in the form of a jQuery plug-in. It deals with the fact that CRLF character sequences count as one character position in a pragmatic way (replace in-place by LF only):
/**
* jQuery plug-in for getting/setting the selection range and text
* within input/textarea element(s). When the selection is set,
* the element will receive focus. When getting the selection,
* some browsers require the element to have focus (IE8 and below).
* It is up to the caller to set the focus first, if so needed.
* #this {jQuery} Input/textarea element(s).
* #param {object} opt_bounds When provided, it sets the range as follows:
* #param {number} opt_bounds.start Optional start of the range. If not
* provided, the start point of the range is not altered.
* #param {number} opt_bounds.end Optional end of the range. If not
* provided, the end point of the range is not altered. If null, the end
* of the text value is assumed.
* #param {number} opt_bounds.text Optional text to put in the range. If
* not provided, no change will be made to the range's text.
* #return {jQuery|object|undefined} When setting: the same as #this to
* allow chaining, when getting, an object {start, end, text, length}
* representing the selection in the first element if that info
* is available, undefined otherwise.
*/
$.fn.selection = function (opt_bounds) {
var bounds, inputRange, input, docRange, value;
function removeCR(s) {
// CRLF counts as one unit in text box, so replace with 1 char
// for correct offsetting
return s.replace(/\r\n/g, '\n');
}
if (opt_bounds === undefined) {
// Get
if (!this.length) {
return;
}
bounds = {};
input = this[0];
if (input.setSelectionRange) {
// Modern browsers
bounds.start = input.selectionStart;
bounds.end = input.selectionEnd;
} else {
// Check browser support
if (!document.selection || !document.selection.createRange) {
return;
}
// IE8 or older
docRange = document.selection.createRange();
// Selection must be confined to input only
if (!docRange || docRange.parentElement() !== input) { return; }
// Create another range that can only extend within the
// input boundaries.
inputRange = input.createTextRange();
inputRange.moveToBookmark(docRange.getBookmark());
// Measure how many characters we can go back within the input:
bounds.start =
-inputRange.moveStart('character', -input.value.length);
bounds.end = -inputRange.moveEnd('character', -input.value.length);
}
// Add properties:
bounds.length = bounds.end - bounds.start;
bounds.text = removeCR(input.value).
substr(bounds.start, bounds.length);
return bounds;
}
// Set
if (opt_bounds.text !== undefined) {
opt_bounds.text = removeCR(opt_bounds.text);
}
return this.each(function () {
bounds = $.extend($(this).selection(), opt_bounds);
bounds.end = bounds.end === null ? this.value.length : bounds.end;
if (opt_bounds.text !== undefined) {
value = removeCR(this.value);
this.value = value.substr(0, bounds.start) + bounds.text +
value.substr(bounds.end);
bounds.end = bounds.start + bounds.text.length;
}
if (this.setSelectionRange) {
// Modern browsers
// Call .focus() to align with IE8 behaviour.
// You can leave that out if you don't care about that.
this.focus();
this.setSelectionRange(bounds.start, bounds.end);
} else if (this.createTextRange) {
// IE8 and before
inputRange = this.createTextRange();
inputRange.collapse(true);
inputRange.moveEnd('character', bounds.end);
inputRange.moveStart('character', bounds.start);
// .select() will also focus the element:
inputRange.select();
}
});
};
Example use:
// Get
console.log($('textarea').selection().text);
// Set text
$('textarea').selection({text: "Hello!"});
// Set starting point of selection
$('textarea').selection({start: 1});
I've written an extension for firefox which highlights all words on a web page (excluding some words in a given list).
What i've noticed is that (besides that my extension is terribly slow) some web pages get "destroyed", more specifically the layout gets destroyed (particularly websites with overlay advertising or fancy drop-down menus).
My code wraps <span> tags around every "word", or to be precise around every token, because i'm splitting the text nodes with a whitespace as seperator.
So is it possible anyway to realize this task without destroying the page's layout?
I'm iterating over all text nodes, split them, and iterate over every token.
When the token is in my list, i don't highlight it, else i wrap the <span> tag around it.
So any suggestions how this could be done faster would be helpful, too.
Here are some screenshots for a correctly highlighted and a not correctly highlighted web page:
right:
en.wikipedia.org before highlighting,
en.wikipedia.org after highlighting.
wrong:
developer.mozilla.org before highlighting,
developer.mozilla.org after highlighting.
OK. Study this code. It searches for all instances of "is" and highlights if it is not surrounded by word characters. Put this in your scratchpad while this tab is focused. You will see that words like "List" and other words containing "Is" are no highlighted, but all the "Is"'s are.
I basically made an addon here for you. You can now release this as an addon called RegEx FindBar and take all the credit....
var doc = gBrowser.contentDocument;
var ctrler = _getSelectionController(doc.defaultView);
var searchRange = doc.createRange();
searchRange.selectNodeContents(doc.documentElement);
let startPt = searchRange.cloneRange();
startPt.collapse(true);
let endPt = searchRange.cloneRange();
endPt.collapse(false);
let retRane = null;
let finder = Cc["#mozilla.org/embedcomp/rangefind;1"].createInstance().QueryInterface(Ci.nsIFind);
finder.caseSensitive = false;
var i = 0;
while (retRange = finder.Find('is', searchRange, startPt, endPt)) {
i++;
var stCont = retRange.startContainer;
var endCont = retRange.endContainer;
console.log('retRange(' + i + ') = ', retRange);
console.log('var txt = retRange.commonAncestorContainer.data',retRange.commonAncestorContainer.data);
//now test if one posiion before startOffset and one position after endOffset are WORD characters
var isOneCharBeforeStCharWordChar; //var that holds if the character before the start character is a word character
if (retRange.startOffset == 0) {
//no characters befor this characte so obviously not a word char
isOneCharBeforeStCharWordChar = false;
} else {
var oneCharBeforeStChar = stCont.data.substr(retRange.startOffset-1,1);
if (/\w/.test(oneCharBeforeStChar)) {
isOneCharBeforeStCharWordChar = true;
} else {
isOneCharBeforeStCharWordChar = false;
}
console.log('oneCharBeforeStChar',oneCharBeforeStChar);
}
var isOneCharAfterEndCharWordChar; //var that holds if the character before the start character is a word character
if (retRange.endOffset == endCont.length - 1) {
//no characters after this characte so obviously not a word char
isOneCharAfterEndCharWordChar = false;
} else {
var oneCharAferEndChar = endCont.data.substr(retRange.endOffset,1); //no need to subtract 1 from endOffset, it takes into account substr 2nd arg is length and is treated like length I THINK
if (/\w/.test(oneCharAferEndChar)) {
isOneCharAfterEndCharWordChar = true;
} else {
isOneCharAfterEndCharWordChar = false;
}
console.log('oneCharAferEndChar',oneCharAferEndChar);
}
if (isOneCharBeforeStCharWordChar == false && isOneCharAfterEndCharWordChar == false) {
//highlight it as surrounding characters are no word characters
_highlightRange(retRange, ctrler);
console.log('highlighted it as it was not surrounded by word charactes');
} else {
console.log('NOT hilte it as it was not surrounded by word charactes');
}
//break;
startPt = retRange.cloneRange();
startPt.collapse(false);
}
/*********************/
function _getEditableNode(aNode) {
while (aNode) {
if (aNode instanceof Ci.nsIDOMNSEditableElement)
return aNode.editor ? aNode : null;
aNode = aNode.parentNode;
}
return null;
}
function _highlightRange(aRange, aController) {
let node = aRange.startContainer;
let controller = aController;
let editableNode = this._getEditableNode(node);
if (editableNode)
controller = editableNode.editor.selectionController;
let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
findSelection.addRange(aRange);
if (editableNode) {
// Highlighting added, so cache this editor, and hook up listeners
// to ensure we deal properly with edits within the highlighting
if (!this._editors) {
this._editors = [];
this._stateListeners = [];
}
let existingIndex = this._editors.indexOf(editableNode.editor);
if (existingIndex == -1) {
let x = this._editors.length;
this._editors[x] = editableNode.editor;
this._stateListeners[x] = this._createStateListener();
this._editors[x].addEditActionListener(this);
this._editors[x].addDocumentStateListener(this._stateListeners[x]);
}
}
}
function _getSelectionController(aWindow) {
// display: none iframes don't have a selection controller, see bug 493658
if (!aWindow.innerWidth || !aWindow.innerHeight)
return null;
// Yuck. See bug 138068.
let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController);
return controller;
}
Oh edit my solution out, will update with proper solution, I see you want to highlight all words
This is the code how firefox highlights stuff without changing document: Finder.jsm - _highlight function. You will have to copy this and use it for the whole document, if you need help let me know and I'll do it.
Here was my solution to highlight all matches of single word: https://stackoverflow.com/a/22206366/1828637
Here man this is how you are going to highlight the whole document, I didn't finish the snippet but this is the start of it: Gist - HighlightTextInDocument
Here's the copy paste answer to highlight everything in the document. As you learn more about it share with us, like how you can highlight with a different color, right now its all pink O_O
function _getEditableNode(aNode) {
while (aNode) {
if (aNode instanceof Ci.nsIDOMNSEditableElement)
return aNode.editor ? aNode : null;
aNode = aNode.parentNode;
}
return null;
}
function _highlightRange(aRange, aController) {
let node = aRange.startContainer;
let controller = aController;
let editableNode = this._getEditableNode(node);
if (editableNode)
controller = editableNode.editor.selectionController;
let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
findSelection.addRange(aRange);
if (editableNode) {
// Highlighting added, so cache this editor, and hook up listeners
// to ensure we deal properly with edits within the highlighting
if (!this._editors) {
this._editors = [];
this._stateListeners = [];
}
let existingIndex = this._editors.indexOf(editableNode.editor);
if (existingIndex == -1) {
let x = this._editors.length;
this._editors[x] = editableNode.editor;
this._stateListeners[x] = this._createStateListener();
this._editors[x].addEditActionListener(this);
this._editors[x].addDocumentStateListener(this._stateListeners[x]);
}
}
}
function _getSelectionController(aWindow) {
// display: none iframes don't have a selection controller, see bug 493658
if (!aWindow.innerWidth || !aWindow.innerHeight)
return null;
// Yuck. See bug 138068.
let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController);
return controller;
}
var doc = gBrowser.contentDocument;
var searchRange = doc.createRange();
searchRange.selectNodeContents(doc.documentElement);
_highlightRange(searchRange,_getSelectionController(gBrowser.contentWindow))
#jervis, I can't make a comment on your comment under #Noitidart code as I don't have 50rep yet. So I have to post here.
Re:
I did it with 'gFindBar._highlightDoc(true, word)' now. I'm using firefox 17, so i dont know if gFindBar is state of the art. – jervis 40 mins ago
But I tested his code and and it works.
Don't use gFindBar.
Copy it and then paste it into your Scratchpad.
Why are you using gFindBar._highlightDoc(true, word) ? I thoght you wanted to highlight everything in the document? Where did you get _highlightDoc from? I don't see that anywhere in #Noitidart's code.
Regading yoru comment on iterate all words and use gFindBar._highlightDoc:
I did it with 'gFindBar._highlightDoc(true, word)' now. I'm using firefox 17, so i dont know if gFindBar is state of the art. – jervis 39 mins ago
Dude why do that.... I saw #Noitidart posted a per word solution on the linked topic: gBrowser.tabContainer.childNodes[0].linkedBrowser.finder.highlight(true, 'YOUR_WORD_HERE'); that is extremely easy, one line and no need to create text nodes spans or anything. You have to run this code on each tab you want to highlight in.
I have multiple vocabulary tables on the same html page.
Above each vocabulary table, I would like to enable users to type a word or phrase in a text input field to view only the table rows that contain the typed string (word or phrase). For example, if you type "good" in the text input field, the table rows that do not contain the string "good" will disappear. This is already working if you go to http://www.amanado.com/idioma-colombiano/, click on "Vocabulario (oficial y de jerga) - palabras y frases comunes" to expand the accordion section, and then type "good" in the text input field below the words "Ingresa palabra o frase en el siguiente campo para filtrar la información de la tabla". After typing "good" into the text input field, all but 7 rows in the vocabulary table should disappear (7 rows remain).
I am having the following 3 issues:
1) I am unable to ignore accents (e.g., é, ñ, ü) in the same way that case is already successfully ignored. For example, if a user enters "que" in the input field, rows that contain "que" and "qué" should not disappear. However, when you type "que", rows that contain "qué" do erroneously disappear. As you can see, if you type "que" into the input field (excluding the quotes), 2 records that contain "que" will remain. And, if you type or paste "qué" into the input field (excluding the quotes), 6 records that contain "qué" will remain.
2) I am trying to use a version of jquery.highlight.js to highlight the string matches in the rows that remain/do not disappear. For an example of how this should look visually, type "que" into the input field where instructed in the 2nd paragraph of this question summary and you will see the string "que" is highlighted in the 2 rows that remain/do not disappear. Note that this is not working correctly because I hardcoded the "que" highlight by inserting the script "$("table td").highlight("que");" into the "head" section of the html page for the purposes of demonstrating that (a) jquery.highlight.js is active/functioning and (b) providing a visual example of how the highlighted text is intended to appear.
3) In addition to the javascript that enables users to enter a word or phrase in a field to view only the table rows that contain the entered word or phrase not successfully ignoring accents (e.g., é, ñ, ü), which is the desired behavior, the jquery.highlight.js script is also not successfully ignoring accents (e.g., é, ñ, ü). For example, type "pues" in the input field where instructed in the 2nd paragraph of this question summary and you will multiple cases of the string "Qué" and "qué" not successfully highlighted in the rows that remain/do not disappear. Remember, I hardcoded the "que" highlight by inserting the script "$("table td").highlight("que");" into the section of the html page, so the strings "que", "qué", "Que" and "Qué" should all be highlighted in the table rows that remain/do not disappear if any of the strings "que", "qué", "Que" or "Qué" are entered into the input field given it is intended that (a) case and (b) accents (e.g., é, ñ, ü) are ignored. It is interesting to note that functionionality to "ignoreAccents" is included in the version of jquery.highlight.js that I am using.
Below are:
(a) the input field as it appears in my html;
(b) the javascript I am using to enable users to enter a word or phrase in a field to view only the table rows that contain the entered word or phrase (for the purpose of brevity, this is referred to below as "filter javascript"); and
(c) the version of jquery.highlight.js javascript I am using to highlight text.
Please note: I am not a software engineer, but I do know how to implement a change if someone tells me what to do specifically (e.g., make this exact change, then make this exact change, then make this exact change). I appreciate any assistance anyone can provide and literal instructions would be especially appreciated. And, It is always my intent to use the least amount of code (e.g., javascript, css, html) to achieve the most.
Additional notes/considerations are included at the bottom of this question summary.
(a) input field starts here
<form class="live-search" action="" method="post">
<p>Ingresa palabra o frase en el siguiente campo para filtrar la información de la tabla</p>
<input class="input-text-tr" type="text" value="Mostrar sólo filas que contengan..." />
<span class="filter-count-tr"></span>
</form>
(a) input field ends here
(b) filter javascript starts here
$(function() {
$(".input-text-tr").on('keyup', function(e) {
var disallow = [37, 38, 39, 40];//ignore arrow keys
if($.inArray(e.which, disallow) > -1) {
return true;
}
var inputField = this,
val = this.value,
pattern = new RegExp(val, "i"),
$group = $(this).closest(".group"),
$trs = $group.find(".myTable tbody tr"),
$s;
if(val === '') {
$s = $trs;
}
else {
$s = $();
$trs.stop(true,true).each(function(i, tr) {
if(val !== inputField.value) {//if user has made another keystroke
return false;//break out of .each() and hence out of the event handler
}
$tr = $(tr);
if ($tr.text().match(pattern)) {
$s = $s.add(tr);
}
});
//$trs.not($s).fadeOut();
$trs.not($s).hide();
}
$group.find(".filter-count-tr").text("(" + $s.show ().length + ")");
}).on('focus blur', function() {
if (this.defaultValue == this.value) this.value = '';
else if (this.value == '') this.value = this.defaultValue;
});
$(".group").each(function() {
$this = $(this);
$this.find(".filter-count-tr").text("(" + $this.find("tbody tr").length + ")");
});
});
(b) filter javascript ends here
(c) jquery.highlight.js javascript starts here
jQuery.extend({
highlight: function (node, re, nodeName, className, ignoreAccents) {
if (node.nodeType === 3) {
var nodeData = node.data;
if (ignoreAccents) {
nodeData = jQuery.removeDiacratics(nodeData);
}
var match = nodeData.match(re);
if (match) {
var highlight = document.createElement(nodeName || 'span');
highlight.className = className || 'highlight'; var wordNode = node.splitText(match.index);
wordNode.splitText(match[0].length);
var wordClone = wordNode.cloneNode(true);
highlight.appendChild(wordClone);
wordNode.parentNode.replaceChild(highlight, wordNode);
return 1; //skip added node in parent
}
} else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
!/(script|style)/i.test(node.tagName) && // ignore script and style nodes
!(node.tagName === nodeName.toUpperCase() &&
node.className === className)) { // skip if already highlighted
for (var i = 0; i < node.childNodes.length; i++) {
i += jQuery.highlight(node.childNodes[i], re, nodeName, className, ignoreAccents);
}
}
return 0;
},
removeDiacratics : function(str) {
var rExps = [
{re:/[\xC0-\xC6]/g, ch:'A'},
{re:/[\xE0-\xE6]/g, ch:'a'},
{re:/[\xC8-\xCB]/g, ch:'E'},
{re:/[\xE8-\xEB]/g, ch:'e'},
{re:/[\xCC-\xCF]/g, ch:'I'},
{re:/[\xEC-\xEF]/g, ch:'i'},
{re:/[\xD2-\xD6]/g, ch:'O'},
{re:/[\xF2-\xF6]/g, ch:'o'},
{re:/[\xD9-\xDC]/g, ch:'U'},
{re:/[\xF9-\xFC]/g, ch:'u'},
{re:/[\xD1]/g, ch:'N'},
{re:/[\xF1]/g, ch:'n'}
];
for (var i = 0, len = rExps.length; i < len; i++) {
str = str.replace(rExps[i].re, rExps[i].ch);
}
return str;
}
});
jQuery.fn.unhighlight = function (options) {
var settings = { className: 'highlight', element: 'span' };
jQuery.extend(settings, options);
return this.find(settings.element + "." + settings.className).each(
function () {
var parent = this.parentNode;
parent.replaceChild(this.firstChild, this);
parent.normalize();
}).end();
};
jQuery.fn.highlight = function (words, options) {
var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false, ignoreAccents : true };
jQuery.extend(settings, options);
if (words.constructor === String) {
words = [words];
}
words = jQuery.grep(words, function(word, i) {
return word != '';
});
words = jQuery.map(words, function(word, i) {
return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
});
if (words.length == 0) {
return this;
}
var flag = settings.caseSensitive ? "" : "i";
var pattern = "(" + words.join("|") + ")";
if (settings.wordsOnly) {
pattern = "\\b" + pattern + "\\b";
}
var re = [];
re.push(new RegExp(pattern, flag));
if (settings.ignoreAccents) {
var wordsNoAccents = jQuery.map(words, function(word, i) {
return jQuery.removeDiacratics(word);
});
var patternNoAccents;
if (settings.wordsOnly) {
// workaround for word separation using \\b
patternNoAccents = "( " + wordsNoAccents.join("|") + " )";
patternNoAccents = "\\b" + patternNoAccents + "\\b";
} else {
patternNoAccents = "(" + wordsNoAccents.join("|") + ")";
}
if (patternNoAccents!=pattern) {
re.push(new RegExp(patternNoAccents, flag));
}
}
return this.each(function () {
for (var i in re) {
jQuery.highlight(this, re[i], settings.element, settings.className, settings.ignoreAccents);
}
});
};
(c) jquery.highlight.js javascript ends here
Additional notes/considerations start here
1) It is my intent to enhance, not depart from, the javascript I am already using to enable users to enter a word or phrase in a field to view only the table rows that contain the entered word or phrase because the javascript I am already using is working with the exception of the above issues (thanks to Beetroot's excellent contributions to a previous question I published).
2) javascript I've found that touches on the functionality I am trying to achieve includes the following 4 examples (note because stackoverflow does not allow me to use more than a couple links in a question, I replaced "http://" with "[http:// here]" in the below examples):
a) [http:// here]demopill.com/jquery-onpage-text-highlighter-and-filter.html [most closely resembles functionality I am trying to achive; seems to successfully filter and highlight as a user enters text into an input field; successfully ignores case, but does not successfully ignore accents (e.g., é, ñ, ü)];
b) [http:// here]stackoverflow.com/search?q=jquery.highlight.js (dialogue on stackoverflow re: ignoring accented characters)
c) [http:// here]www.jquery.info/The-plugin-SearchHighlight (includes a highlight feature); and
d) [http:// here]docs.jquery.com/UI/Effects/Highlight (includes a highlight feature; note that I am already using "jquery ui" on the website referenced in paragraph 2 of this question summary).
Additional notes/considerations end here
Highlighting
With jquery.highlight.js installed on the page ...
change :
$group.find(".filter-count-tr").text("(" + $s.show().length + ")");
to :
$group.find(".filter-count-tr").text("(" + $s.show().unhighlight().highlight(val).length + ")");
However, the accent-insensitivity code below modifies this.
Accent Insensitivity
This seemed almost impossible but I had a breakthrough on finding this which indicates how the hightlight plugin might be modified to offer accent-insensitive highlighting.
To better understand the code, I refactored it into a better plugin (better for me anyway). It now puts no members into the jQuery namespace (previously one) and one member into jQuery.fn (previously two). With the new plugin, setting and unsetting highlights is performed as follows:
$(selector).highlight('set', words, options);
$(selector).highlight('unset', options);
Explanations and further examples are provided with the code (see below).
The 'set' settings include an '.accentInsensitive' option, which operates (I regret) on a limited number of hard-coded (Spanish) accented character groups implemented about as efficiently as I can manage using a private member in the plugin to cache reusable RegExps and replacement strings for later use by the 'set' method. It would be far better to have a generalized "Unicode normalisation" solution but that's something for another day.
The new plugin also afforded the opportunity to split out part of the code as a separate method, .makepattern, with the advantage that RegExp-ready patterns can be used externally, outside the plugin, with provision for them to be reinjected. This feature allows us to use the plugin as a resource for achieving the other aim here - namely accent-insensitive filtering - with absolute certainty that the RegExp patterns used (for highlighting and filtering) are identical.
Here's the plugin code :
/*
* jQuery highlightIt plugin
* by Beetroot-Beetroot
* https://stackoverflow.com/users/1142252/beetroot-beetroot
*
* based on Highlight by Bartek Szopka, 2009
* http://bartaz.github.com/sandbox.js/jquery.highlight.html,
* based on highlight v3 by Johann Burkard
* http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
*
* Most important changes:
* - Code refactored into jQuery preferred plugin pattern.
* - Now called with :
* - $(slector).highlight('set', words, options); previously $(slector).highlight(words, options);
* - $(slector).highlight('unset', options); previously $(slector).unhighlight(options);
* - $().highlight('makePattern', words, options); This new option returns a RegExp-ready pattern that can be used externally and/or re-injected for reuse (see .isPattern option below), thus avoiding remaking the pattern as might otherwise happen.
* - 'set' .isPattern option; When true, this new option indicates that the 'words' parameter is a prepared RegExp-ready pattern.
* - 'set' .accentInsensitive option; This new option is limited to operating on hard-coded character groups (eg, Spanish accented chars), not Unicode normalized (which would be a better approach but much harder to achieve and probably slower).
*
* Usage:
* // wrap every occurrance of text 'lorem' in content
* // with <span class='highlight'> (default options)
* $('#content').highlight('set', 'lorem');
*
* // search for and highlight more terms at once
* // so you can save some time on traversing DOM
* $('#content').highlight(['set', 'lorem', 'ipsum']);
* $('#content').highlight('set', 'lorem ipsum');
*
* // search only for entire word 'lorem'
* $('#content').highlight('set', 'lorem', { wordsOnly: true });
*
* // don't ignore case during search of term 'lorem'
* $('#content').highlight('set', 'lorem', { caseSensitive: true });
*
* // wrap every occurrance of term 'ipsum' in content
* // with <em class='important'>
* $('#content').highlight('set', 'ipsum', { element: 'em', className: 'important' });
*
* // remove default highlight
* $('#content').highlight('unset');
*
* // remove custom highlight
* $('#content').highlight('unset', { element: 'em', className: 'important' });
*
* // get accent-insensitive pattern
* $().highlight('makePattern', { element: 'lorem', {'accentInsensitive':true});
*
*
* Copyright (c) 2009 Bartek Szopka
*
* Licensed under MIT license.
*
*/
(function($) {
// **********************************
// ***** Start: Private Members *****
var pluginName = 'highlight';
var accentedForms = [//Spanish accednted chars
//Prototype ...
//['(c|ç)', '[cç]', '[CÇ]', new RegExp('(c|ç)','g'), new RegExp('(C|Ç)','g')],
['(a|á)', '[aá]'],
['(e|é)', '[eé]'],
['(i|í)', '[ií]'],
['(n|ñ)', '[nñ]'],
['(o|ó)', '[oó]'],
['(u|ú|ü)', '[uúü]']
];
//To save a lot of hard-coding and a lot of unnecessary repetition every time the "set" method is called, each row of accentedForms is now converted to the format of the prototype row, thus providing reusable RegExps and corresponding replacement strings.
//Note that case-sensitivity is established later in the 'set' settings so we prepare separate RegExps for upper and lower case here.
$.each(accentedForms, function(i, af) {
af[2] = af[1].toUpperCase();
af[3] = new RegExp(af[0], 'g');
af[4] = new RegExp(af[0].toUpperCase(), 'g');
});
var h = function(node, re, settings) {
if (node.nodeType === 3) {//text node
var match = node.data.match(re);
if (match) {
var wordNode = node.splitText(match.index);
wordNode.splitText(match[0].length);
$(wordNode).wrap($("<" + settings.element + ">").addClass(settings.className));
return 1;
}
} else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
!/(script|style)/i.test(node.tagName) && // ignore script and style nodes
!(node.tagName === settings.element.toUpperCase() && node.className === settings.className)) { // skip if already highlighted
for (var i = 0; i < node.childNodes.length; i++) {
i += h(node.childNodes[i], re, settings);
}
}
return 0;
};
// ***** Fin: Private Members *****
// ********************************
// *********************************
// ***** Start: Public Methods *****
var methods = {
//This is a utility method. It returns a string, not jQuery.
makePattern: function (words, options) {
var settings = {
'accentInsensitive': false
};
$.extend(settings, options || {});
if (words.constructor === String) {
words = [words];
}
words = $.grep(words, function(word, i) {
return word != '';
});
words = $.map(words, function(word, i) {
return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
});
if (words.length == 0) { return ''; };
var pattern = "(" + words.join("|") + ")";
if (settings.accentInsensitive) {
$.each(accentedForms, function(i, af) {
pattern = pattern.replace(af[3], af[1]).replace(af[4], af[2]);
});
}
return pattern;
},
set: function (words, options) {
var settings = {
'className': 'highlight',
'element': 'span',
'caseSensitive': false,
'wordsOnly': false,
'accentInsensitive': false,
'isPattern': false
};
$.extend(settings, options || {});
var pattern = settings.isPattern ? words : methods.makePattern(words, settings);
if (pattern === '') { return this; };
if (settings.wordsOnly) {
pattern = "\\b" + pattern + "\\b";
}
var flag = settings.caseSensitive ? "" : "i";
var re = new RegExp(pattern, flag);
return this.each(function () {
h(this, re, settings);
});
},
unset: function (options) {
var settings = {
className: 'highlight',
element: 'span'
}, parent;
$.extend(settings, options || {});
return this.find(settings.element + "." + settings.className).each(function () {
parent = this.parentNode;
parent.replaceChild(this.firstChild, this);
parent.normalize();
}).end();
}
};
// ***** Fin: Public Methods *****
// *******************************
// *****************************
// ***** Start: Supervisor *****
$.fn[pluginName] = function( method ) {
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || !method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist in jQuery.' + pluginName );
}
};
// ***** Fin: Supervisor *****
// ***************************
})( jQuery );
And here's the application code for the language site :
$(function() {
$(".text-input").on('keyup', function(e) {
var disallow = [37, 38, 39, 40];//ignore arrow keys
if($.inArray(e.which, disallow) > -1) {
return true;
}
var $group = $(this).closest(".group"),
accent_sensitive = false,
case_sensitive = false,
val = this.value,
pattern = $().highlight('makePattern', val, {
'accentInsensitive': !accent_sensitive,
'caseSensitive': case_sensitive
}),
$trs = $group.find(".myTable tbody tr"),
$s;
if(val === '') {
$s = $trs;
}
else {
$s = $();
$trs.stop(true,true).each(function(i, tr) {
$tr = $(tr);
//if ($tr.text().match(new RegExp(pattern, "i"))) {
if ($tr.text().match(new RegExp(pattern, case_sensitive ? '' : "i"))) {
$s = $s.add(tr);
}
});
$trs.not($s).hide();
}
$group.find(".filter-count-tr").text("(" + $s.show().highlight('unset').highlight('set', pattern, {
'isPattern':true,
'caseSensitive':case_sensitive
}).length + ")");
}).on('focus blur', function() {
if (this.defaultValue == this.value) this.value = '';
else if (this.value == '') this.value = this.defaultValue;
});
$(".group").each(function() {
$this = $(this);
$this.find(".filter-count-tr").text("(" + $this.find("tbody tr").length + ")");
});
});
All tested, so should work if installed properly.
By the way, I used this page as my source for Spanish accented characters.
I need to develop an javascript function to check the mask reversely.
It means,
if I set the mask "###:##",
When I type "1" it shows, "1" When I
type "2" it shows, "21"
When I type "3"
it shows ,"3:21"
etc..
Is it possible to develop like that??
I have no idea how to start it? Can anyone put me on the right direction?
Update:
What the OP wants is a way to pass a custom mask to an input and have it auto-mask on every keypress - my original answer was assuming a hardset mask.
This is not a final solution, but should get you started if you don't want to use a plugin:
Jsfiddle (does not do what OP requests exactly at the moment) - but gets close.
var masker = {
mask : '', // custom mask that will be defined by user
types : {
'#' : '[\\d]', // number
'z': '[\\w]', // alpha
'*': '[\\d\\w]' // number or alpha
},
maskMatch : '', // regex used for matching
maskReplace : '', // replace string
makeTemplate : function(){
// basically we want to take the custom mask
// and make it into a regex match with capture groups
// and a replace string with the added separator(s)
// first we need to split the mask into like groups:
// ex: ###:## -> [#,#,#][:][#,#]
// so we get the indexes to slice it up later
var m = masker.mask.split('');
var len = m.length-1;
var indexes=[];
for (var i=0;i<len;i++){
if(m[i]!=m[i+1]){
indexes.push(i);
}
}
// now we slice it into groups
var groups = [];
for(var i=0;i<=indexes.length;i++){
var start = (i==0) ? 0 : indexes[i-1]+1;
var end = (i==indexes.length) ? m.length : indexes[i]+1;
groups.push(m.slice(start,end));
}
console.log(groups);
var reg = '';
var groupCount=1; // replace starts with $1
var replace = '';
// loop through to see if its using a wildcard
// replace with the wildcard and the number to match
// this will need to be reworked since it assumes
// {1,n} -- can't change the 1 at this moment
for(var i=0;i<groups.length;i++){
var l = groups[i][0];
if(l!='#'&&l!='z'&&l!='*'){
replace+= groups[i].join('');
continue;
}
replace+='$'+groupCount;
groupCount++;
reg+='(';
reg+=masker.types[l];
if(groups[i].length>1){
reg+='{1,';
reg+= groups[i].length;
reg+='}';
}
reg+=')';
}
console.log(reg);
console.log(replace);
masker.maskReplace = replace;
masker.maskMatch = new RegExp(reg);
},
matchIt: function(text)
{
// only replace if is match
if(text.match(masker.maskMatch)){
text = text.replace(masker.maskMatch,masker.maskReplace);
}
return text;
},
setup: function(id){
// you need a way to store the actual numbers (non-masked version)
// this is quick way, but should be incorporated as a variable
var hiddenText = $(id).clone().css({'left':'-9999px','position':'absolute'}).prop('id',id.substring(1,id.length)+'hidden').appendTo($(id));
$('#maskme').bind('keyup',function(e){
// fix this to better handle numpad, etc
var c = String.fromCharCode(e.which);
if(c.match(/[\w\d]/)){
$(id+'hidden').val( $(id+'hidden').val()+c);
$(id).val(masker.matchIt($(id+'hidden').val()));
} else {
// need a way to tell if user has highlighted then backspace
// or highlight and space... basically something for highlighting
if(e.which==8){ // 8 is backspace
// update our hidden value / stored value with what the user wants
var old = $(id+'hidden').val();
$(id+'hidden').val( old.substring(0,old.length-1));
}
}
});
}
}
Then calling it:
masker.mask = '###:##';
masker.makeTemplate();
masker.setup('#maskme');
If you want something more advanced take a look at the jQuery Masked Input project over a github. It already has handlers for all the different keypresses and other events, a more flexible custom matching, and many other things that should be thought about.