diff --git a/core/modules/breakpoint/breakpoint.info b/core/modules/breakpoint/breakpoint.info new file mode 100644 index 0000000..fee160f --- /dev/null +++ b/core/modules/breakpoint/breakpoint.info @@ -0,0 +1,7 @@ +name = Breakpoint +description = Manage breakpoints and breakpoint groups for responsive designs. +package = Core +version = VERSION +core = 8.x + +dependencies[] = config diff --git a/core/modules/breakpoint/breakpoint.module b/core/modules/breakpoint/breakpoint.module new file mode 100644 index 0000000..837035b --- /dev/null +++ b/core/modules/breakpoint/breakpoint.module @@ -0,0 +1,281 @@ +status) { + $theme_breakpoints = breakpoint_get_theme_breakpoint_list($theme_key); + if (!empty($theme_breakpoints)) { + $weight = 0; + // Build a breakpoint group for each theme. + $breakpoint_group = new BreakpointGroup(); + $breakpoint_group->id = $theme_key; + $breakpoint_group->label = $themes[$theme_key]->info['name']; + $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_THEME; + foreach ($theme_breakpoints as $name => $mediaQuery) { + $breakpoint = new Breakpoint; + $breakpoint->name = $name; + $breakpoint->label = ucfirst($name); + $breakpoint->mediaQuery = $mediaQuery; + $breakpoint->source = $theme_key; + $breakpoint->sourceType = Breakpoint::SOURCE_TYPE_THEME; + $breakpoint->status = TRUE; + $breakpoint->weight = $weight++; + $breakpoint->save(); + $breakpoint_group->breakpoints[$breakpoint->id()] = $breakpoint; + } + $breakpoint_group->save(); + + $uri = $breakpoint_group->uri(); + if ($uri) { + $uri_options = $uri; + unset($uri_options['path']); + $uri = $uri['path']; + } + $message = t('The breakpoints from theme %theme are imported.', array( + '%theme' => check_plain($themes[$theme_key]->info['name']), + )); + if (module_exists('breakpoint_ui') && $uri) { + $message .= '

' . l(t('A new breakpoint group is created for theme %theme.', array( + '%theme' => check_plain($themes[$theme_key]->info['name']), + )), $uri, $uri_options); + } + drupal_set_message($message, 'status'); + } + } + } +} + +/** + * Implements hook_themes_disabled(). + * + * Remove breakpoints from all disabled themes. + * + * @param array $theme_list + * An array of theme names. + */ +function breakpoint_themes_disabled($theme_list) { + $breakpoint_groups = entity_load_multiple('breakpoint_group', $theme_list); + foreach ($breakpoint_groups as $breakpoint_group) { + $breakpoint_group->delete(); + // delete all breakpoints defined by this theme. + $names = drupal_container()->get('config.storage')->listAll('breakpoints.breakpoint.' . Breakpoint::SOURCE_TYPE_THEME . '.' . $breakpoint_group->id() . '.'); + $entity_info = entity_get_info('breakpoint'); + + foreach ($names as &$name) { + $name = substr($name, strlen($entity_info['config prefix']) + 1); + } + $breakpoints = entity_load_multiple('breakpoint', $names); + + foreach ($breakpoints as $breakpoint) { + $breakpoint->delete(); + } + } +} + +/** + * Load general settings. + */ +function breakpoint_settings() { + $config = config('breakpoint'); + if ($config->isNew()) { + return FALSE; + } + return (object)$config->get(); +} + +/** + * Save multipliers to settings. + * + * @param array $multipliers + * array containing multipliers. + */ +function breakpoint_settings_save_multipliers($multipliers) { + $config = config('breakpoint'); + $config->set('multipliers', $multipliers); + $config->save(); +} + +/** + * Reload breakpoint groups as they were defined in the theme. + * + * @param string $theme_key + * The name of the theme. + * + * @return BreakpointGroup + * Returns a BreakpointGroup containing the breakpoints defined by the theme. + */ +function breakpoint_group_reload_from_theme($theme_key) { + // Clear caches so theme info is fresh. + system_rebuild_theme_data(); + drupal_theme_rebuild(); + + $themes = list_themes(); + if ($themes[$theme_key]->status) { + $theme_breakpoints = breakpoint_get_theme_breakpoint_list($theme_key); + if (!empty($theme_breakpoints)) { + $weight = 0; + // Build a group for the theme. + $breakpoint_group = new BreakpointGroup(); + $breakpoint_group->id = $theme_key; + $breakpoint_group->label = $themes[$theme_key]->info['name']; + $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_THEME; + foreach ($theme_breakpoints as $name => $mediaQuery) { + $breakpoint = new Breakpoint; + $breakpoint->name = $name; + $breakpoint->label = ucfirst($name); + $breakpoint->mediaQuery = $mediaQuery; + $breakpoint->source = $theme_key; + $breakpoint->sourceType = Breakpoint::SOURCE_TYPE_THEME; + $breakpoint->status = TRUE; + $breakpoint->weight = $weight++; + $breakpoint->save(); + $breakpoint_group->breakpoints[$breakpoint->getConfigName()] = $breakpoint; + } + } + return $breakpoint_group; + } + return FALSE; +} + +/** + * Get a list of available breakpoints from a specified theme. + * + * @param $theme_key + * The name of the theme. + * + * @return + * An array of breakpoints in the form $breakpoint['name'] = 'media query'. + */ +function breakpoint_get_theme_breakpoint_list($theme_key) { + $themes = list_themes(); + if (!isset($themes[$theme_key])) { + return array(); + } + + $config = config($theme_key . '.breakpoints'); + if ($config) { + return $config->get(); + } + return array(); +} + +/** + * Implements hook_entity_info(). + */ +function breakpoint_entity_info() { + // Breakpoint. + $types['breakpoint'] = array( + 'label' => 'Breakpoint', + 'entity class' => 'Drupal\breakpoint\Breakpoint', + 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController', + 'config prefix' => 'breakpoint.breakpoint', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + ); + + // Breakpoint group. + $types['breakpoint_group'] = array( + 'label' => 'Breakpoint group', + 'entity class' => 'Drupal\breakpoint\BreakpointGroup', + 'controller class' => 'Drupal\breakpoint\BreakpointGroupController', + 'config prefix' => 'breakpoint.breakpoint_group', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + ); + + return $types; +} + +/** + * Load one breakpoint group by its identifier. + * + * @param int $id + * The id of the breakpoint group to load. + * + * @return Drupal\Core\Entity\EntityInterface + * The entity object, or FALSE if there is no entity with the given id. + */ +function breakpoint_group_load($id) { + return entity_load('breakpoint_group', $id); +} + +/** + * Load all breakpoint groups. + * + * @return array + * An array of entity objects indexed by their ids. + */ +function breakpoint_group_load_all() { + $breakpoint_groups = entity_load_multiple('breakpoint_group'); + return $breakpoint_groups; +} + +/** + * Load one breakpoint by its identifier. + * + * @param int $id + * The id of the breakpoint to load. + * + * @return Drupal\Core\Entity\EntityInterface + * The entity object, or FALSE if there is no entity with the given id. + */ +function breakpoint_load($id) { + return entity_load('breakpoint', $id); +} + +/** + * Load all breakpoints. + * + * @return array + * An array of entity objects indexed by their ids. + */ +function breakpoint_load_all() { + $breakpoints = entity_load_multiple('breakpoint'); + return $breakpoints; +} + +/** + * Load all breakpoint groups as select options. + */ +function breakpoint_group_select_options() { + $options = array(); + $breakpoint_groups = breakpoint_group_load_all(); + foreach ($breakpoint_groups as $breakpoint_group) { + $options[$breakpoint_group->id()] = $breakpoint_group->label(); + } + return $options; +} diff --git a/core/modules/breakpoint/config/breakpoint.yml b/core/modules/breakpoint/config/breakpoint.yml new file mode 100644 index 0000000..0ba703d --- /dev/null +++ b/core/modules/breakpoint/config/breakpoint.yml @@ -0,0 +1,2 @@ +multipliers: [1x, 1.5x, 2x] + diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php new file mode 100644 index 0000000..671274c --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php @@ -0,0 +1,294 @@ +id)) { + $this->id = $this->buildConfigName(); + } + if (empty($this->label)) { + $this->label = ucfirst($this->name); + } + if (!$this->isValid()) { + throw new Exception(t('Invalid media query detected.')); + } + return parent::save(); + } + + /** + * Get config name. + */ + public function getConfigName() { + return $this->sourceType + . '.' . $this->source + . '.' . $this->name; + } + + /** + * Build config name. + */ + protected function buildConfigName() { + // Check for illegal values in breakpoint source type. + if (!in_array($this->sourceType, array( + Breakpoint::SOURCE_TYPE_CUSTOM, + Breakpoint::SOURCE_TYPE_MODULE, + Breakpoint::SOURCE_TYPE_THEME) + )) { + throw new Exception( + t( + "Expected one of '@custom', '@module' or '@theme' for breakpoint sourceType property but got '@sourcetype'.", + array( + '@custom' => Breakpoint::SOURCE_TYPE_CUSTOM, + '@module' => Breakpoint::SOURCE_TYPE_MODULE, + '@theme' => Breakpoint::SOURCE_TYPE_THEME, + '@sourcetype' => $this->sourceType, + ) + ) + ); + } + // Check for illegal characters in breakpoint source. + if (preg_match('/[^a-z_]+/', $this->source)) { + throw new Exception(t("Invalid value '@source' for breakpoint source property. Breakpoint source property can only contain lowercase letters and underscores.", array('@source' => $this->source))); + } + // Check for illegal characters in breakpoint names. + if (preg_match('/[^0-9a-z_\-]/', $this->name)) { + throw new Exception(t("Invalid value '@name' for breakpoint name property. Breakpoint name property can only contain lowercase alphanumeric characters, underscores (_), and hyphens (-).", array('@name' => $this->name))); + } + return $this->sourceType + . '.' . $this->source + . '.' . $this->name; + } + + /** + * Shortcut function to enable a breakpoint and save it. + * + * @see breakpoint_action_confirm_submit() + */ + public function enable() { + if (!$this->status) { + $this->status = 1; + $this->save(); + } + } + + /** + * Shortcut function to disable a breakpoint and save it. + * + * @see breakpoint_action_confirm_submit() + */ + public function disable() { + if ($this->status) { + $this->status = 0; + $this->save(); + } + } + + /** + * Check if the mediaQuery is valid. + * + * @see isValidMediaQuery() + */ + public function isValid() { + return $this::isValidMediaQuery($this->mediaQuery); + } + + /** + * Check if a mediaQuery is valid. + * + * @see http://www.w3.org/TR/css3-mediaqueries/ + * @see http://www.w3.org/Style/CSS/Test/MediaQueries/20120229/reports/implement-report.html + */ + public static function isValidMediaQuery($media_query) { + $media_features = array( + 'width' => 'length', 'min-width' => 'length', 'max-width' => 'length', + 'height' => 'length', 'min-height' => 'length', 'max-height' => 'length', + 'device-width' => 'length', 'min-device-width' => 'length', 'max-device-width' => 'length', + 'device-height' => 'length', 'min-device-height' => 'length', 'max-device-height' => 'length', + 'orientation' => array('portrait', 'landscape'), + 'aspect-ratio' => 'ratio', 'min-aspect-ratio' => 'ratio', 'max-aspect-ratio' => 'ratio', + 'device-aspect-ratio' => 'ratio', 'min-device-aspect-ratio' => 'ratio', 'max-device-aspect-ratio' => 'ratio', + 'color' => 'integer', 'min-color' => 'integer', 'max-color' => 'integer', + 'color-index' => 'integer', 'min-color-index' => 'integer', 'max-color-index' => 'integer', + 'monochrome' => 'integer', 'min-monochrome' => 'integer', 'max-monochrome' => 'integer', + 'resolution' => 'resolution', 'min-resolution' => 'resolution', 'max-resolution' => 'resolution', + 'scan' => array('progressive', 'interlace'), + 'grid' => 'integer', + ); + if ($media_query) { + // Strip new lines and trim. + $media_query = str_replace(array("\r", "\n"), ' ', trim($media_query)); + + // Remove comments /* ... */. + $media_query = preg_replace('/\/\*[\s\S]*?\*\//', '', $media_query); + + // Check mediaQuery_list: S* [mediaQuery [ ',' S* mediaQuery ]* ]? + $parts = explode(',', $media_query); + foreach ($parts as $part) { + // Split on ' and ' + $query_parts = explode(' and ', trim($part)); + $media_type_found = FALSE; + foreach ($query_parts as $query_part) { + $matches = array(); + // Check expression: '(' S* media_feature S* [ ':' S* expr ]? ')' S* + if (preg_match('/^\(([\w\-]+)(:\s?([\w\-]+))?\)/', trim($query_part), $matches)) { + // Single expression. + if (isset($matches[1]) && !isset($matches[2])) { + if (!array_key_exists($matches[1], $media_features)) { + return FALSE; + } + } + // Full expression. + elseif (isset($matches[3]) && !isset($matches[4])) { + $value = trim($matches[3]); + if (!array_key_exists($matches[1], $media_features)) { + return FALSE; + } + if (is_array($media_features[$matches[1]])) { + // Check if value is allowed. + if (!array_key_exists($value, $media_features[$matches[1]])) { + return FALSE; + } + } + else { + switch ($media_features[$matches[1]]) { + case 'length': + $length_matches = array(); + if (preg_match('/^(\-)?(\d+)?((?:|em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|dpi|dpcm))$/i', trim($value), $length_matches)) { + // Only -0 is allowed. + if ($length_matches[1] === '-' && $length_matches[2] !== '0') { + return FALSE; + } + // If there's a unit, a number is needed as well. + if ($length_matches[2] === '' && $length_matches[3] !== '') { + return FALSE; + } + } + else { + return FALSE; + } + break; + } + } + } + } + // Check [ONLY | NOT]? S* media_type + elseif (preg_match('/((?:only|not)?\s?)([\w\-]+)$/i', trim($query_part), $matches)) { + if ($media_type_found) { + throw new Exception(t('Only when media type allowed.')); + } + $media_type_found = TRUE; + } + else { + throw new Exception(t("Invalid value '@query_part' for breakpoint media query property.", array('@query_part' => $query_part))); + } + } + } + return TRUE; + } + return FALSE; + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php new file mode 100644 index 0000000..afe411e --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php @@ -0,0 +1,124 @@ +loadAllBreakpoints(); + } + + /** + * Overrides Drupal\Core\Entity::save(). + */ + public function save() { + // Only save the keys, but return the full objects. + $this->breakpoints = array_keys($this->breakpoints); + parent::save(); + $this->loadAllBreakpoints(); + } + + /** + * Override and save a breakpoint group. + */ + public function override() { + return entity_get_controller($this->entityType)->override($this); + } + + /** + * Revert a breakpoint group after it has been overridden. + */ + public function revert() { + return entity_get_controller($this->entityType)->revert($this); + } + + /** + * Implements EntityInterface::createDuplicate(). + */ + public function createDuplicate() { + $duplicate = new BreakpointGroup(); + $duplicate->id = ''; + $duplicate->label = t('Clone of') . ' ' . $this->label(); + $duplicate->breakpoints = $this->breakpoints; + return $duplicate; + } + + /** + * Load all breakpoints, remove non-existing ones. + */ + protected function loadAllBreakpoints() { + $breakpoints = $this->breakpoints; + $this->breakpoints = array(); + foreach ($breakpoints as $breakpoint_id) { + $breakpoint = breakpoint_load($breakpoint_id); + if ($breakpoint) { + $this->breakpoints[$breakpoint_id] = $breakpoint; + } + } + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroupController.php b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroupController.php new file mode 100644 index 0000000..217cf7a --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroupController.php @@ -0,0 +1,59 @@ +sourceType == Breakpoint::SOURCE_TYPE_THEME) { + return FALSE; + } + foreach ($breakpoint_group->breakpoints as $key => $breakpoint) { + if ($breakpoint->sourceType == Breakpoint::SOURCE_TYPE_THEME && $breakpoint->source == $breakpoint_group->id()) { + $new_breakpoint = $breakpoint->createDuplicate(); + $new_breakpoint->id = ''; + $new_breakpoint->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + $new_breakpoint->save(); + + // Remove old one, add new one. + unset($breakpoint_group->breakpoints[$key]); + $breakpoint_group->breakpoints[$new_breakpoint->id] = $new_breakpoint; + } + } + $breakpoint_group->overridden = TRUE; + $breakpoint_group->save(); + return $breakpoint_group; + } + + /** + * Revert a breakpoint group after it has been overridden. + */ + public function revert(BreakpointGroup $breakpoint_group) { + if (!$breakpoint_group->overridden || !$breakpoint_group->sourceType == Breakpoint::SOURCE_TYPE_THEME) { + return FALSE; + } + + // Reload all breakpoints from theme. + $reloaded_set = breakpoint_group_reload_from_theme($breakpoint_group->id()); + if ($reloaded_set) { + $breakpoint_group->breakpoints = $reloaded_set->breakpoints; + $breakpoint_group->overridden = FALSE; + $breakpoint_group->save(); + } + return $breakpoint_group; + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointApiTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointApiTest.php new file mode 100644 index 0000000..41b7893 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointApiTest.php @@ -0,0 +1,90 @@ + 'Breakpoint general API functions', + 'description' => 'Test general API functions of the breakpoint module.', + 'group' => 'Breakpoint', + ); + } + + /** + * Test Breakpoint::buildConfigName(). + */ + public function testConfigName() { + $breakpoint = new Breakpoint( + array( + 'label' => drupal_strtolower($this->randomName()), + 'source' => 'custom_module', + // Try an invalid sourceType. + 'sourceType' => 'oops', + ) + ); + + try { + $breakpoint->save(); + } + catch (Exception $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid sourceType is entered.')); + $this->assertEqual((string) $breakpoint->id(), '', t('breakpoint_config_name: No id is set when an invalid sourceType is entered.')); + + // Try an invalid source. + $breakpoint->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + $breakpoint->source = 'custom*_module source'; + $exception = FALSE; + try { + $breakpoint->save(); + } + catch (Exception $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid source is entered.')); + $this->assertEqual((string) $breakpoint->id(), '', t('breakpoint_config_name: No id is set when an invalid sourceType is entered.')); + + // Try an invalid name (make sure there is at least once capital letter). + $breakpoint->source = 'custom_module'; + $breakpoint->name = drupal_ucfirst($this->randomName()); + $exception = FALSE; + try { + $breakpoint->save(); + } + catch (Exception $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid name is entered.')); + $this->assertEqual((string) $breakpoint->id(), '', t('breakpoint_config_name: No id is set when an invalid sourceType is entered.')); + + // Try a valid breakpoint. + $breakpoint->name = drupal_strtolower($this->randomName()); + $breakpoint->mediaQuery = 'all'; + $exception = FALSE; + try { + $breakpoint->save(); + } + catch (Exception $e) { + $exception = TRUE; + } + $this->assertFalse($exception, t('breakpoint_config_name: No exception is thrown when a valid breakpoint is passed.')); + $this->assertEqual($breakpoint->id(), Breakpoint::SOURCE_TYPE_CUSTOM . '.custom_module.' . $breakpoint->name, t('breakpoint_config_name: A id is set when a valid breakpoint is passed.')); + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php new file mode 100644 index 0000000..ddfc683 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php @@ -0,0 +1,63 @@ + 'Breakpoint CRUD operations', + 'description' => 'Test creation, loading, updating, deleting of breakpoints.', + 'group' => 'Breakpoint', + ); + } + + /** + * Test CRUD operations for breakpoints. + */ + public function testBreakpointCrud() { + // Add a breakpoint with minimum data only. + $values = array( + 'label' => drupal_strtolower($this->randomName()), + 'mediaQuery' => '(min-width: 600px)', + ); + + $breakpoint = new Breakpoint($values); + $breakpoint->save(); + + $this->verifyBreakpoint($breakpoint); + + // Test breakpoint_load_all + $all_breakpoints = breakpoint_load_all(); + $config_name = $breakpoint->getConfigName(); + $this->assertTrue(isset($all_breakpoints[$config_name]), t('breakpoint_load_all: New breakpoint is present when loading all breakpoints.')); + $this->verifyBreakpoint($breakpoint, $all_breakpoints[$config_name]); + + // Update the breakpoint. + $breakpoint->weight = 1; + $breakpoint->multipliers['2x'] = '2x'; + $breakpoint->save(); + $this->verifyBreakpoint($breakpoint); + + // Disable the breakpoint. + $breakpoint->disable(); + $this->verifyBreakpoint($breakpoint); + + // Delete the breakpoint. + $breakpoint->delete(); + $this->assertFalse(breakpoint_load($config_name), t('breakpoint_load: Loading a deleted breakpoint returns false.'), t('Breakpoints API')); + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php new file mode 100644 index 0000000..0510da1 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php @@ -0,0 +1,75 @@ + 'Breakpoint group CRUD operations', + 'description' => 'Test creation, loading, updating, deleting of breakpoint groups.', + 'group' => 'Breakpoint', + ); + } + + /** + * Test CRUD operations for breakpoint groups. + */ + public function testBreakpointGroupCrud() { + // Add breakpoints. + $breakpoints = array(); + for ($i = 0; $i <= 3; $i++) { + $width = ($i + 1) * 200; + $values = array( + 'name' => drupal_strtolower($this->randomName()), + 'weight' => $i, + 'mediaQuery' => "(min-width: {$width}px)", + ); + $breakpoint = new Breakpoint($values); + $breakpoint->save(); + $breakpoints[$breakpoint->id()] = $breakpoint; + } + // Add a breakpoint group with minimum data only. + $label = $this->randomName(); + $values = array( + 'label' => $label, + 'id' => drupal_strtolower($label), + ); + + $group = new BreakpointGroup($values); + $group->save(); + $this->verifyBreakpointGroup($group); + + // Update the breakpoint group. + $group->breakpoints = array_keys($breakpoints); + $group->save(); + $this->verifyBreakpointGroup($group); + + // Duplicate the breakpoint group. + $new_set = new BreakpointGroup(); + $new_set->label = t('Clone of') . ' ' . $group->label(); + $new_set->id = ''; + $new_set->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + $new_set->breakpoints = $group->breakpoints; + $duplicated_set = $group->createDuplicate(); + $this->verifyBreakpointGroup($duplicated_set, $new_set); + + // Delete the breakpoint group. + $group->delete(); + $this->assertFalse(breakpoint_group_load($group->id), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API')); + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php new file mode 100644 index 0000000..a644452 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php @@ -0,0 +1,60 @@ +id) : $compare_set; + + foreach ($properties as $property) { + $t_args = array( + '%group' => $group->label(), + '%property' => $property, + ); + if (is_array($compare_set->{$property})) { + $this->assertEqual(array_keys($compare_set->{$property}), array_keys($group->{$property}), t('breakpoint_group_load: Proper %property for breakpoint group %group.', $t_args), $assert_set); + } + else { + $this->assertEqual($compare_set->{$property}, $group->{$property}, t('breakpoint_group_load: Proper %property . for breakpoint group %group.', $t_args), $assert_set); + } + } + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php new file mode 100644 index 0000000..68438dc --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php @@ -0,0 +1,125 @@ + 'Breakpoint media query tests', + 'description' => 'Test validation of media queries.', + 'group' => 'Breakpoint', + ); + } + + /** + * Test valid media queries. + */ + public function testValidMediaQueries() { + $media_queries = array( + // Bartik breakpoints. + '(min-width: 0px)', + 'all and (min-width: 560px) and (max-width:850px)', + 'all and (min-width: 851px)', + // Seven breakpoints. + '(min-width: 0em)', + 'screen and (min-width: 40em)', + // Stark breakpoints. + '(min-width: 0px)', + 'all and (min-width: 480px) and (max-width: 959px)', + 'all and (min-width: 960px)', + '(orientation)', + 'all and (orientation)', + 'not all and (orientation)', + 'only all and (orientation)', + 'screen and (width)', + 'screen and (width: 0)', + 'screen and (width: 0px)', + 'screen and (width: 0em)', + 'screen and (min-width: -0)', + 'screen and (max-width: 0)', + 'screen and (min-width)', + // Multiline and comments. + 'screen and /* this is a comment */ (min-width)', + "screen\nand /* this is a comment */ (min-width)", + "screen\n\nand /* this is\n a comment */ (min-width)", + ); + + foreach ($media_queries as $media_query) { + try { + $this->assertTrue(Breakpoint::isValidMediaQuery($media_query), $media_query . ' is valid.'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $media_query . ' is valid.'); + } + } + } + + /** + * Test invalid media queries. + */ + public function testInvalidMediaQueries() { + $media_queries = array( + 'not (orientation)', + 'only (orientation)', + 'all and not all', + 'screen and (width: 0xx)', + 'screen and (width: -8xx)', + 'screen and (width: -xx)', + 'screen and (width: xx)', + 'screen and (width: px)', + 'screen and (width: -8px)', + 'screen and (width: -0.8px)', + 'screen and (height: 0xx)', + 'screen and (height: -8xx)', + 'screen and (height: -xx)', + 'screen and (height: xx)', + 'screen and (height: px)', + 'screen and (height: -8px)', + 'screen and (height: -0.8px)', + 'screen and (device-width: 0xx)', + 'screen and (device-width: -8xx)', + 'screen and (device-width: -xx)', + 'screen and (device-width: xx)', + 'screen and (device-width: px)', + 'screen and (device-width: -8px)', + 'screen and (device-width: -0.8px)', + 'screen and (device-height: 0xx)', + 'screen and (device-height: -8xx)', + 'screen and (device-height: -xx)', + 'screen and (device-height: xx)', + 'screen and (device-height: px)', + 'screen and (device-height: -8px)', + 'screen and (device-height: -0.8px)', + 'screen and (min-orientation)', + 'screen and (max-orientation)', + 'screen and (min-orientation: landscape)', + 'screen and (max-orientation: landscape)', + 'screen and (orientation: bogus)', + '(orientation: bogus)', + ); + + foreach ($media_queries as $media_query) { + try { + $this->assertFalse(Breakpoint::isValidMediaQuery($media_query), $media_query . ' is not valid.'); + } + catch (Exception $e) { + $this->assertTrue(TRUE, $media_query . ' is not valid.'); + } + } + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php new file mode 100644 index 0000000..ba1ee07 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php @@ -0,0 +1,56 @@ +getConfigName()) : $compare_breakpoint; + foreach ($properties as $property) { + $t_args = array( + '%breakpoint' => $breakpoint->label(), + '%property' => $property, + ); + $this->assertEqual($compare_breakpoint->{$property}, $breakpoint->{$property}, t('breakpoint_load: Proper %property for breakpoint %breakpoint.', $t_args), $assert_group); + } + } +} diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php new file mode 100644 index 0000000..39000e3 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php @@ -0,0 +1,90 @@ + 'Breakpoint Theme functionality', + 'description' => 'Thoroughly test the breakpoints provided by a theme.', + 'group' => 'Breakpoint', + ); + } + + /** + * Drupal\simpletest\WebTestBase\setUp(). + */ + public function setUp() { + parent::setUp(); + theme_enable(array('breakpoint_test_theme')); + } + + /** + * Test the breakpoints provided by a theme. + */ + public function testThemeBreakpoints() { + // Verify the breakpoint group for breakpoint_test_theme was created. + $breakpoint_group_obj = new BreakpointGroup(); + $breakpoint_group_obj->label = 'Breakpoint test theme'; + $breakpoint_group_obj->id = 'breakpoint_test_theme'; + $breakpoint_group_obj->sourceType = Breakpoint::SOURCE_TYPE_THEME; + $breakpoint_group_obj->breakpoints = array( + 'theme.breakpoint_test_theme.mobile' => array(), + 'theme.breakpoint_test_theme.narrow' => array(), + 'theme.breakpoint_test_theme.wide' => array(), + 'theme.breakpoint_test_theme.tv' => array(), + ); + $breakpoint_group_obj->overridden = 0; + + // Verify we can load this breakpoint defined by the theme. + $this->verifyBreakpointGroup($breakpoint_group_obj); + + // Override the breakpoints. + $overridden_set = clone $breakpoint_group_obj; + $breakpoint_group = breakpoint_group_load('breakpoint_test_theme'); + $breakpoint_group = $breakpoint_group->override(); + + // Verify the group is overridden. + $overridden_set->breakpoints = array( + 'custom.breakpoint_test_theme.mobile' => array(), + 'custom.breakpoint_test_theme.narrow' => array(), + 'custom.breakpoint_test_theme.wide' => array(), + 'custom.breakpoint_test_theme.tv' => array(), + ); + $overridden_set->overridden = 1; + $this->verifyBreakpointGroup($overridden_set); + + // Revert the breakpoint group. + $breakpoint_group = breakpoint_group_load('breakpoint_test_theme'); + $breakpoint_group = $breakpoint_group->revert(); + + // Verify the breakpoint group has its original values again when loaded. + $this->verifyBreakpointGroup($breakpoint_group_obj); + + // Disable the test theme and verify the breakpoint group is deleted. + theme_disable(array('breakpoint_test_theme')); + $this->assertFalse(breakpoint_group_load('breakpoint_test_theme'), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API')); + } +} diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.info b/core/modules/breakpoint/tests/breakpoint_theme_test.info new file mode 100644 index 0000000..f934419 --- /dev/null +++ b/core/modules/breakpoint/tests/breakpoint_theme_test.info @@ -0,0 +1,5 @@ +name = Breakpoint theme test +description = Test breakpoints provided by themes +package = Other +core = 8.x +hidden = TRUE diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.module b/core/modules/breakpoint/tests/breakpoint_theme_test.module new file mode 100644 index 0000000..50b5ff0 --- /dev/null +++ b/core/modules/breakpoint/tests/breakpoint_theme_test.module @@ -0,0 +1,13 @@ +loadBreakpointGroup(); + $this->loadAllMappings(); + } + + /** + * Overrides Drupal\Core\Entity::save(). + */ + public function save() { + // Only save the keys, but return the full objects. + if (isset($this->breakpointGroup) && is_object($this->breakpointGroup)) { + $this->breakpointGroup = $this->breakpointGroup->id(); + } + parent::save(); + $this->loadBreakpointGroup(); + $this->loadAllMappings(); + } + + /** + * Implements EntityInterface::createDuplicate(). + */ + public function createDuplicate() { + $duplicate = new PictureMapping(); + $duplicate->id = ''; + $duplicate->label = t('Clone of') . ' ' . $this->label(); + $duplicate->mappings = $this->mappings; + return $duplicate; + } + + /** + * Load breakpointGroup. + */ + protected function loadBreakpointGroup() { + if ($this->breakpointGroup) { + $breakpoint_group = breakpoint_group_load($this->breakpointGroup); + $this->breakpointGroup = $breakpoint_group; + } + } + + /** + * Load all mappings, remove non-existing ones. + */ + protected function loadAllMappings() { + $loaded_mappings = $this->mappings; + $this->mappings = array(); + if ($this->breakpointGroup) { + foreach ($this->breakpointGroup->breakpoints as $breakpoint_id => $breakpoint) { + // Get the mapping for the default multiplier. + $this->mappings[$breakpoint_id]['1x'] = ''; + if (isset($loaded_mappings[$breakpoint_id]['1x'])) { + $this->mappings[$breakpoint_id]['1x'] = $loaded_mappings[$breakpoint_id]['1x']; + } + + // Get the mapping for the other multipliers. + if (isset($breakpoint->multipliers) && !empty($breakpoint->multipliers)) { + foreach ($breakpoint->multipliers as $multiplier => $status) { + if ($status) { + $this->mappings[$breakpoint_id][$multiplier] = ''; + if (isset($loaded_mappings[$breakpoint_id][$multiplier])) { + $this->mappings[$breakpoint_id][$multiplier] = $loaded_mappings[$breakpoint_id][$multiplier]; + } + } + } + } + } + } + } +} diff --git a/core/modules/picture/lib/Drupal/picture/PictureMappingFormController.php b/core/modules/picture/lib/Drupal/picture/PictureMappingFormController.php new file mode 100644 index 0000000..d241524 --- /dev/null +++ b/core/modules/picture/lib/Drupal/picture/PictureMappingFormController.php @@ -0,0 +1,119 @@ +operation == 'duplicate') { + $picture_mapping = $picture_mapping->createDuplicate(); + $this->setEntity($picture_mapping, $form_state); + } + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#maxlength' => 255, + '#default_value' => $picture_mapping->label(), + '#description' => t("Example: 'Main content' or 'Sidebar'."), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $picture_mapping->id(), + '#machine_name' => array( + 'exists' => 'picture_mapping_load', + 'source' => array('label'), + ), + '#disabled' => (bool) $picture_mapping->id() && $this->operation != 'duplicate', + ); + $form['breakpointGroup'] = array( + '#type' => 'select', + '#title' => t('Breakpoint Group'), + '#default_value' => !empty($picture_mapping->breakpointGroup) ? $picture_mapping->breakpointGroup->id() : '', + '#options' => breakpoint_group_select_options(), + '#required' => TRUE, + ); + + $image_styles = image_style_options(TRUE); + foreach ($picture_mapping->mappings as $breakpoint_id => $mapping) { + foreach ($mapping as $multiplier => $image_style) { + $label = $multiplier . ' ' . $picture_mapping->breakpointGroup->breakpoints[$breakpoint_id]->name . ' [' . $picture_mapping->breakpointGroup->breakpoints[$breakpoint_id]->mediaQuery . ']'; + $form['mappings'][$breakpoint_id][$multiplier] = array( + '#type' => 'select', + '#title' => check_plain($label), + '#options' => $image_styles, + '#default_value' => $image_style, + ); + } + } + + $form['#tree'] = TRUE; + + return parent::form($form, $form_state, $picture_mapping); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + */ + protected function actions(array $form, array &$form_state) { + // Only includes a Save action for the entity, no direct Delete button. + return array( + 'submit' => array( + '#value' => t('Save'), + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submit'), + array($this, 'save'), + ), + ), + ); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $picture_mapping = $this->getEntity($form_state); + $picture_mapping->save(); + + watchdog('picture', 'Picture mapping @label saved.', array('@label' => $picture_mapping->label()), WATCHDOG_NOTICE); + drupal_set_message(t('Picture mapping %label saved.', array('%label' => $picture_mapping->label()))); + + $form_state['redirect'] = 'admin/config/media/picturemapping'; + } + +} diff --git a/core/modules/picture/lib/Drupal/picture/PictureMappingListController.php b/core/modules/picture/lib/Drupal/picture/PictureMappingListController.php new file mode 100644 index 0000000..6329f92 --- /dev/null +++ b/core/modules/picture/lib/Drupal/picture/PictureMappingListController.php @@ -0,0 +1,35 @@ +entityInfo['list path']; + $items = parent::hookMenu(); + + // Override the access callback. + $items[$path]['title'] = 'Picture Mappings'; + $items[$path]['description'] = 'Manage list of pictures.'; + $items[$path]['access callback'] = 'user_access'; + $items[$path]['access arguments'] = array('administer pictures'); + + return $items; + } + +} diff --git a/core/modules/picture/lib/Drupal/picture/Tests/PictureAdminUITest.php b/core/modules/picture/lib/Drupal/picture/Tests/PictureAdminUITest.php new file mode 100644 index 0000000..f47c3dd --- /dev/null +++ b/core/modules/picture/lib/Drupal/picture/Tests/PictureAdminUITest.php @@ -0,0 +1,141 @@ + 'Picture administration functionality', + 'description' => 'Thoroughly test the administrative interface of the picture module.', + 'group' => 'Picture', + ); + } + + /** + * Drupal\simpletest\WebTestBase\setUp(). + */ + public function setUp() { + parent::setUp(); + + // Create user. + $this->admin_user = $this->drupalCreateUser(array( + 'administer pictures', + )); + + $this->drupalLogin($this->admin_user); + + // Add breakpoint_group and breakpoints. + $breakpoint_group = new BreakpointGroup(); + $breakpoint_group->id = 'atestset'; + $breakpoint_group->label = 'A test set'; + $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + + $breakpoints = array(); + $breakpoint_names = array('small', 'medium', 'large'); + for ($i = 0; $i < 3; $i++) { + $breakpoint = new Breakpoint; + $breakpoint->name = $breakpoint_names[$i]; + $width = ($i + 1) * 200; + $breakpoint->mediaQuery = "(min-width: {$width}px)"; + $breakpoint->source = 'user'; + $breakpoint->sourceType = 'custom'; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => '2x', + ); + $breakpoint->save(); + $breakpoint_group->breakpoints[$breakpoint->id()] = $breakpoint; + } + $breakpoint_group->save(); + + } + + /** + * Test picture administration functionality. + */ + public function testPictureAdmin() { + // We start without any default mappings. + $this->drupalGet('admin/config/media/picturemapping'); + $this->assertText('There is no Picture mapping yet.'); + + // Add a new picture mapping, our breakpoint set should be selected. + $this->drupalGet('admin/config/media/picturemapping/add'); + $this->assertFieldByName('breakpointGroup', 'atestset'); + + // Create a new group. + $edit = array( + 'label' => 'Mapping One', + 'id' => 'mapping_one', + 'breakpointGroup' => 'atestset', + ); + $this->drupalPost('admin/config/media/picturemapping/add', $edit, t('Save')); + + // Check if the new group is created. + $this->assertResponse(200); + $this->drupalGet('admin/config/media/picturemapping'); + $this->assertNoText('There is no Picture mapping yet.'); + $this->assertText('Mapping One'); + $this->assertText('mapping_one'); + + // Edit the group. + $this->drupalGet('admin/config/media/picturemapping/mapping_one/edit'); + $this->assertFieldByName('label', 'Mapping One'); + $this->assertFieldByName('breakpointGroup', 'atestset'); + + // Check if the dropdows are present for the mappings. + $this->assertFieldByName('mappings[custom.user.small][1x]', ''); + $this->assertFieldByName('mappings[custom.user.small][2x]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x]', ''); + $this->assertFieldByName('mappings[custom.user.medium][2x]', ''); + $this->assertFieldByName('mappings[custom.user.large][1x]', ''); + $this->assertFieldByName('mappings[custom.user.large][2x]', ''); + + // Save mappings for 1x variant only. + $edit = array( + 'label' => 'Mapping One', + 'breakpointGroup' => 'atestset', + 'mappings[custom.user.small][1x]' => 'thumbnail', + 'mappings[custom.user.medium][1x]' => 'medium', + 'mappings[custom.user.large][1x]' => 'large', + ); + $this->drupalPost('admin/config/media/picturemapping/mapping_one/edit', $edit, t('Save')); + $this->drupalGet('admin/config/media/picturemapping/mapping_one/edit'); + $this->assertFieldByName('mappings[custom.user.small][1x]', 'thumbnail'); + $this->assertFieldByName('mappings[custom.user.small][2x]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x]', 'medium'); + $this->assertFieldByName('mappings[custom.user.medium][2x]', ''); + $this->assertFieldByName('mappings[custom.user.large][1x]', 'large'); + $this->assertFieldByName('mappings[custom.user.large][2x]', ''); + + // Delete the mapping. + $this->drupalGet('admin/config/media/picturemapping/mapping_one/delete'); + $this->drupalPost(NULL, array(), t('Delete')); + $this->drupalGet('admin/config/media/picturemapping'); + $this->assertText('There is no Picture mapping yet.'); + } + +} diff --git a/core/modules/picture/lib/Drupal/picture/Tests/PictureFieldDisplayTest.php b/core/modules/picture/lib/Drupal/picture/Tests/PictureFieldDisplayTest.php new file mode 100644 index 0000000..f911cad --- /dev/null +++ b/core/modules/picture/lib/Drupal/picture/Tests/PictureFieldDisplayTest.php @@ -0,0 +1,187 @@ + 'Picture field display tests', + 'description' => 'Test picture display formatter.', + 'group' => 'Picture', + ); + } + + /** + * Drupal\simpletest\WebTestBase\setUp(). + */ + public function setUp() { + parent::setUp(); + + // Create user. + $this->admin_user = $this->drupalCreateUser(array('administer pictures', 'access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles')); + $this->drupalLogin($this->admin_user); + + // Add breakpoint_group and breakpoints. + $breakpoint_group = new BreakpointGroup(); + $breakpoint_group->id = 'atestset'; + $breakpoint_group->label = 'A test set'; + $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + + $breakpoints = array(); + $breakpoint_names = array('small', 'medium', 'large'); + for ($i = 0; $i < 3; $i++) { + $breakpoint = new Breakpoint; + $breakpoint->name = $breakpoint_names[$i]; + $width = ($i + 1) * 200; + $breakpoint->mediaQuery = "(min-width: {$width}px)"; + $breakpoint->source = 'user'; + $breakpoint->sourceType = 'custom'; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => '2x', + ); + $breakpoint->save(); + $breakpoint_group->breakpoints[$breakpoint->id()] = $breakpoint; + } + $breakpoint_group->save(); + + // Add picture mapping. + $picture_mapping = new PictureMapping(); + $picture_mapping->id = 'mapping_one'; + $picture_mapping->label = 'Mapping One'; + $picture_mapping->breakpointGroup = 'atestset'; + $picture_mapping->save(); + $picture_mapping->mappings['custom.user.small']['1x'] = 'thumbnail'; + $picture_mapping->mappings['custom.user.medium']['1x'] = 'medium'; + $picture_mapping->mappings['custom.user.large']['1x'] = 'large'; + $picture_mapping->save(); + } + + /** + * Test picture formatters on node display for public files. + */ + public function testPictureFieldFormattersPublic() { + $this->_testPictureFieldFormatters('public'); + } + + /** + * Test picture formatters on node display for private files. + */ + public function testPictureFieldFormattersPrivate() { + // Remove access content permission from anonymous users. + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access content' => FALSE)); + $this->_testPictureFieldFormatters('private'); + } + + /** + * Test picture formatters on node display. + */ + public function _testPictureFieldFormatters($scheme) { + $field_name = strtolower($this->randomName()); + $this->createImageField($field_name, 'article', array('uri_scheme' => $scheme)); + // Create a new node with an image attached. + $test_image = current($this->drupalGetTestFiles('image')); + $nid = $this->uploadNodeImage($test_image, $field_name, 'article'); + $node = node_load($nid, TRUE); + + // Use the picture formatter. + $instance = field_info_instance('node', $field_name, 'article'); + $instance['display']['default']['type'] = 'picture'; + $instance['display']['default']['module'] = 'picture'; + + // Test that the default formatter is being used. + $image_uri = file_load($node->{$field_name}[LANGUAGE_NOT_SPECIFIED][0]['fid'])->uri; + $image_info = array( + 'uri' => $image_uri, + 'width' => 40, + 'height' => 20, + ); + $default_output = theme('image', $image_info); + $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.'); + + // Use the picture formatter linked to file formatter. + $instance = field_info_instance('node', $field_name, 'article'); + $instance['display']['default']['type'] = 'picture'; + $instance['display']['default']['module'] = 'picture'; + $instance['display']['default']['settings']['image_link'] = 'file'; + field_update_instance($instance); + $default_output = l(theme('image', $image_info), file_create_url($image_uri), array('html' => TRUE)); + $this->drupalGet('node/' . $nid); + $this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.'); + // Verify that the image can be downloaded. + $this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.'); + if ($scheme == 'private') { + // Only verify HTTP headers when using private scheme and the headers are + // sent by Drupal. + $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png', 'Content-Type header was sent.'); + $this->assertEqual($this->drupalGetHeader('Content-Disposition'), 'inline; filename="' . $test_image->filename . '"', 'Content-Disposition header was sent.'); + $this->assertTrue(strstr($this->drupalGetHeader('Cache-Control'),'private') !== FALSE, 'Cache-Control header was sent.'); + + // Log out and try to access the file. + $this->drupalLogout(); + $this->drupalGet(file_create_url($image_uri)); + $this->assertResponse('403', 'Access denied to original image as anonymous user.'); + + // Log in again. + $this->drupalLogin($this->admin_user); + } + + // Use the picture formatter with a picture mapping. + $instance['display']['default']['settings']['picture_mapping'] = 'mapping_one'; + field_update_instance($instance); + // Output should contain all image styles and all breakpoints. + $this->drupalGet('node/' . $nid); + $this->assertRaw('/styles/thumbnail/'); + $this->assertRaw('/styles/medium/'); + $this->assertRaw('/styles/large/'); + $this->assertRaw('media="(min-width: 200px)"'); + $this->assertRaw('media="(min-width: 400px)"'); + $this->assertRaw('media="(min-width: 600px)"'); + + // Test the fallback image style. + $instance['display']['default']['settings']['image_link'] = ''; + $instance['display']['default']['settings']['fallback_image_style'] = 'large'; + field_update_instance($instance); + + $this->drupalGet(image_style_url('large', $image_uri)); + $image_info['uri'] = $image_uri; + $image_info['width'] = 480; + $image_info['height'] = 240; + $image_info['style_name'] = 'large'; + $default_output = ''; + $this->drupalGet('node/' . $nid); + $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.'); + + if ($scheme == 'private') { + // Log out and try to access the file. + $this->drupalLogout(); + $this->drupalGet(image_style_url('large', $image_uri)); + $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.'); + } + } + +} diff --git a/core/modules/picture/picture.info b/core/modules/picture/picture.info new file mode 100644 index 0000000..0b7929b --- /dev/null +++ b/core/modules/picture/picture.info @@ -0,0 +1,9 @@ +name = Picture +description = Picture element +package = Core +version = VERSION +core = 8.x +dependencies[] = breakpoint +dependencies[] = config +dependencies[] = image +configure = admin/config/media/picturemapping \ No newline at end of file diff --git a/core/modules/picture/picture.module b/core/modules/picture/picture.module new file mode 100644 index 0000000..3b87455 --- /dev/null +++ b/core/modules/picture/picture.module @@ -0,0 +1,489 @@ + array( + 'title' => t('Administer Pictures'), + 'description' => t('Administer Pictures'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function picture_menu() { + $items = array(); + + $items['admin/config/media/picturemapping'] = array( + 'title' => 'Picture Mappings', + 'description' => 'Manage picture mappings', + 'access arguments' => array('administer pictures'), + 'weight' => 10, + 'page callback' => 'picture_mapping_page', + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/add'] = array( + 'title' => 'Add picture mapping', + 'page callback' => 'picture_mapping_page_add', + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'type' => MENU_LOCAL_ACTION, + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/%picture_mapping/edit'] = array( + 'title' => 'Edit picture mapping', + 'page callback' => 'picture_mapping_page_edit', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/%picture_mapping/duplicate'] = array( + 'title' => 'Duplicate picture mapping', + 'page callback' => 'picture_mapping_page_duplicate', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'file' => 'picture_mapping.admin.inc', + ); + $items['admin/config/media/picturemapping/%picture_mapping/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('picture_mapping_action_confirm', 4, 5), + 'access callback' => 'user_access', + 'access arguments' => array('administer pictures'), + 'file' => 'picture_mapping.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_entity_info(). + */ +function picture_entity_info() { + $types['picture_mapping'] = array( + 'label' => 'Picture mapping', + 'entity class' => 'Drupal\picture\PictureMapping', + 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController', + 'config prefix' => 'picture.mappings', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + 'form controller class' => array( + 'default' => 'Drupal\picture\PictureMappingFormController', + 'add' => 'Drupal\picture\PictureMappingFormController', + 'duplicate' => 'Drupal\picture\PictureMappingFormController', + ), + 'list controller class' => 'Drupal\picture\PictureMappingListController', + 'list path' => 'admin/config/media/picturemapping', + 'uri callback' => 'picture_mapping_uri', + ); + + return $types; +} + +/** + * Implements hook_library_info(). + */ +function picture_library_info() { + $libraries['matchmedia'] = array( + 'title' => t('Matchmedia'), + 'website' => 'https://github.com/attiks/picturefill-proposal', + 'version' => '0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/picturefill/matchmedia.js' => array('type' => 'file', 'weight' => -10, 'group' => JS_DEFAULT), + ), + ); + $libraries['picturefill'] = array( + 'title' => t('Picturefill'), + 'website' => 'https://github.com/attiks/picturefill-proposal', + 'version' => '0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/picturefill/picturefill.js' => array('type' => 'file', 'weight' => -10, 'group' => JS_DEFAULT), + ), + ); + return $libraries; +} + +/** + * Load one picture by its identifier. + */ +function picture_mapping_load($id) { + return entity_load('picture_mapping', $id); +} + +/** + * Load all pictures. + */ +function picture_mapping_load_all() { + $picture_mappings = entity_load_multiple('picture_mapping'); + return $picture_mappings; +} + +/** + * Picture uri callback. + */ +function picture_mapping_uri(PictureMapping $picture_mapping) { + return array( + 'path' => 'admin/config/media/picturemapping/' . $picture_mapping->id(), + ); +} + +/** + * Picture uri callback. + */ +function picture_mapping_set_uri(PictureMapping $picture_mapping) { + return array( + 'path' => 'admin/config/media/picturemapping/' . $picture_mapping->id(), + ); +} + +/** + * Implements hook_theme(). + */ +function picture_theme() { + return array( + 'picture' => array( + 'variables' => array( + 'style_name' => NULL, + 'path' => NULL, + 'width' => NULL, + 'height' => NULL, + 'alt' => '', + 'title' => NULL, + 'attributes' => array(), + 'breakpoints' => array(), + ), + ), + 'picture_formatter' => array( + 'variables' => array( + 'item' => NULL, + 'path' => NULL, + 'image_style' => NULL, + 'breakpoints' => array(), + ), + ), + 'picture_source' => array( + 'variables' => array( + 'src' => NULL, + 'srcset' => NULL, + 'dimension' => NULL, + 'media' => NULL, + ), + ), + ); +} + +/** + * Implements hook_field_formatter_info(). + */ +function picture_field_formatter_info() { + $formatters = array( + 'picture' => array( + 'label' => t('Picture'), + 'field types' => array('image'), + 'settings' => array('picture_mapping' => '', 'fallback_image_style' => '', 'image_link' => ''), + ), + ); + + return $formatters; +} + +/** + * Implements hook_field_formatter_settings_form(). + */ +function picture_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $picture_options = array(); + $picture_mappings = picture_mapping_load_all(); + if ($picture_mappings && !empty($picture_mappings)) { + foreach ($picture_mappings as $machine_name => $picture_mapping) { + $picture_options[$machine_name] = $picture_mapping->label(); + } + } + + $element['picture_mapping'] = array( + '#title' => t('Picture mapping'), + '#type' => 'select', + '#default_value' => $settings['picture_mapping'], + '#required' => TRUE, + '#options' => $picture_options, + ); + + $image_styles = image_style_options(FALSE); + $element['fallback_image_style'] = array( + '#title' => t('Fallback image style'), + '#type' => 'select', + '#default_value' => $settings['fallback_image_style'], + '#empty_option' => t('Automatic'), + '#options' => $image_styles, + ); + + $link_types = array( + 'content' => t('Content'), + 'file' => t('File'), + ); + $element['image_link'] = array( + '#title' => t('Link image to'), + '#type' => 'select', + '#default_value' => $settings['image_link'], + '#empty_option' => t('Nothing'), + '#options' => $link_types, + ); + + return $element; +} + +/** + * Implements hook_field_formatter_settings_summary(). + */ +function picture_field_formatter_settings_summary($field, $instance, $view_mode) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $summary = array(); + + $picture_mapping = picture_mapping_load($settings['picture_mapping']); + if ($picture_mapping) { + $summary[] = t('Picture mapping: @picture_mapping', array('@picture_mapping' => $picture_mapping->label())); + } + else { + $summary[] = t("Picture mapping doesn't exists"); + } + + $image_styles = image_style_options(FALSE); + unset($image_styles['']); + if (isset($image_styles[$settings['fallback_image_style']])) { + $summary[] = t('Fallback Image style: @style', array('@style' => $image_styles[$settings['fallback_image_style']])); + } + else { + $summary[] = t('Automatic fallback'); + } + + $link_types = array( + 'content' => t('Linked to content'), + 'file' => t('Linked to file'), + ); + // Display this setting only if image is linked. + if (isset($link_types[$settings['image_link']])) { + $summary[] = $link_types[$settings['image_link']]; + } + + return implode('
', $summary); +} + +/** + * Implements hook_field_formatter_view(). + */ +function picture_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = array(); + // Check if the formatter involves a link. + if ($display['settings']['image_link'] == 'content') { + $uri = $entity->uri(); + } + elseif ($display['settings']['image_link'] == 'file') { + $link_file = TRUE; + } + + $breakpoint_styles = array(); + $fallback_image_style = ''; + $picture_mapping = picture_mapping_load($display['settings']['picture_mapping']); + if ($picture_mapping) { + foreach ($picture_mapping->mappings as $breakpoint_name => $multipliers) { + if (!empty($multipliers)) { + foreach ($multipliers as $multiplier => $image_style) { + if (!empty($image_style)) { + if (empty($fallback_image_style)) { + $fallback_image_style = $image_style; + } + if (!isset($breakpoint_styles[$breakpoint_name])) { + $breakpoint_styles[$breakpoint_name] = array(); + } + $breakpoint_styles[$breakpoint_name][$multiplier] = $image_style; + } + } + } + } + } + + if (isset($display['settings']['fallback_image_style']) && !empty($display['settings']['fallback_image_style'])) { + $fallback_image_style = $display['settings']['fallback_image_style']; + } + + foreach ($items as $delta => $item) { + if (isset($link_file)) { + $uri = array( + 'path' => file_create_url($item['uri']), + 'options' => array(), + ); + } + $element[$delta] = array( + '#theme' => 'picture_formatter', + '#attached' => array('library' => array( + array('picture', 'matchmedia'), + array('picture', 'picturefill'), + )), + '#item' => $item, + '#image_style' => $fallback_image_style, + '#breakpoints' => $breakpoint_styles, + '#path' => isset($uri) ? $uri : '', + ); + } + + return $element; +} + +function theme_picture_formatter($variables) { + if (!isset($variables['breakpoints']) || empty($variables['breakpoints'])) { + return theme('image_formatter', $variables); + } + + $item = $variables['item']; + + // Do not output an empty 'title' attribute. + if (isset($item['title']) && drupal_strlen($item['title']) == 0) { + unset($item['title']); + } + + $item['style_name'] = $variables['image_style']; + $item['breakpoints'] = $variables['breakpoints']; + + if (!isset($item['path']) && isset($variables['uri'])) { + $item['path'] = $variables['uri']; + } + $output = theme('picture', $item); + + if (isset($variables['path']['path'])) { + $path = $variables['path']['path']; + $options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); + $options['html'] = TRUE; + $output = l($output, $path, $options); + } + return $output; +} + +/** + * Theme a picture element. + */ +function theme_picture($variables) { + // Make sure that width and height are proper values + // If they exists we'll output them + // @see http://www.w3.org/community/respimg/2012/06/18/florians-compromise/ + if (isset($variables['width']) && empty($variables['width'])) { + unset($variables['width']); + unset($variables['height']); + } + elseif (isset($variables['height']) && empty($variables['height'])) { + unset($variables['width']); + unset($variables['height']); + } + + $sources = array(); + $output = array(); + + // Fallback image, output as source with media query. + $sources[] = array( + 'src' => image_style_url($variables['style_name'], $variables['uri']), + 'dimensions' => picture_get_image_dimensions($variables), + ); + + // All breakpoints and multipliers. + foreach ($variables['breakpoints'] as $breakpoint_name => $multipliers) { + $breakpoint = breakpoint_load($breakpoint_name); + if ($breakpoint) { + $new_sources = array(); + foreach ($multipliers as $multiplier => $image_style) { + $new_source = $variables; + $new_source['style_name'] = $image_style; + $new_source['#multiplier'] = $multiplier; + $new_sources[] = $new_source; + } + + // Only one image, use src. + if (count($new_sources) == 1) { + $sources[] = array( + 'src' => image_style_url($new_sources[0]['style_name'], $new_sources[0]['uri']), + 'dimensions' => picture_get_image_dimensions($new_sources[0]), + 'media' => $breakpoint->mediaQuery, + ); + } + else { + // Mutliple images, use srcset. + $srcset = array(); + foreach ($new_sources as $new_source) { + $srcset[] = image_style_url($new_source['style_name'], $new_source['uri']) . ' ' . $new_source['#multiplier']; + } + $sources[] = array( + 'srcset' => implode(', ', $srcset), + 'dimensions' => picture_get_image_dimensions($new_sources[0]), + 'media' => $breakpoint->mediaQuery, + ); + } + } + } + + if (!empty($sources)) { + $attributes = array(); + foreach (array('alt', 'title') as $key) { + if (isset($variables[$key])) { + $attributes[$key] = $variables[$key]; + } + } + $output[] = ''; + + // add source tags to the output. + foreach ($sources as $source) { + $output[] = theme('picture_source', $source); + } + + // output the fallback image. + $output[] = ''; + $output[] = ''; + return implode("\n", $output); + } +} + +function theme_picture_source($variables) { + $output = array(); + if (isset($variables['media']) && !empty($variables['media'])) { + if (!isset($variables['srcset'])) { + $output[] = ''; + $output[] = ''; + } + elseif (!isset($variables['src'])) { + $output[] = ''; + $output[] = ''; + } + } + else { + $output[] = ''; + $output[] = ''; + } + return implode("\n", $output); +} + +function picture_get_image_dimensions($variables) { + // Determine the dimensions of the styled image. + $dimensions = array( + 'width' => $variables['width'], + 'height' => $variables['height'], + ); + + image_style_transform_dimensions($variables['style_name'], $dimensions); + + return $dimensions; +} diff --git a/core/modules/picture/picture_mapping.admin.inc b/core/modules/picture/picture_mapping.admin.inc new file mode 100644 index 0000000..a035504 --- /dev/null +++ b/core/modules/picture/picture_mapping.admin.inc @@ -0,0 +1,79 @@ +render(); +} + +/** + * Page callback: Presents the picture editing form. + * + * @see picture_menu() + */ +function picture_mapping_page_edit($picture_mapping) { + drupal_set_title(t('Edit picture mapping @label', array('@label' => $picture_mapping->label())), PASS_THROUGH); + return entity_get_form($picture_mapping); +} + +/** + * Page callback: Provides the new picture addition form. + * + * @see picture_menu() + */ +function picture_mapping_page_add() { + $picture_mapping = entity_create('picture_mapping', array()); + $form = entity_get_form($picture_mapping); + return $form; +} + +/** + * Page callback: Form constructor for picture action confirmation form. + * + * @see picture_menu() + */ +function picture_mapping_action_confirm($form, &$form_state, $picture_mapping, $action) { + // Always provide entity id in the same form key as in the entity edit form. + if (in_array($action, array('delete'))) { + $form['id'] = array('#type' => 'value', '#value' => $picture_mapping->id()); + $form['action'] = array('#type' => 'value', '#value' => $action); + $form_state['picture_mapping'] = $picture_mapping; + $form = confirm_form($form, + t('Are you sure you want to @action the picture_mapping %title?', array('@action' => $action, '%title' => $picture_mapping->label())), + 'admin/config/media/picturemapping', + $action == 'delete' ? t('This action cannot be undone.') : '', + t(drupal_ucfirst($action)), + t('Cancel') + ); + } + return $form; +} + +/** + * Form submission handler for picture_action_confirm(). + */ +function picture_mapping_action_confirm_submit($form, &$form_state) { + $picture_mapping = $form_state['picture_mapping']; + $action = $form_state['values']['action']; + $picture_mapping->{$action}(); + $verb = ''; + switch ($action) { + case 'delete': + $verb = 'deleted'; + break; + } + drupal_set_message(t('Picture mapping %label has been @action.', array('%label' => $picture_mapping->label(), '@action' => $verb))); + watchdog('picture', 'Picture mapping %label has been @action.', array('%label' => $picture_mapping->label(), '@action' => $verb), WATCHDOG_NOTICE); + $form_state['redirect'] = 'admin/config/media/picturemapping'; +} diff --git a/core/modules/picture/picturefill/matchmedia.js b/core/modules/picture/picturefill/matchmedia.js new file mode 100644 index 0000000..adce5ab --- /dev/null +++ b/core/modules/picture/picturefill/matchmedia.js @@ -0,0 +1,2 @@ +/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ +window.matchMedia=window.matchMedia||(function(e,f){var c,a=e.documentElement,b=a.firstElementChild||a.firstChild,d=e.createElement("body"),g=e.createElement("div");g.id="mq-test-1";g.style.cssText="position:absolute;top:-100em";d.appendChild(g);return function(h){g.innerHTML='­';a.insertBefore(d,b);c=g.offsetWidth==42;a.removeChild(d);return{matches:c,media:h}}})(document); \ No newline at end of file diff --git a/core/modules/picture/picturefill/picturefill.js b/core/modules/picture/picturefill/picturefill.js new file mode 100644 index 0000000..db80012 --- /dev/null +++ b/core/modules/picture/picturefill/picturefill.js @@ -0,0 +1,125 @@ +/*! Picturefill - Author: Scott Jehl, 2012 | License: MIT/GPLv2 */ +/* + Picturefill: A polyfill for proposed behavior of the picture element, which does not yet exist, but should. :) + * Notes: + * For active discussion of the picture element, see http://www.w3.org/community/respimg/ + * While this code does work, it is intended to be used only for example purposes until either: + A) A W3C Candidate Recommendation for is released + B) A major browser implements +*/ +(function( w ){ + + // Enable strict mode + "use strict"; + + // User preference for HD content when available + var prefHD = false || w.localStorage && w.localStorage[ "picturefill-prefHD" ] === "true", + hasHD; + + // Test if `` is supported natively, if so, exit - no polyfill needed. + if ( !!( w.document.createElement( "picture" ) && w.document.createElement( "source" ) && w.HTMLPictureElement ) ){ + return; + } + + w.picturefill = function() { + function _copyAttributes(src, tar) { + if (src.getAttribute( "width" ) && src.getAttribute( "height" )) { + tar.width = src.getAttribute( "width" ); + tar.height = src.getAttribute( "height" ); + } + } + + var ps = w.document.getElementsByTagName( "picture" ); + + // Loop the pictures + for( var i = 0, il = ps.length; i < il; i++ ){ + var sources = ps[ i ].getElementsByTagName( "source" ), + picImg = null, + matches = []; + + // If no sources are found, they're likely erased from the DOM. Try finding them inside comments. + if( !sources.length ){ + var picText = ps[ i ].innerHTML, + frag = w.document.createElement( "div" ), + // For IE9, convert the source elements to divs + srcs = picText.replace( /(<)source([^>]+>)/gmi, "$1div$2" ).match( /]+>/gmi ); + + frag.innerHTML = srcs.join( "" ); + sources = frag.getElementsByTagName( "div" ); + } + + // See which sources match + for( var j = 0, jl = sources.length; j < jl; j++ ){ + var media = sources[ j ].getAttribute( "media" ); + // if there's no media specified, OR w.matchMedia is supported + if( !media || ( w.matchMedia && w.matchMedia( media ).matches ) ){ + matches.push( sources[ j ] ); + } + } + + // Find any existing img element in the picture element + picImg = ps[ i ].getElementsByTagName( "img" )[ 0 ]; + + if( matches.length ){ + // Grab the most appropriate (last) match. + var match = matches.pop(), + srcset = match.getAttribute( "srcset" ); + + if( !picImg ){ + picImg = w.document.createElement( "img" ); + picImg.alt = ps[ i ].getAttribute( "alt" ); + ps[ i ].appendChild( picImg ); + } + + if( srcset ) { + var screenRes = ( prefHD && w.devicePixelRatio ) || 1, // Is it worth looping through reasonable matchMedia values here? + sources = srcset.split(","); // Split comma-separated `srcset` sources into an array. + + hasHD = w.devicePixelRatio > 1; + + for( var res = sources.length, r = res - 1; r >= 0; r-- ) { // Loop through each source/resolution in `srcset`. + var source = sources[ r ].replace(/^\s*/, '').replace(/\s*$/, '').split(" "), // Remove any leading whitespace, then split on spaces. + resMatch = parseFloat( source[1], 10 ); // Parse out the resolution for each source in `srcset`. + + if( screenRes >= resMatch ) { + if( picImg.getAttribute( "src" ) !== source[0] ) { + var newImg = document.createElement("img"); + + newImg.src = source[0]; + // When the image is loaded, set a width equal to that of the original’s intrinsic width divided by the screen resolution: + newImg.onload = function() { + // Clone the original image into memory so the width is unaffected by page styles: + var w = this.cloneNode( true ).width; + if (w > 0) { + this.width = ( w / resMatch ); + } + } + _copyAttributes(match, newImg); + picImg.parentNode.replaceChild( newImg, picImg ); + } + break; // We’ve matched, so bail out of the loop here. + } + } + } else { + // No `srcset` in play, so just use the `src` value: + picImg.src = match.getAttribute( "src" ); + _copyAttributes(match, picImg); + } + } + } + }; + + // Run on resize and domready (w.load as a fallback) + if( w.addEventListener ){ + w.addEventListener( "resize", w.picturefill, false ); + w.addEventListener( "DOMContentLoaded", function(){ + w.picturefill(); + // Run once only + w.removeEventListener( "load", w.picturefill, false ); + }, false ); + w.addEventListener( "load", w.picturefill, false ); + } + else if( w.attachEvent ){ + w.attachEvent( "onload", w.picturefill ); + } +})( this ); \ No newline at end of file diff --git a/core/themes/bartik/config/bartik.breakpoints.yml b/core/themes/bartik/config/bartik.breakpoints.yml index 9eb9cb2..501f417 100644 --- a/core/themes/bartik/config/bartik.breakpoints.yml +++ b/core/themes/bartik/config/bartik.breakpoints.yml @@ -1,3 +1,3 @@ mobile: '(min-width: 0px)' -narrow: 'all and (min-width: 560px) and (max-width:850px)' +narrow: 'all and (min-width: 560px) and (max-width: 850px)' wide: 'all and (min-width: 851px)' diff --git a/core/themes/bartik/css/layout.css b/core/themes/bartik/css/layout.css index fd69c49..4817eef 100644 --- a/core/themes/bartik/css/layout.css +++ b/core/themes/bartik/css/layout.css @@ -71,7 +71,7 @@ body, padding: 0 10px; } -@media all and (min-width: 560px) and (max-width:850px) { +@media all and (min-width: 560px) and (max-width: 850px) { #sidebar-first, .region-triptych-first,