Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to copy a table column name? #227

Open
Norlandz opened this issue Feb 6, 2024 · 11 comments
Open

How to copy a table column name? #227

Norlandz opened this issue Feb 6, 2024 · 11 comments

Comments

@Norlandz
Copy link

Norlandz commented Feb 6, 2024

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?

@mwouts
Copy link
Owner

mwouts commented Feb 6, 2024

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

@Norlandz
Copy link
Author

Norlandz commented Feb 7, 2024

@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.

@mwouts
Copy link
Owner

mwouts commented Feb 10, 2024

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

image

@Norlandz
Copy link
Author

Norlandz commented Feb 11, 2024

@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

@mwouts
Copy link
Owner

mwouts commented Feb 11, 2024

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

@Norlandz
Copy link
Author

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.)

@mwouts mwouts closed this as not planned Won't fix, can't repro, duplicate, stale Mar 5, 2024
@mwouts mwouts reopened this Apr 21, 2024
@mwouts
Copy link
Owner

mwouts commented Apr 21, 2024

I am reopening this issue as now with datatables==2.0.1 the index name trick does not work anymore.

@mwouts
Copy link
Owner

mwouts commented Apr 25, 2024

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.

@Norlandz
Copy link
Author

Norlandz commented Jun 15, 2024

@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 select event (and then maybe e.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>

@mwouts
Copy link
Owner

mwouts commented Jun 16, 2024

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?

@AllanJard
Copy link

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants