Skip to content

Commit f308514

Browse files
committed
Add support for chapters in an external container
1 parent aa9f8fc commit f308514

17 files changed

Lines changed: 677 additions & 142 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ The following attributes are supported on both the \<audio\> and \<video\> eleme
191191
adjacent to the player.
192192
- **data-use-transcript-button** - optional; set to "false" to exclude transcript button from controller. If using the data-transcript-div attribute to write the transcript to an external container (e.g., on a dedicated transcript page), you might not want users to be able to toggle the transcript off.
193193
- **data-transcript-title** - optional; override default transcript title (default is "Transcript", or "Lyrics" if the data-lyrics-mode attribute is present)
194+
- **data-chapters-div** - optional; id of an external div in which to display a list of chapters.
195+
The list of chapters is generated automatically if a chapters track is available in a WebVTT file.
196+
If this attribute is not provided and chapter are available, chapters will be displayed in a popup menu triggered by the Chapters button.
197+
- **data-use-chapters-button** - optional; set to "false" to exclude chapters button from controller. If using the data-chapters-div attribute to write the chapters to an external container, you might not want users to be able to toggle the chapters off.
198+
- **data-chapters-title** - optional; override default chapters title (default is "Chapters"). A null value (data-chapters-title="") eliminates the title altogether.
199+
- **data-chapters-default** - optional; identify ID of default chapter (must correspond with the text or value immediately above the timestamp in your chapter's WebVTT file. If this attribute is present, the media will be advanced to this start time. Otherwise it will start at the beginning. (See also **data-start-time**).
194200
- **data-lyrics-mode** - optional; forces a line break between and within captions in the transcript
195201
- **data-debug** - optional; if present will write messages to the developer console
196202
- **data-volume** - optional; set the default volume (0 to 10; default is 7 to avoid overpowering screen reader audio)

build/ableplayer.dist.js

Lines changed: 179 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,22 @@
115115
this.lyricsMode = true;
116116
}
117117

118-
if ($(media).data('transcript-title') !== undefined) {
118+
if ($(media).data('chapters-div') !== undefined && $(media).data('chapters-div') !== "") {
119+
this.chaptersDivLocation = $(media).data('chapters-div');
120+
}
121+
122+
if ($(media).data('chapters-title') !== undefined) {
119123
// NOTE: empty string is valid; results in no title being displayed
120-
this.transcriptTitle = $(media).data('transcript-title');
124+
this.chaptersTitle = $(media).data('chapters-title');
125+
}
126+
127+
if ($(media).data('chapters-default') !== undefined && $(media).data('chapters-default') !== "") {
128+
this.defaultChapter = $(media).data('chapters-default');
129+
this.chapter = this.defaultChapter; // this.chapter is the id of the default chapter (as defined within WebVTT file)
130+
}
131+
132+
if ($(media).data('use-chapters-button') !== undefined && $(media).data('use-chapters-button') === false) {
133+
this.useChaptersButton = false;
121134
}
122135

123136
if ($(media).data('youtube-id') !== undefined && $(media).data('youtube-id') !== "") {
@@ -348,6 +361,12 @@
348361
// it might be desirable for the transcript to always be ON, with no toggle
349362
// This can be overridden with data-transcript-button="false"
350363
this.useTranscriptButton = true;
364+
365+
// useChaptersButton - on by default if there's a track with kind="chapters"
366+
// However, if chapters is written to an external div via data-chapters-div
367+
// it might be desirable for the chapters to always be ON, with no toggle
368+
// This can be overridden with data-chapters-button="false"
369+
this.useChaptersButton = true;
351370

352371
this.playing = false; // will change to true after 'playing' event is triggered
353372

@@ -596,6 +615,9 @@
596615
thisObj.updateCaption();
597616
thisObj.updateTranscript();
598617
thisObj.showSearchResults();
618+
if (thisObj.defaultChapter) {
619+
thisObj.seekToDefaultChapter();
620+
}
599621
});
600622
});
601623
};
@@ -1736,6 +1758,7 @@
17361758
(function ($) {
17371759
// See section 4.1 of dev.w3.org/html5/webvtt for format details.
17381760
AblePlayer.prototype.parseWebVTT = function(srcFile,text) {
1761+
17391762
// Normalize line ends to \n.
17401763
text = text.replace(/(\r\n|\n|\r)/g,'\n');
17411764

@@ -1933,6 +1956,7 @@
19331956
}
19341957

19351958
function parseCue(state) {
1959+
19361960
var nextLine = peekLine(state);
19371961
var cueId;
19381962
var errString;
@@ -1981,8 +2005,8 @@
19812005
settings: cueSettings,
19822006
components: components
19832007
});
1984-
}
1985-
2008+
}
2009+
19862010
function getCueSettings(state) {
19872011
var cueSettings = {};
19882012
while (state.text.length > 0 && state.text[0] !== '\n') {
@@ -1993,7 +2017,6 @@
19932017
return cueSettings;
19942018
}
19952019

1996-
19972020
function getCuePayload(state) {
19982021
// Parser based on instructions in draft.
19992022
var result = {type: 'internal', tagName: '', value: '', classes: [], annotation: '', parent: null, children: [], language: ''};
@@ -2544,48 +2567,22 @@
25442567
if (this.includeTranscript) {
25452568
this.injectTranscriptArea();
25462569
this.addTranscriptAreaEvents();
2547-
}
2548-
2570+
}
25492571
this.injectAlert();
25502572
this.injectPlaylist();
25512573
};
25522574

25532575
AblePlayer.prototype.injectOffscreenHeading = function () {
25542576
// Add offscreen heading to the media container.
2555-
// To fine the nearest heading in the ancestor tree,
2556-
// loop over each parent of $ableDiv until a heading is found
2557-
// If multiple headings are found beneath a given parent, get the closest
2558-
// The heading injected in $ableDiv is one level deeper than the closest heading
2559-
var headingType;
2560-
2561-
var $parents = this.$ableDiv.parents();
2562-
$parents.each(function(){
2563-
var $this = $(this);
2564-
var $thisHeadings = $this.find('h1, h2, h3, h4, h5, h6');
2565-
var numHeadings = $thisHeadings.length;
2566-
if(numHeadings){
2567-
headingType = $thisHeadings.eq(numHeadings-1).prop('tagName');
2568-
return false;
2569-
}
2570-
});
2571-
if (typeof headingType === 'undefined') {
2572-
var headingType = 'h1';
2573-
}
2574-
else {
2575-
// Increment closest heading by one if less than 6.
2576-
var headingNumber = parseInt(headingType[1]);
2577-
headingNumber += 1;
2578-
if (headingNumber > 6) {
2579-
headingNumber = 6;
2580-
}
2581-
headingType = 'h' + headingNumber.toString();
2582-
}
2583-
this.playerHeadingLevel = headingNumber;
2577+
// The heading injected in $ableDiv is one level deeper than the closest parent heading
2578+
// as determined by getNextHeadingLevel()
2579+
var headingType;
2580+
this.playerHeadingLevel = this.getNextHeadingLevel(this.$ableDiv); // returns in integer 1-6
2581+
headingType = 'h' + this.playerHeadingLevel.toString();
25842582
this.$headingDiv = $('<' + headingType + '>');
25852583
this.$ableDiv.prepend(this.$headingDiv);
25862584
this.$headingDiv.addClass('able-offscreen');
2587-
this.$headingDiv.text(this.tt.playerHeading);
2588-
2585+
this.$headingDiv.text(this.tt.playerHeading);
25892586
};
25902587

25912588
AblePlayer.prototype.injectBigPlayButton = function () {
@@ -2717,6 +2714,96 @@
27172714
}
27182715
};
27192716

2717+
AblePlayer.prototype.populateChaptersDiv = function() {
2718+
2719+
var thisObj, headingLevel, headingType, headingId, $chaptersHeading,
2720+
$chaptersNav, $chaptersList, $chapterItem, $chapterButton,
2721+
i, itemId, chapter, buttonId, hasDefault,
2722+
getFocusFunction, getHoverFunction, getBlurFunction, getClickFunction,
2723+
$thisButton, $thisListItem, $prevButton, $nextButton, blurListener;
2724+
2725+
thisObj = this;
2726+
2727+
if ($('#' + this.chaptersDivLocation)) {
2728+
this.$chaptersDiv = $('#' + this.chaptersDivLocation);
2729+
this.$chaptersDiv.addClass('able-chapters-div');
2730+
2731+
// add optional header
2732+
if (this.chaptersTitle) {
2733+
headingLevel = this.getNextHeadingLevel(this.$chaptersDiv);
2734+
headingType = 'h' + headingLevel.toString();
2735+
headingId = this.mediaId + '-chapters-heading';
2736+
$chaptersHeading = $('<' + headingType + '>', {
2737+
'class': 'able-chapters-heading',
2738+
'id': headingId
2739+
}).text(this.chaptersTitle);
2740+
this.$chaptersDiv.append($chaptersHeading);
2741+
}
2742+
2743+
$chaptersNav = $('<nav>');
2744+
if (this.chaptersTitle) {
2745+
$chaptersNav.attr('aria-labeledby',headingId);
2746+
}
2747+
else {
2748+
$chaptersNav.attr('aria-label',this.tt.chapters);
2749+
}
2750+
2751+
$chaptersList = $('<ul>');
2752+
for (i in this.chapters) {
2753+
chapter = this.chapters[i];
2754+
itemId = this.mediaId + '-chapters-' + i; // TODO: Maybe not needed???
2755+
$chapterItem = $('<li></li>');
2756+
$chapterButton = $('<button>',{
2757+
'type': 'button',
2758+
'val': i
2759+
}).text(this.flattenCueForCaption(chapter));
2760+
2761+
// add event listeners
2762+
getClickFunction = function (time) {
2763+
return function () {
2764+
$(this).closest('ul').find('li')
2765+
.removeClass('able-current-chapter')
2766+
.attr('aria-selected','');
2767+
$(this).closest('li')
2768+
.addClass('able-current-chapter')
2769+
.attr('aria-selected','true');
2770+
thisObj.seekTo(time);
2771+
}
2772+
};
2773+
$chapterButton.on('click',getClickFunction(chapter.start)); // works with Enter too
2774+
$chapterButton.on('focus',function() {
2775+
$(this).closest('ul').find('li').removeClass('able-focus');
2776+
$(this).closest('li').addClass('able-focus');
2777+
});
2778+
$chapterItem.on('hover',function() {
2779+
$(this).closest('ul').find('li').removeClass('able-focus');
2780+
$(this).addClass('able-focus');
2781+
});
2782+
$chapterItem.on('mouseleave',function() {
2783+
$(this).removeClass('able-focus');
2784+
});
2785+
$chapterButton.on('blur',function() {
2786+
$(this).closest('li').removeClass('able-focus');
2787+
});
2788+
2789+
// put it all together
2790+
$chapterItem.append($chapterButton);
2791+
$chaptersList.append($chapterItem);
2792+
if (this.defaultChapter == chapter.id) {
2793+
$chapterButton.attr('aria-selected','true').parent('li').addClass('able-current-chapter');
2794+
hasDefault = true;
2795+
}
2796+
}
2797+
}
2798+
if (!hasDefault) {
2799+
// select the first button
2800+
$chaptersList.find('button').first().attr('aria-selected','true')
2801+
.parent('li').addClass('able-current-chapter');
2802+
}
2803+
$chaptersNav.append($chaptersList);
2804+
this.$chaptersDiv.append($chaptersNav);
2805+
};
2806+
27202807
AblePlayer.prototype.splitPlayerIntoColumns = function (feature) {
27212808
// feature is either 'transcript' or 'sign'
27222809
// if present, player is split into two column, with this feature in the right column
@@ -2918,7 +3005,6 @@
29183005

29193006
// Create and fill in the popup menu forms for various controls.
29203007
AblePlayer.prototype.setupPopups = function () {
2921-
29223008
var popups, thisObj, hasDefault, i, j,
29233009
tracks, trackList, trackItem, track,
29243010
radioName, radioId, trackButton, trackLabel,
@@ -2940,7 +3026,7 @@
29403026
if (this.captions.length > 0) {
29413027
popups.push('captions');
29423028
}
2943-
if (this.chapters.length > 0) {
3029+
if (this.chapters.length > 0 && this.useChaptersButton) {
29443030
popups.push('chapters');
29453031
}
29463032
}
@@ -3239,7 +3325,7 @@
32393325
bll.push('transcript');
32403326
}
32413327

3242-
if (this.mediaType === 'video' && this.hasChapters) {
3328+
if (this.mediaType === 'video' && this.hasChapters && this.useChaptersButton) {
32433329
bll.push('chapters');
32443330
}
32453331

@@ -3966,10 +4052,27 @@
39664052
AblePlayer.prototype.setupChapters = function (track, cues) {
39674053
// NOTE: WebVTT supports nested timestamps (to form an outline)
39684054
// This is not currently supported.
4055+
var i=0;
39694056
this.hasChapters = true;
3970-
this.chapters = cues;
4057+
this.chapters = cues;
4058+
if (this.chaptersDivLocation) {
4059+
this.populateChaptersDiv();
4060+
}
39714061
};
39724062

4063+
AblePlayer.prototype.seekToDefaultChapter = function() {
4064+
// this function is only called if this.defaultChapter is not null
4065+
// step through chapters looking for default
4066+
var i=0;
4067+
while (i < this.chapters.length) {
4068+
if (this.chapters[i].id === this.defaultChapter) {
4069+
// found the default chapter! Seek to it
4070+
this.seekTo(this.chapters[i].start);
4071+
}
4072+
i++;
4073+
}
4074+
};
4075+
39734076
AblePlayer.prototype.setupMetadata = function(track, cues) {
39744077
// NOTE: Metadata is currently only supported if data-meta-div is provided
39754078
// The player does not display metadata internally
@@ -5277,6 +5380,40 @@
52775380
})(jQuery);
52785381

52795382
(function ($) {
5383+
5384+
AblePlayer.prototype.getNextHeadingLevel = function($element) {
5385+
5386+
// Finds the nearest heading in the ancestor tree
5387+
// Loops over each parent of the current element until a heading is found
5388+
// If multiple headings are found beneath a given parent, get the closest
5389+
// Returns an integer (1-6) representing the next available heading level
5390+
5391+
var $parents, $foundHeadings, numHeadings, headingType, headingNumber;
5392+
5393+
$parents = $element.parents();
5394+
$parents.each(function(){
5395+
$foundHeadings = $(this).find('h1, h2, h3, h4, h5, h6');
5396+
numHeadings = $foundHeadings.length;
5397+
if (numHeadings) {
5398+
headingType = $foundHeadings.eq(numHeadings-1).prop('tagName');
5399+
return false;
5400+
}
5401+
});
5402+
if (typeof headingType === 'undefined') {
5403+
// page has no headings
5404+
headingNumber = 1;
5405+
}
5406+
else {
5407+
// Increment closest heading by one if less than 6.
5408+
headingNumber = parseInt(headingType[1]);
5409+
headingNumber += 1;
5410+
if (headingNumber > 6) {
5411+
headingNumber = 6;
5412+
}
5413+
}
5414+
return headingNumber;
5415+
};
5416+
52805417
AblePlayer.prototype.countProperties = function(obj) {
52815418
// returns the number of properties in an object
52825419
var count, prop;

0 commit comments

Comments
 (0)