diff --git a/core/includes/common.inc b/core/includes/common.inc
index 17e1363..b91c4db 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -6589,7 +6589,7 @@ function drupal_common_theme() {
'variables' => array(),
),
'table' => array(
- 'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => TRUE, 'empty' => ''),
+ 'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => TRUE, 'responsive' => TRUE, 'empty' => ''),
),
'meter' => array(
'variables' => array('display_value' => NULL, 'form' => NULL, 'high' => NULL, 'low' => NULL, 'max' => NULL, 'min' => NULL, 'optimum' => NULL, 'value' => NULL, 'percentage' => NULL, 'attributes' => array()),
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index f807979..83b5300 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -33,6 +33,16 @@ const MARK_NEW = 1;
const MARK_UPDATED = 2;
/**
+ * A responsive table class.
+ */
+const RWD_ADVISABLE = 'advisable';
+
+/**
+ * A responsive table class.
+ */
+const RWD_HELPFUL = 'helpful';
+
+/**
* @} End of "defgroup content_flags".
*/
@@ -649,7 +659,7 @@ function _theme_build_registry($theme, $base_theme, $theme_engine) {
* their base theme), direct sub-themes of sub-themes, etc. The keys are
* the themes' machine names, and the values are the themes' human-readable
* names. This element is not set if there are no themes on the system that
- * declare this theme as their base theme.
+ * declare this theme as their base theme.
*/
function list_themes($refresh = FALSE) {
$list = &drupal_static(__FUNCTION__, array());
@@ -1767,6 +1777,9 @@ function theme_breadcrumb($variables) {
* - "field": The database field represented in the table column (required
* if user is to be able to sort on this column).
* - "sort": A default sort order for this column ("asc" or "desc").
+ * - "class": An array of values for 'class' attribute. In particular,
+ * less important columns should have 'adviseable' or 'helpful' values.
+ * Themes may hide less important columns for narrow viewports.
* - Any HTML attributes, such as "colspan", to apply to the column header
* cell.
* - rows: An array of table rows. Every row is an array of cells, or an
@@ -1837,6 +1850,7 @@ function theme_table($variables) {
$caption = $variables['caption'];
$colgroups = $variables['colgroups'];
$sticky = $variables['sticky'];
+ $responsive = $variables['responsive'];
$empty = $variables['empty'];
// Add sticky headers, if applicable.
@@ -1846,6 +1860,13 @@ function theme_table($variables) {
// This is needed to target tables constructed by this function.
$attributes['class'][] = 'sticky-enabled';
}
+ // If columns are hidden, add a link to show the columns.
+ if (count($header) && $responsive) {
+ drupal_add_library('system', 'drupal.tableresponsive');
+ // Add 'responsive-enabled' class to the table to identify it for JS.
+ // This is needed to target tables constructed by this function.
+ $attributes['class'][] = 'responsive-enabled';
+ }
$output = '
\n";
@@ -1902,16 +1923,31 @@ function theme_table($variables) {
$rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message')));
}
+ $responsive = array();
// Format the table header:
if (count($header)) {
$ts = tablesort_init($header);
// HTML requires that the thead tag has tr tags in it followed by tbody
// tags. Using ternary operator to check and see if we have any rows.
$output .= (count($rows) ? ' ' : '
');
+ $i=0;
foreach ($header as $cell) {
+ $i++;
+
+ // Track responsive classes for each column as needed.
+ if (!empty($cell['class']) && is_array($cell['class'])) {
+ if (in_array(RWD_ADVISABLE, $cell['class'])) {
+ $responsive[$i] = RWD_ADVISABLE;
+ }
+ elseif (in_array(RWD_HELPFUL, $cell['class'])) {
+ $responsive[$i] = RWD_HELPFUL;
+ }
+ }
+
$cell = tablesort_header($cell, $header, $ts);
$output .= _theme_table_cell($cell, TRUE);
}
+
// Using ternary operator to close the tags based on whether or not there are rows
$output .= (count($rows) ? "
\n" : "\n");
}
@@ -1952,7 +1988,20 @@ function theme_table($variables) {
$output .= ' ';
$i = 0;
foreach ($cells as $cell) {
- $cell = tablesort_cell($cell, $header, $ts, $i++);
+ $i++;
+ // Add active class if needed for sortable tables.
+ $cell = tablesort_cell($cell, $header, $ts, $i);
+
+ // Copy advisable/helpful class from header to cell as needed.
+ if (isset($responsive[$i])) {
+ if (is_array($cell)) {
+ $cell['class'][] = $responsive[$i];
+ }
+ else {
+ $cell = array('data' => $cell, 'class' => $responsive[$i]);
+ }
+ }
+
$output .= _theme_table_cell($cell);
}
$output .= "
\n";
diff --git a/core/misc/debounce.js b/core/misc/debounce.js
new file mode 100644
index 0000000..b8f0e6b
--- /dev/null
+++ b/core/misc/debounce.js
@@ -0,0 +1,33 @@
+/**
+ * Returns a function that will not invoked while it continues to be called,
+ * until the wait period has experied without a call to the function.
+ *
+ * Use this function to prevent frequent calls to functions, for
+ * example like event handlers attached to the resize event.
+ *
+ * To use debounce, pass your function to debounce as the first argument
+ * and the waiting period as the second.
+ *
+ * If immediate is passed as true, the provided function will be invoked before
+ * the wait time has elapsed instead of after, once the debounce-wrapped function
+ * ceases to be called.
+ */
+Drupal.debounce = function (fn, wait, immediate) {
+ var timeout;
+ return function () {
+ var context = this;
+ var args = arguments;
+ var deferred = function () {
+ timeout = null;
+ if (!immediate) {
+ fn.apply(context, args);
+ }
+ };
+ var invoke = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(deferred, wait);
+ if (invoke) {
+ fn.apply(context, args);
+ }
+ };
+};
diff --git a/core/misc/tableheader.js b/core/misc/tableheader.js
index 225cb73..46fe18a 100644
--- a/core/misc/tableheader.js
+++ b/core/misc/tableheader.js
@@ -101,7 +101,10 @@ Drupal.tableHeader.prototype.eventhandlerRecalculateStickyHeader = function (eve
var vOffset = (document.documentElement.scrollTop || document.body.scrollTop) - this.vPosition;
this.stickyVisible = vOffset > 0 && vOffset < this.vLength;
this.stickyTable.css({ left: (-hScroll + this.hPosition) + 'px', visibility: this.stickyVisible ? 'visible' : 'hidden' });
-
+ // If the sticky header is no longer visible, remove the width calculation flag.
+ if (!this.stickyVisible && 'widthCalculated' in this) {
+ delete this.widthCalculated;
+ }
// Only perform expensive calculations if the sticky header is actually
// visible or when forced.
if (this.stickyVisible && (calculateWidth || !this.widthCalculated)) {
diff --git a/core/misc/tableresponsive.js b/core/misc/tableresponsive.js
new file mode 100644
index 0000000..bb754aa
--- /dev/null
+++ b/core/misc/tableresponsive.js
@@ -0,0 +1,143 @@
+/**
+ * tableresponsive.js
+ *
+ * Behaviors to facilitate the presentation of tables across screens of any size.
+ */
+(function ($) {
+
+"use strict";
+
+ /**
+ * Attach the responsiveTable function to Drupal.behaviors.
+ */
+ Drupal.behaviors.responsiveTable = {
+ attach: function (context, settings) {
+ $(context).find('table.responsive-enabled').once('tableresponsive', function () {
+ $(this).data("drupal-tableresponsive", new Drupal.responsiveTable(this));
+ });
+ }
+ };
+ /**
+ * A responsive table hides columns at small screen sizes, leaving the most
+ * important columns visible to the end user. Users should not be prevented from
+ * accessing all columns, however. This class adds a toggle to a table with
+ * hidden columns that exposes the columns. Exposing the columns will likely
+ * break layouts, but it provides the user with a means to access data, which
+ * is a guiding principle of responsive design.
+ */
+ Drupal.responsiveTable = function (table) {
+ var self = this;
+ this.$table = $(table);
+ this.showText = Drupal.t('Show all columns');
+ this.hideText = Drupal.t('Hide unimportant columns');
+ // Build a link that will toggle the column visibility.
+ this.$columnToggle = $('', {
+ 'href': '#',
+ 'text': this.showText,
+ 'class': 'responsive-table-toggle'
+ })
+ .data('drupal-tableresponsive', {})
+ .bind('click.drupal-tableresponsivetable', $.proxy(this, 'eventhandlerToggleColumns'));
+ // Store a reference to the header elements of the table so that the DOM is
+ // traversed only once to find them.
+ this.$headers = this.$table.find('th');
+ // Attach a resize handler to the window.
+ $(window)
+ .bind('resize.drupal-tableresponsivetable', Drupal.debounce($.proxy(this, 'eventhandlerEvaluateColumnVisibility'), 250))
+ .triggerHandler('resize.drupal-tableresponsivetable');
+ };
+ /**
+ * Associates an action link with the table that will show hidden columns.
+ * Columns are assumed to be hidden if their header's display property is none
+ * or if the visibility property is hidden.
+ */
+ Drupal.responsiveTable.prototype.eventhandlerEvaluateColumnVisibility = function (event) {
+ var self = this;
+ var $headers = this.$headers;
+ var $toggle = this.$columnToggle;
+ var $hiddenHeaders = $headers.filter('.advisable:hidden, .helpful:hidden');
+ var hiddenLength = $hiddenHeaders.length;
+ var toggleData = $toggle.data('drupal-tableresponsive');
+ // If the table has hidden columns, associate an action link with the table
+ // to show the columns.
+ if (hiddenLength > 0) {
+ $toggle
+ .insertBefore(this.$table);
+ }
+ // When the toggle is sticky, its presence is maintained because the user has
+ // interacted with it. This is necessary to keep the link visible if the user
+ // adjusts screen size and changes the visibilty of columns.
+ if ((!('sticky' in toggleData) && hiddenLength === 0) || ('sticky' in toggleData && !toggleData.sticky && hiddenLength === 0)) {
+ $toggle.detach();
+ delete this.$columnToggle.data('drupal-tableresponsive').sticky;
+ }
+ };
+ /**
+ * Reveal hidden columns and hide any columns that were revealed because they were
+ * previously hidden.
+ */
+ Drupal.responsiveTable.prototype.eventhandlerToggleColumns = function (event) {
+ event.preventDefault();
+ var self = this;
+ var $headers = this.$headers;
+ var $hiddenHeaders = this.$headers.filter('.advisable:hidden, .helpful:hidden');
+ this.$revealedCells = this.$revealedCells || $();
+ // Reveal hidden columns.
+ if ($hiddenHeaders.length > 0) {
+ $hiddenHeaders.each(function (index, element) {
+ var $header = $(this);
+ var position = Number($header.prevAll('th').length);
+ $('tbody tr', this.$table).each(function (index, element) {
+ var $row = $(this);
+ var $cells = $row.find('td:eq(' + position + ')');
+ $cells.show();
+ // Keep track of the revealed cells, so they can be hidden later.
+ self.$revealedCells = $().add(self.$revealedCells).add($cells);
+ });
+ $header.show();
+ // Keep track of the revealed headers, so they can be hidden later.
+ self.$revealedCells = $().add(self.$revealedCells).add($header);
+
+
+ });
+ this.$columnToggle.text(this.hideText);
+ this.$columnToggle.data('drupal-tableresponsive').sticky = true;
+ }
+ // Hide revealed columns.
+ else {
+ this.$revealedCells.hide();
+ this.$columnToggle.text(this.showText);
+ // Strip out display
+ this.$revealedCells.each(function (index, element) {
+ var $cell = $(this);
+ var style = $cell.attr('style');
+ var properties = style.split(';');
+ var newProps = [];
+ // The columns should simply have the display table-cell property
+ // removed, which the jQuery hide method does. The hide method
+ // also adds display none to the element. The element should be
+ // returned to the same state it was in before the columns were
+ // revealed, so it is necessary to remove the display none
+ // value from the style attribute.
+ var match = /^display\s*\:\s*none$/;
+ for (var i = 0; i < properties.length; i++) {
+ var prop = properties[i]
+ prop.trim();
+ // Find the display:none property and remove it.
+ var isDisplayNone = match.exec(prop);
+ if (isDisplayNone) {
+ continue;
+ }
+ newProps.push(prop);
+ }
+ // Return the rest of the style attribute values to the element.
+ $cell.attr('style', newProps.join(';'));
+ });
+ delete this.$revealedCells;
+ this.$columnToggle.data('drupal-tableresponsive').sticky = false;
+ // Refresh the toggle link.
+ $(window)
+ .triggerHandler('resize.drupal-tableresponsivetable');
+ }
+ };
+})(jQuery);
diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc
index 6210cc5..5737ea4 100644
--- a/core/modules/comment/comment.admin.inc
+++ b/core/modules/comment/comment.admin.inc
@@ -73,9 +73,9 @@ function comment_admin_overview($form, &$form_state, $arg) {
$status = ($arg == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED;
$header = array(
'subject' => array('data' => t('Subject'), 'field' => 'subject'),
- 'author' => array('data' => t('Author'), 'field' => 'name'),
- 'posted_in' => array('data' => t('Posted in'), 'field' => 'node_title'),
- 'changed' => array('data' => t('Updated'), 'field' => 'c.changed', 'sort' => 'desc'),
+ 'author' => array('data' => t('Author'), 'field' => 'name', 'class' => array(RWD_ADVISABLE)),
+ 'posted_in' => array('data' => t('Posted in'), 'field' => 'node_title', 'class' => array(RWD_HELPFUL)),
+ 'changed' => array('data' => t('Updated'), 'field' => 'c.changed', 'sort' => 'desc', 'class' => array(RWD_HELPFUL)),
'operations' => array('data' => t('Operations')),
);
diff --git a/core/modules/dblog/dblog.admin.inc b/core/modules/dblog/dblog.admin.inc
index 8928f46..b3fe920 100644
--- a/core/modules/dblog/dblog.admin.inc
+++ b/core/modules/dblog/dblog.admin.inc
@@ -30,11 +30,11 @@ function dblog_overview() {
$header = array(
'', // Icon column.
- array('data' => t('Type'), 'field' => 'w.type'),
- array('data' => t('Date'), 'field' => 'w.wid', 'sort' => 'desc'),
+ array('data' => t('Type'), 'field' => 'w.type', 'class' => array(RWD_ADVISABLE)),
+ array('data' => t('Date'), 'field' => 'w.wid', 'sort' => 'desc', 'class' => array(RWD_HELPFUL)),
t('Message'),
- array('data' => t('User'), 'field' => 'u.name'),
- array('data' => t('Operations')),
+ array('data' => t('User'), 'field' => 'u.name', 'class' => array(RWD_ADVISABLE)),
+ array('data' => t('Operations'), 'class' => array(RWD_HELPFUL)),
);
$query = db_select('watchdog', 'w')
diff --git a/core/modules/field_ui/field_ui.admin.inc b/core/modules/field_ui/field_ui.admin.inc
index 5871be0..cc2bd1c 100644
--- a/core/modules/field_ui/field_ui.admin.inc
+++ b/core/modules/field_ui/field_ui.admin.inc
@@ -17,7 +17,11 @@ function field_ui_fields_list() {
$modules = system_rebuild_module_data();
- $header = array(t('Field name'), t('Field type'), t('Used in'));
+ $header = array(
+ t('Field name'),
+ array('data' => t('Field type'), 'class' => array(RWD_ADVISABLE)),
+ t('Used in'),
+ );
$rows = array();
foreach ($instances as $entity_type => $type_bundles) {
foreach ($type_bundles as $bundle => $bundle_instances) {
diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc
index d425232..4678bdc 100644
--- a/core/modules/node/node.admin.inc
+++ b/core/modules/node/node.admin.inc
@@ -436,14 +436,32 @@ function node_admin_nodes() {
// Build the sortable table header.
$header = array(
- 'title' => array('data' => t('Title'), 'field' => 'n.title'),
- 'type' => array('data' => t('Content type'), 'field' => 'n.type'),
- 'author' => t('Author'),
- 'status' => array('data' => t('Status'), 'field' => 'n.status'),
- 'changed' => array('data' => t('Updated'), 'field' => 'n.changed', 'sort' => 'desc')
+ 'title' => array(
+ 'data' => t('Title'),
+ 'field' => 'n.title',
+ ),
+ 'type' => array(
+ 'data' => t('Content type'),
+ 'field' => 'n.type',
+ 'class' => array(RWD_ADVISABLE),
+ ),
+ 'author' => array(
+ 'data' => t('Author'),
+ 'class' => array(RWD_HELPFUL),
+ ),
+ 'status' => array(
+ 'data' => t('Status'),
+ 'field' => 'n.status',
+ ),
+ 'changed' => array(
+ 'data' => t('Updated'),
+ 'field' => 'n.changed',
+ 'sort' => 'desc',
+ 'class' => array(RWD_HELPFUL)
+ ,)
);
if ($multilingual) {
- $header['language_name'] = array('data' => t('Language'), 'field' => 'n.langcode');
+ $header['language_name'] = array('data' => t('Language'), 'field' => 'n.langcode', 'class' => array(RWD_HELPFUL));
}
$header['operations'] = array('data' => t('Operations'));
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 8aa3ed4..4c3825a 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -917,8 +917,8 @@ function system_modules($form, $form_state = array()) {
'#header' => array(
array('data' => t('Enabled'), 'class' => array('checkbox')),
t('Name'),
- t('Version'),
- t('Description'),
+ array('data' => t('Version'), 'class' => array(RWD_ADVISABLE)),
+ array('data' => t('Description'), 'class' => array(RWD_HELPFUL)),
array('data' => t('Operations'), 'colspan' => 3),
),
// Ensure that the "Core" package fieldset comes first.
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 3c434ae..8d57348 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1237,6 +1237,15 @@ function system_library_info() {
),
);
+ // Drupal's debounce API.
+ $libraries['drupal.debounce'] = array(
+ 'title' => 'Drupal debounce API',
+ 'version' => VERSION,
+ 'js' => array(
+ 'core/misc/debounce.js' => array('group' => JS_DEFAULT, 'cache' => FALSE),
+ ),
+ );
+
// Drupal's progress indicator.
$libraries['drupal.progress'] = array(
'title' => 'Drupal progress indicator',
@@ -1276,6 +1285,18 @@ function system_library_info() {
),
);
+ // Drupal's responsive table API.
+ $libraries['drupal.tableresponsive'] = array(
+ 'title' => 'Drupal responsive table API',
+ 'version' => VERSION,
+ 'js' => array(
+ 'core/misc/tableresponsive.js' => array('group' => JS_DEFAULT, 'cache' => FALSE),
+ ),
+ 'dependencies' => array(
+ array('system', 'drupal.debounce'),
+ ),
+ );
+
// Drupal's collapsible fieldset.
$libraries['drupal.collapse'] = array(
'title' => 'Drupal collapsible fieldset',
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index fac9723..c2a3e31 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -145,10 +145,10 @@ function user_admin_account() {
$header = array(
'username' => array('data' => t('Username'), 'field' => 'u.name'),
- 'status' => array('data' => t('Status'), 'field' => 'u.status'),
- 'roles' => array('data' => t('Roles')),
- 'member_for' => array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc'),
- 'access' => array('data' => t('Last access'), 'field' => 'u.access'),
+ 'status' => array('data' => t('Status'), 'field' => 'u.status', 'class' => array(RWD_HELPFUL)),
+ 'roles' => array('data' => t('Roles'), 'class' => array(RWD_HELPFUL)),
+ 'member_for' => array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc', 'class' => array(RWD_HELPFUL)),
+ 'access' => array('data' => t('Last access'), 'field' => 'u.access', 'class' => array(RWD_HELPFUL)),
'operations' => array('data' => t('Operations')),
);
@@ -1034,4 +1034,3 @@ function user_admin_role_delete_confirm_submit($form, &$form_state) {
drupal_set_message(t('The role has been deleted.'));
$form_state['redirect'] = 'admin/people/roles';
}
-
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index 117a2d5..3a11800 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -1733,3 +1733,21 @@ div.admin-panel .description {
padding-bottom: 2em;
}
}
+
+/**
+ * Responsive tables
+ */
+@media screen and (max-width:28.125em) { /* 450px */
+ th.helpful,
+ td.helpful,
+ th.advisable,
+ td.advisable {
+ display: none;
+ }
+}
+@media screen and (max-width:45em) { /* 720px */
+ th.helpful,
+ td.helpful {
+ display: none;
+ }
+}
diff --git a/core/themes/seven/style-rtl.css b/core/themes/seven/style-rtl.css
index e98c968..b58f5f0 100644
--- a/core/themes/seven/style-rtl.css
+++ b/core/themes/seven/style-rtl.css
@@ -84,11 +84,6 @@ ul.primary {
/**
* Page layout.
*/
-#page {
- padding: 20px 0 40px 0;
- margin-left: 40px;
- margin-right: 40px;
-}
#secondary-links ul.links li {
padding: 0 0 10px 10px;
}
diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css
index 8d7164b..c3949ee 100644
--- a/core/themes/seven/style.css
+++ b/core/themes/seven/style.css
@@ -341,13 +341,25 @@ ul.secondary li.active a.active {
* Page layout.
*/
#page {
- padding: 20px 0 40px 0; /* LTR */
- margin-right: 40px; /* LTR */
- margin-left: 40px; /* LTR */
+ padding: 20px 0 40px 0;
+ margin-right: 0.8125em;
+ margin-left: 0.8125em;
background: #fff;
position: relative;
color: #333;
}
+@media screen and (min-width:28.125em) { /* 450px */
+ #page {
+ margin-left: 1.25em;
+ margin-right: 1.25em;
+ }
+}
+@media screen and (min-width:45em) { /* 720px */
+ #page {
+ margin-left: 2.5em;
+ margin-right: 2.5em;
+ }
+}
#secondary-links ul.links li {
padding: 0 10px 10px 0; /* LTR */
}
@@ -523,8 +535,23 @@ table.system-status-report tr.error {
tr td:last-child {
border-right: 1px solid #bebfb9; /* LTR */
}
-
-
+/**
+ * Responsive tables
+ */
+@media screen and (max-width:28.125em) { /* 450px */
+ th.helpful,
+ td.helpful,
+ th.advisable,
+ td.advisable {
+ display: none;
+ }
+}
+@media screen and (max-width:45em) { /* 720px */
+ th.helpful,
+ td.helpful {
+ display: none;
+ }
+}
/**
* Fieldsets.
*
diff --git a/core/themes/stark/css/layout.css b/core/themes/stark/css/layout.css
index a31a774..7055238 100644
--- a/core/themes/stark/css/layout.css
+++ b/core/themes/stark/css/layout.css
@@ -93,3 +93,21 @@ img {
width: 20%;
}
}
+
+/**
+ * Responsive tables
+ */
+@media screen and (max-width:28.125em) { /* 450px */
+ th.helpful,
+ td.helpful,
+ th.advisable,
+ td.advisable {
+ display: none;
+ }
+}
+@media screen and (max-width:45em) { /* 720px */
+ th.helpful,
+ td.helpful {
+ display: none;
+ }
+}