Skip to content

Instantly share code, notes, and snippets.

@zorji
Created July 11, 2020 10:06
Show Gist options
  • Save zorji/fe8a5f1d61abc6daa438c99c7c65cee0 to your computer and use it in GitHub Desktop.
Save zorji/fe8a5f1d61abc6daa438c99c7c65cee0 to your computer and use it in GitHub Desktop.
Listen Gmail embed PDF reader selected text

This is an attempt to write a Chrome extension for Gmail that can capture the user selection for further processing. document.addEventListener('selectionchange') only works for the email body but does not work for the embed Gmail PDF reader.

By inspecting the Gmail PDF reader, I tried a different way to retrieve user selection.

The current PDF reader in Gmail is in the following structure:

<html>
  <body>
    ...
      <div role="document">
        <div> // page container, contains one page
          <div> // link container, contains all clickable links
            <a href="">link1</a>
            <a href="">link2</a>
            ...
          </div>
          <div> // text container, contains all text
            <h2>text1</h2>
            <p style="top: a%; left: b%; width: a%; height: d%;">text2</p>
            <p style="top: a%; left: b%; width: a%; height: d%;">text3</p>
            ...
          </div>
          <div></div> // not sure what it is
          <div> // selection continer, contains selections
            <div style="top: a%; left: b%; width: a%; height: d%;"></div>
          </div>
          ...
      </div>
      ... // other pages
  </body>
</html>

When a user opens a PDF, the Gmail JS insert <div role="document" /> into the page.

The defined document.addEventListener('click') the evaluate the click-event target.

When a user made a selection, the click-event target is the page container.

The remaining logic compares the style attribute of the of all children inside the selection container with all the children inside the text container. If the area overlaps, the text of the text container child is the selected text.

Please note the selection div is created after the click-event, so in the logic it implemented a 100ms setTimeout.

var onClick = (event) => {
setTimeout(() => {
console.log('user clicked')
const $target = event.target
if ($target.parentNode.parentNode.getAttribute('role') === 'document') {
console.log('the parent.parent is [role=document], it is clicking inside the PDF reader')
const $page = $target.parentNode
const $link = $target // link container, we don't use it here
const $text = $page.children[1] // text container
const $highlight = $page.children[3] // selection container
const highlights = $highlight.children
const indexed = {}
for (const t of $text.children) {
const { style } = t
const { top, left } = style
if (top && top !== '' && left && left !== '') {
// index all text children using top+left
// however, indexing will fail to match if the user did a partial selection
// I don't have enough time to implement a better solution here
const index = JSON.stringify({ top, left })
indexed[index] = t
}
}
const texts = []
for (const h of highlights) {
const { style } = h
const { top, left } = style
if (top && top !== '' && left && left !== '') {
const index = JSON.stringify({ top, left })
// see if the selection matches a text child
const $text = indexed[index]
if ($text) {
texts.push($text.innerText)
}
}
}
// prints out the selected text
console.log('selected text: ' + texts.join(''))
} else {
console.log('not clicking on the PDF reader')
}
}, 100)
}
document.addEventListener('click', (event) => {
onClick(event)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment