Sync scrollbars for wide Tabulators

Nothing fancy to show. But I needed to write some custom javascript to get the scrollbars of wide tabulators to sync with each other. It took some time to figure out, so I wanted to share it:

import pandas as pd
import panel as pn

pn.extension("tabulator")

js = """
<script type="text/javascript">
function $$$(selector, rootNode=document.body) {
    const arr = []

    const traverser = node => {
        // 1. decline all nodes that are not elements
        if(node.nodeType !== Node.ELEMENT_NODE) {
            return
        }

        // 2. add the node to the array, if it matches the selector
        if(node.matches(selector)) {
            arr.push(node)
        }

        // 3. loop through the children
        const children = node.children
        if (children.length) {
            for(const child of children) {
                traverser(child)
            }
        }

        // 4. check for shadow DOM, and loop through it's children
        const shadowRoot = node.shadowRoot
        if (shadowRoot) {
            const shadowChildren = shadowRoot.children
            for(const shadowChild of shadowChildren) {
                traverser(shadowChild)
            }
        }
    }

    traverser(rootNode)
    return arr
}

async function sync_scrolling() {
  tables = $$$(".sync-table")

  const elements = []
  for (let i = 0; i < tables.length; i++) {
    var e = null

    // Wait for the table to appear
    while (e === null) {
      await new Promise((r) => setTimeout(r, 100))
      e = tables[i].shadowRoot.querySelector(
        ".pnx-tabulator .tabulator-tableholder"
      )
    }
    elements.push(e)
  }

  var ignore_scroll_events = false
  for (let i = 0; i < elements.length; i++) {
    elements[i].onscroll = (e) => {
      if (ignore_scroll_events) return
      ignore_scroll_events = true
      for (let j = 0; j < elements.length; j++) {
        if (elements[j] == elements[i]) continue
        elements[j].scrollLeft = elements[i].scrollLeft
        elements[j].scrollTop = elements[i].scrollTop
      }
      ignore_scroll_events = false
    }
  }
}

sync_scrolling()
</script>
"""

df = pd.DataFrame(range(10))
pn.Row(
    pn.Column(
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.pane.HTML(js),
        width=200,
    )
).servable()
5 Likes