Greasemonkey, XPath: find all links within table row - xpath

Given:
<tr>
<td>Keyword 1</td>
<td>Keyword 2</td>
<td>Keyword 3</td>
</tr>
<tr>
<td>Keyword 4</td>
<td>Keyword 5</td>
<td>Keyword 6</td>
</tr>
I need to match each URI within the table cells. The keyword is consistent throughout the document. I can match links for the entire document with no trouble:
var links_in_document = document.evaluate(
"//a[starts-with(text(),'Keyword')]",
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);
However, even though I have an easy way to reference the TR node, I can't seem to find the right XPath to obtain the links in the row. The snippet below seems to give me the first link in the first TD, but not the rest. Help?
var links_in_row = document.evaluate(
".//a[starts-with(text(),'Keyword')]",
row,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);
(where 'row' is the context node).
Edited: perhaps I wasn't clear, I can find the links from document level just fine. I am trying to isolate the links in a single row by using the TR node as the context for the XPath.
Edit: solution, for interest. The broken markup I was working on had no id attributes, so I added some and was able to proceed. Snippet:
var exhibit_link;
for( var i = 0; i < all_exhibit_links.snapshotLength; i++ ) {
exhibit_link = all_exhibit_links.snapshotItem( i );
// The rows have no unique ID, so we need to give them one.
// This will give the XPath something to 'latch onto'.
exhibit_link.parentNode.parentNode.id = 'ex_link_row_' + i.toString();
exhibit_link.addEventListener( "click",
function( event ) {
var row_id = event.target.parentNode.parentNode.id;
// Find only those links that are within rows with the corresponding id
var row_links = document.evaluate(
"id('" + row_id + "')/td/a[starts-with(text(),'Exhibit')]",
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null);
// Open each link in a new tab
for( var j = 0; j < row_links.snapshotLength; j++ ) {
row_link = row_links.snapshotItem( j );
GM_openInTab( row_link.href );
}
// Suppress the original function of the link
event.stopPropagation();
event.preventDefault();
},
true );
}

a quick test in the JavaScript Shell with your html example and the following code:
var links_in_row = document.evaluate( ".//a[starts-with(text(),'Keyword')]"
, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var i = 0;
while( (link = links_in_row.snapshotItem(i) ) != null) {
print(link.innerHTML);i++;
}
prints out:
Keyword 1
Keyword 2
Keyword 3
which suggests it is working correctly.
Only change i made was not to start at the row level, but at the document...

Extending on what bert wrote, this works for me.
var rows = document.evaluate( "//tr"
, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var i = 0;
while( (row = rows.snapshotItem(i) ) != null) {
print( 'NEW ROW----');
var links = document.evaluate(".//a[starts-with(text(),'Keyword')]",
row, null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var k = 0;
while ((link = links.snapshotItem(k)) != null) {
print( link.innerHTML );
k++;
}
i++;
}
Prints out:
NEW ROW----
Keyword 1
Keyword 2
Keyword 3
NEW ROW----
Keyword 4
Keyword 5
Keyword 6
I think there's something missing outside of what was copy pasted.
bert should get the answer for this one IMHO.

Try:
descendant::*[self::a[starts-with(text(), 'Keyword')]]

Related

Code Rewite for tuple and if else statements by using LINQ

In my C# application i am using linq. I need a help what is the syntax for if-elseif- using linq in single line. Data, RangeDate are the inputs. Here is the code:
var Date1 = RangeData.ToList();
int record =0;
foreach (var tr in Date1)
{
int id =0;
if (tr.Item1 != null && tr.Item1.port != null)
{
id = tr.Item1.port.id;
}
else if (tr.Item2 != null && tr.Item2.port != null)
{
id = tr.Item2.port.id;
}
if (id >0)
{
if(Data.Trygetvalue(id, out cdat)
{
// Do some operation. (var cdata = SumData(id, tr.item2.port.Date)
record ++;
}
}
}
I think your code example is false, your record variable is initialized to 0 on each loop so increment it is useless .
I suppose that you want to count records in your list which have an id, you can achieve this with one single Count() :
var record = Date1.Count(o => (o.Item1?.port?.id ?? o.Item2?.port?.id) > 0);
You can use following code:
var count = RangeData.Select(x => new { Id = x.Item1?.port?.id ?? x.Item2?.port?.id ?? 0, Item = x })
.Count(x =>
{
int? cdate = null; // change int to your desired type over here
if (x.Id > 0 && Data.Trygetvalue(x.Id, out cdat))
{
// Do some operation. (var cdata = SumData(x.Id, x.Item.Item2.port.Date)
return true;
}
return false;
});
Edit:
#D Stanley is completely right, LINQ is wrong tool over here. You can refactor few bits of your code though:
var Date1 = RangeData.ToList();
int record =0;
foreach (var tr in Date1)
{
int? cdat = null; // change int to your desired type over here
int id = tr.Item1?.port?.id ?? tr.Item2?.port?.id ?? 0;
if (id >0 && Data.Trygetvalue(id, out cdat))
{
// Do some operation. (var cdata = SumData(id, tr.Item2.port.Date)
record ++;
}
}
Linq is not the right tool here. Linq is for converting or querying a collection. You are looping over a collection and "doing some operation". Depending on what that operation is, trying to shoehorn it into a Linq statement will be harder to understand to an outside reader, difficult to debug, and hard to maintain.
There is absolutely nothing wrong with the loop that you have. As you can tell from the other answers, it's difficult to wedge all of the information you have into a "single-line" statement just to use Linq.

Footable fine filtering

I have this code to have a select field filter through a Footable. It works but it's straining more results than needed. Example: "Article in National Journal" option is filtering rows with both "Article in National Journal" and "Article in International Journal". How can I make it more precise? Thank you.
jQuery(function () {
jQuery('#projectos').footable().bind('footable_filtering', function (e) {
var selected = jQuery('.filter-status').find(':selected').text();
if (selected && selected.length > 0) {
e.filter += (e.filter && e.filter.length > 0) ? ' ' + selected : selected;
e.clear = !e.filter;
}
});
jQuery('.clear-filter').click(function (e) {
e.preventDefault();
jQuery('.filter-status').val('');
jQuery('#projectos').trigger('footable_clear_filter');
});
jQuery('.filter-status').change(function (e) {
e.preventDefault();
jQuery('#projectos').trigger('footable_filter', {filter: jQuery('#filter').val()});
});
});
When Footable filters, it uses the entire text from the whole row and it uses indexof() to test. You can see this in footable.filter.js in the filterFunction function.
I had to do 3 things to solve the problem.
Replace window.footable.options.filter.filterFunction with my own function
Do a per column match instead of the whole row. Depending on the HTML in your row, the spaces between the columns could be lost causing the first word of a column to concatenate with the last word of the previous column.
Use a regex match instead of indexof(). This allows you to match a whole word. As an example, if you us indexof() for "be" in "Don't be evil, because that's not good" will return 6 and 15 even though 15 is the beginning of a completely different word.
Here's the function: (I'm sure there are loads of improvements. Feel free to edit...)
window.footable.options.filter.filterFunction = function(index) {
var $t = $(this),
$table = $t.parents('table:first'),
filter = $table.data('current-filter').toUpperCase(),
columns = $t.find('td');
var regEx = new RegExp("\\b" + filter + "\\b");
var result = false;
for (i = 0; i < columns.length; i++) {
var text = $(columns[i]).text();
result = regEx.test(text.toUpperCase());
if (result === true)
break;
if (!$table.data('filter-text-only')) {
text = $(columns[i]).data("value");
if (text)
result = regEx.test(text.toString().toUpperCase());
}
if (result === true)
break;
}
return result;
};
You can find a plunk here: http://plnkr.co/edit/P2DWDtyHP3xmoUIcvgDe

Set selection on tekst inside CKEditor

I'm having trouble to select text in CKEditor(3.6). As we use plain text i dont know how to use correctly the range selectors.
HTML code of the CKEditor:
<body spellcheck="false" class="rf-ed-b" contenteditable="true">
<br>
Cross those that apply:<br>
<br>
<br>
[«dummy»] If he/she is tall<br>
<br>
[«dummy»] If he/she is a male<br>
<br>
[«dummy»] If he/shi is a minor<br>
<br>
Specialties:<br>
<br>
[«dummy»] «Write here the specialties if known»<br>
<br>
<br>
«You are now done with filling in this form»<br>
</body>
With the keys 'CRTL+N' I want to go to the next filleble spot:
«[label]»
I tried stuff like:
var editor = CKEDITOR.instances['MyEditor'];
var findString = '«';
var element = editor.document.getBody();
var ranges = editor.getSelection().getRanges();
var startIndex = element.getHtml().indexOf(findString);
if (startIndex != -1) {
ranges[0].setStart(element.getFirst(), startIndex);
ranges[0].setEnd(element.getFirst(), startIndex + 5);
editor.getSelection().selectRanges([ranges[0]]);
}
Error:
Exception: Index or size is negative or greater than the allowed amount
While totally stripepd down it kinda works a bit:
var editor = CKEDITOR.instances['MyEditor'];
var ranges = editor.getSelection().getRanges();
var startIndex = 10;
if (startIndex != -1) {
ranges[0].setStart(element.getFirst(), startIndex);
ranges[0].setEnd(element.getFirst(), startIndex + 5);
editor.getSelection().selectRanges([ranges[0]]);
}
here it selects 5th till 10th char on first row.
I used the following sources:
example on Stackoverflow
Another stackoverflow example
CKEditor dom selection API
All solutions i can find work with html nodes.
How can set selection range on the '«' till next '»'
I've managed to solve this solution. Meanwhile i also upgraded CKeditor to 4.0.
This shouldnt have an impact on the solution.
It is a lot of code in JS.
On my keybinding i call the following JS function: getNextElement()
In this solution it also searches behind the cursor, this makes it possible to step through multiple find results.
Also the view gets scrolled to the next search result
var textNodes = [], scrollTo=0,ranges = [];
function getNextElement(){
var editor =null;
ranges = [];
// I dont know the ID of the editor, but i know there is only one the page
for(var i in CKEDITOR.instances){
editor = CKEDITOR.instances[i];
}
if(editor ==null){
return;
}
editor.focus();
var startRange = editor.getSelection().getRanges()[0];
var cursorData ="",cursorOffset=0,hasCursor = false;
if(startRange != null && startRange.endContainer.$.nodeType == CKEDITOR.NODE_TEXT){
cursorOffset = startRange.startOffset;
cursorData = startRange.endContainer.$.data;
hasCursor = true;
}
var element;
element = editor.document.getBody().getLast().getParent();
var selection = editor.getSelection();
// Recursively search for text nodes starting from root.
textNodes = [];
getTextNodes( element );
var foundElement = false;
foundElement = iterateEditor(editor,hasCursor,cursorData,cursorOffset);
if(!foundElement){
foundElement =iterateEditor(editor,false,"",0);
}
if(foundElement){
// Select the range with the first << >>.
selection.selectRanges( ranges );
jQuery(".cke_wysiwyg_frame").contents().scrollTop(scrollTo);
}
}
function iterateEditor(editor,hasCursor,cursorData,cursorOffset){
var foundElement = false;
var rowNr = 0;
var text, range;
var foundNode = false;
if(!hasCursor){
foundNode = true;
}
// Iterate over and inside the found text nodes. If some contains
// phrase "<< >>", create a range that selects this word.
for (var i = textNodes.length; i--; ) {
text = textNodes[ i ];
if ( text.type == CKEDITOR.NODE_ELEMENT && text.getName() == "br" ){
rowNr++;
} else if ( text.type == CKEDITOR.NODE_TEXT ) {
var sameNode = false;
if(text.$.data == cursorData){
foundNode = true;
sameNode = true;
}
if(foundNode){
var startIndex = -1;
var endIndex = 1;
if(sameNode){
// Check inside the already selected node if the text has multiple hits on the searchphrase
var indicesStart = getIndicesOf('\u00AB', text.getText());
var indicesEnd = getIndicesOf('\u00BB', text.getText());
for (var j = indicesStart.length; j--; ) {
if(indicesStart[j] > cursorOffset){
startIndex = indicesStart[j];
endIndex = indicesEnd[j];
}
}
} else{
startIndex = text.getText().indexOf( '\u00AB' );
endIndex = text.getText().indexOf( '\u00BB' );
}
if ( startIndex > -1 && (!sameNode || startIndex > cursorOffset)) {
range = editor.createRange();
range.setStart( text, startIndex );
foundElement = true;
// calculate the height the window should scroll to focus the selected element
scrollTo = (rowNr)*20;
}
if ( endIndex > -1 && foundElement ) {
range.setEnd( text, endIndex+1 );
ranges.push( range );
return true;
}
}
}
}
}
function getIndicesOf(searchStr, str) {
var startIndex = 0, searchStrLen = searchStr.length;
var index, indices = [];
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
function getTextNodes( element ) {
var children = element.getChildren(), child;
for ( var i = children.count(); i--; ) {
child = children.getItem( i );
textNodes.push( child );
}
}

Why does focusing on an input field of a jqGrid editable row cause the row to be 'de-selected'?

I've a jqGrid instance which featured with multiselection and inline editing. I wrote some code in the beforeSelectRow event so that the multiple selection can be achieved by ctrl-key, and range selection by shift-key; as well as single selection by without pressing any hotkey. In order to enable the inline editing, I'll call editRow when the row is selected twice. However, I get into two strange problems that:
Clicking on the input element of any field of the editable row, will make the row be 'de-selected'
After re-selecting another row to get into the edit mode, the previous selected row can't be restored even the restoreRow is called on the target row properly.
--EDITED--
Select a row
Select a row again to get into edit mode
Focus the input box, will 'de-select' the row
Select & edit another row, the previous row can't be restored.
-- CODE --
beforeSelectRow: function (rowid, e) {
var $this = $(this);
var url_for_save_edit = this.p.editurl;
var last_selected_row = this.p.selrow;
var cur_selected_rows = $this.data(key_selected_rows);
if (true == e.shiftKey) {
if (rowid != last_selected) {
var min = parseInt((rowid < last_selected_row ) ? rowid : last_selected_row );
var max = parseInt((rowid > last_selected_row ) ? rowid : last_selected_row );
for (var i = min + 1; i < max; i++) {
$this.jqGrid(method_setSelection, i, false);
}
}
} else if (false == e.ctrlKey) {
$this.resetSelection();
if (!$.string.isNullOrEmpty(url_for_save_edit)) {
if (!$.isNull(cur_selected_rows) && (cur_selected_rows.length <= 1) && (rowid == last_selected_row)) {
var curr_edit_row = $this.data(key_curr_edit_row);
console.log('>>>>> curr selected rows = ', cur_selected_rows, ', last selected row = ', last_selected_row, 'curr editing row = ', curr_edit_row, ', to be editable row: ', rowid);
if (rowid != curr_edit_row) {
if (!$.isNull(curr_edit_row)) {
console.log('>>>>> restoring row ', curr_edit_row);
$this.jqGrid('restoreRow', curr_edit_row, null);
}
console.log('>>>>> set row as editable: ', rowid);
$this.jqGrid('editRow', rowid, false, null, null, null, null, null, null, null);
$this.data(key_curr_edit_row, rowid);
}
}
}
}
return true;
}
In your code you suppose that the rows are sorted by rowid in asc order (see if (true == e.shiftKey)). I think you should better use sortname and sortorder parameters of jqGrid. Morover you can use jQuery.next or jQuery.prev to get to the next or previous <tr> element.
Moreover I don't understand why you use $this.data(key_selected_rows) instead of the standard this.p.selarrrow.
Sorry, but I would recommend you to rewrite your program and use more DOM properties of <tr> (grid row) and <table> elements. For example the rowIndex property of <tr> could gives you the index of the the row in the rows collection of the <table> element. You can get the next/previous row just using the this.rows[index+1] or this.rows[index+1] (this in the beforeSelectRow point to the DOM representation of the <table> element).
To your main question: if you want to prevent row selection or unselection in some situation you should return false from the beforeSelectRow.

A Dual Ajax ListBox

Do you guys know of any .net controls with 2 listboxes that can move items from left to right and vice versa?!
Like a dual listbox type of thing.
I have already looked at http://ajaxlistbox.codeplex.com/ it seems to be pretty sweet.
just want to know any suggestions.
Here's a way:
Make two ListBoxes, the first shows all available item, the second shows what the user chooses. You also need a TextBox to hold a copy of the chosen items, since it is not possible to retrieve ListBox items in C# if they were added via javascript. Make the TextBox hidden, so the user cannot accidentally mess up the items.
Click an item in the first listbox and it appears in the second "chosen" listbox. Click on chosen item in the second list and it disappears. You could alter this so items are removed from the first list after being chosen.
I call AddJavascript from my Page_Load method.
ListBoxFilteredProfiles is my first TextBox, ListBoxSelectedProfiles is the second.
private void AddJavascript()
{
// This javascript function adds the item selected in one listbox to another listbox.
// Duplicates are not allowed, items are inserted in alphabetical order.
string OnChangeProfileListBoxJavascript =
#"<script language=JavaScript>
<!--
function OnChangeSelectedProfiles()
{
var Target = document.getElementById('" + ListBoxSelectedProfiles.ID + #"');
var Source = document.getElementById('" + ListBoxFilteredProfiles.ID + #"');
var TB = document.getElementById('" + TextBoxProfiles .ID + #"');
if ((Source != null) && (Target != null)) {
var newOption = new Option(); // a new ListItem
newOption.text = Source.options[ Source.options.selectedIndex ].text;
newOption.value = Source.options[ Source.options.selectedIndex ].value;
var jj = 0;
for( jj = 0; jj < Target.options.length; ++jj ) {
if ( newOption.text == Target.options[ jj ].text ) { return true; } // don't add if already in the list
if ( newOption.text < Target.options[ jj ].text ) { break; } // add the new item at this position
}
for( var kk = Target.options.length; kk > jj; --kk ) { // bump the remaining list items up one position
var bumpItem = new Option();
bumpItem.text = Target.options[ kk-1 ].text; // copy the preceding item
bumpItem.value = Target.options[ kk-1 ].value;
Target.options[ kk ] = bumpItem;
}
Target.options[ jj ] = newOption; // Append the item in Target
if (TB != null) {
// Copy all the selected profiles into the hidden textbox. The C# codebehind gets the selections from the textbox because listbox values added via javascript are not accessible.
TB.value = '';
for( var jj= 0; jj < Target.options.length; ++jj ) { TB.value = TB.value + Target.options[ jj ].value + '\n'; }
}
}
}
// -->
</script> ";
// This javascript function removes an item from a listbox.
string OnChangeRemoveListBoxItemJavascript =
#"<script language=JavaScript>
<!--
function OnChangeRemoveProfile()
{
var Target = document.getElementById('" + ListBoxSelectedProfiles.ID + #"');
var TB = document.getElementById('" + TextBoxProfiles.ID + #"');
Target.remove( Target.options.selectedIndex );
TB.value = '';
// Copy all the selected profiles into the hidden textbox. The C# codebehind gets the selections from the textbox because listbox values added via javascript are not accessible.
for( var jj= 0; jj < Target.options.length; ++jj ) { TB.value = TB.value + Target.options[ jj ].value + '\n'; }
}
// -->
</script> ";
ClientScript.RegisterStartupScript( typeof(Page), "OnChangeSelectedProfiles", OnChangeProfileListBoxJavascript );
ClientScript.RegisterStartupScript( typeof(Page), "OnChangeRemoveProfile", OnChangeRemoveListBoxItemJavascript );
ListBoxFilteredProfiles.Attributes.Add("onchange", "OnChangeSelectedProfiles()" );
ListBoxSelectedProfiles.Attributes.Add("onchange", "OnChangeRemoveProfile()" );
}

Resources