(function ($) { // Events: // startTracking(event, position) // tracking(event, position) // stopTracking(event, position) window. AccessibleSlider = function(mediaType, div, orientation, length, min, max, bigInterval, label, className, trackingMedia, initialState) { // mediaType is either 'audio' or 'video' // div is the host element around which the slider will be built // orientation is either 'horizontal' or 'vertical' // length is the width or height of the slider, depending on orientation // min is the low end of the slider scale // max is the high end of the slider scale // bigInterval is the number of steps supported by page up/page down (set to 0 if not supported) // (smallInterval, defined as nextStep below, is always set to 1) - this is the interval supported by arrow keys // label is used within an aria-label attribute to identify the slider to screen reader users // className is used as the root within class names (e.g., 'able-' + classname + '-head') // trackingMedia is true if this is a media timeline; otherwise false // initialState is either 'visible' or 'hidden' var thisObj; thisObj = this; // Initialize some variables. this.position = 0; // Note: position does not change while tracking. this.tracking = false; this.trackDevice = null; // 'mouse' or 'keyboard' this.keyTrackPosition = 0; this.lastTrackPosition = 0; this.nextStep = 1; this.inertiaCount = 0; this.bodyDiv = $(div); // Add divs for tracking amount of media loaded and played if (trackingMedia) { this.loadedDiv = $('
'); this.playedDiv = $('
'); } // Add a seekhead this.seekHead = $('
',{ 'orientation': orientation, 'class': 'able-' + className + '-head' }); if (initialState === 'visible') { this.seekHead.attr('tabindex', '0'); } else { this.seekHead.attr('tabindex', '-1'); } // Since head is focusable, it gets the aria roles/titles. this.seekHead.attr({ 'role': 'slider', 'aria-label': label, 'aria-valuemin': min, 'aria-valuemax': max }); this.timeTooltip = $('
'); this.bodyDiv.append(this.timeTooltip); this.timeTooltip.attr('role', 'tooltip'); this.timeTooltip.addClass('able-tooltip'); this.bodyDiv.append(this.loadedDiv); this.bodyDiv.append(this.playedDiv); this.bodyDiv.append(this.seekHead); this.bodyDiv.wrap('
'); this.wrapperDiv = this.bodyDiv.parent(); if (orientation === 'horizontal') { this.wrapperDiv.width(length); this.loadedDiv.width(0); } else { this.wrapperDiv.height(length); this.loadedDiv.height(0); } this.wrapperDiv.addClass('able-' + className + '-wrapper'); if (trackingMedia) { this.loadedDiv.addClass('able-' + className + '-loaded'); this.playedDiv.width(0); this.playedDiv.addClass('able-' + className + '-played'); // Set a default duration. User can call this dynamically if duration changes. this.setDuration(max); } this.seekHead.hover(function (event) { thisObj.overHead = true; thisObj.refreshTooltip(); }, function (event) { thisObj.overHead = false; if (!thisObj.overBody && thisObj.tracking && thisObj.trackDevice === 'mouse') { thisObj.stopTracking(thisObj.pageXToPosition(event.pageX)); } thisObj.refreshTooltip(); }); this.seekHead.mousemove(function (event) { if (thisObj.tracking && thisObj.trackDevice === 'mouse') { thisObj.trackHeadAtPageX(event.pageX); } }); this.seekHead.focus(function (event) { thisObj.overHead = true; thisObj.refreshTooltip(); }); this.seekHead.blur(function (event) { thisObj.overHead = false; thisObj.refreshTooltip(); }); this.bodyDiv.hover(function () { thisObj.overBody = true; thisObj.refreshTooltip(); }, function (event) { thisObj.overBody = false; thisObj.overBodyMousePos = null; thisObj.refreshTooltip(); if (!thisObj.overHead && thisObj.tracking && thisObj.trackDevice === 'mouse') { thisObj.stopTracking(thisObj.pageXToPosition(event.pageX)); } }); this.bodyDiv.mousemove(function (event) { thisObj.overBodyMousePos = { x: event.pageX, y: event.pageY }; if (thisObj.tracking && thisObj.trackDevice === 'mouse') { thisObj.trackHeadAtPageX(event.pageX); } thisObj.refreshTooltip(); }); this.bodyDiv.mousedown(function (event) { thisObj.startTracking('mouse', thisObj.pageXToPosition(event.pageX)); thisObj.trackHeadAtPageX(event.pageX); if (!thisObj.seekHead.is(':focus')) { thisObj.seekHead.focus(); } event.preventDefault(); }); this.seekHead.mousedown(function (event) { thisObj.startTracking('mouse', thisObj.pageXToPosition(thisObj.seekHead.offset() + (thisObj.seekHead.width() / 2))); if (!thisObj.bodyDiv.is(':focus')) { thisObj.bodyDiv.focus(); } event.preventDefault(); }); this.bodyDiv.mouseup(function (event) { if (thisObj.tracking && thisObj.trackDevice === 'mouse') { thisObj.stopTracking(thisObj.pageXToPosition(event.pageX)); } }) this.seekHead.mouseup(function (event) { if (thisObj.tracking && thisObj.trackDevice === 'mouse') { thisObj.stopTracking(thisObj.pageXToPosition(event.pageX)); } }); this.bodyDiv.keydown(function (event) { // Home if (event.which === 36) { thisObj.trackImmediatelyTo(0); } // End else if (event.which === 35) { thisObj.trackImmediatelyTo(thisObj.duration); } // Left arrow or down arrow else if (event.which === 37 || event.which === 40) { thisObj.arrowKeyDown(-1); } // Right arrow or up arrow else if (event.which === 39 || event.which === 38) { thisObj.arrowKeyDown(1); } // Page up else if (event.which === 33 && bigInterval > 0) { thisObj.arrowKeyDown(bigInterval); } // Page down else if (event.which === 34 && bigInterval > 0) { thisObj.arrowKeyDown(-bigInterval); } else { return; } event.preventDefault(); }); this.bodyDiv.keyup(function (event) { if (event.which >= 33 && event.which <= 40) { if (thisObj.tracking && thisObj.trackDevice === 'keyboard') { thisObj.stopTracking(thisObj.keyTrackPosition); } event.preventDefault(); } }); } AccessibleSlider.prototype.arrowKeyDown = function (multiplier) { if (this.tracking && this.trackDevice === 'keyboard') { this.keyTrackPosition = this.boundPos(this.keyTrackPosition + (this.nextStep * multiplier)); this.inertiaCount += 1; if (this.inertiaCount === 20) { this.inertiaCount = 0; this.nextStep *= 2; } this.trackHeadAtPosition(this.keyTrackPosition); } else { this.nextStep = 1; this.inertiaCount = 0; this.keyTrackPosition = this.boundPos(this.position + (this.nextStep * multiplier)); this.startTracking('keyboard', this.keyTrackPosition); this.trackHeadAtPosition(this.keyTrackPosition); } }; /* AccessibleSlider.prototype.pageUp = function (multiplier) { if (this.tracking && this.trackDevice === 'keyboard') { this.keyTrackPosition = this.boundPos(this.keyTrackPosition + (this.nextStep * multiplier)); this.inertiaCount += 1; if (this.inertiaCount === 20) { this.inertiaCount = 0; this.nextStep *= 2; } this.trackHeadAtPosition(this.keyTrackPosition); } else { this.nextStep = 1; this.inertiaCount = 0; this.keyTrackPosition = this.boundPos(this.position + (this.nextStep * multiplier)); this.startTracking('keyboard', this.keyTrackPosition); this.trackHeadAtPosition(this.keyTrackPosition); } }; */ AccessibleSlider.prototype.pageXToPosition = function (pageX) { var offset = pageX - this.bodyDiv.offset().left; var position = this.duration * (offset / this.bodyDiv.width()); return this.boundPos(position); }; AccessibleSlider.prototype.boundPos = function (position) { return Math.max(0, Math.min(position, this.duration)); } AccessibleSlider.prototype.setDuration = function (duration) { if (duration !== this.duration) { this.duration = duration; this.resetHeadLocation(); this.seekHead.attr('aria-valuemax', duration); } }; AccessibleSlider.prototype.setWidth = function (width) { this.wrapperDiv.width(width); this.resizeDivs(); this.resetHeadLocation(); }; AccessibleSlider.prototype.getWidth = function () { return this.wrapperDiv.width(); }; AccessibleSlider.prototype.resizeDivs = function () { this.playedDiv.width(this.bodyDiv.width() * (this.position / this.duration)); this.loadedDiv.width(this.bodyDiv.width() * this.buffered); }; // Stops tracking, sets the head location to the current position. AccessibleSlider.prototype.resetHeadLocation = function () { var ratio = this.position / this.duration; var center = this.bodyDiv.width() * ratio; this.seekHead.css('left', center - (this.seekHead.width() / 2)); if (this.tracking) { this.stopTracking(this.position); } }; AccessibleSlider.prototype.setPosition = function (position, updateLive) { this.position = position; this.resetHeadLocation(); this.refreshTooltip(); this.resizeDivs(); this.updateAriaValues(position, updateLive); } // TODO: Native HTML5 can have several buffered segments, and this actually happens quite often. Change this to display them all. AccessibleSlider.prototype.setBuffered = function (ratio) { this.buffered = ratio; this.redrawDivs; } AccessibleSlider.prototype.startTracking = function (device, position) { if (!this.tracking) { this.trackDevice = device; this.tracking = true; this.bodyDiv.trigger('startTracking', [position]); } }; AccessibleSlider.prototype.stopTracking = function (position) { this.trackDevice = null; this.tracking = false; this.bodyDiv.trigger('stopTracking', [position]); this.setPosition(position, true); }; AccessibleSlider.prototype.trackHeadAtPageX = function (pageX) { var position = this.pageXToPosition(pageX); var newLeft = pageX - this.bodyDiv.offset().left - (this.seekHead.width() / 2); newLeft = Math.max(0, Math.min(newLeft, this.bodyDiv.width() - this.seekHead.width())); this.lastTrackPosition = position; this.seekHead.css('left', newLeft); this.reportTrackAtPosition(position); }; AccessibleSlider.prototype.trackHeadAtPosition = function (position) { var ratio = position / this.duration; var center = this.bodyDiv.width() * ratio; this.lastTrackPosition = position; this.seekHead.css('left', center - (this.seekHead.width() / 2)); this.reportTrackAtPosition(position); }; AccessibleSlider.prototype.reportTrackAtPosition = function (position) { this.bodyDiv.trigger('tracking', [position]); this.updateAriaValues(position, true); }; AccessibleSlider.prototype.updateAriaValues = function (position, updateLive) { // TODO: Localize, move to another function. var pHours = Math.floor(position / 3600); var pMinutes = Math.floor((position % 3600) / 60); var pSeconds = Math.floor(position % 60); var pHourWord = pHours === 1 ? 'hour' : 'hours'; var pMinuteWord = pMinutes === 1 ? 'minute' : 'minutes'; var pSecondWord = pSeconds === 1 ? 'second' : 'seconds'; var descriptionText; if (pHours > 0) { descriptionText = pHours + ' ' + pHourWord + ', ' + pMinutes + ' ' + pMinuteWord + ', ' + pSeconds + ' ' + pSecondWord; } else if (pMinutes > 0) { descriptionText = pMinutes + ' ' + pMinuteWord + ', ' + pSeconds + ' ' + pSecondWord; } else { descriptionText = pSeconds + ' ' + pSecondWord; } /* Comment to stop live region from generating or being used. */ if (!this.liveAriaRegion) { this.liveAriaRegion = $('', { 'class': 'able-offscreen', 'aria-live': 'polite' }); this.wrapperDiv.append(this.liveAriaRegion); } if (updateLive && (this.liveAriaRegion.text() !== descriptionText)) { this.liveAriaRegion.text(descriptionText); } // Uncomment the following lines to use aria values instead of separate live region. this.seekHead.attr('aria-valuetext', descriptionText); this.seekHead.attr('aria-valuenow', Math.floor(position).toString()); }; AccessibleSlider.prototype.trackImmediatelyTo = function (position) { this.startTracking('keyboard', position); this.trackHeadAtPosition(position); this.keyTrackPosition = position; }; AccessibleSlider.prototype.refreshTooltip = function () { if (this.overHead) { this.timeTooltip.show(); if (this.tracking) { this.timeTooltip.text(this.positionToStr(this.lastTrackPosition)); } else { this.timeTooltip.text(this.positionToStr(this.position)); } this.setTooltipPosition(this.seekHead.position().left + (this.seekHead.width() / 2)); } else if (this.overBody && this.overBodyMousePos) { this.timeTooltip.show(); this.timeTooltip.text(this.positionToStr(this.pageXToPosition(this.overBodyMousePos.x))); this.setTooltipPosition(this.overBodyMousePos.x - this.bodyDiv.offset().left); } else { this.timeTooltip.hide(); } }; AccessibleSlider.prototype.setTooltipPosition = function (x) { this.timeTooltip.css({ left: x - (this.timeTooltip.width() / 2) - 10, bottom: this.seekHead.height() + 10 }); }; AccessibleSlider.prototype.positionToStr = function (seconds) { // same logic as misc.js > formatSecondsAsColonTime() var dHours = Math.floor(seconds / 3600); var dMinutes = Math.floor(seconds / 60) % 60; var dSeconds = Math.floor(seconds % 60); if (dSeconds < 10) { dSeconds = '0' + dSeconds; } if (dHours > 0) { if (dMinutes < 10) { dMinutes = '0' + dMinutes; } return dHours + ':' + dMinutes + ':' + dSeconds; } else { return dMinutes + ':' + dSeconds; } }; })(jQuery);