Version: 1.0 Date: 2026-04-02 Status: Draft Project: Able Player v5.0
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.
| 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 |
- 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
| 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 |
| 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 |
| 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 |
| 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).
| 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 |
| 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 |
Current pattern wrapping every module:
(function ($) {
// module code using $
})(jQuery);Replace with:
(function () {
// module code using vanilla JS
})();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.
Migration will proceed in 4 phases, each producing a working, testable build.
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.
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.
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.
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) |
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.
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.
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.
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])
jQuery's .hide() / .show() remembers the element's previous display value. Vanilla approach:
- Preferred: Use
el.hidden = true/falsewhere semantically appropriate (setsdisplay: nonevia 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
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.
| 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 |
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 | -- |
- 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
| 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 |
<!-- 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><!-- 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.
- Zero references to
$,jQuery, orjqueryin any source file underscripts/ -
jqueryremoved frompackage.jsondependencies - All demo HTML files load and function without jQuery
-
npm testpasses - 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.jsbundle size is smaller than v4.x build