A Medium-like Popover Without Any Dependencies

My blog missed the ability of sharing an article. At current, I have yet to add a visible button to share a blog post. I'm not being lazy, I'm still planning on how the website would look and where to put it. I also need a commenting system which is still in the pipeline.

There were times where I wanted to tweet a section of my blog, on Twitter, without the manual labour of copy-paste. As I made a recent tweet, it was high time I did something about it.

For this task, I made use of Google's search engine in which I was led to few popovers:

Popper.js is not really what I wanted and selection-sharer... not really-ish due to its use of jQuery. jQuery is great but I never wanted to use it in a React app such as this blog is. I also needed full control. I'm a huge fan of vanilla JavaScript. This simple share button should popover nicely, quoted the highlighted text on click then share to Twitter. Currently you can only share with Twitter. Later I'll add Facebook. You can easily add Facebook share link after reading this tutorial.

During this blog post, I will be using tooltip and popover interchangeably.

The React Component

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  // We'll fill in this later
  componentDidMount() {}
  
    render() {
    return (
      <div className="ToolTipShare">
        <div className="ToolTipShare__inner">
          <div className="ToolTipShare__inner__buttons">
            <button className="ToolTipShare__inner__buttons__item">
              <svg className="svgIcon-use" width="25" height="25"><path d="M21.725 5.338c-.744.47-1.605.804-2.513 1.006a3.978 3.978 0 0 0-2.942-1.293c-2.22 0-4.02 1.81-4.02 4.02 0 .32.034.63.07.94-3.31-.18-6.27-1.78-8.255-4.23a4.544 4.544 0 0 0-.574 2.01c.04 1.43.74 2.66 1.8 3.38-.63-.01-1.25-.19-1.79-.5v.08c0 1.93 1.38 3.56 3.23 3.95-.34.07-.7.12-1.07.14-.25-.02-.5-.04-.72-.07.49 1.58 1.97 2.74 3.74 2.8a8.49 8.49 0 0 1-5.02 1.72c-.3-.03-.62-.04-.93-.07A11.447 11.447 0 0 0 8.88 21c7.386 0 11.43-6.13 11.414-11.414.015-.21.01-.38 0-.578a7.604 7.604 0 0 0 2.01-2.08 7.27 7.27 0 0 1-2.297.645 3.856 3.856 0 0 0 1.72-2.23"></path></svg>
            </button>
          </div>
        </div>
        <div className="ToolTipShare__arrow">
          <span className="ToolTipShare__arrow__span"></span>
        </div>
      </div>
    )
  }
  
  export default ToolTipShare

With the base in place, we'll focus on adding in the functionality bit by bit.

Content area

import React, { Component } from 'react'
import ToolTipShare from '../ToolTipShare'

[...]

return (
  <div className="ArticleShow__content">
    <h1>My awesome content</h1>
    <p>This little light of mine, I'm gonna let it shine.</p>
    <ToolTipShare content={'.ArticleShow__content'} />
  </div>
)

We import our ToolTipShare component into our content area and pass a prop with the name class Β or id of the content's element. In this case I've use the name of my class.

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  [...]
  
  getSelectionText() {
    let text = "";
    if (window.getSelection) {
      text = window.getSelection().toString();
    } else if (document.selection && document.selection.type != "Control") {
      text = document.selection.createRange().text;
    }

    return text;
  }
  
  [...]
  
  export default ToolTipShare

We use Β a function to give us the selected text, this.getSelectionText(). To be able to call this function, we wrap it inside of an event listener, mouseup.

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  componentDidMount() {
    this.onMouseUp()
  }
  
  onMouseUp() {
    const contentElement = document.querySelector(this.props.content)
    contentElement.addEventListener('mouseup', () => {

      const selectedText = this.getSelectionText()

      // Double-click can return an empty string
      if (selectedText) {
        // Show the hidden tooltip
      }

    }, false)
  }
  
  getSelectionText() {...}
  
  [...]
  
  export default ToolTipShare

Once our component has mounted, we listen for a mouseup event within our container element, this.props.content. You could use an id if you like but querySelector gets the first element of the class or id given to it. I'll include the css after, but for now our ToolTipShare will be visibility: hidden by default. Then we use JavaScript to add and remove a visible class.

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  componentDidMount() {
    this.onMouseUp()
  }
  
  onMouseUp() {
    const contentElement = document.querySelector(this.props.content)
    contentElement.addEventListener('mouseup', (e) => {

      const selectedText = this.getSelectionText()

      // Double-click can return an empty string
      if (selectedText) {
        // Show the hidden tooltip
        this.showTooltip()
      }

    }, false)
  }
  
  getSelectionText() {...}
  
  showTooltip() {
    const toolTipElement = document.querySelector('.ToolTipShare')
    const toolTipElementRect = toolTipElement.getBoundingClientRect();
    const contentElement = document.querySelector(this.props.content)
    const contentElementRect = contentElement.getBoundingClientRect()
    const selection = window.getSelection()
    const oRange = selection.getRangeAt(0) //get the text range
    const selectedTextRect = oRange.getBoundingClientRect()
    const toolTipCenter = toolTipElementRect.width / 2
    const topOffset = 48
    const leftOffset = 2
    const top = (selectedTextRect.top - contentElementRect.top) - topOffset
    const left = selectedTextRect.left - contentElementRect.left
    const centerLeft = (left + (selectedTextRect.width / 2) - toolTipCenter)

    toolTipElement.style.top = `${top}px`
    toolTipElement.style.left = `${centerLeft - leftOffset}px`
    toolTipElement.classList.add('ToolTipShare--active')
  }
  
  [...]
  
  export default ToolTipShare

showTooltip()

Double-click or highlighting a text, or a few words, will display the popover centred and over the selected or highlighted text/words. To achieve this position, we make use of an element's getBoundingClientRect(). You can console.log(toolTipElementRect) to see what's inside its object. Personally I had to use some offset values to position my tooltip correctly. After getting the correct position, I applied the style to the element then show the tooltip. Read more about how to get the x,y position of an element.

Inside getSelectionText(), you can console.log the text variable then select any word and you should see the tooltip over the selected text and also you'll see the text displays in your console. We now need to dismiss the popover.

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  componentDidMount() {
    this.onMouseUp()
  }
  
  onMouseUp() {
    const contentElement = document.querySelector(this.props.content)
    contentElement.addEventListener('mouseup', () => {

      const selectedText = this.getSelectionText()

      // Double-click can return an empty string
      if (selectedText) {
        // Show the hidden tooltip
        this.showTooltip()
        this.onMouseDown()
      }

    }, false)
  }
  
  onMouseDown() {
    document.addEventListener('mousedown', () => {
      this.removeTooltipClass()
    }, false)
  }
  
  removeTooltipClass() {
    const toolTipElement = document.querySelector('.ToolTipShare')
    toolTipElement.classList.remove('ToolTipShare--active')
  }
  
  getSelectionText() {...}
  
  showTooltip() {...}
  
  [...]
  
  export default ToolTipShare

onMouseDown() & removeTooltipClass()

Nothing complex here. All we are doing is to remove the active class to hide our tooltip. We break this into another function as we'll be removing the same class later on. Once we are showing our tooltip, we initiate a mousedown event to hide the tooltip. At this point we have a fully working tooltip but we need to do something once someone clicks the Twitter icon, right?

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  componentDidMount() {...}
  
  onMouseUp() {...}
  
  onMouseDown() {...}
  
  removeTooltipClass() {...}
  
  getSelectionText() {...}
  
  showTooltip() {...}
  
  twitterShareLink() {
    // For width and height, you could pass in these as a param πŸ€·πŸ½β€β™‚οΈ
    const w = '900'
    const h = '500'
    const twitUrl = 'https://twitter.com/intent/tweet'
    const originalReferer = window.location.href
    const url = originalReferer
    const text = 'πŸ’¬ ' + this.getSelectionText()
    const via = 'mayneweb'
    const fullUrl = `${twitUrl}?text=${text}&url=${url}&via=${via}&original_referer=${originalReferer}`

    // Fixes dual-screen position                         Most browsers      Firefox
    // Fixes dual-screen position                         Most browsers      Firefox
    const dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left;
    const dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top;

    const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
    const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;

    const left = ((width / 2) - (w / 2)) + dualScreenLeft;
    const top = ((height / 2) - (h / 2)) + dualScreenTop;
    const newWindow = window.open(fullUrl, '', 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);

    // Puts focus on the newWindow
    if (window.focus) {
      newWindow.focus();
    }

    this.removeTooltipClass()
  }
  
  [...]
  
  export default ToolTipShare

We could call window.open with fullUrl and leave it as that but you'll notice that when the window is opened, it's positioned on the far right of your screen. I never liked that. A bit of Googling goes a far way to get the window centered.

And finally

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  componentDidMount() {...}
  
  onMouseUp() {...}
  
  onMouseDown() {...}
  
  removeTooltipClass() {...}
  
  getSelectionText() {...}
  
  showTooltip() {...}
  
  twitterShareLink() {...}
  
  render() {
    return (
      <div className="ToolTipShare">
        <div className="ToolTipShare__inner">
          <div className="ToolTipShare__inner__buttons">
            <button onClick={this.twitterShareLink.bind(this)} className="ToolTipShare__inner__buttons__item">
              <svg className="svgIcon-use" width="25" height="25"><path d="M21.725 5.338c-.744.47-1.605.804-2.513 1.006a3.978 3.978 0 0 0-2.942-1.293c-2.22 0-4.02 1.81-4.02 4.02 0 .32.034.63.07.94-3.31-.18-6.27-1.78-8.255-4.23a4.544 4.544 0 0 0-.574 2.01c.04 1.43.74 2.66 1.8 3.38-.63-.01-1.25-.19-1.79-.5v.08c0 1.93 1.38 3.56 3.23 3.95-.34.07-.7.12-1.07.14-.25-.02-.5-.04-.72-.07.49 1.58 1.97 2.74 3.74 2.8a8.49 8.49 0 0 1-5.02 1.72c-.3-.03-.62-.04-.93-.07A11.447 11.447 0 0 0 8.88 21c7.386 0 11.43-6.13 11.414-11.414.015-.21.01-.38 0-.578a7.604 7.604 0 0 0 2.01-2.08 7.27 7.27 0 0 1-2.297.645 3.856 3.856 0 0 0 1.72-2.23"></path></svg>
            </button>
          </div>
        </div>
        <div className="ToolTipShare__arrow">
          <span className="ToolTipShare__arrow__span"></span>
        </div>
      </div>
    )
  }
  
  export default ToolTipShare

We call an onClick attribute on our button to trigger our function with a bind(this) as we'd need access to our class function this.removeTooltipClass() to remove the active class. Putting it all together we now have:

import React, { Component } from 'react'

class ToolTipShare extends Component {
  constructor(props) {
    super(props);
    this.state = {}
  }

  componentDidMount() {
    this.onMouseUp()
  }

  onMouseUp() {
    const contentElement = document.querySelector(this.props.content)
    contentElement.addEventListener('mouseup', () => {

      const selectedText = this.getSelectionText()

      // Double-click can return an empty string
      if (selectedText) {
        this.showTooltip()
        this.onMouseDown()
      }

    }, false)
  }

  onMouseDown() {
    document.addEventListener('mousedown', (e) => {
      this.removeTooltipClass()
    }, false)
  }

  removeTooltipClass() {
    const toolTipElement = document.querySelector('.ToolTipShare')
    toolTipElement.classList.remove('ToolTipShare--active')
  }

  getSelectionText() {
    let text = "";
    if (window.getSelection) {
      text = window.getSelection().toString();
    } else if (document.selection && document.selection.type != "Control") {
      text = document.selection.createRange().text;
    }

    return text;
  }

  twitterShareLink() {
    const w = '900'
    const h = '500'
    const twitUrl = 'https://twitter.com/intent/tweet'
    const originalReferer = window.location.href
    const url = originalReferer
    const text = 'πŸ’¬ ' + this.getSelectionText()
    const via = 'mayneweb'
    const fullUrl = `${twitUrl}?text=${text}&url=${url}&via=${via}&original_referer=${originalReferer}`

    // Fixes dual-screen position                         Most browsers      Firefox
    // Fixes dual-screen position                         Most browsers      Firefox
    const dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left;
    const dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top;

    const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
    const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;

    const left = ((width / 2) - (w / 2)) + dualScreenLeft;
    const top = ((height / 2) - (h / 2)) + dualScreenTop;
    const newWindow = window.open(fullUrl, '', 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);

    // Puts focus on the newWindow
    if (window.focus) {
      newWindow.focus();
    }

    this.removeTooltipClass()
  }

  showTooltip() {
    const toolTipElement = document.querySelector('.ToolTipShare')
    const toolTipElementRect = toolTipElement.getBoundingClientRect();
    const contentElement = document.querySelector(this.props.content)
    const contentElementRect = contentElement.getBoundingClientRect()
    const selection = window.getSelection()
    const oRange = selection.getRangeAt(0) //get the text range
    const selectedTextRect = oRange.getBoundingClientRect()
    const toolTipCenter = toolTipElementRect.width / 2
    const topOffset = 48
    const leftOffset = 2
    const top = (selectedTextRect.top - contentElementRect.top) - topOffset
    const left = selectedTextRect.left - contentElementRect.left
    const centerLeft = (left + (selectedTextRect.width / 2) - toolTipCenter)

    toolTipElement.style.top = `${top}px`
    toolTipElement.style.left = `${centerLeft - leftOffset}px`
    toolTipElement.classList.add('ToolTipShare--active')
  }

  render() {
    return (
      <div className="ToolTipShare">
        <div className="ToolTipShare__inner">
          <div className="ToolTipShare__inner__buttons">
            <button onClick={this.twitterShareLink.bind(this)} className="ToolTipShare__inner__buttons__item">
              <svg className="svgIcon-use" width="25" height="25"><path d="M21.725 5.338c-.744.47-1.605.804-2.513 1.006a3.978 3.978 0 0 0-2.942-1.293c-2.22 0-4.02 1.81-4.02 4.02 0 .32.034.63.07.94-3.31-.18-6.27-1.78-8.255-4.23a4.544 4.544 0 0 0-.574 2.01c.04 1.43.74 2.66 1.8 3.38-.63-.01-1.25-.19-1.79-.5v.08c0 1.93 1.38 3.56 3.23 3.95-.34.07-.7.12-1.07.14-.25-.02-.5-.04-.72-.07.49 1.58 1.97 2.74 3.74 2.8a8.49 8.49 0 0 1-5.02 1.72c-.3-.03-.62-.04-.93-.07A11.447 11.447 0 0 0 8.88 21c7.386 0 11.43-6.13 11.414-11.414.015-.21.01-.38 0-.578a7.604 7.604 0 0 0 2.01-2.08 7.27 7.27 0 0 1-2.297.645 3.856 3.856 0 0 0 1.72-2.23"></path></svg>
            </button>
          </div>
        </div>
        <div className="ToolTipShare__arrow">
          <span className="ToolTipShare__arrow__span"></span>
        </div>
      </div>
    )
  }
}

export default ToolTipShare

Did you see the need for jQuery here no matter if you're not using a front-end framework? πŸ€·πŸ½β€β™‚οΈ I don't. That's all there is to it. Hope you've found this useful for your project. You may see ways to improve this but for now, this should suffice. For the css, I have it up on gist. Until next time, ✌🏼