diff --git a/jquery.fixedheadertable.js b/jquery.fixedheadertable.js index aed7e15..61b3fb9 100644 --- a/jquery.fixedheadertable.js +++ b/jquery.fixedheadertable.js @@ -1,22 +1,22 @@ /*! -* jquery.fixedHeaderTable. The jQuery fixedHeaderTable plugin -* -* Copyright (c) 2011 Mark Malek -* http://fixedheadertable.com -* -* Licensed under MIT -* http://www.opensource.org/licenses/mit-license.php -* -* http://docs.jquery.com/Plugins/Authoring -* jQuery authoring guidelines -* -* Launch : October 2009 -* Version : 1.3 -* Released: May 9th, 2011 -* -* -* all CSS sizing (width,height) is done in pixels (px) -*/ + * jquery.fixedHeaderTable. The jQuery fixedHeaderTable plugin + * + * Copyright (c) 2011 Mark Malek + * http://fixedheadertable.com + * + * Licensed under MIT + * http://www.opensource.org/licenses/mit-license.php + * + * http://docs.jquery.com/Plugins/Authoring + * jQuery authoring guidelines + * + * Launch : October 2009 + * Version : 1.3 + * Released: May 9th, 2011 + * + * + * all CSS sizing (width,height) is done in pixels (px) + */ (function($) { @@ -30,11 +30,11 @@ themeClass: 'fht-default', borderCollapse: true, - fixedColumn: false, // fixed first column + fixedColumns: 0, // fixed first columns sortable: false, autoShow: true, // hide table after its created footer: false, // show footer - cloneHeadToFoot: false, // clone head and use as footer + cloneHeadToFoot: false, // clone head and use as footer autoResize: false, // resize table if its parent wrapper changes size create: null // callback after plugin completes @@ -54,7 +54,7 @@ return this.each(function() { var $self = $(this), // reference the jQuery version of the current DOM element - self = this; // reference to the actual DOM element + self = this; // reference to the actual DOM element if ( helpers._isTable($self) ) { methods.setup.apply(this, Array.prototype.slice.call(arguments, 1)); @@ -67,38 +67,38 @@ }); }, - - /* - * Setup table structure for fixed headers and optional footer - */ + + /* + * Setup table structure for fixed headers and optional footer + */ setup: function( options ) { var $self = $(this), - self = this, - $thead = $self.find('thead'), - $tfoot = $self.find('tfoot'), - $tbody = $self.find('tbody'), - $wrapper, - $divHead, - $divFoot, - $divBody, - $fixedHeadRow, - $temp, - tfootHeight = 0; + self = this, + $thead = $self.find('thead'), + $tfoot = $self.find('tfoot'), + $tbody = $self.find('tbody'), + $wrapper, + $divHead, + $divFoot, + $divBody, + $fixedHeadRow, + $temp, + tfootHeight = 0; settings.includePadding = helpers._isPaddingIncludedWithWidth(); settings.scrollbarOffset = helpers._getScrollbarWidth(); - settings.themeClassName = settings.themeClass; - - if ( settings.width.search('%') > -1 ) { - var widthMinusScrollbar = $self.parent().width() - settings.scrollbarOffset; - } else { - var widthMinusScrollbar = settings.width - settings.scrollbarOffset; - } - + settings.themeClassName = settings.themeClass; + + if ( settings.width.search('%') > -1 ) { + var widthMinusScrollbar = $self.parent().width() - settings.scrollbarOffset; + } else { + var widthMinusScrollbar = settings.width - settings.scrollbarOffset; + } + $self.css({ - width: widthMinusScrollbar - }); - + width: widthMinusScrollbar + }); + if ( !$self.closest('.fht-table-wrapper').length ) { $self.addClass('fht-table'); @@ -107,40 +107,40 @@ $wrapper = $self.closest('.fht-table-wrapper'); - if ( settings.fixedColumn == true && $wrapper.find('.fht-fixed-column').length == 0 ) { - $self.wrap('
'); - - var $fixedColumn = $('
').prependTo($wrapper), - $fixedBody = $wrapper.find('.fht-fixed-body'); + if ( settings.fixedColumns > 0 && $wrapper.find('.fht-fixed-column').length == 0 ) { + $self.wrap('
'); + + var $fixedColumns = $('
').prependTo($wrapper), + $fixedBody = $wrapper.find('.fht-fixed-body'); } $wrapper.css({ - width: settings.width, - height: settings.height - }) - .addClass(settings.themeClassName); + width: settings.width, + height: settings.height + }) + .addClass(settings.themeClassName); if ( !$self.hasClass('fht-table-init') ) { $self.wrap('
'); - + } - $divBody = $self.closest('.fht-tbody'); - + $divBody = $self.closest('.fht-tbody'); + var tableProps = helpers._getTableProps($self); helpers._setupClone( $divBody, tableProps.tbody ); if ( !$self.hasClass('fht-table-init') ) { - if ( settings.fixedColumn == true ) { - $divHead = $('
').prependTo($fixedBody); - } else { - $divHead = $('
').prependTo($wrapper); - } + if ( settings.fixedColumns > 0 ) { + $divHead = $('
').prependTo($fixedBody); + } else { + $divHead = $('
').prependTo($wrapper); + } $thead.clone().appendTo($divHead.find('table')); } else { - $divHead = $wrapper.find('div.fht-thead'); + $divHead = $wrapper.find('div.fht-thead'); } helpers._setupClone( $divHead, tableProps.thead ); @@ -155,29 +155,29 @@ */ if ( settings.footer == true ) { - helpers._setupTableFooter( $self, self, tableProps ); - - if ( !$tfoot.length ) { - $tfoot = $wrapper.find('div.fht-tfoot table'); - } - - tfootHeight = $tfoot.outerHeight(true); + helpers._setupTableFooter( $self, self, tableProps ); + + if ( !$tfoot.length ) { + $tfoot = $wrapper.find('div.fht-tfoot table'); + } + + tfootHeight = $tfoot.outerHeight(true); } var tbodyHeight = $wrapper.height() - $thead.outerHeight(true) - tfootHeight - tableProps.border; $divBody.css({ - 'height': tbodyHeight - }); + 'height': tbodyHeight + }); $self.addClass('fht-table-init'); if ( typeof(settings.altClass) !== 'undefined' ) { - methods.altRows.apply( self ); + methods.altRows.apply( self ); } - if ( settings.fixedColumn == true ) { - helpers._setupFixedColumn( $self, self, tableProps ); + if ( settings.fixedColumns > 0 ) { + helpers._setupFixedColumn( $self, self, tableProps ); } if ( !settings.autoShow ) { @@ -195,7 +195,7 @@ */ resize: function( options ) { var $self = $(this), - self = this; + self = this; return self; }, @@ -205,12 +205,12 @@ */ altRows: function( arg1 ) { var $self = $(this), - self = this, - altClass = ( typeof(arg1) !== 'undefined' ) ? arg1 : settings.altClass; + self = this, + altClass = ( typeof(arg1) !== 'undefined' ) ? arg1 : settings.altClass; $self.closest('.fht-table-wrapper') - .find('tbody tr:odd:not(:hidden)') - .addClass(altClass); + .find('tbody tr:odd:not(:hidden)') + .addClass(altClass); }, /* @@ -218,33 +218,33 @@ */ show: function( arg1, arg2, arg3 ) { var $self = $(this), - self = this, - $wrapper = $self.closest('.fht-table-wrapper'); + self = this, + $wrapper = $self.closest('.fht-table-wrapper'); - // User provided show duration without a specific effect + // User provided show duration without a specific effect if ( typeof(arg1) !== 'undefined' && typeof(arg1) === 'number' ) { - - $wrapper.show(arg1, function() { - $.isFunction(arg2) && arg2.call(this); - }); + + $wrapper.show(arg1, function() { + $.isFunction(arg2) && arg2.call(this); + }); - return self; - + return self; + } else if ( typeof(arg1) !== 'undefined' && typeof(arg1) === 'string' - && typeof(arg2) !== 'undefined' && typeof(arg2) === 'number' ) { - // User provided show duration with an effect - - $wrapper.show(arg1, arg2, function() { - $.isFunction(arg3) && arg3.call(this); - }); - - return self; - + && typeof(arg2) !== 'undefined' && typeof(arg2) === 'number' ) { + // User provided show duration with an effect + + $wrapper.show(arg1, arg2, function() { + $.isFunction(arg3) && arg3.call(this); + }); + + return self; + } - + $self.closest('.fht-table-wrapper') - .show(); - $.isFunction(arg1) && arg1.call(this); + .show(); + $.isFunction(arg1) && arg1.call(this); return self; }, @@ -254,33 +254,33 @@ */ hide: function( arg1, arg2, arg3 ) { var $self = $(this), - self = this, - $wrapper = $self.closest('.fht-table-wrapper'); - + self = this, + $wrapper = $self.closest('.fht-table-wrapper'); + // User provided show duration without a specific effect if ( typeof(arg1) !== 'undefined' && typeof(arg1) === 'number' ) { - $wrapper.hide(arg1, function() { - $.isFunction(arg3) && arg3.call(this); - }); - - return self; - } else if ( typeof(arg1) !== 'undefined' && typeof(arg1) === 'string' - && typeof(arg2) !== 'undefined' && typeof(arg2) === 'number' ) { - - $wrapper.hide(arg1, arg2, function() { - $.isFunction(arg3) && arg3.call(this); - }); - - return self; - } + $wrapper.hide(arg1, function() { + $.isFunction(arg3) && arg3.call(this); + }); - $self.closest('.fht-table-wrapper') - .hide(); - - $.isFunction(arg3) && arg3.call(this); - + return self; + } else if ( typeof(arg1) !== 'undefined' && typeof(arg1) === 'string' + && typeof(arg2) !== 'undefined' && typeof(arg2) === 'number' ) { + + $wrapper.hide(arg1, arg2, function() { + $.isFunction(arg3) && arg3.call(this); + }); + + return self; + } + + $self.closest('.fht-table-wrapper') + .hide(); + + $.isFunction(arg3) && arg3.call(this); + + - return self; }, @@ -289,15 +289,15 @@ */ destroy: function() { var $self = $(this), - self = this, - $wrapper = $self.closest('.fht-table-wrapper'); - + self = this, + $wrapper = $self.closest('.fht-table-wrapper'); + $self.insertBefore($wrapper) - .removeAttr('style') - .append($wrapper.find('tfoot')) - .removeClass('fht-table fht-table-init') - .find('.fht-cell') - .remove(); + .removeAttr('style') + .append($wrapper.find('tfoot')) + .removeClass('fht-table fht-table-init') + .find('.fht-cell') + .remove(); $wrapper.remove(); @@ -309,15 +309,15 @@ // private methods var helpers = { - /* - * return boolean - * True if a thead and tbody exist. - */ + /* + * return boolean + * True if a thead and tbody exist. + */ _isTable: function( $obj ) { var $self = $obj, - hasTable = $self.is('table'), - hasThead = $self.find('thead').length > 0, - hasTbody = $self.find('tbody').length > 0; + hasTable = $self.is('table'), + hasThead = $self.find('thead').length > 0, + hasTbody = $self.find('tbody').length > 0; if ( hasTable && hasThead && hasTbody ) { return true; @@ -333,31 +333,31 @@ */ _bindScroll: function( $obj, tableProps ) { var $self = $obj, - $wrapper = $self.closest('.fht-table-wrapper'), - $thead = $self.siblings('.fht-thead'), - $tfoot = $self.siblings('.fht-tfoot'); + $wrapper = $self.closest('.fht-table-wrapper'), + $thead = $self.siblings('.fht-thead'), + $tfoot = $self.siblings('.fht-tfoot'); $self.bind('scroll', function() { - if ( settings.fixedColumn == true ) { - var $fixedColumn = $wrapper.find('.fht-fixed-column'); + if ( settings.fixedColumns > 0 ) { + var $fixedColumns = $wrapper.find('.fht-fixed-column'); - $fixedColumn.find('.fht-tbody table') + $fixedColumns.find('.fht-tbody table') .css({ 'margin-top': -$self.scrollTop() - }); + }); } - $thead.find('table') - .css({ - 'margin-left': -this.scrollLeft - }); - - if ( settings.cloneHeadToFoot ) { - $tfoot.find('table') - .css({ - 'margin-left': -this.scrollLeft - }); - } + $thead.find('table') + .css({ + 'margin-left': -this.scrollLeft + }); + + if ( settings.cloneHeadToFoot ) { + $tfoot.find('table') + .css({ + 'margin-left': -this.scrollLeft + }); + } }); }, @@ -366,13 +366,13 @@ */ _fixHeightWithCss: function ( $obj, tableProps ) { if ( settings.includePadding ) { - $obj.css({ - 'height': $obj.height() + tableProps.border - }); + $obj.css({ + 'height': $obj.height() + tableProps.border + }); } else { - $obj.css({ - 'height': $obj.parent().height() + tableProps.border - }); + $obj.css({ + 'height': $obj.parent().height() + tableProps.border + }); } }, @@ -381,88 +381,100 @@ */ _fixWidthWithCss: function( $obj, tableProps ) { if ( settings.includePadding ) { - $obj.css({ - 'width': $obj.width() + tableProps.border - }); + $obj.css({ + 'width': $obj.width() + tableProps.border + }); } else { - $obj.css({ - 'width': $obj.parent().width() + tableProps.border - }); + $obj.css({ + 'width': $obj.parent().width() + tableProps.border + }); } }, /* * return void */ - _setupFixedColumn: function ( $obj, obj, tableProps ) { - var $self = $obj, - self = obj, - $wrapper = $self.closest('.fht-table-wrapper'), - $fixedBody = $wrapper.find('.fht-fixed-body'), - $fixedColumn = $wrapper.find('.fht-fixed-column'), - $thead = $('
'), - $tbody = $('
'), - $tfoot = $('
'), - $firstThChild = $fixedBody.find('.fht-thead thead tr th:first-child'), - $firstTdChildren, - fixedColumnWidth = $firstThChild.outerWidth(true) + tableProps.border, - fixedBodyWidth = $wrapper.width(), - fixedBodyHeight = $fixedBody.find('.fht-tbody').height() - settings.scrollbarOffset, - $newRow; - - // Fix cell heights - helpers._fixHeightWithCss( $firstThChild, tableProps ); - helpers._fixWidthWithCss( $firstThChild, tableProps ); - $firstTdChildren = $fixedBody.find('tbody tr td:first-child') - .each( function(index) { - helpers._fixHeightWithCss( $(this), tableProps ); - helpers._fixWidthWithCss( $(this), tableProps ); - }); - - // clone header - $thead.appendTo($fixedColumn) - .find('tr') - .append($firstThChild.clone()); - - $tbody.appendTo($fixedColumn) - .css({ - 'margin-top': -1, - 'height': fixedBodyHeight + tableProps.border - }); - $firstTdChildren.each(function(index) { - $newRow = $('').appendTo($tbody.find('tbody')); - - if ( settings.altClass && $(this).parent().hasClass(settings.altClass) ) { - $newRow.addClass(settings.altClass); - } - - $(this).clone() - .appendTo($newRow); - }); - - // set width of fixed column wrapper - $fixedColumn.css({ - 'width': fixedColumnWidth - }); - - // set width of body table wrapper - $fixedBody.css({ - 'width': fixedBodyWidth - }); - - // setup clone footer with fixed column - if ( settings.footer == true || settings.cloneHeadToFoot == true ) { - var $firstTdFootChild = $fixedBody.find('.fht-tfoot thead tr th:first-child'); - - helpers._fixHeightWithCss( $firstTdFootChild, tableProps ); - $tfoot.appendTo($fixedColumn) - .find('tr') - .append($firstTdFootChild.clone()); - $tfoot.css({ - 'top': settings.scrollbarOffset - }); - } - }, + _setupFixedColumn: function ( $obj, obj, tableProps ) { + var $self = $obj, + self = obj, + $wrapper = $self.closest('.fht-table-wrapper'), + $fixedBody = $wrapper.find('.fht-fixed-body'), + $fixedColumn = $wrapper.find('.fht-fixed-column'), + $thead = $('
'), + $tbody = $('
'), + $tfoot = $('
'), + $firstThChildren,// = $fixedBody.find('.fht-thead thead tr th:first-child'), + $firstTdChildren, + fixedColumnWidth,// = $firstThChild.outerWidth(true) + tableProps.border, + fixedBodyWidth = $wrapper.width(), + fixedBodyHeight = $fixedBody.find('.fht-tbody').height() - settings.scrollbarOffset, + $newRow; + + $firstThChildren = $fixedBody.find('.fht-thead thead tr th:lt(' + settings.fixedColumns + ')'); + fixedColumnWidth = settings.fixedColumns * tableProps.border; + $firstThChildren.each(function(index) { + fixedColumnWidth += $(this).outerWidth(true); + }); + + // Fix cell heights + helpers._fixHeightWithCss( $firstThChildren, tableProps ); + helpers._fixWidthWithCss( $firstThChildren, tableProps ); + + firstTdChildrenSelector = 'tbody tr td:not(:nth-child(n+' + (settings.fixedColumns + 1) + '))'; + $firstTdChildren = $fixedBody.find(firstTdChildrenSelector) + .each( function(index) { + helpers._fixHeightWithCss( $(this), tableProps ); + helpers._fixWidthWithCss( $(this), tableProps ); + }); + + // clone header + $thead.appendTo($fixedColumn) + .find('tr') + .append($firstThChildren.clone()); + + $tbody.appendTo($fixedColumn) + .css({ + 'margin-top': -1, + 'height': fixedBodyHeight + tableProps.border + }); + + var $newRow; + $firstTdChildren.each(function(index) { + if (index % settings.fixedColumns == 0) { + $newRow = $('').appendTo($tbody.find('tbody')); + + if ( settings.altClass && $(this).parent().hasClass(settings.altClass) ) { + $newRow.addClass(settings.altClass); + } + } + + $(this).clone() + .appendTo($newRow); + }); + + // set width of fixed column wrapper + $fixedColumn.css({ + 'width': fixedColumnWidth + }); + + // set width of body table wrapper + $fixedBody.css({ + 'width': fixedBodyWidth + }); + + // setup clone footer with fixed column + if ( settings.footer == true || settings.cloneHeadToFoot == true ) { + var $firstTdFootChild = $fixedBody.find('.fht-tfoot thead tr th:lt(' + settings.fixedColumns + ')'); + + helpers._fixHeightWithCss( $firstTdFootChild, tableProps ); + $tfoot.appendTo($fixedColumn) + .find('tr') + .append($firstTdFootChild.clone()); + $tfoot.css({ + 'top': settings.scrollbarOffset + }); + } + }, /* * return void @@ -470,41 +482,41 @@ _setupTableFooter: function ( $obj, obj, tableProps ) { var $self = $obj, - self = obj, - $wrapper = $self.closest('.fht-table-wrapper'), - $tfoot = $self.find('tfoot'), - $divFoot = $wrapper.find('div.fht-tfoot'); - + self = obj, + $wrapper = $self.closest('.fht-table-wrapper'), + $tfoot = $self.find('tfoot'), + $divFoot = $wrapper.find('div.fht-tfoot'); + if ( !$divFoot.length ) { - if ( settings.fixedColumn == true ) { - $divFoot = $('
').appendTo($wrapper.find('.fht-fixed-body')); - } else { - $divFoot = $('
').appendTo($wrapper); - } + if ( settings.fixedColumns > 0 ) { + $divFoot = $('
').appendTo($wrapper.find('.fht-fixed-body')); + } else { + $divFoot = $('
').appendTo($wrapper); + } } switch (true) { - case !$tfoot.length && settings.cloneHeadToFoot == true && settings.footer == true: - - var $divHead = $wrapper.find('div.fht-thead'); - - $divFoot.empty(); - $divHead.find('table') - .clone() - .appendTo($divFoot); - - break; - case $tfoot.length && settings.cloneHeadToFoot == false && settings.footer == true: - - $divFoot.find('table') - .append($tfoot) - .css({ - 'margin-top': -tableProps.border - }); - - helpers._setupClone( $divFoot, tableProps.tfoot ); - - break; + case !$tfoot.length && settings.cloneHeadToFoot == true && settings.footer == true: + + var $divHead = $wrapper.find('div.fht-thead'); + + $divFoot.empty(); + $divHead.find('table') + .clone() + .appendTo($divFoot); + + break; + case $tfoot.length && settings.cloneHeadToFoot == false && settings.footer == true: + + $divFoot.find('table') + .append($tfoot) + .css({ + 'margin-top': -tableProps.border + }); + + helpers._setupClone( $divFoot, tableProps.tfoot ); + + break; } }, @@ -524,17 +536,17 @@ borderCollapse = 1; if ( settings.borderCollapse == true ) { - borderCollapse = 2; + borderCollapse = 2; } - - tableProp.border = ( $obj.find('th:first-child').outerWidth() - $obj.find('th:first-child').innerWidth() ) / borderCollapse; - + + tableProp.border = ( $obj.find('th:first-child').outerWidth() - $obj.find('th:first-child').innerWidth() ) / borderCollapse; + $obj.find('thead tr:first-child th').each(function(index) { tableProp.thead[index] = $(this).width() + tableProp.border; }); $obj.find('tfoot tr:first-child td').each(function(index) { - tableProp.tfoot[index] = $(this).width() + tableProp.border; + tableProp.tfoot[index] = $(this).width() + tableProp.border; }); $obj.find('tbody tr:first-child td').each(function(index) { @@ -550,16 +562,16 @@ */ _setupClone: function( $obj, cellArray ) { var $self = $obj, - selector = ( $self.find('thead').length ) ? - 'thead th' : - ( $self.find('tfoot').length ) ? - 'tfoot td' : - 'tbody td', - $cell; + selector = ( $self.find('thead').length ) ? + 'thead th' : + ( $self.find('tfoot').length ) ? + 'tfoot td' : + 'tbody td', + $cell; $self.find(selector).each(function(index) { $cell = ( $(this).find('div.fht-cell').length ) ? $(this).find('div.fht-cell') : $('
').appendTo($(this)); - + $cell.css({ 'width': parseInt(cellArray[index]) }); @@ -571,7 +583,7 @@ if ( !$(this).closest('.fht-tbody').length && $(this).is(':last-child') && !$(this).closest('.fht-fixed-column').length ) { var padding = ( ( $(this).innerWidth() - $(this).width() ) / 2 ) + settings.scrollbarOffset; $(this).css({ - 'padding-right': padding + 'px' + 'padding-right': padding + 'px' }); } }); @@ -585,23 +597,23 @@ */ _isPaddingIncludedWithWidth: function() { var $obj = $('
test
'), - defaultHeight, - newHeight; - + defaultHeight, + newHeight; + $obj.appendTo('body'); defaultHeight = $obj.find('td').height(); $obj.find('td') - .css('height', $obj.find('tr').height()); - + .css('height', $obj.find('tr').height()); + newHeight = $obj.find('td').height(); $obj.remove(); if ( defaultHeight != newHeight ) { - return true; + return true; } else { - return false; + return false; } }, @@ -614,24 +626,24 @@ var scrollbarWidth = 0; if ( !scrollbarWidth ) { - if ( $.browser.msie ) { - var $textarea1 = $('') - .css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'), - $textarea2 = $('') - .css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'); - scrollbarWidth = $textarea1.width() - $textarea2.width() + 2; // + 2 for border offset - $textarea1.add($textarea2).remove(); - } else { - var $div = $('
') - .css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: -1000 }) - .prependTo('body').append('
').find('div') - .css({ width: '100%', height: 200 }); - scrollbarWidth = 100 - $div.width(); - $div.parent().remove(); - } - } - - return scrollbarWidth; + if ( $.browser.msie ) { + var $textarea1 = $('') + .css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'), + $textarea2 = $('') + .css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'); + scrollbarWidth = $textarea1.width() - $textarea2.width() + 2; // + 2 for border offset + $textarea1.add($textarea2).remove(); + } else { + var $div = $('
') + .css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: -1000 }) + .prependTo('body').append('
').find('div') + .css({ width: '100%', height: 200 }); + scrollbarWidth = 100 - $div.width(); + $div.parent().remove(); + } + } + + return scrollbarWidth; } } @@ -643,13 +655,13 @@ // call the respective method return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - // if an object is given as method OR nothing is given as argument + // if an object is given as method OR nothing is given as argument } else if ( typeof method === 'object' || !method ) { // call the initialization method return methods.init.apply(this, arguments); - // otherwise + // otherwise } else { // trigger an error