diff --git a/core/includes/common.inc b/core/includes/common.inc index c9913a2..7b1bd52 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -6561,7 +6561,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 2d1d375..ad5863d 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -34,6 +34,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". */ @@ -1759,6 +1769,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 @@ -1829,6 +1842,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. @@ -1838,6 +1852,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"; @@ -1894,16 +1915,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"); } @@ -1944,7 +1980,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/tabledrag.js b/core/misc/tabledrag.js index 07c50d5..230f588 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -50,6 +50,7 @@ Drupal.tableDrag = function (table, tableSettings) { var $table = $(table); // Required object variables. + this.$table = $(table); this.table = table; this.tableSettings = tableSettings; this.dragObject = null; // Used to hold information about a current drag operation. @@ -143,7 +144,8 @@ Drupal.tableDrag = function (table, tableSettings) { * 'Drupal.tableDrag.showWeight' localStorage value. */ Drupal.tableDrag.prototype.initColumns = function () { - var $table = $(this.table), hidden, cell, columnIndex; + var $table = this.$table; + var hidden, cell, columnIndex; for (var group in this.tableSettings) { if (this.tableSettings.hasOwnProperty(group)) { // Find the first field in this group. for (var d in this.tableSettings[group]) { diff --git a/core/misc/tableresponsive.js b/core/misc/tableresponsive.js new file mode 100644 index 0000000..dd08929 --- /dev/null +++ b/core/misc/tableresponsive.js @@ -0,0 +1,146 @@ +/** + * tableresponsive.js + * + * Behaviors to facilitate the presentation of tables across screens of any size. + */ +(function ($) { + +"use strict"; + +/** + * Attach the tableResponsive function to Drupal.behaviors. + */ +Drupal.behaviors.tableResponsive = { + attach: function (context, settings) { + var $tables = $(context).find('table.responsive-enabled').once('tableresponsive'); + if ($tables.length) { + for (var i = 0, il = $tables.length; i < il; i++) { + TableResponsive.tables.push(new TableResponsive($tables[i])); + } + } + } +}; + +/** + * 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. + */ +function TableResponsive (table) { + this.$table = $(table); + this.table = this.$table.data('drupal-table'); + this.showText = Drupal.t('Show all columns'); + this.hideText = Drupal.t('Hide unimportant columns'); + // 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'); + // Build a link that will toggle the column visibility. + + // Add a link before the table for users to show or hide weight columns. + this.$link = $('') + .attr({ + title: Drupal.t('Expose table cells that were hidden to make the table fit within a small screen.'), + 'aria-disabled': 'false' + }) + .on('click', $.proxy(this, 'eventhandlerToggleColumns')); + + this.$table.before($('
').append(this.$link)); + + // Attach a resize handler to the window. + $(window) + .on('resize.drupal-tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility')) + .trigger('resize.drupal-tableresponsive'); +} + +$.extend(TableResponsive, { + tables: [] +}); + + /** + * 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. + */ +$.extend(TableResponsive.prototype, { + eventhandlerEvaluateColumnVisibility: function (e) { + var sticky = +this.$link.attr('data-sticky'); + var hiddenLength = this.$headers.filter('.advisable:hidden, .helpful:hidden').length; + // If the table has hidden columns, associate an action link with the table + // to show the columns. + if (hiddenLength > 0) { + this.$link.show().text(this.showText).attr('aria-disabled', 'false'); + } + // 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 && hiddenLength === 0) { + this.$link.hide().text(this.hideText).attr('aria-disabled', 'true'); + } + }, + /** + * Reveal hidden columns and hide any columns that were revealed because they were + * previously hidden. + */ + eventhandlerToggleColumns: function (e) { + e.preventDefault(); + var self = this; + 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 = $header.prevAll('th').length; + self.$table.find('tbody tr').each(function () { + var $cells = $(this).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.$link.text(this.hideText).attr('data-sticky', 1); + } + // Hide revealed columns. + else { + this.$revealedCells.hide(); + // Strip out display + this.$revealedCells.each(function (index, element) { + var $cell = $(this); + var properties = $cell.attr('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(';')); + }); + this.$link.text(this.showText).attr('data-sticky', 0); + // Refresh the toggle link. + $(window).trigger('resize.drupal-tableresponsive'); + } + } +}); + +Drupal.TableResponsive = TableResponsive; + +})(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 15f2354..3e56751 100644 --- a/core/modules/dblog/dblog.admin.inc +++ b/core/modules/dblog/dblog.admin.inc @@ -33,11 +33,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 29d586e..0b67da6 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 5cbe4ef..5c4a198 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 2bf4343..39d89f3 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -921,8 +921,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.base-rtl.css b/core/modules/system/system.base-rtl.css index d01792e..2d5c63f 100644 --- a/core/modules/system/system.base-rtl.css +++ b/core/modules/system/system.base-rtl.css @@ -1,4 +1,3 @@ - /** * @file * Generic theme-independent base styles. @@ -29,6 +28,13 @@ } /** + * Table behavior. + */ +.table-actions > a { + margin-left: 1.5em; + margin-right: 0; +} +/** * TableDrag behavior. */ .draggable a.tabledrag-handle { diff --git a/core/modules/system/system.base.css b/core/modules/system/system.base.css index 610d94d..52cf776 100644 --- a/core/modules/system/system.base.css +++ b/core/modules/system/system.base.css @@ -1,4 +1,3 @@ - /** * @file * Generic theme-independent base styles. @@ -136,8 +135,18 @@ div.tree-child-horizontal { text-align: right; /* LTR */ } + +/** + * Table behavior. + * + * @see core/misc/table.js + */ +.table-actions > a { + margin-right: 1.5em; /* LTR */ +} + /** - * TableHeader behavior. + * Tableheader behavior. * * @see tableheader.js */ diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 922b8a7..ad2a734 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1297,12 +1297,26 @@ function system_library_info() { ), ); + // Drupal's tableheader library. + $libraries['drupal.tableheader'] = array( + 'title' => 'Drupal tableheader API', + 'version' => VERSION, + 'js' => array( + 'core/misc/tableheader.js' => array('group' => JS_LIBRARY,), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'jquery.once'), + ), + ); + // Drupal's tabledrag library. $libraries['drupal.tabledrag'] = array( 'title' => 'Drupal tabledrag', 'version' => VERSION, 'js' => array( - 'core/misc/tabledrag.js' => array('group' => JS_LIBRARY, 'weight' => -1), + 'core/misc/tabledrag.js' => array('group' => JS_LIBRARY, 'cache' => FALSE), ), 'dependencies' => array( array('system', 'jquery'), @@ -1313,6 +1327,20 @@ 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_LIBRARY,), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'jquery.once'), + ), + ); + // 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 3c39f24..4ccc806 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')), ); @@ -1051,4 +1051,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 7532a4a..c18b106 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1708,3 +1708,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 fd9f2ee..7b6240b 100644 --- a/core/themes/seven/style-rtl.css +++ b/core/themes/seven/style-rtl.css @@ -1,4 +1,3 @@ - /** * Generic elements. */ @@ -71,10 +70,8 @@ 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; } ul.links li, ul.inline li { diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css index 86f3fc5..a2e5b5f 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -1,4 +1,3 @@ - /** * Generic elements. */ @@ -314,13 +313,43 @@ 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 */ +} +#secondary-links ul.links li a { + font-size: 0.923em; + background: #777; + color: #fff; + text-align: center; + padding: 5px; + height: 55px; + width: 80px; + overflow: hidden; + -moz-border-radius: 5px; + border-radius: 5px; +} +#secondary-links ul.links li a:hover { + background: #999; +} ul.links li, ul.inline li { padding-right: 1em; /* LTR */ @@ -474,8 +503,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..fb1b7f3 100644 --- a/core/themes/stark/css/layout.css +++ b/core/themes/stark/css/layout.css @@ -1,4 +1,3 @@ - /** * @file * Stark layout method @@ -93,3 +92,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; + } +}