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; + } +}