Tabulator Keybindings

Hello. I’m trying to get custom keybindings working with Tabulator. The goal is to use up/down arrows to navigate the table and use left/right arrows to change the value of the mark column between -1,0,1. The code is working but there are a couple issues:

  1. Left/right arrows change the mark column but they also scroll the table. Can the key-based scrolling be disabled?
    2. The tabulator configuration option is not working at all. For example, the header sort is still enabled. Am I using the wrong keywords? Corrected.

Working example:

import panel as pn
import numpy as np
import pandas as pd
import panel as pn

pn.extension('tabulator', css_files=[pn.io.resources.CSS_URLS['font-awesome']])

script = """
<script>
const doc = window.parent.document;
buttons = Array.from(doc.querySelectorAll('button[type=button]'));
const left_button = buttons.find(el => el.innerText === 'LEFT');
const right_button = buttons.find(el => el.innerText === 'RIGHT');
const down_button = buttons.find(el => el.innerText === 'DOWN');
const up_button = buttons.find(el => el.innerText === 'UP');
doc.addEventListener('keydown', function(e) {
    switch (e.keyCode) {
        case 37: // (37 = left arrow)
            left_button.click();
            break;
        case 39: // (39 = right arrow)
            right_button.click();
            break;
        case 40: // (40 = down arrow)
            down_button.click();
            break;
        case 38: // (38 = up arrow)
            up_button.click();
            break;
    }
});
</script>
"""

html = pn.pane.HTML(script)

rows = 50
cols = 30

df = pd.DataFrame(
    np.random.rand(rows, cols),
    columns=[f'col_{i}' for i in range(cols)],
)

df['mark'] = [0] * rows

tab_config = {
#    'columnDefaults': {
#        'resizable': False,
    'headerSort': False, # corrected (remove columnDefaults key)
#    },
    'keybindings': False,
    #     'keybindings': {
    #         "navUp": False,
    #         "navDown": False,
    #         "navRight": False,
    #         "navLeft": False,
    #         "scrollPageUp": False,
    #         "scrollPageDown": False,
    #     },
}

table = pn.widgets.Tabulator(
    df,
    disabled=True,
    selectable=True,
    configuration=tab_config,
    frozen_columns=['index', 'mark'],
    width=600,
    height=400,
)


def highlight(row, column):
    if row[column] == -1:
        return ['background-color: lightcoral'] * len(row)
    elif row[column] == 0:
        return [''] * len(row)
    elif row[column] == 1:
        return ['background-color: lightgreen'] * len(row)


table.style.apply(highlight, column='mark', axis=1)

button_left = pn.widgets.Button(name="LEFT")
button_right = pn.widgets.Button(name="RIGHT")
button_down = pn.widgets.Button(name="DOWN")
button_up = pn.widgets.Button(name="UP")


def callback_up(event):
    print('callback_up', event)
    if len(table.selection) > 0:
        row_index = [i - 1 for i in sorted(table.selection)]
        if row_index[0] >= 0:
            table.selection = row_index


def callback_down(event):
    print('callback_down')
    if len(table.selection) > 0:
        row_index = [i + 1 for i in sorted(table.selection)]
        if row_index[-1] < len(df):
            table.selection = row_index


def callback_left(event):
    print('callback_left', event)
    if len(table.selection) > 0:
        for i in table.selection:
            val = table.value.loc[i, 'mark']
            if val > -1:
                table.patch({'mark': [(i, val - 1)]})


def callback_right(event):
    print('callback_right')
    if len(table.selection) > 0:
        for i in table.selection:
            val = table.value.loc[i, 'mark']
            if val < 1:
                table.patch({'mark': [(i, val + 1)]})


button_up.on_click(callback_up)
button_down.on_click(callback_down)
button_left.on_click(callback_left)
button_right.on_click(callback_right)

pn.Column(
    pn.Column(button_left,
              button_left.param.clicks,
              button_right,
              button_right.param.clicks,
              visible=False),
    pn.Column(button_up,
              button_up.param.clicks,
              button_down,
              button_down.param.clicks,
              visible=False),
    html,
    table,
).servable()

Thanks to @Hoxbro for the keybinding example.

1 Like

Resolved the header sort configuration.

Instead of:

tab_config = {
    'columnDefaults': {
        'headerSort': False,
    },
}

… it should be …

tab_config = {
        'headerSort': False,
}

When comparing pn.widgets.Tabulator with pn.widgets.DataFrame, I see that arrow key navigation is not consistent between them. With pn.widgets.DataFrame, the arrow keys navigate cells and only scroll when the selected cell is out of the viewing area. With pn.widgets.Tabulator, the arrow keys scroll only the page by default (and mouse hover seems to interfere with this a bit). The DataFrame cell navigation mode is the preferrable default in my opinion.

Which brings me back to my original question, how to disable the Tabulator page scrolling and only perform the custom action?