Select text in a column of an html table - javascript

Is it possible to select the text (i.e. have it highlighted so that it can be copy+pasted) of every cell in one vertical column of an HTML table.
Is there a JavaScript method, or perhaps an equivalent in some browsers to the Alt-Click-Drag shortcut used in many text editors?
Or is this impossible?

What you're looking for is called Range object (TextRange in IE).
Update:
Here's a working code to do what you're suggesting: http://jsfiddle.net/4BwGG/3/
While capturing cell contents, you can format them in any manner you wish. I'm simply appending a new line every time.
Note:
Works fine in FF 3 and above
IE (before 9) and Chrome do not support multiple selection.
Chrome doesn't highlight all cells (but captures all content). Same goes for IE9
IE 7 & 8 will throw an error.
An alternative is apply a CSS style that simulates highlighting on click of column header and loop through all cells to capture their content. Look and feel of this approach may differ from native selection's look (unless you somehow capture select event and alter the appearnce).
Then use jQuery copy plugin to copy them to clipboard.

Some code review tools implement this to allow copying & pasting code from one side of a side-by-side diff. I looked into how ReviewBoard pulls it off.
The gist is:
When a column selection begins, style the cells in all other columns with user-select: none (and its prefixed variants, if necessary). This creates the appearance of a column selection. The other columns are still secretly selected, so you have to...
Intercept the copy event and change its payload to reflect the contents of the selected column.
The ReviewBoard code to do this consists of this CSS and this JavaScript.
I pulled it out into a fairly minimal jsbin demo.
Here's the CSS to create the appearance of a single-column selection (you add the selecting-left class to the table when the left column is being selected, or selecting-right for the right):
.selecting-left td.right,
.selecting-left td.right *,
.selecting-right td.left,
.selecting-right td.left *,
user-select: none;
}
.selecting-left td.right::selection,
.selecting-left td.right *::selection,
.selecting-right td.left::selection,
.selecting-right td.left *::selection,
background: transparent;
}
Here's the JavaScript to intercept the copy event and plug in a single column's worth of data:
tableEl.addEventListener('copy', function(e) {
var clipboardData = e.clipboardData;
var text = getSelectedText();
clipboardData.setData('text', text);
e.preventDefault();
});
function getSelectedText() {
var sel = window.getSelection(),
range = sel.getRangeAt(0),
doc = range.cloneContents(),
nodes = doc.querySelectorAll('tr'),
text = '';
var idx = selectedColumnIdx; // 0 for left, 1 for right
if (nodes.length === 0) {
text = doc.textContent;
} else {
[].forEach.call(nodes, function(tr, i) {
var td = tr.cells[tr.cells.length == 1 ? 0 : idx];
text += (i ? '\n' : '') + td.textContent;
});
}
return text;
}
There's also some less interesting code to add the selecting-left and selecting-right classes at the start of a selection. This would require a bit more work to generalize to n-column tables.
This seems to work well in practice, but it's surprising how hard it is!

Here is a hack that doesn't involve javascript at all:
Step 1: open the inspector
For Chrome on mac, press command + option + J.
Step 2: select a random cell using the selector tool
For Chrome on mac, click the selector icon on the top left corner of the inspector to enter the selector mode.
Then click a random cell in the table.
Step 3: hide all cells by editing CSS
Click the New Style Rule button (see image below)
then enter this rule (you may want to modify it a little bit depending on your HTML)
tr td {
display: none; # hide all cells
}
Now all cells should have disappeared.
Step 4: display only the column that you want by editing CSS
Go ahead and add another rule above that one:
tr td:nth-child(2) { # replace 2 with the index of the column you want to copy. 2 means the second column
display: table-cell; # display that column
}
Now the column you want to copy from should have reappeared.
All the other columns should be invisible and can't be selected.
Step 5: just copy that column!
Note
You can restore the page by refreshing.
I find this work perfectly if you just want to select one column or two.

You could have a div which gets populated with the column data on click and apply a css class to give the columns the appearence of being selected
something like this:
var $mytable = $("#mytable"),
$copydiv = $("#copy_div");
$mytable.find("td").click(function(){
//get the column index
var $this = $(this),
index = $this.parent().children().index($this);
//find all cells in the same column
$mytable.find("tr:nth-child(" + index + ")").removeClass("selected").each(function () {
var $this = $(this);
$this.addClass("selected");
$copydiv.html($this.html() + "<br />");
});
});
or you could have a separate table for each column, but I don't think that would be worth it.

WIP: CSS only solution using :has() selector
The new :has() selector gave me hope in solving this issue without JS. The idea was to disable text selection for all cells, and only activate it for cells of a column that is hovered.
So you would have rule like this:
table:has(tr td:nth-child(1):hover) tr td:nth-child(1) {
-webkit-user-select: auto;
user-select: auto;
}
A complete sample can be found here: https://codepen.io/catlan/pen/XWELegW
This is work in progress, because in the current version of Safari (15.6.1), the display of the text range disappears after the selection is done, only to reappear after moving the cursor for a few pixel. See https://bugs.webkit.org/show_bug.cgi?id=244445
It seems to work fine in Chrome starting with Version 105.

Related

Javascript - Create elements using 'surroundContents' breaks when new selection overlaps previously selected text

I've been working on how to allow visitors to select multiple sections of text wrapped in <p> tags to highlight them, and then click a button to remove all highlights.
Selected text highlights fine with added classList to created <span> elements.
If user selects some text that overlaps highlighted text,
Uncaught DOMException: Failed to execute 'surroundContents' on 'Range': The Range has partially selected a non-Text node.
Removing the classList produces fragmented text with empty spans.
I have tried to remove the child elements from the parent, but that removes the original text as well as the tag element.
I think the DOMException is because of the created span tag, but I'm not sure how to remove them when a new selection overlaps.
I have looked at many SO articles, but they seem to focus on JQuery, which I am not using. There is still a lot I do not understand about JavaScript, so MDN has helped a bit, but I sometimes struggle to apply the concepts.
// HIGHLIGHT SELECTIONS
const elementToHighlight = document.getElementById('higlight-this');
elementToHighlight.addEventListener('mouseup', selection);
function selection() {
let element = document.createElement('span');
element.classList.add('hl');
window.getSelection().getRangeAt(0).surroundContents(element);
}
// REMOVE hl CLASS
const removeClassListFromAll = document.getElementById('remove');
removeClassListFromAll.addEventListener('click', () => {
let grabHighlighted = document.getElementsByClassName('hl');
while (grabHighlighted.length) {
grabHighlighted[0].classList.remove('hl');
// grabHighlighted[0].parentElement.removeChild.(grabHighlighted[0]);
}
});
.hl {
background-color: yellow;
}
<section id="higlight-this">
<p>Some text to test with. Make it look good with highlights!.<br>
If you don't, It won't be useful for you.</p>
</section>
<button id="remove">remove</button>
the main problem is your selection container is not right try to identify your selection using console.log() and add if else or switch like :
if(window.getSelection().getRangeAt(0).commonAncestorContainer.nodeName == "P")
window.getSelection().getRangeAt(0).surroundContents(element);
else { //this case when i select from top to bottom with the button
window.getSelection().getRangeAt(0).commonAncestorContainer.querySelector("#higlight-this").querySelector("p").getRangeAt(0).surroundContents(element);
}

Scroll to closest visible tr after filtering a table

I'm using JQuery to filter a table (calling tr.hide() on non-matching rows). The table resides within a scrollable div. The problem: unfortunately, on filtering the list, the user loses his/her scroll position every time.
Is there a clean way to
obtain the top row of the current view port before scrolling
scroll to the very same row if it is still visible after filtering
or, if the row is no longer visible, scroll to the closest neighbor row (above or below), which is still visible
just add an anchor in the row you want to keep and go to this anchor after you filtered
Although it say's experimental, there is the element.scrollIntoView. It does seem to be supported by most major browsers. And you could always polyfill for those that don't.
https://developer.mozilla.org/en/docs/Web/API/Element/scrollIntoView#Browser_compatibility
It would look something like this. I don't use jQuery enough to use it in my example, so I will leave that is an exercise for the reader.
var rowToScrollTo = null;
for (var i = 0; i < rows.length; i++)
{
// Find the first visible row.
if (rows[i].offsetTop > scrollableDiv.scrollTop && shouldBeVisible(rows[i]))
{
rowToScrollTo = rows[i];
break;
}
}
// Show/hide rows
scrollableDiv.scrollTop = rowToScrollTo.offsetTop;

Select text in HTML table via Javascript?

Is it possible to select text (as if the user dragged his mouse across an HTML word in the browser) with the help of Javascript?
I have some JS code that searches an HTML table for a user's string input. I would then like for this code to select/focus on this word if it is found.
var targetTable = document.getElementById("accountTable");
for (var rowIndex = 1 + searchStart; rowIndex < targetTable.rows.length; rowIndex++)
{
var rowData = '';
rowData = targetTable.rows.item(rowIndex).cells.item(0).textContent;
if (rowData.indexOf(str) != -1)
{
//select word and focus on it in user's browser?
}
}
You could replace the found string with the same string but inside of a span with a specific class.
For Example if the search term was "alpha":
<td>The alphabet is long.</td>
<td>The <span class="highlighted">alpha</span>bet is long.</td>
An when the search changes you would have to revert all highlighted spans back to plain text.
If you want to actually select using the browsers selection there is a limited way discussed here: Programmatically select text in a contenteditable HTML element?
It looks as if you want to use the Selection object. At the basic level you can just set its anchor and focus node and offset, or you can also create a multi-range selection.
In order to highlight an element by hovering, the easiest way to do this would be to use css.
Example:
You have html code:
<p id="test">Word</p>
CSS File:
#word:hover{ color: #FFFF00; }
That will color the text by hovering the element.
Edit, if you had multiple words... You would want to separate them in an element per word. For example:
<text class="word">each</text><text class="word">word</text><text class="word">is</text><text class="word">selectable</text>
.word:hover{ color: #FFFF00; }
Here's the JS Fiddle: http://jsfiddle.net/braTe/

Expanding/collapsing table rows while the text shortens/lengthens

I have, for example, 3 rows from a table. Each of the rows gets its content from a database. By default when the row is collapsed, the text should be shortened -
trs[i].innerText = trs[i].innerText.substring(0,25) + '...';
When a user clicks on the div it expands, and if clicked again - collapses.
if(tr.style.height == "150px")
{
tr.style.height = "20px";
}
else {
tr.style.height = "150px";
}
So far, so good, but I want the text to be shortened only if the div is collapsed. With my solution when the page loads the text is shortened, but when the row is expanded it remains shortened. How can I fix this? I can only think of when the row expands to call an AJAX function which returns the original data, but I don't think it's the best possible way. Thanks in advance.
First you'd have to store the full text before shortening it.
trs[i].fullText = trs[i].innerText;
trs[i].innerText = trs[i].innerText.substring(0,25) + '...';
Then you can restore it in the handler:
tr.innerText = tr.fullText;
tr.style.height = "150px";
Here's a jsFiddle. I'm using jQuery to select the elements, but you already have the correct tr's anyway. If you were using jQuery you could also use $(tr).data("fullText", $(tr).text()) which avoids some minor memory leaks with adding properties to DOM elements in old versions of Internet Explorer.

jqGrid GridUnload/ GridDestroy

When I use $('#mygrid').jqGrid('GridUnload'); my grid is destroyed: no pager/ no header.
In a wiki I found:
The only difference to previous method is that the grid is destroyed, but the
table element and pager (if any) are left ready to be used again.
I can't find any difference between GridUnload/ GridDestroy or do I something wrong?
I use jqGrid 3.8.
To be able to create jqGrid on the page you have to insert an empty <table> element on the place of the page where you want see the grid. The simplest example of the table element is <table id="mygrid"></table>.
The empty <table> element itself will be not seen on the page till you call $('#mygrid').jqGrid({...}) and the grid elements like column headers will be created.
The method GridDestroy works like jQuery.remove. It deletes all elements which belong to the grid inclusve the <table> element.
The method GridUnload on the other hand delete all, but the empty <table> element stay on the page. So you are able to create new grid on the same place. The method GridUnload is very usefull if you need create on one place different grids depend on different conditions. Look at the old answer with the demo. The demo shows how two different grids can by dynamically created on the same place. If you would be just replace GridUnload in the code to GridDestroy the demo will be not work: after destroying of the first grid no other grids will be created on the same place.
In addition to Oleg's answer I would like to point out that GridUnload does a little more that just remove the grid from the table. It removes the original HTML table element(and the pager), and ads an identical one in its place(at least in 4.5.4 it does).
This means that if you attached some event handlers to the table HTML element(i.e with jquery on, like ('#gridID').on('event','selector',handler)) they will also be removed. Consiquently the events will not fire on the new grid if you replace the old grid with a new one...
Oleg's answer works fine for me as long as I have no Group headers.
When I add group header row with 'setGroupHeaders'
the results of a 'GridUnload' followed by a $('#mygrid').jqGrid({...}) are not consistent.
It works fine in Chrome but not in IE11.
In IE11, each 'jqg-third-row-header' item ends up rendered on different rows (diagonally).
I am using free-jqGrid:query.jqgrid.src.js version 4.13.4 for debugging.
I traced the problem down to code, in this file, that begins with line 9936:
if (o.useColSpanStyle) {
// Increase the height of resizing span of visible headers
$htable.find("span.ui-jqgrid-resize").each(function () {
var $parent = $(this).parent();
if ($parent.is(":visible")) {
this.style.cssText = "height:" + $parent.height() + "px !important; cursor:col-resize;";
//this.style.cssText = "height:" + $parent.css('line-height'); + "px !important;cursor:col-resize;";
}
});
// Set position of the sortable div (the main lable)
// with the column header text to the middle of the cell.
// One should not do this for hidden headers.
$htable.find(".ui-th-column>div").each(function () {
var $ts = $(this), $parent = $ts.parent();
if ($parent.is(":visible") && $parent.is(":has(span.ui-jqgrid-resize)") && !($ts.hasClass("ui-jqgrid-rotate") || $ts.hasClass("ui-jqgrid-rotateOldIE"))) {
// !!! it seems be wrong now
$ts.css("top", ($parent.height() - $ts.outerHeight(true)) / 2 + "px");
// $ts.css("top", ($parent.css('line-height') - $ts.css('line-height')) / 2 + "px");
}
});
}
$(ts).triggerHandler("jqGridAfterSetGroupHeaders");
});
This code sets the height and top css values related to each 'jqg-third-row-header' item. This leads to a tall and diagonal layout of the 'jqg-third-row-header'
Potential Bug:.
The $parent.height() and $ts.height() methods, above, return the former jqGrid table height in IE11. In Chrome they return the 'th' computed height(top = 0).
I added and tested the 2 commented lines that use line-height.
IE11 works fine when line-height is used.
I do not completely understand the JqGrid resize logic, so this may not be a fix.
Alternate Solution:
If you specify.
colModel:
{
label: 'D',
name: 'W',
width: 6,
align: 'center',
resizable:false //required for IE11 multiple calls to this init()
},
When resizable is false the code above is not encountered and the height and top are not set.
Oleg's jqGrid is a very nice control. Perhaps he can test his demo grid with a groupheader on IE11.

Categories