character string from R as HTML in JS() - javascript

I'm trying to make a datatable object that has a child table with each row. I'm following the format of a Child Rows example given by Yihui Xie.
The example shows how to pass a simple string in, and I can even do rudimentary tables. But I don't want to hardcode a lot of HTML, since R has great facilities to write HTML for me.
My problem is that if I user R to generate an character string as HTML, when I pass it into the datatable object through JS, it reads it as literal text instead of rendering it as HTML.
I'm sure it's a simple solution, but using htmltools::htmlPreserve and htmltools::HTML don't change anything, and I suspect I'll have to do something within the javascript.
DT::datatable({
mtcars$rows <-
paste0("<tr><td>Gas Mileage</td><td>", mtcars$mpg, "</td></tr>",
"<tr><td>Quarter Mile</td><td>", mtcars$qsec, "</td></tr>")
cbind(' ' = '⊕', mtcars)
},
escape = c(-2, -13),
selection = "single",
options = list(
columnDefs = list(
list(visible = FALSE, targets = c(0, 13)),
list(orderable = FALSE, className = 'details-control', targets = 1)
)
),
callback = JS("
table.column(1).nodes().to$().css({cursor: 'pointer'});
var format = function(d) {
return '<table>' +
'<tr><td>Variable</td><td>Value</td></tr>' +
d[13] +
'</table>';
};
table.on('click', 'td.details-control', function() {
var td = $(this), row = table.row(td.closest('tr'));
if (row.child.isShown()) {
row.child.hide();
td.html('⊕');
} else {
row.child(format(row.data())).show();
td.html('&CircleMinus;');
}
});"
)
)

Try replacing escape = c(-2, -13) with escape = FALSE.
Looking at the source code, the escape variable is passed into the escapeData function of datatables which looks like this. Hopefully it'll give you a bit of guidance as to how to correctly custom-specify what should/shouldn't be escaped. :)
# `i` here is your `escape` variable which can be either TRUE/FALSE,
# indices, or colnames.
escapeData = function(data, i, colnames) {
if (is.null(data) || prod(dim(data)) == 0 || identical(i, FALSE)) return(data)
// see below for definition of convertIdx
i = convertIdx(i, colnames, ncol(data))
# only escape character columns (no need to escape numeric or logical columns)
data[i] = lapply(data[i], function(x) {
if (is.character(x) || is.factor(x)) htmlEscape(x) else x
})
data
}
# convertIdx looks like this:
# convert character indices to numeric
convertIdx = function(i, names, n = length(names), invert = FALSE) {
if (!is.character(i)) return({
if (invert) {
if (is.numeric(i)) -i else if (is.logical(i)) !i else {
stop('Indices must be either character, numeric, or logical')
}
} else i
})
if (is.null(names)) stop('The data must have column names')
o = setNames(seq_len(n), names)
i = o[i]
if (any(is.na(i)))
stop("Some column names in the 'escape' argument not found in data")
if (invert) o[-i] else i
}

Related

Incorrect Placement of Comma in Sum

I have a table with a text input on each line. Users can specify a dollar amount within each text box. My code loops through each text input and simply sums up the values. My problem is that when a user enters a value over >= 1,000,000 the sum becomes incorrect. For example, when a user enters 1,000,000 the sum is 1,000.
function init_icheck() {
$('#datatable input[type=checkbox]').iCheck({
checkboxClass: 'icheckbox_square-blue',
increaseArea: '10%'
});
}
// When Pay in Full Checkbox is Checked fill in Pay This Time Field with Invoice Amount Due Value
function paynow() {
var payFull = $('input[type="checkbox"].payfull');
payFull.on('ifChecked', function(event) {
$(this).parents('tr').find('.paynow').val($(this).val().replace('$', ''));
CalcFooter();
});
}
// If Pay in Full Unchecked then remove value from respective Pay This Time Input
// Only bind the ifUnchecked event if the checkbox is checked
function remove_checkbox() {
var payFull = $('input[type="checkbox"].payfull');
payFull.on('ifUnchecked', function(event) {
if ($(this).parents('tr').find('.paynow').val() == $(this).val().replace('$', '')) {
$(this).parents('tr').find('.paynow').val('');
CalcFooter();
}
});
}
// If Pay This Time changes recalculate total
function recalc_total() {
$('.paynow').keyup(function() {
var $ThisCheck = $(this).parents('tr').find('.payfull');
// Add Commas if # is over 1,000
$(this).val(addCommas($(this).val().replace(/,/g, '')));
if ($(this).val() == $ThisCheck.val().replace('$', '')) {
$ThisCheck.iCheck('check');
} else {
$ThisCheck.iCheck('uncheck');
}
CalcFooter();
});
}
// Recalc Function
function CalcFooter() {
var amtPage = 0;
var amtTotal = 0;
var Sum = 0;
$('.paynow').each(function(index, Obj) {
var value = parseFloat($(this).val().replace(',', ''));
if (!isNaN(value)) amtPage += value;
});
$('#datatable').DataTable().$('.paynow').each(function(index, Obj) {
var value = parseFloat($(this).val().replace(',', ''));
if (!isNaN(value)) amtTotal += value;
});
$('#amounttopay').text(
'Page: $' + addCommas(amtPage.toFixed(2)) +
' / Total: $' + addCommas(amtTotal.toFixed(2))
);
}
// Add Commas if value > 1,000
addCommas = function(input) {
// If the regex doesn't match, `replace` returns the string unmodified
return (input.toString()).replace(
// Each parentheses group (or 'capture') in this regex becomes an argument
// to the function; in this case, every argument after 'match'
/^([-+]?)(0?)(\d+)(.?)(\d+)$/g,
function(match, sign, zeros, before, decimal, after) {
// Less obtrusive than adding 'reverse' method on all strings
var reverseString = function(string) {
return string.split('').reverse().join('');
};
// Insert commas every three characters from the right
var insertCommas = function(string) {
// Reverse, because it's easier to do things from the left
var reversed = reverseString(string);
// Add commas every three characters
var reversedWithCommas = reversed.match(/.{1,3}/g).join(',');
// Reverse again (back to normal)
return reverseString(reversedWithCommas);
};
// If there was no decimal, the last capture grabs the final digit, so
// we have to put it back together with the 'before' substring
return sign + (decimal ? insertCommas(before) + decimal + after : insertCommas(before + after));
}
);
};
// Reinitialize iCheck on Pagination Change
$('#datatable').on('draw.dt', function() {
init_icheck();
paynow();
recalc_total();
remove_checkbox();
CalcFooter();
});
// Initialize Datatables
$('#datatable').dataTable({
"stateSave": true,
"oLanguage": {
"sSearch": "Search Results:"
}
});
I have a simple jsfiddle that illustrates this issue. I thank you in advance for pointing me in the right direction.
http://jsfiddle.net/tgf59ezr/14/
Using .replace(',', '') only replaces the first instance of the searched string, causing the number parsing to not work as intended.
Use this instead:
.replace(/,/g, '')
The g means replace all instances.
http://jsfiddle.net/da03j0aa/
Reference: String.prototype.replace()

Filtering firstname and lastname using datatable.js

I have created a application with dataTable for filtering firstname and lastname, I am having a firstname and lastname text-field through which I would filter firstname and lastname. The filtering is working but the issue is that firstname and lastname filtering is doing from both the textfield, actually I need to make it into specific in such a way that firstname textfield is only for firstname filtering and lastname textfield is only for lastname filtering
Can anyone please tell me some solution for this?
JSFiddle
My code is as given below:
$(document).ready(function () {
myTable = $('#myTable').dataTable({
"bSort": false,
"bInfo": false,
"bLengthChange": false,
"bPaginate": false,
"scrollY": "300px",
"scrollX": "100%",
"scrollCollapse": true,
});
new $.fn.dataTable.FixedColumns(myTable, {
leftColumns: 1,
rightColumns: 1
});
$('#firstNameTextBox').keyup(function () {
filterNames(this.value, 0);
});
$('#secondNameTextBox').keyup(function () {
filterNames(this.value, 0);
});
function filterNames(value) {
myTable.fnFilter(value, 0, false, false, false, false);
}
});
One way is by using custom filtering (>1.10)
This piece of code will be executed every time the table is drawn. The method takes the first and second name from the fields and creates a regular expression. The data[0].replace removes the spaces in between first and second names and coverts to an array which is later used to test again the regex.
$.fn.dataTable.ext.search.push(
function( settings, data, dataIndex ) {
var firstName = $('#firstNameTextBox').val();
var secondName = $('#secondNameTextBox').val();
var fnRegex = new RegExp(firstName, 'i');
var snRegex = new RegExp(secondName, 'i');
var name = data[0].replace(/[^\w\s]|_/g, function ($1) { return ' ' + $1 + ' ';}).replace(/[ ]+/g, ' ').split(' '); // courtesy: http://stackoverflow.com/a/6162630
return fnRegex.test(name[0]) && snRegex.test(name[1]);
}
);
Explanation:
var fnRegex = new RegExp(firstName, 'i');
This will create a regex like this as you type in first name textfield
/J/i
/Je/i
/Jef/i
/Jeff/i
The same is with second name textfield
var snRegex = new RegExp(secondName, 'i');
Since the table data looks like this Jefferey Sam. The below .replace() method line will break it into an array like this ["Jeffrey", "Sam"] so that regex can check individually on each element.
var name = data[0].replace(/[^\w\s]|_/g, function ($1) {
return ' ' + $1 + ' ';
})
.replace(/[ ]+/g, ' ')
.split(' ');
$.fn.datatable.ext.search iterates over each row and adds those rows into the table which return true. (which is the last line)
return fnRegex.test(name[0]) && snRegex.test(name[1]);
both first name and second name regular expression test against the first value of the array (first name) and the second value of the array (second name).
regex.test will return true or false.
If the user types Jeff in first name text field and Sa in second field, the below is how $.fn.datatable.ext.search.push will look like for each row
return /Jeff/i.test("Jefferey") && /Sa/i.test("Sa"); // true && true ===> true (so this will be added);
return /Jeff/i.test("Linsu") && /Sa/i.test("Lessi"); // false && false ===> false (so this will NOT be added).
return ..... // so on for the rest of the rows
Here is a demo http://jsfiddle.net/dhirajbodicherla/189Lp6u6/2/
Versions < 1.10
The draw method alone is changed to below
var myTable = $('').dataTable() with (lowercase D)
myTable.api().draw();
Here is a demo http://jsfiddle.net/dhirajbodicherla/189Lp6u6/3/
Hope this helps

Parse semi-structured values

it's my first question here. I tried to find an answer but couldn't, honestly, figure out which terms should I use, so sorry if it has been asked before.
Here it goes:
I have thousands of records in a .txt file, in this format:
(1, 3, 2, 1, 'John (Finances)'),
(2, 7, 2, 1, 'Mary Jane'),
(3, 7, 3, 2, 'Gerald (Janitor), Broflowski'),
... and so on. The first value is the PK, the other 3 are Foreign Keys, the 5th is a string.
I need to parse them as JSON (or something) in Javascript, but I'm having troubles because some strings have parentheses+comma (on 3rd record, "Janitor", e.g.), so I can't use substring... maybe trimming the right part, but I was wondering if there is some smarter way to parse it.
Any help would be really appreciated.
Thanks!
You can't (read probably shouldn't) use a regular expression for this. What if the parentheses contain another pair or one is mismatched?
The good news is that you can easily construct a tokenizer/parser for this.
The idea is to keep track of your current state and act accordingly.
Here is a sketch for a parser I've just written here, the point is to show you the general idea. Let me know if you have any conceptual questions about it.
It works demo here but I beg you not to use it in production before understanding and patching it.
How it works
So, how do we build a parser:
var State = { // remember which state the parser is at.
BeforeRecord:0, // at the (
DuringInts:1, // at one of the integers
DuringString:2, // reading the name string
AfterRecord:3 // after the )
};
We'll need to keep track of the output, and the current working object since we'll parse these one at a time.
var records = []; // to contain the results
var state = State.BeforeRecord;
Now, we iterate the string, keep progressing in it and read the next character
for(var i = 0;i < input.length; i++){
if(state === State.BeforeRecord){
// handle logic when in (
}
...
if(state === State.AfterRecord){
// handle that state
}
}
Now, all that's left is to consume it into the object at each state:
If it's at ( we start parsing and skip any whitespaces
Read all the integers and ditch the ,
After four integers, read the string from ' to the next ' reaching the end of it
After the string, read until the ) , store the object, and start the cycle again.
The implementation is not very difficult too.
The parser
var State = { // keep track of the state
BeforeRecord:0,
DuringInts:1,
DuringString:2,
AfterRecord:3
};
var records = []; // to contain the results
var state = State.BeforeRecord;
var input = " (1, 3, 2, 1, 'John (Finances)'), (2, 7, 2, 1, 'Mary Jane'), (3, 7, 3, 2, 'Gerald (Janitor), Broflowski')," // sample input
var workingRecord = {}; // what we're reading into.
for(var i = 0;i < input.length; i++){
var token = input[i]; // read the current input
if(state === State.BeforeRecord){ // before reading a record
if(token === ' ') continue; // ignore whitespaces between records
if(token === '('){ state = State.DuringInts; continue; }
throw new Error("Expected ( before new record");
}
if(state === State.DuringInts){
if(token === ' ') continue; // ignore whitespace
for(var j = 0; j < 4; j++){
if(token === ' ') {token = input[++i]; j--; continue;} // ignore whitespace
var curNum = '';
while(token != ","){
if(!/[0-9]/.test(token)) throw new Error("Expected number, got " + token);
curNum += token;
token = input[++i]; // get the next token
}
workingRecord[j] = Number(curNum); // set the data on the record
token = input[++i]; // remove the comma
}
state = State.DuringString;
continue; // progress the loop
}
if(state === State.DuringString){
if(token === ' ') continue; // skip whitespace
if(token === "'"){
var str = "";
token = input[++i];
var lenGuard = 1000;
while(token !== "'"){
str+=token;
if(lenGuard-- === 0) throw new Error("Error, string length bounded by 1000");
token = input[++i];
}
workingRecord.str = str;
token = input[++i]; // remove )
state = State.AfterRecord;
continue;
}
}
if(state === State.AfterRecord){
if(token === ' ') continue; // ignore whitespace
if(token === ',') { // got the "," between records
state = State.BeforeRecord;
records.push(workingRecord);
workingRecord = {}; // new record;
continue;
}
throw new Error("Invalid token found " + token);
}
}
console.log(records); // logs [Object, Object, Object]
// each object has four numbers and a string, for example
// records[0][0] is 1, records[0][1] is 3 and so on,
// records[0].str is "John (Finances)"
I echo Ben's sentiments about regular expressions usually being bad for this, and I completely agree with him that tokenizers are the best tool here.
However, given a few caveats, you can use a regular expression here. This is because any ambiguities in your (, ), , and ' can be attributed (AFAIK) to your final column; as all of the other columns will always be integers.
So, given:
The input is perfectly formed (with no unexpected (, ), , or ').
Each record is on a new line, per your edit
The only new lines in your input will be to break to the next record
... the following should work (Note "new lines" here are \n. If they're \r\n, change them accordingly):
var input = /* Your input */;
var output = input.split(/\n/g).map(function (cols) {
cols = cols.match(/^\((\d+), (\d+), (\d+), (\d+), '(.*)'\)/).slice(1);
return cols.slice(0, 4).map(Number).concat(cols[4]);
});
The code splits on new lines, then goes through row by row and splits into cells using a regular expression, which greedily attributes as much as it can to the final cell. It then turns the first 4 elements into integers, and sticks the 5th element (the string) onto the end.
This gives you an array of records, where each record is itself an array. The first 4 elements are your PK's (as integers) and your 5th element is the string.
For example, given your input, use output[0][4] to get "Gerald (Janitor), Broflowski", and output[1][0] to get the first PK 2 for the second record (don't forget JavaScript arrays are zero-indexed).
You can see it working here: http://jsfiddle.net/56ThR/
Another option would be to convert it into something that looks like an Array and eval it. I know it is not recommended to use eval, but it's a cool solution :)
var lines = input.split("\n");
var output = [];
for(var v in lines){
// Remove opening (
lines[v] = lines[v].slice(1);
// Remove closing ) and what is after
lines[v] = lines[v].slice(0, lines[v].lastIndexOf(')'));
output[v] = eval("[" + lines[v] + "]");
}
So, the eval parameter would look like: [1, 3, 2, 1, 'John (Finances)'], which is indeed an Array.
Demo: http://jsfiddle.net/56ThR/3/
And, it can also be written shorter like this:
var lines = input.split("\n");
var output = lines.map( function(el) {
return eval("[" + el.slice(1).slice(0, el.lastIndexOf(')') - 1) + "]");
});
Demo: http://jsfiddle.net/56ThR/4/
You can always do it "manually" :)
var lines = input.split("\n");
var output = [];
for(var v in lines){
output[v] = [];
// Remove opening (
lines[v] = lines[v].slice(1);
// Get integers
for(var i = 0; i < 4; ++i){
var pos = lines[v].indexOf(',');
output[v][i] = parseInt(lines[v].slice(0, pos));
lines[v] = lines[v].slice(pos+1);
}
// Get string betwen apostrophes
lines[v] = lines[v].slice(lines[v].indexOf("'") + 1);
output[v][4] = lines[v].slice(0, lines[v].indexOf("'"));
}
Demo: http://jsfiddle.net/56ThR/2/
What you have here is basically a csv (comma separated value) file which you wish to parse.
The easiest way would be to use an wxternal library that will take care of most of the issues you have
Example: jquery csv library is a good one. https://code.google.com/p/jquery-csv/

How can I set DataTables row callbacks in a Shiny R application?

In rCharts, one can set JS callbacks of DataTables using a special string notation: #! function(par) {...} !#. For example, let's look into the following R code:
#JS callback to truncate long strings in table cells and add a tooltip
callback = "#!
function (nRow) {
$('td', nRow).each(function (index) {
var maxChars = 80;
var unfilteredText = $(this).text();
if (unfilteredText.length > maxChars && maxChars > 3) {
$(this).attr('title', unfilteredText);
$(this).html(unfilteredText.substring(0, maxChars-4) + '...');
}
});
return nRow;
} !#"
result <- dTable(df, aaSorting = list(c(5, "desc")), sPaginationType="full_numbers",
fnRowCallback=callback)
Is this possible in Shiny DataTables?
I just stumbled across this question and was able to use the information here, along with help from section 4.5 of this post to solve this problem. In order to get this to work, you would simply do the following:
library(DT)
long_strings = replicate(10, paste(sample(c(0:9, letters, LETTERS), 135, replace = TRUE), collapse = ""))
dat <- data.frame(x = 1:10,
y = month.abb[1:10],
z = long_strings,
stringsAsFactors = FALSE)
DT::datatable(dat,
options = list(
rowCallback = JS("function(row, data) {",
" var max_chars = 80, full_text = data[3];",
" if (full_text.length > max_chars) {",
" $('td:eq(3)', row).attr('title', full_text);",
" $('td:eq(3)', row).html(full_text.substring(0, max_chars - 4) + '...');",
" }",
"}")))
It's important to note that since we want this to operate on a per row basis, we use the rowCallback function inside of the options parameter. This is in contrast to the callback function that can be used on the entire table, which is its own parameter for DT::datatable.
Also take note of the fact that you don't have to call .text() or .innerHTML or anything of that sort on data[3]. The value that is returned is the text value of that cell.
Hopefully someone in the future stumbles across this and finds it beneficial.

jquery filter script not ignoring diacritics and not highlighting string matches as user enters filter text into text input field

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.

Categories