How to convert (or simply use) a jQuery plugin wth Stimulus

54 views Asked by At

I'd like to use this plugin in a stimulus controller

https://github.com/soxofaan/scrollocue/blob/master/js/scrollocue.js

// scrollocue.js
(function(window, $) {

    $.fn.scrollocue = function(options) {

        // Handle given options and default settings.

My stimulus controller

import { Controller } from '@hotwired/stimulus';
import $ from 'jquery';
window.$ = $;
import '../js/scrollocue.js';

Of course, this doesn't work, but I don't know how to pass the jQuery object to the function.

I can't use require, because everything is now an ES6 module. Most of the time these plugins are published on jsdelivr, then it's easy, but I'm not sure how to handle this directly.

1

There are 1 answers

2
LB Ben Johnston On

You may be best to port this to Stimulus, this avoids an external dependency and lets you tweak the code to do what you want. Also, Stimulus gives you a really powerful way to change keyboard shortcuts and initial loading behaviour by just changing HTML attributes.

Stimulus controller

Here's a basic port of the jQuery code to Stimulus.

  • This does not do 'easing' scrolling but this should not be too complex to add, you may already be using an animation library like https://animate.style/ and can just leverage classes to scroll.
  • We have a Stimulus class of active that allows you to determine what class name gets set (or a group of them if you are using Tailwind for example).
  • We also have an active target, this gets set by the Controller code and allows Stimulus to be 'reactive' to changes to its own DOM elements.
  • Finally, there are two values; index (the state that tracks what index is active, OR it can be used to set an initial index in the HTML) and lines (a CSS selector allowing the HTML to determine the elements to be focused on when moving up/down.
  • Leveraging the ...valueChanged callbacks means we can think about how the Controller reacts to state changes (the index changing).
  • When the index value changes, it clears/removes all active target attributes and then allows the new index value target to be set.
  • When a controller has this new target added, we can leverage the target connected/disconnected callbacks to add/remove the active classes.
import { Controller } from '@hotwired/stimulus';

/**
 * @see https://github.com/soxofaan/scrollocue/blob/master/js/scrollocue.js
 */
export default class extends Controller {
  static classes = ['active'];
  static targets = ['active'];
  static values = {
    index: { default: -1, type: Number },
    lines: { default: 'h1, p', type: String },
  };

  connect() {
    this.clear();
    if (this.indexValue < 0) this.indexValue = 0;
  }

  get lines() {
    return [...this.element.querySelectorAll(this.linesValue)];
  }

  clear() {
    [...this.activeTargets].forEach((element) => {
      element.removeAttribute(`data-${this.identifier}-target`);
    });
  }

  down({ params: { amount = 1 } = {} } = {}) {
    this.indexValue = Math.min(this.indexValue + amount, this.lines.length - 1);
  }

  up({ params: { amount = 1 } = {} } = {}) {
    this.indexValue = Math.max(0, this.indexValue - amount);
  }

  indexValueChanged(currentIndex) {
    this.clear();
    if (currentIndex === -1) return;
    const element = this.lines[currentIndex];
    element.setAttribute(`data-${this.identifier}-target`, 'active');
  }

  activeTargetConnected(element) {
    element.classList.add(...this.activeClasses);
    element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }

  activeTargetDisconnected(element) {
    element.classList.remove(...this.activeClasses);
  }
}

HTML

The rest of the 'event' listeners are set by Stimulus action attributes, here we can set up the click/up/down presses moving the item up and down.

<section
    id="container"
    data-controller="teleprompt"
    data-teleprompt-active-class="active"
    data-action="click->teleprompt#down keydown.space@document->teleprompt#down keydown.down@document->teleprompt#down keyup.up@document->teleprompt#up keydown.esc@document->teleprompt#clear"
>
  <div data-action="keydown.k@document->teleprompt#down keyup.j@document->teleprompt#up" data-teleprompt-amount-param="10"></div>
  <h1>Scrollocue</h1>

  <div class="section meta">
    <p>A simple autocue/teleprompter system.</p>
    <p>Just use the arrows or spacebar to scroll.</p>
    <p>Now, let's walk through some text.</p>
  </div>

  <!-- ... other content -->
</section>
<style>
  .active {
    outline: 3px solid green;
    outline-offset: -3px;
  }
</style>