diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index fff63c1..d937411 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -173,6 +173,9 @@ Block module Book module - Peter Wolanin 'pwolanin' http://drupal.org/user/49851 +Breakpoint module +- Peter Droogmans 'attiks' http://drupal.org/user/105002 + Color module - ? 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..a7f9523 --- /dev/null +++ b/core/modules/breakpoint/breakpoint.module @@ -0,0 +1,555 @@ +' . t('About') . ''; + $output .= '

' . t('The Breakpoint module allows the management of breakpoints and breakpoint groups for responsive designs.') . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Breakpoints') . '
'; + $output .= '
' . t('Breakpoints can be defined by themes or other modules, a breakpoints consist of a name and a media query.') . '
'; + $output .= '
' . t('Breakpoint groups') . '
'; + $output .= '
' . t('Breakpoints can be organized into breakpoint groups, so that it is easier to manage and use them.') . '
'; + $output .= '
' . t('Multipliers') . '
'; + $output .= '
' . t('Multipliers can be defined for each breakpoint and are needed to handle screens with high dpi.') . '
'; + $output .= '
'; + + return $output; + } +} + +/** + * Implements hook_enable(). + * + * Import breakpoints from all enabled themes. + */ +function breakpoint_enable() { + // Import breakpoints from themes. + $themes = list_themes(); + _breakpoint_theme_enabled($theme_list); + + // Import breakpoints from modules. + $modules = module_list(); + _breakpoint_modules_enabled($modules); +} + +/** + * Implements hook_themes_enabled(). + * + * @param array $theme_list + * An array of theme names. + * + * @see _breakpoint_theme_enabled() + */ +function breakpoint_themes_enabled($theme_list) { + _breakpoint_theme_enabled($theme_list); +} + +/** + * Implements hook_themes_disabled(). + * + * @param array $theme_list + * An array of theme names. + * + * @see _breakpoint_delete_breakpoints() + */ +function breakpoint_themes_disabled($theme_list) { + _breakpoint_delete_breakpoints($theme_list, Breakpoint::SOURCE_TYPE_THEME); +} + +/** + * Implements hook_modules_enabled(). + * + * @param array $modules + * An array of the modules that were enabled. + * + * @see _breakpoint_modules_enabled() + */ +function breakpoint_modules_enabled($modules) { + _breakpoint_modules_enabled($modules); +} + +/** + * Implements hook_modules_uninstalled(). + * + * @param array $modules + * An array of the modules that were uninstalled. + * + * @see _breakpoint_delete_breakpoints() + */ +function breakpoint_modules_uninstalled($modules) { + _breakpoint_delete_breakpoints($modules, Breakpoint::SOURCE_TYPE_MODULE); +} + +/** + * Import breakpoints from all new enabled themes. + * + * @param array $theme_list + * An array of theme names. + */ +function _breakpoint_theme_enabled($theme_list) { + $themes = list_themes(); + foreach ($theme_list as $theme_key) { + if ($themes[$theme_key]->status) { + $media_queries = breakpoint_get_theme_media_queries($theme_key); + _breakpoint_import_media_queries($theme_key, $themes[$theme_key]->info['name'], Breakpoint::SOURCE_TYPE_THEME, $media_queries); + // Import custom groups. + _breakpoint_import_breakpoint_groups($module, Breakpoint::SOURCE_TYPE_THEME); + } + } +} + +/** + * Import breakpoints from all new enabled modules. + * + * @param array $modules + * An array of the modules that were enabled. + */ +function _breakpoint_modules_enabled($modules) { + foreach ($modules as $module) { + $media_queries = breakpoint_get_module_media_queries($module); + _breakpoint_import_media_queries($module, $module, Breakpoint::SOURCE_TYPE_MODULE, $media_queries); + // Import custom groups. + _breakpoint_import_breakpoint_groups($module, Breakpoint::SOURCE_TYPE_MODULE); + } +} + +/** + * Import media queries from a theme or module and create a default group. + * + * @param string $group_name + * Machine readable name of the breakpoint group. + * @param string $label + * Human readable name of the breakpoint group. + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + * @param array $media_queries + * Array of media queries keyed by id. + */ +function _breakpoint_import_media_queries($group_name, $label, $source_type, $media_queries) { + if (!empty($media_queries)) { + // Create a new breakpoint group if it doesn't exist. + $breakpoint_group = _breakpoint_group_create_or_load($group_name, $label, $source_type); + + // Load all media queries, create a breakpoint for each one and add them + // to this breakpoint group. + foreach ($media_queries as $name => $media_query) { + $breakpoint_group->addBreakpointFromMediaQuery($name, $media_query); + } + + $breakpoint_group->save(); + } +} + +/** + * Import breakpoint groups from theme or module. + * + * @param string $source + * The theme or module name + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + */ +function _breakpoint_import_breakpoint_groups($source, $source_type) { + $breakpoint_groups = config($source . '.breakpoint_groups'); + if ($breakpoint_groups) { + foreach ($breakpoint_groups->get() as $group_name => $data) { + // Breakpoints is mandatory, extra check since this is coming from config. + if (isset($data['breakpoints']) && !empty($data['breakpoints'])) { + // Create a new breakpoint group if it doesn't exist. + $breakpoint_group = _breakpoint_group_create_or_load($group_name, isset($data['label']) ? $data['label'] : $group_name, $source_type); + // Add the breakpoints. + $breakpoint_group->addBreakpoints($data['breakpoints']); + $breakpoint_group->save(); + } + } + } +} + +/** + * Remove breakpoints from all disabled themes or uninstalled modules. + * The source type has to match the original source type, otherwise the group + * will not be deleted. + * + * @param array $list + * A list of modules or themes that are disabled. + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + */ +function _breakpoint_delete_breakpoints($list, $source_type) { + $ids = drupal_container()->get('config.storage')->listAll('breakpoint.breakpoint_group.' . $source_type . '.'); + $entity_info = entity_get_info('breakpoint_group'); + + // Remove the breakpoint.breakpoint part of the breakpoint identifier. + foreach ($ids as &$id) { + $id = drupal_substr($id, drupal_strlen($entity_info['config prefix']) + 1); + } + $breakpoint_groups = entity_load_multiple('breakpoint_group', $ids); + + foreach ($breakpoint_groups as $breakpoint_group) { + if ($breakpoint_group->sourceType == $source_type && in_array($breakpoint_group->source, $list)) { + // Delete the automatically created breakpoint group. + $breakpoint_group->delete(); + + // Get all breakpoints defined by this theme/module. + $breakpoint_ids = drupal_container()->get('config.storage')->listAll('breakpoint.breakpoint.' . $source_type . '.' . $breakpoint_group->id() . '.'); + $entity_info = entity_get_info('breakpoint'); + + // Remove the breakpoint.breakpoint part of the breakpoint identifier. + foreach ($breakpoint_ids as &$breakpoint_id) { + $breakpoint_id = drupal_substr($breakpoint_id, drupal_strlen($entity_info['config prefix']) + 1); + } + $breakpoints = entity_load_multiple('breakpoint', $breakpoint_ids); + + // Make sure we only delete breakpoints defined by this theme/module. + foreach ($breakpoints as $breakpoint) { + if ($breakpoint->sourceType == $source_type && $breakpoint->source == $breakpoint_group->name) { + $breakpoint->delete(); + } + } + } + } + + // Delete groups defined by a module/theme even if that module/theme didn't + // define any breakpoints. + foreach ($ids as $id) { + // Delete all breakpoint groups defined by the theme or module. + _breakpoint_delete_breakpoint_groups($id, $source_type); + } +} + +/** + * Remove breakpoint groups from all disabled themes or uninstalled modules. + * + * @param array $group_id + * Machine readable name of the breakpoint group. + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + */ +function _breakpoint_delete_breakpoint_groups($group_id, $source_type) { + $breakpoint_groups = entity_load_multiple('breakpoint_group'); + foreach ($breakpoint_groups as $breakpoint_group) { + if ($breakpoint_group->sourceType == $source_type && $breakpoint_group->source == $group_id) { + $breakpoint_group->delete(); + } + } +} +/** + * Reload a breakpoint group from a theme or a module based on its name, type + * and source attribute. + * + * @param string $group_name + * Machine_readable name of the breakpoint group. + * @param string $type + * The type of group to reload. + * Allowed values are + * Breakpoint::SOURCE_TYPE_THEME + * Breakpoint::SOURCE_TYPE_MODULE + * @param string $name + * The name of the theme or module. + * + * @return Drupal\breakpoint\BreakpointGroup|false + * Returns a BreakpointGroup containing the breakpoints defined by the module. + */ +function breakpoint_group_reload($group_name, $type, $source) { + if ($group_name == $source) { + return _breakpoint_group_reload_default_group($group_name, $type); + } + else { + return _breakpoint_group_reload_custom_group($group_name, $type, $source); + } +} + +/** + * Reload a default breakpoint group from the theme or module. A default + * breakpoint group is a group that was created by the breakpoint module to + * group all the media queries defined by a particular module or theme. A + * default breakpoint group's name attribute will allways equal its source + * attribute (which is the module's or theme's name). + * + * @param string $source + * The name of the theme or module and the machine readable name of the + * breakpoint group. + * @param string $type + * The type of group to reload. + * Allowed values are + * Breakpoint::SOURCE_TYPE_THEME + * Breakpoint::SOURCE_TYPE_MODULE + * + * @return Drupal\breakpoint\BreakpointGroup|false + * Returns a BreakpointGroup containing the breakpoints defined by the theme. + * + * @see breakpoint_group_reload() + */ +function _breakpoint_group_reload_default_group($source, $type) { + switch ($type) { + case Breakpoint::SOURCE_TYPE_THEME: + // Clear caches so theme info is fresh. + system_rebuild_theme_data(); + drupal_theme_rebuild(); + + $themes = list_themes(); + if (isset($themes[$source]) && $themes[$source]->status) { + $theme_breakpoints = breakpoint_get_theme_media_queries($source); + if (!empty($theme_breakpoints)) { + return BreakpointGroup::ImportMediaQueries($source, $themes[$source]->info['name'], Breakpoint::SOURCE_TYPE_THEME, $theme_breakpoints); + } + } + break; + case Breakpoint::SOURCE_TYPE_MODULE: + // Clear caches so module info is fresh. + system_rebuild_module_data(); + module_list_reset(); + + $modules = module_list(); + if (isset($modules[$source])) { + $module_breakpoints = breakpoint_get_module_media_queries($source); + if (!empty($theme_breakpoints)) { + return BreakpointGroup::ImportMediaQueries($source, $source, Breakpoint::SOURCE_TYPE_MODULE, $module_breakpoints); + } + } + break; + } + return FALSE; +} + +/** + * Reload a custom breakpoint group from the theme or module. A custom + * breakpoint group is a breakpoint group that was created by the breakpoint + * module because the theme or module had in defined in its config file. A + * default group's name attribute will allways differ from its source attribute + * (which is the module's or theme's name). + * + * @param string $group_name + * Machine readable name of the breakpoint group. + * @param string $type + * The type of group to reload. + * Allowed values are + * Breakpoint::SOURCE_TYPE_THEME + * Breakpoint::SOURCE_TYPE_MODULE + * @param string $source + * The name of the theme or module. + * + * @return Drupal\breakpoint\BreakpointGroup|false + * Returns a BreakpointGroup containing the breakpoints defined by the module. + * + * @see breakpoint_group_reload() + */ +function _breakpoint_group_reload_custom_group($group_name, $type, $source) { + $breakpoint_groups = FALSE; + switch ($type) { + case Breakpoint::SOURCE_TYPE_THEME: + // Clear caches so theme info is fresh. + system_rebuild_theme_data(); + drupal_theme_rebuild(); + + $themes = list_themes(); + if (isset($themes[$source]) && $themes[$source]->status) { + $breakpoint_groups = config($source . '.breakpoint_groups'); + } + break; + case Breakpoint::SOURCE_TYPE_MODULE: + // Clear caches so module info is fresh. + system_rebuild_module_data(); + module_list_reset(); + + $modules = module_list(); + if (isset($modules[$source])) { + $breakpoint_groups = config($source . '.breakpoint_groups'); + } + break; + } + if ($breakpoint_groups) { + foreach ($breakpoint_groups->get() as $name => $data) { + if ($name == $group_name) { + // Breakpoints is mandatory, extra check since this is coming from config. + if (isset($data['breakpoints']) && !empty($data['breakpoints'])) { + if ($breakpoint_group = BreakpointGroup::ImportBreakpointGroup($source, $type, $name, isset($data['label']) ? $data['label'] : drupal_ucfirst($data[$name]), $data['breakpoints'])) { + return $breakpoint_group; + } + } + } + } + } + return FALSE; +} + +/** + * Get a list of available breakpoints from a specified theme. + * + * @param string $theme_key + * The name of the theme. + * + * @return array + * An array of breakpoints in the form $breakpoint['name'] = 'media query'. + */ +function breakpoint_get_theme_media_queries($theme_key) { + $themes = list_themes(); + if (!isset($themes[$theme_key])) { + return array(); + } + + $config = config($theme_key . '.breakpoints'); + if ($config) { + return $config->get(); + } + return array(); +} + +/** + * Get a list of available breakpoints from a specified module. + * + * @param string $module + * The name of the module. + * + * @return array + * An array of breakpoints in the form $breakpoint['name'] = 'media query'. + */ +function breakpoint_get_module_media_queries($module) { + if (!module_exists($module)) { + return array(); + } + + $config = config($module . '.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\Core\Config\Entity\ConfigStorageController', + 'config prefix' => 'breakpoint.breakpoint_group', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + ); + + return $types; +} + +/** + * Load one breakpoint group by its identifier. + * + * @param string $id + * The id of the breakpoint group to load. + * + * @return Drupal\breakpoint\BreakpointGroup|false + * The breakpoint group, or FALSE if there is no entity with the given id. + * + * @todo Remove this in a follow-up issue. + * @see http://drupal.org/node/1798214 + */ +function breakpoint_group_load($id) { + return entity_load('breakpoint_group', $id); +} + +/** + * Load one breakpoint by its identifier. + * + * @param int $id + * The id of the breakpoint to load. + * + * @return Drupal\breakpoint\Breakpoint + * The entity object, or FALSE if there is no entity with the given id. + * + * @todo Remove this in a follow-up issue. + * @see http://drupal.org/node/1798214 + */ +function breakpoint_load($id) { + return entity_load('breakpoint', $id); +} + +/** + * Load all breakpoint groups as select options. + * + * @return array + * An array containing breakpoint group labels indexed by their ids. + */ +function breakpoint_group_labels() { + $options = array(); + $breakpoint_groups = entity_load_multiple('breakpoint_group'); + foreach ($breakpoint_groups as $breakpoint_group) { + $options[$breakpoint_group->id()] = $breakpoint_group->label(); + } + asort($options); + return $options; +} + +/** + * Load all breakpoints as select options. + * + * @return array + * An array containing breakpoints indexed by their ids. + */ +function breakpoint_labels() { + $options = array(); + $breakpoints = entity_load_multiple('breakpoint'); + foreach ($breakpoints as $breakpoint) { + $options[$breakpoint->id()] = $breakpoint->label() . ' (' . $breakpoint->source . ' - ' . $breakpoint->sourceType . ') [' . $breakpoint->mediaQuery . ']'; + } + + return $options; +} + +/** + * Helper function to easily create/load a breakpoint group. + * + * @param string $name + * Machine readable name of the breakpoint group. + * @param string $label + * Human readable name of the breakpoint group. + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + * + * @return Drupal\breakpoint\BreakpointGroup + */ +function _breakpoint_group_create_or_load($name, $label, $source_type) { + // Try loading the breakpoint group. + $breakpoint_group = entity_load('breakpoint_group', $source_type . '.' . $name . '.' . $name); + // Create a new breakpoint group if it doesn't exist. + if (!$breakpoint_group) { + // Build a new breakpoint group. + $breakpoint_group = entity_create('breakpoint_group', array( + 'name' => $name, + 'label' => $label, + 'source' => $name, + 'sourceType' => $source_type, + )); + } + return $breakpoint_group; +} 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..ca8828d --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php @@ -0,0 +1,379 @@ +isValid()) { + throw new InvalidBreakpointException('Invalid data detected.'); + } + + // Build an id if non is set. + // Since a particular name can be used by multiple theme/modules we need + // to make a unique id. + if (empty($this->id)) { + $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name; + } + + // Set the label if none is set. + if (empty($this->label)) { + $this->label = $this->name; + } + + // Remove ununsed multipliers. + $this->multipliers = array_filter($this->multipliers); + + // Always add '1x' multiplier. + if (!array_key_exists('1x', $this->multipliers)) { + $this->multipliers = array('1x' => '1x') + $this->multipliers; + } + return parent::save(); + } + + /** + * Overrides a breakpoint and saves it. + * + * @return Drupal\breakpoint\Breakpoint|false + */ + public function override() { + // Custom breakpoint can't be overridden. + if ($this->overridden || $this->sourceType === Breakpoint::SOURCE_TYPE_CUSTOM) { + return FALSE; + } + + // Mark breakpoint as overridden. + $this->overridden = TRUE; + $this->originalMediaQuery = $this->mediaQuery; + $this->save(); + return $this; + } + + /** + * Reverts a breakpoint and saves it. + * + * @return Drupal\breakpoint\Breakpoint|false + */ + public function revert() { + if (!$this->overridden || $this->sourceType === Breakpoint::SOURCE_TYPE_CUSTOM) { + return FALSE; + } + + $this->overridden = FALSE; + $this->mediaQuery = $this->originalMediaQuery; + $this->save(); + return $this; + } + + /** + * Duplicates a breakpoint. + * + * The new breakpoint inherits the media query. + * + * @return Drupal\breakpoint\Breakpoint + */ + public function duplicate() { + return entity_create('breakpoint', array( + 'mediaQuery' => $this->mediaQuery, + )); + } + + /** + * Enables a breakpoint and save it. + */ + public function enable() { + if (!$this->status) { + $this->status = TRUE; + $this->save(); + } + } + + /** + * Disables a breakpoint and save it. + */ + public function disable() { + if ($this->status) { + $this->status = FALSE; + $this->save(); + } + } + + /** + * Checks if the breakpoint is valid. + * + * @throws Drupal\breakpoint\InvalidBreakpointSourceTypeException + * @throws Drupal\breakpoint\InvalidBreakpointSourceException + * @throws Drupal\breakpoint\InvalidBreakpointNameException + * @throws Drupal\breakpoint\InvalidBreakpointMediaQueryException + * + * @see isValidMediaQuery() + */ + public function isValid() { + // 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 InvalidBreakpointSourceTypeException(format_string('Invalid source type @source_type', array( + '@source_type' => $this->sourceType, + ))); + } + // Check for illegal characters in breakpoint source. + if (preg_match('/[^a-z_]+/', $this->source)) { + throw new InvalidBreakpointSourceException(format_string("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 InvalidBreakpointNameException(format_string("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::isValidMediaQuery($this->mediaQuery); + } + + /** + * Checks if the breakpoint is editable. + * + * @return boolean + * Returns true if the breakpoint is editable, otherwise returns false. + */ + public function isEditable() { + // Custom breakpoints are always editable. + if ($this->sourceType == Breakpoint::SOURCE_TYPE_CUSTOM) { + return TRUE; + } + // Overridden breakpoints are editable. + if ($this->overridden) { + return TRUE; + } + return FALSE; + } + + /** + * Checks if a mediaQuery is valid. + * + * @throws Drupal\breakpoint\InvalidBreakpointMediaQueryException + * + * @return true + * Returns true if the media query is valid. + * + * @see http://www.w3.org/TR/css3-mediaqueries/ + * @see http://www.w3.org/Style/CSS/Test/MediaQueries/20120229/reports/implement-report.html + * @see https://github.com/adobe/webkit/blob/master/Source/WebCore/css/ + */ + 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)) { + throw new InvalidBreakpointMediaQueryException('Invalid media feature detected.'); + } + } + // Full expression. + elseif (isset($matches[3]) && !isset($matches[4])) { + $value = trim($matches[3]); + if (!array_key_exists($matches[1], $media_features)) { + throw new InvalidBreakpointMediaQueryException('Invalid media feature detected.'); + } + if (is_array($media_features[$matches[1]])) { + // Check if value is allowed. + if (!array_key_exists($value, $media_features[$matches[1]])) { + throw new InvalidBreakpointMediaQueryException('Value is not allowed.'); + } + } + else { + switch ($media_features[$matches[1]]) { + case 'length': + $length_matches = array(); + if (preg_match('/^(\-)?(\d+(?:\.\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') { + throw new InvalidBreakpointMediaQueryException('Invalid length detected.'); + } + // If there's a unit, a number is needed as well. + if ($length_matches[2] === '' && $length_matches[3] !== '') { + throw new InvalidBreakpointMediaQueryException('Unit found, value is missing.'); + } + } + else { + throw new InvalidBreakpointMediaQueryException('Invalid unit detected.'); + } + 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 InvalidBreakpointMediaQueryException('Only one media type is allowed.'); + } + $media_type_found = TRUE; + } + else { + throw new InvalidBreakpointMediaQueryException('Invalid media query detected.'); + } + } + } + return TRUE; + } + throw new InvalidBreakpointMediaQueryException('Media query is empty.'); + } +} 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..4795f2c --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php @@ -0,0 +1,368 @@ +loadAllBreakpoints(); + } + + /** + * Overrides Drupal\Core\Entity\Entity::save(). + */ + public function save() { + // Check if everything is valid. + if (!$this->isValid()) { + throw new InvalidBreakpointException('Invalid data detected.'); + } + if (empty($this->id)) { + $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name; + } + // Only save the keys, but return the full objects. + $this->breakpoints = array_keys($this->breakpoints); + parent::save(); + $this->loadAllBreakpoints(); + } + + /** + * Checks if the breakpoint group is valid. + * + * @throws Drupal\breakpoint\InvalidBreakpointSourceTypeException + * @throws Drupal\breakpoint\InvalidBreakpointSourceException + * + * @return true + * Returns true if the breakpoint group is valid. + */ + public function isValid() { + // Check for illegal values in breakpoint group source type. + if (!in_array($this->sourceType, array( + Breakpoint::SOURCE_TYPE_CUSTOM, + Breakpoint::SOURCE_TYPE_MODULE, + Breakpoint::SOURCE_TYPE_THEME) + )) { + throw new InvalidBreakpointSourceTypeException(format_string('Invalid source type @source_type', array( + '@source_type' => $this->sourceType, + ))); + } + // Check for illegal characters in breakpoint group source. + if (preg_match('/[^a-z_]+/', $this->source) || empty($this->source)) { + throw new InvalidBreakpointSourceException(format_string("Invalid value '@source' for breakpoint group source property. Breakpoint group source property can only contain lowercase letters and underscores.", array('@source' => $this->source))); + } + // Check for illegal characters in breakpoint group name. + if (preg_match('/[^a-z0-9_]+/', $this->name || empty($this->name))) { + throw new InvalidBreakpointNameException(format_string("Invalid value '@name' for breakpoint group name property. Breakpoint group name property can only contain lowercase letters, numbers and underscores.", array('@name' => $this->name))); + } + return TRUE; + } + + /** + * Overrides a breakpoint group. + * + * @return Drupal\breakpoint\BreakpointGroup + */ + public function override() { + // Custom breakpoint group can't be overridden. + if ($this->overridden || $this->sourceType === Breakpoint::SOURCE_TYPE_CUSTOM) { + return FALSE; + } + + // Mark all breakpoints as overridden. + foreach ($this->breakpoints as $key => $breakpoint) { + if ($breakpoint->sourceType === $this->sourceType && $breakpoint->source == $this->name) { + $breakpoint->override(); + } + } + + // Mark breakpoint group as overridden. + $this->overridden = TRUE; + $this->save(); + return $this; + } + + /** + * Reverts a breakpoint group after it has been overridden. + * + * @return Drupal\breakpoint\BreakpointGroup + */ + public function revert() { + if (!$this->overridden || $this->sourceType === Breakpoint::SOURCE_TYPE_CUSTOM) { + return FALSE; + } + $reloaded_group = breakpoint_group_reload($this->name, $this->sourceType, $this->source); + if ($reloaded_group) { + $this->breakpoints = $reloaded_group->breakpoints; + $this->overridden = FALSE; + $this->save(); + } + return $this; + } + + /** + * Duplicates a breakpoint group. + * + * The new breakpoint group inherits the breakpoints. + * + * @return Drupal\breakpoint\BreakpointGroup + */ + public function duplicate() { + return entity_create('breakpoint_group', array( + 'breakpoints' => array_keys($this->breakpoints), + 'name' => 'clone_of_' . $this->name, + )); + } + + /** + * Checks if the breakpoint group is editable. + * + * @return boolean + * Returns true if the group is editable, otherwise returns false. + */ + public function isEditable() { + // Custom breakpoint groups are always editable. + if ($this->sourceType == Breakpoint::SOURCE_TYPE_CUSTOM) { + return TRUE; + } + // Overridden breakpoints groups are editable. + if ($this->overridden) { + return TRUE; + } + return FALSE; + } + + /** + * Adds a breakpoint using a name and a media query. + * + * @param string $name + * The name of the breakpoint. + * @param string $media_query + * Media query. + */ + public function addBreakpointFromMediaQuery($name, $media_query) { + // Use the existing breakpoint if it exists. + $breakpoint = entity_load('breakpoint', $this->sourceType . '.' . $this->name . '.' . $name); + if (!$breakpoint) { + // Build a new breakpoint. + $breakpoint = entity_create('breakpoint', array( + 'name' => $name, + 'label' => drupal_ucfirst($name), + 'mediaQuery' => $media_query, + 'source' => $this->name, + 'sourceType' => $this->sourceType, + 'weight' => count($this->breakpoints), + )); + $breakpoint->save(); + } + $this->breakpoints[$breakpoint->id()] = $breakpoint; + } + + /** + * Loads breakpoints from a theme/module and build a default group. + * + * @param string $group_name + * Machine readable name of the breakpoint group. + * @param string $label + * Human readable name of the breakpoint group. + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + * @param array $media_queries + * Array of media queries keyed by id. + * + * @return \Drupal\breakpoint\BreakpointGroup|false + * Return the new breakpoint group containing all breakpoints. + */ + public static function ImportMediaQueries($group_name, $label, $source_type, $media_queries) { + // Breakpoint group config names are {source_type}.{source}.{name}. + $breakpoint_group = entity_load('breakpoint_group', $source_type . '.' . $group_name . '.' . $group_name); + if (!$breakpoint_group) { + // Build a new breakpoint group. + $breakpoint_group = entity_create('breakpoint_group', array( + 'name' => $group_name, + 'label' => $label, + 'source' => $group_name, + 'sourceType' => $source_type, + )); + } + else { + // Reset label. + $breakpoint_group->label = $label; + } + + foreach ($media_queries as $name => $media_query) { + $breakpoint_group->addBreakpointFromMediaQuery($name, $media_query); + } + return $breakpoint_group; + } + + /** + * Imports breakpoint groups from a theme or module. + * + * @param string $source + * Source of the breakpoint group: theme_key or module name. + * @param string $sourceType + * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE. + * @param string $name + * Machine readable name of the breakpoint group. + * @param string $label + * Human readable name of the breakpoint group. + * @param array $breakpoints + * Array of breakpoints, using either the short name or the full name. + * + * @return \Drupal\breakpoint\BreakpointGroup|false + * Return the new breakpoint group containing all breakpoints. + */ + public static function ImportBreakpointGroup($source, $source_type, $name, $label, $breakpoints) { + // Use the existing breakpoint group if it exists. + // Breakpoint group config names are {source_type}.{source}.{name}. + $breakpoint_group = entity_load('breakpoint_group', $source_type . '.' . $source . '.' . $name); + if (!$breakpoint_group) { + $breakpoint_group = entity_create('breakpoint_group', array( + 'name' => $name, + 'label' => !empty($label) ? $label : $name, + 'source' => $source, + 'sourceType' => $source_type, + )); + } + else { + // Reset label. + $breakpoint_group->label = !empty($label) ? $label : $name; + } + + // Add breakpoints to the group. + foreach ($breakpoints as $breakpoint_name) { + // Check if breakpoint exists, assume full name. + $breakpoint = entity_load('breakpoint', $source_type . '.' . $source . '.' . $breakpoint_name); + // If the breakpoint doesn't exist, try using the short name. + if (!$breakpoint) { + $breakpoint = entity_load('breakpoint', $breakpoint_name); + } + if ($breakpoint) { + // Add breakpoint to group. + $breakpoint_group->breakpoints[$breakpoint->id()] = $breakpoint; + } + } + return $breakpoint_group; + } + + /** + * Adds one or more breakpoints to this group. + * + * The breakpoint name is either the machine_name or the id of a breakpoint. + * + * @param type $breakpoints + * Array containing breakpoints keyed by their id. + */ + public function addBreakpoints($breakpoints) { + foreach ($breakpoints as $breakpoint_name) { + // Check if breakpoint exists, assume $breakpoint_name is a machine name. + $breakpoint = entity_load('breakpoint', $this->sourceType . '.' . $this->source . '.' . $breakpoint_name); + // If the breakpoint doesn't exist, assume $breakpoint_name is an id. + if (!$breakpoint) { + $breakpoint = entity_load('breakpoint', $breakpoint_name); + } + if ($breakpoint) { + // Add breakpoint to group. + $this->breakpoints[$breakpoint->id()] = $breakpoint; + } + } + } + + /** + * Loads all breakpoints, remove non-existing ones. + * + * @return array + * Array containing breakpoints keyed by their id. + */ + 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/InvalidBreakpointException.php b/core/modules/breakpoint/lib/Drupal/breakpoint/InvalidBreakpointException.php new file mode 100644 index 0000000..6889fa2 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/InvalidBreakpointException.php @@ -0,0 +1,13 @@ + 'Breakpoint general API functions', + 'description' => 'Test general API functions of the breakpoint module.', + 'group' => 'Breakpoint', + ); + } + + /** + * Test Breakpoint::buildConfigName(). + */ + public function testConfigName() { + // Try an invalid sourceType. + $breakpoint = entity_create('breakpoint', array( + 'label' => drupal_strtolower($this->randomName()), + 'source' => 'custom_module', + 'sourceType' => 'oops', + )); + + $exception = FALSE; + try { + $breakpoint->save(); + } + catch (InvalidBreakpointSourceTypeException $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid sourceType is entered.')); + + // Try an invalid source. + $breakpoint->id = ''; + $breakpoint->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + $breakpoint->source = 'custom*_module source'; + + $exception = FALSE; + try { + $breakpoint->save(); + } + catch (InvalidBreakpointSourceException $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid source is entered.')); + + // Try an invalid name (make sure there is at least once capital letter). + $breakpoint->id = ''; + $breakpoint->source = 'custom_module'; + $breakpoint->name = drupal_ucfirst($this->randomName()); + + $exception = FALSE; + try { + $breakpoint->save(); + } + catch (InvalidBreakpointNameException $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid name is entered.')); + + // Try a valid breakpoint. + $breakpoint->id = ''; + $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..b6ee27d --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php @@ -0,0 +1,73 @@ + '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. + $breakpoint = entity_create('breakpoint', array( + 'label' => drupal_strtolower($this->randomName()), + 'mediaQuery' => '(min-width: 600px)', + )); + $breakpoint->save(); + + $this->verifyBreakpoint($breakpoint); + + // Test breakpoint_load_all + $all_breakpoints = entity_load_multiple('breakpoint'); + $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); + + // Enable the breakpoint. + $breakpoint->enable(); + $this->verifyBreakpoint($breakpoint); + + // Try to override the breakpoint. + $overridden = $breakpoint->override(); + $this->assertIdentical(FALSE, $overridden, t('Custom breakpoints can not be overridden.'), t('Breakpoint API')); + + // Try to revert the breakpoint. + $reverted = $breakpoint->revert(); + $this->assertIdentical(FALSE, $reverted, t('Custom breakpoints can not be reverted.'), t('Breakpoint API')); + + // 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/BreakpointGroupAPITest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php new file mode 100644 index 0000000..6639bda --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php @@ -0,0 +1,81 @@ + 'Breakpoint group general API functions', + 'description' => 'Test general API functions of the breakpoint module.', + 'group' => 'Breakpoint', + ); + } + + /** + * Test Breakpoint::buildConfigName(). + */ + public function testConfigName() { + // Try an invalid sourceType. + $label = $this->randomName(); + $breakpoint_group = entity_create('breakpoint_group', array( + 'label' => $label, + 'name' => drupal_strtolower($label), + 'source' => 'custom_module', + 'sourceType' => 'oops', + )); + + $exception = FALSE; + try { + $breakpoint_group->save(); + } + catch (InvalidBreakpointSourceTypeException $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('An exception is thrown when an invalid sourceType is entered.')); + + // Try an invalid source. + $breakpoint_group->name = ''; + $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM; + $breakpoint_group->source = 'custom*_module source'; + + $exception = FALSE; + try { + $breakpoint_group->save(); + } + catch (InvalidBreakpointSourceException $e) { + $exception = TRUE; + } + $this->assertTrue($exception, t('An exception is thrown when an invalid source is entered.')); + + // Try a valid breakpoint_group. + $breakpoint_group->name = 'test'; + $breakpoint_group->source = 'custom_module_source'; + + $exception = FALSE; + try { + $breakpoint_group->save(); + } + catch (\Exception $e) { + $exception = TRUE; + } + $this->assertFalse($exception, t('No exception is thrown when a valid data is passed.')); + } +} 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..3d8fd9b --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php @@ -0,0 +1,72 @@ + '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; + $breakpoint = entity_create('breakpoint', array( + 'name' => drupal_strtolower($this->randomName()), + 'weight' => $i, + 'mediaQuery' => "(min-width: {$width}px)", + )); + $breakpoint->save(); + $breakpoints[$breakpoint->id()] = $breakpoint; + } + // Add a breakpoint group with minimum data only. + $label = $this->randomName(); + + $group = entity_create('breakpoint_group', array( + 'label' => $label, + 'name' => drupal_strtolower($label), + )); + $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 = entity_create('breakpoint_group', array( + 'breakpoints' => $group->breakpoints, + 'name' => 'clone_of_' . $group->name, + )); + $duplicated_set = $group->duplicate(); + $this->verifyBreakpointGroup($duplicated_set, $new_set); + + // Delete the breakpoint group. + $group->delete(); + $this->assertFalse(entity_load('breakpoint_group', $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..145a638 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php @@ -0,0 +1,67 @@ +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 { + $t_args = array( + '%group' => $group->label(), + '%property' => $property, + '%property1' => $compare_set->{$property}, + '%property2' => $group->{$property}, + ); + $this->assertEqual($compare_set->{$property}, $group->{$property}, t('breakpoint_group_load: Proper %property: %property1 == %property2 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..1ebda06 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php @@ -0,0 +1,122 @@ + '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 (max-width: 0.3)', + '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) { + $this->assertTrue(Breakpoint::isValidMediaQuery($media_query), $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 (InvalidBreakpointMediaQueryException $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..bb89e52 --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php @@ -0,0 +1,57 @@ +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..1df4b7d --- /dev/null +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php @@ -0,0 +1,194 @@ + '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 = entity_create('breakpoint_group', array( + 'label' => 'Breakpoint test theme', + 'name' => 'breakpoint_test_theme', + 'source' => 'breakpoint_test_theme', + 'sourceType' => Breakpoint::SOURCE_TYPE_THEME, + 'overridden' => FALSE, + 'id' => Breakpoint::SOURCE_TYPE_THEME . '.breakpoint_test_theme.breakpoint_test_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(), + ); + + // 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 = entity_load('breakpoint_group', $breakpoint_group_obj->id()); + $breakpoint_group = $breakpoint_group->override(); + + $overridden_set->overridden = 1; + $this->verifyBreakpointGroup($overridden_set); + + // Verify the breakpoints in this breakpoint group are overridden. + foreach (array_keys($breakpoint_group_obj->breakpoints) as $breakpoint_id) { + $breakpoint = entity_load('breakpoint', $breakpoint_id); + $this->assertTrue($breakpoint->overridden, t('Breakpoint @breakpoint should be overridden', array('@breakpoint' => $breakpoint->label())), t('Breakpoint API')); + } + + // Revert the breakpoint group. + $breakpoint_group = entity_load('breakpoint_group', $breakpoint_group_obj->id()); + $breakpoint_group = $breakpoint_group->revert(); + + // Verify the breakpoint group has its original values again when loaded. + $this->verifyBreakpointGroup($breakpoint_group_obj); + + // Verify the breakpoints in this breakpoint group are no longer overridden. + foreach (array_keys($breakpoint_group_obj->breakpoints) as $breakpoint_id) { + $breakpoint = entity_load('breakpoint', $breakpoint_id); + $this->assertFalse($breakpoint->overridden, t('Breakpoint @breakpoint should not be overridden', array('@breakpoint' => $breakpoint->label())), t('Breakpoint API')); + } + + // Disable the test theme and verify the breakpoint group is deleted. + theme_disable(array('breakpoint_test_theme')); + $this->assertFalse(entity_load('breakpoint_group', $breakpoint_group_obj->id()), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API')); + } + + /** + * Test the breakpoints defined by the custom group. + */ + public function testThemeBreakpointGroup() { + // Verify the breakpoint group 'test' was created by breakpoint_test_theme. + $breakpoint_group_obj = entity_create('breakpoint_group', array( + 'label' => 'Test', + 'name' => 'test', + 'sourceType' => Breakpoint::SOURCE_TYPE_THEME, + 'source' => 'breakpoint_test_theme', + 'overridden' => FALSE, + 'id' => Breakpoint::SOURCE_TYPE_THEME . '.breakpoint_test_theme.test', + )); + $breakpoint_group_obj->breakpoints = array( + 'theme.breakpoint_test_theme.mobile' => array('1.5x', '2.x'), + 'theme.breakpoint_test_theme.narrow' => array(), + 'theme.breakpoint_test_theme.wide' => array(), + ); + + // Verify we can load this breakpoint defined by the theme. + $this->verifyBreakpointGroup($breakpoint_group_obj); + + // Disable the test theme and verify the breakpoint group is deleted. + theme_disable(array('breakpoint_test_theme')); + $this->assertFalse(entity_load('breakpoint_group', $breakpoint_group_obj->id()), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API')); + } + + /** + * Test the breakpoints defined by the custom group in the module. + */ + public function testThemeBreakpointGroupModule() { + // Call the import manually, since the testbot needs to enable the module + // first, otherwise the theme isn't detected. + _breakpoint_import_breakpoint_groups('breakpoint_theme_test', Breakpoint::SOURCE_TYPE_MODULE); + + // Verify the breakpoint group 'module_test' was created by + // breakpoint_theme_test module. + $breakpoint_group_obj = entity_create('breakpoint_group', array( + 'label' => 'Test Module', + 'name' => 'module_test', + 'sourceType' => Breakpoint::SOURCE_TYPE_MODULE, + 'source' => 'breakpoint_theme_test', + 'overridden' => FALSE, + 'id' => Breakpoint::SOURCE_TYPE_MODULE . '.breakpoint_theme_test.module_test', + )); + $breakpoint_group_obj->breakpoints = array( + 'theme.breakpoint_test_theme.mobile' => array(), + 'theme.breakpoint_test_theme.narrow' => array(), + 'theme.breakpoint_test_theme.wide' => array(), + ); + + // 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 = entity_load('breakpoint_group', $breakpoint_group_obj->id()); + $breakpoint_group = $breakpoint_group->override(); + + $overridden_set->overridden = 1; + $this->verifyBreakpointGroup($overridden_set); + + // This group uses breakpoints defined by an other theme. This means the + // breakpoints should *not* be overridden. + foreach (array_keys($breakpoint_group_obj->breakpoints) as $breakpoint_id) { + $breakpoint = entity_load('breakpoint', $breakpoint_id, TRUE); + $this->assertFalse($breakpoint->overridden, t('Breakpoint @breakpoint should not be overridden.', array('@breakpoint' => $breakpoint->label())), t('Breakpoint API')); + } + + // Revert the breakpoint group. + $breakpoint_group = entity_load('breakpoint_group', $breakpoint_group_obj->id()); + $breakpoint_group = $breakpoint_group->revert(); + + // Verify the breakpoint group has its original values again when loaded. + $this->verifyBreakpointGroup($breakpoint_group_obj); + + // Verify the breakpoints in this breakpoint group are not overridden. + foreach (array_keys($breakpoint_group_obj->breakpoints) as $breakpoint_id) { + $breakpoint = entity_load('breakpoint', $breakpoint_id); + $this->assertFalse($breakpoint->overridden, t('Breakpoint @breakpoint should not be overridden.', array('@breakpoint' => $breakpoint->label())), t('Breakpoint API')); + } + + // Disable the test theme and verify the breakpoint group still exists. + theme_disable(array('breakpoint_test_theme')); + $this->assertTrue(entity_load('breakpoint_group', $breakpoint_group_obj->id()), 'Breakpoint group still exists if theme is disabled.'); + + // Disable the test module and verify the breakpoint group still exists. + module_disable(array('breakpoint_theme_test')); + $this->assertTrue(entity_load('breakpoint_group', $breakpoint_group_obj->id()), 'Breakpoint group still exists if module is disabled.'); + + // Uninstall the test module and verify the breakpoint group is deleted. + module_uninstall(array('breakpoint_theme_test')); + $this->assertFalse(entity_load('breakpoint_group', $breakpoint_group_obj->id()), 'Breakpoint group is removed if module is uninstalled.'); + } + +} 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..d2896f6 --- /dev/null +++ b/core/modules/breakpoint/tests/breakpoint_theme_test.info @@ -0,0 +1,6 @@ +name = Breakpoint theme test +description = Test breakpoints provided by themes +package = Other +core = 8.x +hidden = TRUE +dependencies[] = breakpoint 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 @@ +