itables
itables copied to clipboard
How to copy a table column name?
How to copy a table column name?
The current behavior when click on the table column name triggers a sort.
Is it possible to allow something like alt+mouse drag,, to select & copy the name,, instead of triggering sort?
Hey @Norlandz , that's a great question and actually I often wonder how to do that.
Your question is actually for the https://datatables.net/ project - the underlying JavaScript library that we use to display the tables.
Would you mind searching/asking for this on the datatables forum and keep us posted?
Thanks
@mwouts I may look into it when I have time. (Never tried this library in js before.)
Btw, as for css workaround user-select / pointer-events, I tried, seemed not working.
Hey @Norlandz , possibly the most convenient workaround is to give a name to your index (see below).
Alternatively, the ordering component can be deactivated with columnDefs=[{"targets":"_all", "orderable": False}].
Let me know if that answers your question.
import pandas as pd
from itables import init_notebook_mode, show
init_notebook_mode(all_interactive=True)
# The column cannot be selected
df = pd.DataFrame({'column':[5]})
df
# Adding a named index lets you select the column (but not the index name)
df.index.name = 'index'
df
# Make all columns not orderable
show(df, columnDefs=[{"targets":"_all", "orderable": False}])
# Make all columns not orderable for all tables
import itables.options as opt
opt.columnDefs=[{"targets":"_all", "orderable": False}]
df
@mwouts Good to know, but this workaround is adding extra info to the df & require more visual space.
What I do instead is: inject a javascript -- if you click on the column name -> it selects the column name (but sorts it too... i could interrupt the click event of sort, but for simplicity i didnt) . (Still, not the best though.)
jsscript = """
<script>
"use strict";
// hopefully, this will work without memory leak ...
console.log('inject js -- one click select table column name');
// function ready(fn) {
// if (document.readyState !== 'loading') { fn(); return; }
// document.addEventListener('DOMContentLoaded', fn);
// }
// ready(async function () {});
let mpp_eltTh_WithListener_prev = new Map();
// this requires constantly adding new listener to newly added elements (& discard old listeners)
document.addEventListener('click', function (ev) {
// await new Promise((r) => setTimeout(r, 2000));
const arr_elt = document.querySelectorAll('th');
const mpp_eltTh_WithListener_new = new Map();
for (const elt of arr_elt) {
// console.log(elt);
// if (!(elt instanceof HTMLTableCellElement)) throw new TypeError();
const listener_prev = mpp_eltTh_WithListener_prev.get(elt);
if (listener_prev === undefined) {
// new element detected -- add new listener
const listener_new = (ev) => {
// https://developer.mozilla.org/en-US/docs/Web/API/Selection/addRange
const selection = window.getSelection();
if (selection === null)
return;
if (selection.rangeCount > 0) {
selection.removeAllRanges(); // must dk .
}
const range = document.createRange();
range.selectNode(elt);
selection.addRange(range);
};
elt.addEventListener('click', listener_new);
mpp_eltTh_WithListener_new.set(elt, listener_new);
}
else {
// already have listener
mpp_eltTh_WithListener_prev.delete(elt); // delete (exclude) from old map, so that in a later operation it wont be unregistered
mpp_eltTh_WithListener_new.set(elt, listener_prev);
}
}
// clear up old Map, replace with new Map -- remember to delete (exclude) retained elemnt first before go to this step
for (const [elt_prev, listener_prev] of mpp_eltTh_WithListener_prev) {
elt_prev.removeEventListener('click', listener_prev);
// []
// According to the jquery Documentation when using remove() method over an element, all event listeners are removed from memory. This affects the element it selft and all child nodes. If you want to keep the event listners in memory you should use .detach() instead
// <>
// https://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory#:~:text=According%20to%20the%20jquery%20Documentation,detach()%20instead.
// em // whatever
}
mpp_eltTh_WithListener_prev = mpp_eltTh_WithListener_new;
});
</script>
"""
display(HTML(jsscript))
original ts code
// hopefully, this will work without memory leak ...
console.log('inject js -- one click select table column name');
// function ready(fn) {
// if (document.readyState !== 'loading') { fn(); return; }
// document.addEventListener('DOMContentLoaded', fn);
// }
// ready(async function () {});
let mpp_eltTh_WithListener_prev = new Map<HTMLTableCellElement, (ev: MouseEvent) => void>();
// this requires constantly adding new listener to newly added elements (& discard old listeners)
document.addEventListener('click', function (ev) {
// await new Promise((r) => setTimeout(r, 2000));
const arr_elt = document.querySelectorAll('th');
const mpp_eltTh_WithListener_new = new Map<HTMLTableCellElement, (ev: MouseEvent) => void>();
for (const elt of arr_elt) {
// console.log(elt);
// if (!(elt instanceof HTMLTableCellElement)) throw new TypeError();
const listener_prev = mpp_eltTh_WithListener_prev.get(elt);
if (listener_prev === undefined) {
// new element detected -- add new listener
const listener_new = (ev) => {
// https://developer.mozilla.org/en-US/docs/Web/API/Selection/addRange
const selection = window.getSelection();
if (selection === null) return;
if (selection.rangeCount > 0) {
selection.removeAllRanges(); // must dk .
}
const range = document.createRange();
range.selectNode(elt);
selection.addRange(range);
};
elt.addEventListener('click', listener_new);
mpp_eltTh_WithListener_new.set(elt, listener_new);
} else {
// already have listener
mpp_eltTh_WithListener_prev.delete(elt); // delete (exclude) from old map, so that in a later operation it wont be unregistered
mpp_eltTh_WithListener_new.set(elt, listener_prev);
}
}
// clear up old Map, replace with new Map -- remember to delete (exclude) retained elemnt first before go to this step
for (const [elt_prev, listener_prev] of mpp_eltTh_WithListener_prev) {
elt_prev.removeEventListener('click', listener_prev);
// []
// According to the jquery Documentation when using remove() method over an element, all event listeners are removed from memory. This affects the element it selft and all child nodes. If you want to keep the event listners in memory you should use .detach() instead
// <>
// https://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory#:~:text=According%20to%20the%20jquery%20Documentation,detach()%20instead.
// em // whatever
}
mpp_eltTh_WithListener_prev = mpp_eltTh_WithListener_new;
});
// how to let tsc use let instead of var in generated js
// ;wrong; h:\Using\t1-vite>npx tsc --target ES6 ./src/main.tsx
// h:\Using\t1-vite\compileThis>tsc -p tsconfig.json
Update: code modified for constant checking new table element
Thanks @Norlandz ! It might make sense to ask for a fix in https://github.com/DataTables/DataTablesSrc, can I let you open an issue or even PR there if you feel that's the right move? Thanks
Thanks @Norlandz ! It might make sense to ask for a fix in https://github.com/DataTables/DataTablesSrc, can I let you open an issue or even PR there if you feel that's the right move? Thanks
I didnt read into the API / source code of DataTables, (there might be api for picking the column name), not sure a issue / PR is appropriate.
The code above is just a trivial workaround. (If you want to use it anywhere its totally fine)
(I may not submit an issue for now until I read through the api in js in future, but if you want to do that instead its fine too.) (You can close the issue here in the meantime if you want.)
I am reopening this issue as now with datatables==2.0.1 the index name trick does not work anymore.
I got an answer and a working example on the datatables forum: https://datatables.net/forums/discussion/comment/231058
It involves setting a data attribute on the header cells (<th data-dt-order="icon-only">Name</th>) and then set custom listeners in Javascript, so that's possibly a bit more involved than what I can develop or maintain at the moment, but at least we have a path towards this. Also, according to the comments on the thread that might become easier in a future version of datatables.
@mwouts
Actually, if I have looked more into the js, I should know the easiest solution is injecting this js:
js = """
for (const eventType of ['select', 'selectstart']) {
document.addEventListener(eventType, function (event) {
if (event.target.nodeType && event.target.nodeType === 3) { // NodeType.TEXT_NODE
const parentElement = event.target.parentElement;
if (parentElement && parentElement.classList.contains('dt-column-title') && parentElement.getAttribute('role') === 'button') {
event.stopPropagation();
}
}
}, true);
}
"""
display(HTML(f"<script>{js}</script>"))
-
Explain: before: the lib Datatables captures the
selectevent (and then maybee.preventDefault()or whatever stops you from selecting). now: you stop the event from being captured by the lib Datatables, so the select would work as normal. -
Note,Warning: this may prevent the lib Datatables or other js script from doing its custom behavior / functionalities, which you may or may not want.
For an example-reference of the itables es table structure in html:
<div id="itables_cb1146f3_8b1e_4108_85ad_2b3a13fc4e35_wrapper" class="dt-container dt-empty-footer">
<div class="dt-layout-row dt-layout-table">
<div class="dt-layout-cell">
<table id="itables_cb1146f3_8b1e_4108_85ad_2b3a13fc4e35" class="display nowrap compact dataTable" data-quarto-disable-processing="true" style="width: 2733.83px; float: left">
<colgroup>
<col data-dt-column="0" style="width: 34px" />
<col data-dt-column="1" style="width: 75.875px" />
(//...)
</colgroup>
<thead>
<tr style="text-align: right" role="row">
<th data-dt-column="0" rowspan="1" colspan="1" class="dt-type-numeric dt-orderable-asc dt-orderable-desc" aria-label=": Activate to sort" tabindex="0">
<span class="dt-column-title" role="button"></span><span class="dt-column-order"></span>
</th>
<th data-dt-column="1" rowspan="1" colspan="1" class="dt-orderable-asc dt-orderable-desc" aria-label="gender: Activate to sort" tabindex="0">
<span class="dt-column-title" role="button">gender</span><span class="dt-column-order"></span>
</th>
<th data-dt-column="2" rowspan="1" colspan="1" class="dt-type-numeric dt-orderable-asc dt-orderable-desc" aria-label="age: Activate to sort" tabindex="0">
<span class="dt-column-title" role="button">age</span><span class="dt-column-order"></span>
</th>
(//...)
</tr>
</thead>
<tbody>
<tr>
<td class="dt-type-numeric">3</td>
<td>Male</td>
<td class="dt-type-numeric">22</td>
<td class="sorting_1">Yes</td>
<td class="dt-type-numeric">2</td>
<td class="dt-type-numeric">1</td>
(//...)
</tr>
(//...)
</tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
Hi @Norlandz, thank you for the above, it looks great! I will give it a try in the coming days.
Ideally I would like to integrate this either in the dt_for_itables package, or at least in the html template, but then we would need to figure how to do the above for only the selection events within the current table.
Also let me ping @AllanJard to see if that seems ok to him or whether he has a different recommendation.
Allan, what we are trying to achieve here is to make the columns selectable (so that we can copy the column titles and paste them in the Python shell). Currently we cannot select them (the attempt to select triggers a reordering of the column content). On the forum thread quoted two comments ago you suggested to use order.listener to restrict the sorting event to the icon, and mentioned possible future work. I guess order.listener is still what you'd expect us to use? What do you think about the interception of the selection event as above?
It is possible to use the Select extension to select a column by clicking on a cell in it (example here - click the "Select columns" button).
However, yes, if you want to click in the header, what I would suggest doing is adding an icon that can be used to select the column, and stop the click event from bubbling up the DOM (e.preventPropagation()), to stop it reaching the event listener.
Future work: Yes, I have a plan to make this sort of thing easier, and hopefully will be able to work on it later in the year :)