Skip to content

Instantly share code, notes, and snippets.

@esteinborn
Created April 2, 2026 18:07
Show Gist options
  • Select an option

  • Save esteinborn/663da306dff9b564120ddc8203bd7733 to your computer and use it in GitHub Desktop.

Select an option

Save esteinborn/663da306dff9b564120ddc8203bd7733 to your computer and use it in GitHub Desktop.
PRD to remove jQuery dependency from ableplayer

PRD: Migrate Able Player from jQuery to Vanilla JavaScript

Version: 1.0 Date: 2026-04-02 Status: Draft Project: Able Player v5.0


1. Executive Summary

Able Player is a fully accessible, cross-browser HTML5 media player (~15,500 lines of JS across 28 modules). It currently depends on jQuery 3.5+ (slim) for DOM manipulation, event handling, element creation, and utility functions. This PRD defines the plan to remove jQuery entirely and replace all usage with vanilla JavaScript, eliminating the external dependency and reducing the total bundle size.

Recent work in v4.8.0 already replaced $.ajax() with Fetch API, jQuery deferreds with native Promises, and JS animations with CSS transitions. Approximately 528 jQuery calls across 23 files remain.


2. Goals

Goal Measure of Success
Remove jQuery as a dependency package.json has no jQuery entry; no <script> tag for jQuery required by consumers
Zero functional regressions All existing demos and test suite pass identically
Maintain full accessibility All ARIA attributes, keyboard navigation, and screen reader behavior unchanged
Reduce bundle size ableplayer.min.js is smaller than current build + jQuery slim (~70 KB gzipped savings)
Maintain browser support Chrome, Firefox, Safari, Edge (current versions); iOS Safari; Android Chrome/Firefox
No API breaking changes for consumers data-able-player attribute initialization and public methods remain identical

3. Non-Goals

  • Rewriting the architecture or module system (keep prototype-based structure)
  • Adding TypeScript
  • Adding a framework (React, Vue, etc.)
  • Changing the Grunt build system
  • Changing the CSS or visual design
  • Adding new features

4. Scope of jQuery Usage

4.1 Files by jQuery Density

Tier Files Total Calls Strategy
Heavy (50+) vts.js (103), ableplayer-base.js (98), buildplayer.js (55), preference.js (53) 309 Migrate methodically; highest risk
Medium (10-49) transcript.js (36), dragdrop.js (31), track.js (18), initialize.js (17), event.js (16), control.js (15), chapters.js (15), dialog.js (14), search.js (10) 172 Migrate per-module
Light (1-9) metadata.js (9), slider.js (7), sign.js (7), description.js (7), volume.js (6), translation.js (4), youtube.js (2), vimeo.js (2), caption.js (2), misc.js (1) 47 Quick conversions

4.2 jQuery Patterns to Replace

4.2.1 DOM Querying & Traversal

jQuery Vanilla Equivalent Occurrences
$(selector) document.querySelector(selector) / document.querySelectorAll(selector) ~528
.find(sel) el.querySelector(sel) / el.querySelectorAll(sel) ~120
.children() el.children ~19
.parent() el.parentElement ~18
.closest(sel) el.closest(sel) ~8
.first() el.querySelector(sel) or [0] ~20
.last() els[els.length - 1] ~3
.eq(n) els[n] ~45
.filter(sel) Array.from(els).filter(...) ~4

4.2.2 DOM Manipulation

jQuery Vanilla Equivalent Occurrences
.attr(name, val) el.setAttribute(name, val) / el.getAttribute(name) ~238
.removeAttr(name) el.removeAttribute(name) ~14
.prop(name, val) el[name] = val ~26
.data(key, val) el.dataset[key] or a WeakMap ~81
.text(val) el.textContent = val ~116
.html(val) el.innerHTML = val (with DOMPurify) ~34
.val(val) el.value = val ~21
.append(child) el.append(child) ~160
.prepend(child) el.prepend(child) ~12
.remove() el.remove() ~20
.addClass(cls) el.classList.add(cls) ~65
.removeClass(cls) el.classList.remove(cls) ~41
.hasClass(cls) el.classList.contains(cls) ~6
.css(prop, val) el.style[prop] = val ~69
.hide() el.style.display = 'none' or el.hidden = true ~72
.show() el.style.display = '' or el.hidden = false ~34
.width() / .height() el.offsetWidth / el.offsetHeight or el.getBoundingClientRect() ~53
.position() el.getBoundingClientRect() relative to offset parent ~13

4.2.3 Element Creation

jQuery Vanilla Equivalent
$('<div>', { class: 'x', role: 'y' }) Object.assign(document.createElement('div'), ...) + setAttribute

This is the most pervasive pattern in the codebase (~200+ instances). A small helper function is warranted here (see Section 6.1).

4.2.4 Event Handling

jQuery Vanilla Equivalent Occurrences
.on(event, handler) el.addEventListener(event, handler) ~111
.on(event, selector, handler) Manual event delegation or el.addEventListener + check e.target.closest(selector) varies
.off(event, handler) el.removeEventListener(event, handler) ~5
.trigger(event) el.dispatchEvent(new Event(event)) ~50
$(document).ready(fn) document.addEventListener('DOMContentLoaded', fn) or just run at end of <body> 1

4.2.5 jQuery Utilities

jQuery Vanilla Equivalent Occurrences
$.inArray(val, arr) arr.indexOf(val) ~7
$.contains(parent, child) parent.contains(child) ~3
$.when(p1, p2) Promise.all([p1, p2]) ~2
$.isEmptyObject(obj) Object.keys(obj).length === 0 ~1
$.each(arr, fn) arr.forEach(fn) ~1

4.2.6 IIFE Wrapper Pattern

Current pattern wrapping every module:

(function ($) {
  // module code using $
})(jQuery);

Replace with:

(function () {
  // module code using vanilla JS
})();

4.2.7 jQuery Object Storage on this

The player stores jQuery-wrapped elements as instance properties:

this.$media = $(media);
this.$ableDiv = $('<div>', { class: 'able' });

These need to become raw DOM element references:

this.media = media;
this.ableDiv = document.createElement('div');
this.ableDiv.className = 'able';

Convention: Remove the $ prefix from all property names. This is an internal change only -- these properties are not part of the public API.


5. Migration Strategy

5.1 Phased Approach

Migration will proceed in 4 phases, each producing a working, testable build.

Phase 1: Foundation (Infrastructure & Utilities)

Objective: Create minimal helper utilities and migrate the lightest files.

Task Files Est. jQuery Calls
Create dom-helpers.js utility module (see 6.1) New file 0
Migrate light-usage files misc.js, caption.js, youtube.js, vimeo.js, translation.js, volume.js 17
Update IIFE wrappers in migrated files Same files --
Update test infrastructure to remove jQuery dependency __tests__/ --

Exit Criteria: All migrated files work without jQuery. Demos using YouTube/Vimeo still function.

Phase 2: Features (Mid-Complexity Modules)

Objective: Migrate feature modules that don't touch core player construction.

Task Files Est. jQuery Calls
Migrate content modules description.js, sign.js, metadata.js, slider.js 30
Migrate UI feature modules search.js, dialog.js, chapters.js, control.js 54
Migrate interaction modules dragdrop.js, transcript.js, track.js 85
Migrate event handling event.js 16

Exit Criteria: All feature modules work with vanilla JS. Caption, transcript, chapter, and description features verified across demos.

Phase 3: Core (Heavy Modules)

Objective: Migrate the core initialization and build modules.

Task Files Est. jQuery Calls
Migrate preferences system preference.js 53
Migrate player builder buildplayer.js 55
Migrate core initialization ableplayer-base.js, initialize.js 115
Convert $-prefixed instance properties All files (find/replace + verify) --

Exit Criteria: Player initializes and fully functions without jQuery loaded on the page.

Phase 4: Cleanup & Finalize

Objective: Remove all jQuery traces and ship.

Task Details
Migrate VTS tool vts.js (103 calls) -- standalone tool, can be done in parallel
Remove jQuery from package.json Delete dependency entry
Remove jQuery <script> tags from all demos ~60 HTML files
Update Gruntfile.js Remove any jQuery-related build config
Update .jshintrc Remove "jquery": true
Update README and documentation Remove jQuery prerequisite instructions
Update CHANGELOG.md Document the breaking change for consumers
Final cross-browser QA Full test matrix (see Section 7)

6. Technical Design Decisions

6.1 Helper Utility: dom-helpers.js

A single small module to reduce repetitive vanilla JS boilerplate for the two most common patterns:

/**
 * Create a DOM element with attributes.
 * Replaces: $('<tag>', { attr: val, ... })
 */
function createElement(tag, attributes) {
  var el = document.createElement(tag);
  if (attributes) {
    Object.keys(attributes).forEach(function(key) {
      if (key === 'class' || key === 'className') {
        el.className = attributes[key];
      } else {
        el.setAttribute(key, attributes[key]);
      }
    });
  }
  return el;
}

This replaces ~200+ element creation patterns with minimal abstraction.

6.2 data() Replacement Strategy

jQuery's .data() stores arbitrary JS objects on elements without polluting the DOM. Two options:

Approach Pros Cons
el.dataset Native, simple, visible in DOM String-only values; requires serialization for objects
WeakMap Supports any value type; no DOM pollution; auto-GC Requires a module-level map instance

Decision: Use el.dataset for simple string/boolean values (the majority of current usage). Use a shared WeakMap for the few cases storing objects or arrays.

6.3 Event Delegation

jQuery's .on(event, selector, handler) provides built-in event delegation. Replace with:

parentEl.addEventListener(event, function(e) {
  var target = e.target.closest(selector);
  if (target && parentEl.contains(target)) {
    handler.call(target, e);
  }
});

Audit each delegated handler to determine if delegation is still necessary or if a direct listener suffices.

6.4 $.Deferred / $.when

Already largely replaced in v4.8.0 with native Promises. Any remaining instances convert to:

  • $.Deferred() -> new Promise((resolve, reject) => { ... })
  • $.when(a, b) -> Promise.all([a, b])

6.5 .hide() / .show() Strategy

jQuery's .hide() / .show() remembers the element's previous display value. Vanilla approach:

  • Preferred: Use el.hidden = true/false where semantically appropriate (sets display: none via UA stylesheet)
  • Fallback: Use el.style.display = 'none' / el.style.display = '' (restores CSS-defined display)
  • Add a CSS utility class .able-hidden { display: none !important; } for cases needing specificity

6.6 Instance Property Renaming ($-prefix removal)

All this.$propertyName references become this.propertyName. Since these are internal to the library and not documented as public API, this is a safe internal refactor. A codebase-wide search-and-replace per property, verified by grep, handles this.


7. Testing & QA Plan

7.1 Unit / Integration Tests

Action Detail
Remove require("jquery") from test files Replace with jsdom built-in DOM APIs
Add migration regression tests One test per major feature: captions, chapters, transcript, descriptions, search, preferences, drag/drop
Run existing test suite after each phase npm test must pass before proceeding

7.2 Manual QA Matrix

Each phase must be verified against this matrix before merging:

Feature Chrome Firefox Safari Edge iOS Safari Android Chrome
Play/pause/seek audio
Play/pause/seek video
Keyboard navigation
Closed captions (WebVTT)
Interactive transcript
Chapter navigation
Audio descriptions
User preferences dialog
Volume slider
Playback rate
YouTube embed
Vimeo embed
Sign language window
Search in transcript
Drag/drop transcript window
Playlist
Screen reader (NVDA/VoiceOver) -- -- VO -- VO --

7.3 Accessibility Audit

  • Run each demo with a screen reader (VoiceOver on macOS/iOS, NVDA on Windows) after Phase 3
  • Verify all ARIA attributes are intact via DOM inspection
  • Verify keyboard focus management (tab order, focus trapping in dialogs)
  • Verify live region announcements still fire

8. Risks & Mitigations

Risk Impact Likelihood Mitigation
Behavioral differences between jQuery and vanilla DOM methods Subtle bugs (e.g., .attr() vs .prop() nuances) Medium Thorough per-method testing; refer to jQuery source for edge cases
.data() migration breaks stored object references Features silently fail Medium Audit all .data() usage; use WeakMap for non-string data
Event delegation gaps cause missed events Buttons/controls stop responding Medium Test every interactive control after migration
.width() / .height() return different values Layout breaks for transcript/sign windows Low jQuery uses offsetWidth internally; match the same
Consumer code depends on jQuery being loaded by Able Player Breaking change for downstream users High Document clearly in CHANGELOG; major version bump (v5.0)
vts.js is a standalone tool with 103 jQuery calls Large single-file migration Low Can be done independently in Phase 4

9. Consumer Migration Guide (to include in release notes)

Before (v4.x)

<!-- jQuery required before Able Player -->
<script src="https://code.jquery.com/jquery-3.7.1.slim.min.js"></script>
<script src="ableplayer.min.js"></script>

After (v5.0)

<!-- No jQuery needed -->
<script src="ableplayer.min.js"></script>

No changes required to HTML markup (data-able-player attribute) or player behavior. Consumers who were loading jQuery solely for Able Player can remove it.


10. Definition of Done

  • Zero references to $, jQuery, or jquery in any source file under scripts/
  • jquery removed from package.json dependencies
  • All demo HTML files load and function without jQuery
  • npm test passes
  • Manual QA matrix (Section 7.2) fully checked off on latest Chrome, Firefox, Safari, Edge
  • Screen reader testing completed (VoiceOver, NVDA)
  • README updated to remove jQuery prerequisite
  • CHANGELOG documents the change
  • Version bumped to 5.0.0
  • ableplayer.min.js bundle size is smaller than v4.x build
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment