diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 7766952..cb7a84d 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -34,6 +34,22 @@ const MARK_NEW = 1; const MARK_UPDATED = 2; /** + * A responsive table class; hide table cell on narrow devices. + * + * Indicates that a column has medium priority and thus can be hidden on narrow + * width devices and shown on medium+ width devices (i.e. tablets and desktops). + */ +const RESPONSIVE_PRIORITY_MEDIUM = 'priority-medium'; + +/** + * A responsive table class; only show table cell on wide devices. + * + * Indicates that a column has low priority and thus can be hidden on narrow + * and medium viewports and shown on wide devices (i.e. desktops). + */ +const RESPONSIVE_PRIORITY_LOW = 'priority-low'; + +/** * @} End of "defgroup content_flags". */ @@ -1759,6 +1775,14 @@ 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 the 'class' attribute. In particular, + * the least important columns that can be hidden on narrow and medium + * width screens should have a 'priority-low' class, referenced with the + * RESPONSIVE_PRIORITY_LOW constant. Columns that should be shown on + * medium+ wide screens should be marked up with a class of + * 'priority-medium', referenced by with the RESPONSIVE_PRIORITY_MEDIUM + * constant. Themes may hide columns with one of these two classes on + * narrow viewports to save horizontal space. * - 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 +1853,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 +1863,15 @@ function theme_table($variables) { // This is needed to target tables constructed by this function. $attributes['class'][] = 'sticky-enabled'; } + // If the table has headers and it should react responsively to columns hidden + // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM + // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors. + 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,13 +1928,28 @@ 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. Only the header + // cells for a column are marked up with the responsive classes by a + // module developer or themer. The responsive classes on the header cells + // must be transferred to the content cells. + if (!empty($cell['class']) && is_array($cell['class'])) { + if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) { + $responsive[$i] = RESPONSIVE_PRIORITY_MEDIUM; + } + elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) { + $responsive[$i] = RESPONSIVE_PRIORITY_LOW; + } + } $cell = tablesort_header($cell, $header, $ts); $output .= _theme_table_cell($cell, TRUE); } @@ -1944,7 +1993,19 @@ 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 RESPONSIVE_PRIORITY_LOW/RESPONSIVE_PRIORITY_MEDIUM + // 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"; @@ -2865,7 +2926,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/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..1eb1c37 --- /dev/null +++ b/core/misc/tableresponsive.js @@ -0,0 +1,144 @@ +(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])); + } + } + } +}; + +/** + * The TableResponsive object optimizes table presentation for all screen sizes. + * + * 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 = $(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.tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility')) + .trigger('resize.tableresponsive'); +} + +/** + * Extend the TableResponsive function with a list of managed tables. + */ +$.extend(TableResponsive, { + tables: [] +}); + +/** + * Associates an action link with the table that will show hidden columns. + * + * Columns are assumed to be hidden if their header has the class priority-low + * or priority-medium. + */ +$.extend(TableResponsive.prototype, { + eventhandlerEvaluateColumnVisibility: function (e) { + var sticky = +this.$link.data('sticky'); + var hiddenLength = this.$headers.filter('.priority-medium:hidden, .priority-low: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'); + } + }, + // Toggle the visibility of columns classed with either 'priority-low' or + // 'priority-medium'. + eventhandlerToggleColumns: function (e) { + e.preventDefault(); + var self = this; + var $hiddenHeaders = this.$headers.filter('.priority-medium:hidden, .priority-low: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).data('sticky', 1); + } + // Hide revealed columns. + else { + this.$revealedCells.hide(); + // Strip the 'display:none' declaration from the style attributes of + // the table cells that .hide() added. + this.$revealedCells.each(function (index, element) { + var $cell = $(this); + var properties = $cell.attr('style').split(';'); + var newProps = []; + // The hide method 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).data('sticky', 0); + // Refresh the toggle link. + $(window).trigger('resize.tableresponsive'); + } + } +}); +// Make the TableResponsive object available in the Drupal namespace. +Drupal.TableResponsive = TableResponsive; + +})(jQuery); diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc index 6210cc5..4a3220f 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(RESPONSIVE_PRIORITY_MEDIUM)), + 'posted_in' => array('data' => t('Posted in'), 'field' => 'node_title', 'class' => array(RESPONSIVE_PRIORITY_LOW)), + 'changed' => array('data' => t('Updated'), 'field' => 'c.changed', 'sort' => 'desc', 'class' => array(RESPONSIVE_PRIORITY_LOW)), 'operations' => array('data' => t('Operations')), ); diff --git a/core/modules/dblog/dblog.admin.inc b/core/modules/dblog/dblog.admin.inc index 15f2354..012d01e 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(RESPONSIVE_PRIORITY_MEDIUM)), + array('data' => t('Date'), 'field' => 'w.wid', 'sort' => 'desc', 'class' => array(RESPONSIVE_PRIORITY_LOW)), t('Message'), - array('data' => t('User'), 'field' => 'u.name'), - array('data' => t('Operations')), + array('data' => t('User'), 'field' => 'u.name', 'class' => array(RESPONSIVE_PRIORITY_MEDIUM)), + array('data' => t('Operations'), 'class' => array(RESPONSIVE_PRIORITY_LOW)), ); $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 bb99234..4de8017 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(RESPONSIVE_PRIORITY_MEDIUM)), + 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..313dabe 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(RESPONSIVE_PRIORITY_MEDIUM), + ), + 'author' => array( + 'data' => t('Author'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ), + 'status' => array( + 'data' => t('Status'), + 'field' => 'n.status', + ), + 'changed' => array( + 'data' => t('Updated'), + 'field' => 'n.changed', + 'sort' => 'desc', + 'class' => array(RESPONSIVE_PRIORITY_LOW) + ,) ); if ($multilingual) { - $header['language_name'] = array('data' => t('Language'), 'field' => 'n.langcode'); + $header['language_name'] = array('data' => t('Language'), 'field' => 'n.langcode', 'class' => array(RESPONSIVE_PRIORITY_LOW)); } $header['operations'] = array('data' => t('Operations')); diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 2bf4343..6440210 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(RESPONSIVE_PRIORITY_MEDIUM)), + array('data' => t('Description'), 'class' => array(RESPONSIVE_PRIORITY_LOW)), 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 e5dc768..9d5a67a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1313,6 +1313,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 f9b1c23..180b9f1 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(RESPONSIVE_PRIORITY_LOW)), + 'roles' => array('data' => t('Roles'), 'class' => array(RESPONSIVE_PRIORITY_LOW)), + 'member_for' => array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc', 'class' => array(RESPONSIVE_PRIORITY_LOW)), + 'access' => array('data' => t('Last access'), 'field' => 'u.access', 'class' => array(RESPONSIVE_PRIORITY_LOW)), 'operations' => array('data' => t('Operations')), ); diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index 7532a4a..67f488c 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.priority-medium, + td.priority-medium { + display: none; + } +} +@media screen and (max-width:45em) { /* 720px */ + th.helpful, + td.helpful { + display: none; + } +} diff --git a/core/themes/seven/maintenance-page.tpl.php b/core/themes/seven/maintenance-page.tpl.php index c53c072..835aa6c 100644 --- a/core/themes/seven/maintenance-page.tpl.php +++ b/core/themes/seven/maintenance-page.tpl.php @@ -32,9 +32,14 @@
- - - +
diff --git a/core/themes/seven/style-rtl.css b/core/themes/seven/style-rtl.css index fd9f2ee..2d87a5f 100644 --- a/core/themes/seven/style-rtl.css +++ b/core/themes/seven/style-rtl.css @@ -71,11 +71,6 @@ ul.primary { /** * Page layout. */ -#page { - padding: 20px 0 40px 0; - margin-left: 40px; - margin-right: 40px; -} ul.links li, ul.inline li { padding-left: 1em; @@ -169,11 +164,11 @@ div.admin-options div.form-item { } /* Maintenance theming */ -body.in-maintenance #logo { +body.in-maintenance #sidebar-first { float: right; - padding: 0 0 0 20px; } body.in-maintenance #content { + float: left; padding-left: 20px; padding-right: 0; } diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css index 86f3fc5..d72c15a 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -314,12 +314,24 @@ ul.secondary li.active a.active { * Page layout. */ #page { - padding: 20px 0 40px 0; /* LTR */ - margin-right: 40px; /* LTR */ - margin-left: 40px; /* LTR */ background: #fff; - position: relative; color: #333; + margin-left: 0.8125em; + margin-right: 0.8125em; + padding: 20px 0 40px 0; + position: relative; +} +@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; + } } ul.links li, ul.inline li { @@ -477,6 +489,23 @@ tr td:last-child { /** + * Responsive tables. + */ +@media screen and (max-width:28.125em) { /* 450px */ + th.priority-low, + td.priority-low, + th.priority-medium, + td.priority-medium { + display: none; + } +} +@media screen and (max-width:45em) { /* 720px */ + th.priority-low, + td.priority-low { + display: none; + } +} +/** * Fieldsets. * * Fieldset legends are displayed like containers in Seven. However, several @@ -786,24 +815,29 @@ div.admin-options div.form-item { } /* Maintenance theming */ -body.in-maintenance #logo { - float: left; - padding: 0 20px 0 0; +body.in-maintenance #sidebar-first { + float: left; /* LTR */ + max-width: 200px; + width: 25%; } body.in-maintenance #content { - padding-right: 20px; /* LTR */ + float: right; /* LTR */ + max-width: 550px; clear: none; + width: 72%; } body.in-maintenance #page { overflow: auto; - max-width: 730px; + max-width: 770px; margin: 0 auto; - padding: 2em 20px 40px; + padding-top: 2em; + width: 90%; } body.in-maintenance #branding h1 { - max-width: 730px; + max-width: 770px; margin: 0 auto; float: none; + width: 90%; } body.in-maintenance .form-radios .form-type-radio { padding: 2px 0; @@ -820,6 +854,14 @@ body.in-maintenance #logo { margin-bottom: 1.5em; max-width: 180px; } +@media all and (max-width: 768px) { + body.in-maintenance #sidebar-first, + body.in-maintenance #content { + float: left; + max-width: none; + width: auto; + } +} ol.task-list { margin-left: 0; /* LTR */ list-style-type: none; @@ -838,6 +880,11 @@ ol.task-list li.done { background: transparent url(images/task-check.png) no-repeat 0 50%; color: green; } +@media all and (max-width: 768px) { + ol.task-list li { + float: left; + } +} /* Overlay theming */ .overlay #branding { diff --git a/core/themes/stark/css/layout.css b/core/themes/stark/css/layout.css index a31a774..f7160cb 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.priority-low, + td.priority-low, + th.priority-medium, + td.priority-medium { + display: none; + } +} +@media screen and (max-width:45em) { /* 720px */ + th.priority-low, + td.priority-low { + display: none; + } +}