From 169eb721d27fc34bedfced939ed4797b5a2be4c9 Mon Sep 17 00:00:00 2001 From: Allan Jardine Date: Wed, 14 Dec 2011 10:38:20 +0000 Subject: [PATCH] New: oLanguage.oAira.sSortAscending and oLanguage.oAria.sSortDescending language strings which are attached to the label for a column header when it can be sorted. Basically the same as before, but now customisable. New: iTabIndex initialisaiton option - allow the developer to decide what tab index could be given to the table. Reverted: Two button navigation using A tags again - important for if there is no CSS New: ARIA supoprt for the full numebrs pagination style Changed: Tidy up the way events are added to the pagination and headers to reduce code. Changes: Complete ARIA support for column headers so we now consider bSortable --- media/css/demo_table.css | 12 +- media/js/jquery.dataTables.js | 282 ++++++++++++++++++----------- media/src/api/api.internal.js | 4 +- media/src/core/core.constructor.js | 1 + media/src/core/core.draw.js | 16 +- media/src/core/core.sort.js | 40 ++-- media/src/core/core.support.js | 27 +++ media/src/ext/ext.paging.js | 114 +++++------- media/src/model/model.init.js | 73 +++++++- media/src/model/model.settings.js | 8 +- 10 files changed, 359 insertions(+), 218 deletions(-) diff --git a/media/css/demo_table.css b/media/css/demo_table.css index db8ebfa5..eefb5597 100644 --- a/media/css/demo_table.css +++ b/media/css/demo_table.css @@ -82,6 +82,11 @@ float: left; cursor: pointer; *cursor: hand; + color: #111 !important; +} +.paginate_disabled_previous:hover, .paginate_enabled_previous:hover, +.paginate_disabled_next:hover, .paginate_enabled_next:hover { + text-decoration: none !important; } .paginate_disabled_previous:active, .paginate_enabled_previous:active, .paginate_disabled_next:active, .paginate_enabled_next:active { @@ -90,7 +95,7 @@ .paginate_disabled_previous, .paginate_disabled_next { - color: #666; + color: #666 !important; } .paginate_disabled_previous, .paginate_enabled_previous { padding-left: 23px; @@ -197,7 +202,10 @@ table.display td.center { .sorting_desc_disabled { background: url('../images/sort_desc_disabled.png') no-repeat center right; } - + +th:active { + outline: none; +} diff --git a/media/js/jquery.dataTables.js b/media/js/jquery.dataTables.js index a968b214..e70b4f5a 100644 --- a/media/js/jquery.dataTables.js +++ b/media/js/jquery.dataTables.js @@ -1068,9 +1068,12 @@ for ( i=0, iLen=oSettings.aoColumns.length ; i 0 && aaSort[0][0] == i ) + if ( aoColumns[i].bSortable ) { - nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" ); - - var nextSort = (typeof aoColumns[i].asSorting[ aaSort[0][2]+1 ] !== 'undefined') ? - aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0]; - nTh.setAttribute('aria-label', aoColumns[i].sTitle+': activate to sort column '+ - (nextSort=="asc" ? "ascending" : "descending") ); + if ( aaSort.length > 0 && aaSort[0][0] == i ) + { + nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" ); + + var nextSort = (typeof aoColumns[i].asSorting[ aaSort[0][2]+1 ] !== 'undefined') ? + aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0]; + nTh.setAttribute('aria-label', aoColumns[i].sTitle+ + (nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); + } + else + { + nTh.setAttribute('aria-label', aoColumns[i].sTitle+ + (aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); + } } else { - nTh.setAttribute('aria-label', aoColumns[i].sTitle+': activate to sort column '+ - (aoColumns[i].asSorting[0]=="asc" ? "ascending" : "descending") ); + nTh.setAttribute('aria-label', aoColumns[i].sTitle); } } @@ -3963,7 +3965,7 @@ */ function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback ) { - var sortingFn = function (e) { + _fnBindAction( nNode, {}, function (e) { /* If the column is not sortable - don't to anything */ if ( oSettings.aoColumns[iDataIndex].bSortable === false ) { @@ -4068,15 +4070,7 @@ { fnCallback( oSettings ); } - }; - - $(nNode) - .bind( 'click.DT', sortingFn ) - .bind( 'keypress.DT', function (e) { - if ( e.which === 13 ) { - sortingFn(e); - } - } ); + } ); } @@ -4682,6 +4676,33 @@ return oOut; } + + /** + * Bind an event handers to allow a click or return key to activate the callback. + * This is good for accessability since a return on the keyboard will have the + * same effect as a click, if the element has focus. + * @param {element} n Element to bind the action to + * @param {object} oData Data object to pass to the triggered function + * @param {function) fn Callback function for when the event is triggered + * @private + */ + function _fnBindAction( n, oData, fn ) + { + $(n) + .bind( 'click.DT', oData, function (e) { + fn(e); + n.blur(); // Remove focus outline for mouse users + } ) + .bind( 'keypress.DT', oData, function (e){ + if ( e.which === 13 ) { + fn(e); + } } ) + .bind( 'selectstart.DT', function () { + /* Take the brutal approach to cancelling text selection */ + return false; + } ); + } + @@ -5922,7 +5943,9 @@ "_fnSetCellData": _fnSetCellData, "_fnGetObjectDataFn": _fnGetObjectDataFn, "_fnSetObjectDataFn": _fnSetObjectDataFn, - "_fnApplyColumnDefs": _fnApplyColumnDefs + "_fnApplyColumnDefs": _fnApplyColumnDefs, + "_fnBindAction": _fnBindAction, + "_fnExtend": _fnExtend }; $.extend( DataTable.ext.oApi, this.oApi ); @@ -6064,6 +6087,7 @@ _fnMap( oSettings, oInit, "sCookiePrefix" ); _fnMap( oSettings, oInit, "sDom" ); _fnMap( oSettings, oInit, "bSortCellsTop" ); + _fnMap( oSettings, oInit, "iTabIndex" ); _fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" ); _fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" ); _fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" ); @@ -8387,6 +8411,25 @@ "iScrollLoadGap": 100, + /** + * By default DataTables allows keyboard navigation of the table (sorting, paging, + * and filtering) by adding a tabindex attribute to the required elements. This + * allows you to tab through the controls and press the enter key to activate them. + * The tabindex is default 0, meaning that the tab follows the flow of the document. + * You can overrule this using this parameter if you wish. + * @type int + * @default 0 + * + * @example + * $(document).ready(function() { + * $('#example').dataTable( { + * "iTabIndex": 1 + * } ); + * } ); + */ + "iTabIndex": 0, + + /** * All strings that DataTables uses in the user interface that it creates * are defined in this object, allowing you to modified them individually or @@ -8395,15 +8438,62 @@ */ "oLanguage": { /** - * Pnagation string used by DataTables for the two built-in pagination + * Strings that are used for WAI-ARIA labels and controls only (these are not + * actually visible on the page, but will be read by screenreaders, and thus + * must be internationalised as well). + * @namespace + */ + "oAria": { + /** + * ARIA label that is added to the table headers when the column may be + * sorted ascending by activing the column (click or return when focused). + * Note that the column header is prefixed to this string. + * @type string + * @default : activate to sort column ascending + * + * @example + * $(document).ready(function() { + * $('#example').dataTable( { + * "oLanguage": { + * "oAria": { + * "sSortAscending": " - click/return to sort ascending" + * } + * } + * } ); + * } ); + */ + "sSortAscending": ": activate to sort column ascending", + + /** + * ARIA label that is added to the table headers when the column may be + * sorted descending by activing the column (click or return when focused). + * Note that the column header is prefixed to this string. + * @type string + * @default : activate to sort column ascending + * + * @example + * $(document).ready(function() { + * $('#example').dataTable( { + * "oLanguage": { + * "oAria": { + * "sSortDescending": " - click/return to sort descending" + * } + * } + * } ); + * } ); + */ + "sSortDescending": ": activate to sort column descending" + }, + + /** + * Pagination string used by DataTables for the two built-in pagination * control types ("two_button" and "full_numbers") * @namespace */ "oPaginate": { /** * Text to use when using the 'full_numbers' type of pagination for the - * button - * to take the user to the first page. + * button to take the user to the first page. * @type string * @default First * @@ -9759,7 +9849,13 @@ * @type string * @default null */ - "sInstance": null + "sInstance": null, + + /** + * tabindex attribute value that is added to DataTables control elements, allowing + * keyboard navigation of the table and its controls. + */ + "iTabIndex": 0 }; /** @@ -9909,31 +10005,19 @@ }; var sAppend = (!oSettings.bJUI) ? - '
'+oLang.sPrevious+'
'+ - '
'+oLang.sNext+'
' + ''+oLang.sPrevious+''+ + ''+oLang.sNext+'' : - '
'+ - '
'; + ''+ + ''; $(nPaging).append( sAppend ); - var els = $('div', nPaging); + var els = $('a', nPaging); var nPrevious = els[0], nNext = els[1]; - $(nPrevious) - .bind( 'click.DT', { action: "previous" }, fnClickHandler ) - .bind( 'keypress.DT', { action: "previous" }, function (e){ - if ( e.which === 13 ) { - fnClickHandler(e); - } } ) - .bind( 'selectstart.DT', function () { return false; } ); /* Take the brutal approach to cancelling text selection */ - $(nNext) - .bind( 'click.DT', { action: "next" }, fnClickHandler ) - .bind( 'keypress.DT', { action: "next" }, function (e){ - if ( e.which === 13 ) { - fnClickHandler(e); - } } ) - .bind( 'selectstart.DT', function () { return false; } ); + oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nNext, {action: "next"}, fnClickHandler ); /* ID the first elements only */ if ( typeof oSettings.aanFeatures.p == "undefined" ) @@ -10013,11 +10097,11 @@ }; $(nPaging).append( - ''+oLang.sFirst+''+ - ''+oLang.sPrevious+''+ + ''+oLang.sFirst+''+ + ''+oLang.sPrevious+''+ ''+ - ''+oLang.sNext+''+ - ''+oLang.sLast+'' + ''+oLang.sNext+''+ + ''+oLang.sLast+'' ); var els = $('a', nPaging); var nFirst = els[0], @@ -10025,15 +10109,10 @@ nNext = els[2], nLast = els[3]; - $(nFirst).bind( 'click.DT', { action: "first" }, fnClickHandler ); - $(nPrev).bind( 'click.DT', { action: "previous" }, fnClickHandler ); - $(nNext).bind( 'click.DT', { action: "next" }, fnClickHandler ); - $(nLast).bind( 'click.DT', { action: "last" }, fnClickHandler ); - - /* Take the brutal approach to cancelling text selection */ - $('span', nPaging) - .bind( 'mousedown.DT', function () { return false; } ) - .bind( 'selectstart.DT', function () { return false; } ); + oSettings.oApi._fnBindAction( nFirst, {action: "first"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nPrev, {action: "previous"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nNext, {action: "next"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nLast, {action: "last"}, fnClickHandler ); /* ID the first elements only */ if ( typeof oSettings.aanFeatures.p == "undefined" ) @@ -10067,6 +10146,14 @@ var sList = ""; var iStartButton, iEndButton, i, iLen; var oClasses = oSettings.oClasses; + var anButtons, anStatic, nPaginateList; + var an = oSettings.aanFeatures.p; + var fnClick = function(e) { + /* Use the information in the element to jump to the required page */ + oSettings.oApi._fnPageChange( oSettings, e.data.page ); + fnCallbackDraw( oSettings ); + e.preventDefault(); + }; /* Pages calculation */ if (iPages < iPageCount) @@ -10094,21 +10181,11 @@ for ( i=iStartButton ; i<=iEndButton ; i++ ) { sList += (iCurrentPage !== i) ? - ''+oSettings.fnFormatNumber(i)+'' : - ''+oSettings.fnFormatNumber(i)+''; + ''+oSettings.fnFormatNumber(i)+'' : + ''+oSettings.fnFormatNumber(i)+''; } /* Loop over each instance of the pager */ - var an = oSettings.aanFeatures.p; - var anButtons, anStatic, nPaginateList; - var fnClick = function(e) { - /* Use the information in the element to jump to the required page */ - oSettings.oApi._fnPageChange( oSettings, parseInt(this.innerHTML,10) - 1 ); - fnCallbackDraw( oSettings ); - e.preventDefault(); - }; - var fnFalse = function () { return false; }; - for ( i=0, iLen=an.length ; i 0 && aaSort[0][0] == i ) + if ( aoColumns[i].bSortable ) { - nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" ); - - var nextSort = (typeof aoColumns[i].asSorting[ aaSort[0][2]+1 ] !== 'undefined') ? - aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0]; - nTh.setAttribute('aria-label', aoColumns[i].sTitle+': activate to sort column '+ - (nextSort=="asc" ? "ascending" : "descending") ); + if ( aaSort.length > 0 && aaSort[0][0] == i ) + { + nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" ); + + var nextSort = (typeof aoColumns[i].asSorting[ aaSort[0][2]+1 ] !== 'undefined') ? + aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0]; + nTh.setAttribute('aria-label', aoColumns[i].sTitle+ + (nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); + } + else + { + nTh.setAttribute('aria-label', aoColumns[i].sTitle+ + (aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); + } } else { - nTh.setAttribute('aria-label', aoColumns[i].sTitle+': activate to sort column '+ - (aoColumns[i].asSorting[0]=="asc" ? "ascending" : "descending") ); + nTh.setAttribute('aria-label', aoColumns[i].sTitle); } } @@ -182,7 +190,7 @@ function _fnSort ( oSettings, bApplyClasses ) */ function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback ) { - var sortingFn = function (e) { + _fnBindAction( nNode, {}, function (e) { /* If the column is not sortable - don't to anything */ if ( oSettings.aoColumns[iDataIndex].bSortable === false ) { @@ -287,15 +295,7 @@ function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback ) { fnCallback( oSettings ); } - }; - - $(nNode) - .bind( 'click.DT', sortingFn ) - .bind( 'keypress.DT', function (e) { - if ( e.which === 13 ) { - sortingFn(e); - } - } ); + } ); } diff --git a/media/src/core/core.support.js b/media/src/core/core.support.js index c7813ee3..df984ccb 100644 --- a/media/src/core/core.support.js +++ b/media/src/core/core.support.js @@ -182,3 +182,30 @@ function _fnExtend( oOut, oExtender ) return oOut; } + +/** + * Bind an event handers to allow a click or return key to activate the callback. + * This is good for accessability since a return on the keyboard will have the + * same effect as a click, if the element has focus. + * @param {element} n Element to bind the action to + * @param {object} oData Data object to pass to the triggered function + * @param {function) fn Callback function for when the event is triggered + * @private + */ +function _fnBindAction( n, oData, fn ) +{ + $(n) + .bind( 'click.DT', oData, function (e) { + fn(e); + n.blur(); // Remove focus outline for mouse users + } ) + .bind( 'keypress.DT', oData, function (e){ + if ( e.which === 13 ) { + fn(e); + } } ) + .bind( 'selectstart.DT', function () { + /* Take the brutal approach to cancelling text selection */ + return false; + } ); +} + diff --git a/media/src/ext/ext.paging.js b/media/src/ext/ext.paging.js index b1c594a0..4fcb6742 100644 --- a/media/src/ext/ext.paging.js +++ b/media/src/ext/ext.paging.js @@ -31,31 +31,19 @@ $.extend( DataTable.ext.oPagination, { }; var sAppend = (!oSettings.bJUI) ? - '
'+oLang.sPrevious+'
'+ - '
'+oLang.sNext+'
' + ''+oLang.sPrevious+''+ + ''+oLang.sNext+'' : - '
'+ - '
'; + ''+ + ''; $(nPaging).append( sAppend ); - var els = $('div', nPaging); + var els = $('a', nPaging); var nPrevious = els[0], nNext = els[1]; - $(nPrevious) - .bind( 'click.DT', { action: "previous" }, fnClickHandler ) - .bind( 'keypress.DT', { action: "previous" }, function (e){ - if ( e.which === 13 ) { - fnClickHandler(e); - } } ) - .bind( 'selectstart.DT', function () { return false; } ); /* Take the brutal approach to cancelling text selection */ - $(nNext) - .bind( 'click.DT', { action: "next" }, fnClickHandler ) - .bind( 'keypress.DT', { action: "next" }, function (e){ - if ( e.which === 13 ) { - fnClickHandler(e); - } } ) - .bind( 'selectstart.DT', function () { return false; } ); + oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nNext, {action: "next"}, fnClickHandler ); /* ID the first elements only */ if ( typeof oSettings.aanFeatures.p == "undefined" ) @@ -135,11 +123,11 @@ $.extend( DataTable.ext.oPagination, { }; $(nPaging).append( - ''+oLang.sFirst+''+ - ''+oLang.sPrevious+''+ + ''+oLang.sFirst+''+ + ''+oLang.sPrevious+''+ ''+ - ''+oLang.sNext+''+ - ''+oLang.sLast+'' + ''+oLang.sNext+''+ + ''+oLang.sLast+'' ); var els = $('a', nPaging); var nFirst = els[0], @@ -147,15 +135,10 @@ $.extend( DataTable.ext.oPagination, { nNext = els[2], nLast = els[3]; - $(nFirst).bind( 'click.DT', { action: "first" }, fnClickHandler ); - $(nPrev).bind( 'click.DT', { action: "previous" }, fnClickHandler ); - $(nNext).bind( 'click.DT', { action: "next" }, fnClickHandler ); - $(nLast).bind( 'click.DT', { action: "last" }, fnClickHandler ); - - /* Take the brutal approach to cancelling text selection */ - $('span', nPaging) - .bind( 'mousedown.DT', function () { return false; } ) - .bind( 'selectstart.DT', function () { return false; } ); + oSettings.oApi._fnBindAction( nFirst, {action: "first"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nPrev, {action: "previous"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nNext, {action: "next"}, fnClickHandler ); + oSettings.oApi._fnBindAction( nLast, {action: "last"}, fnClickHandler ); /* ID the first elements only */ if ( typeof oSettings.aanFeatures.p == "undefined" ) @@ -189,6 +172,14 @@ $.extend( DataTable.ext.oPagination, { var sList = ""; var iStartButton, iEndButton, i, iLen; var oClasses = oSettings.oClasses; + var anButtons, anStatic, nPaginateList; + var an = oSettings.aanFeatures.p; + var fnClick = function(e) { + /* Use the information in the element to jump to the required page */ + oSettings.oApi._fnPageChange( oSettings, e.data.page ); + fnCallbackDraw( oSettings ); + e.preventDefault(); + }; /* Pages calculation */ if (iPages < iPageCount) @@ -216,21 +207,11 @@ $.extend( DataTable.ext.oPagination, { for ( i=iStartButton ; i<=iEndButton ; i++ ) { sList += (iCurrentPage !== i) ? - ''+oSettings.fnFormatNumber(i)+'' : - ''+oSettings.fnFormatNumber(i)+''; + ''+oSettings.fnFormatNumber(i)+'' : + ''+oSettings.fnFormatNumber(i)+''; } /* Loop over each instance of the pager */ - var an = oSettings.aanFeatures.p; - var anButtons, anStatic, nPaginateList; - var fnClick = function(e) { - /* Use the information in the element to jump to the required page */ - oSettings.oApi._fnPageChange( oSettings, parseInt(this.innerHTML,10) - 1 ); - fnCallbackDraw( oSettings ); - e.preventDefault(); - }; - var fnFalse = function () { return false; }; - for ( i=0, iLen=an.length ; i