diff --git a/core/modules/grid/grid.admin.inc b/core/modules/grid/grid.admin.inc new file mode 100644 index 0000000..b71c511 --- /dev/null +++ b/core/modules/grid/grid.admin.inc @@ -0,0 +1,67 @@ +render(); +} + +/** + * Page callback: Presents the grid editing form. + * + * @see grid_menu() + */ +function grid_page_edit(Grid $grid) { + drupal_set_title(t('Edit grid @label', array('@label' => $grid->label())), PASS_THROUGH); + return entity_get_form($grid); +} + +/** + * Page callback: Provides the new grid addition form. + * + * @see grid_menu() + */ +function grid_page_add() { + $grid = entity_create('grid', array()); + return entity_get_form($grid); +} + +/** + * Page callback: Form constructor for grid deletion confirmation form. + * + * @see grid_menu() + */ +function grid_confirm_delete($form, &$form_state, Grid $grid) { + // Always provide entity id in the same form key as in the entity edit form. + $form['id'] = array('#type' => 'value', '#value' => $grid->id()); + $form_state['grid'] = $grid; + return confirm_form($form, + t('Are you sure you want to remove the grid %title?', array('%title' => $grid->label())), + 'admin/structure/grids', + t('This action cannot be undone.'), + t('Delete'), + t('Cancel') + ); +} + +/** + * Form submission handler for grid_confirm_delete(). + */ +function grid_confirm_delete_submit($form, &$form_state) { + $grid = $form_state['grid']; + $grid->delete(); + drupal_set_message(t('Grid %label has been deleted.', array('%label' => $grid->label()))); + watchdog('grid', 'Grid %label has been deleted.', array('%label' => $grid->label()), WATCHDOG_NOTICE); + $form_state['redirect'] = 'admin/structure/grids'; +} diff --git a/core/modules/grid/grid.info b/core/modules/grid/grid.info new file mode 100644 index 0000000..9dd6db4 --- /dev/null +++ b/core/modules/grid/grid.info @@ -0,0 +1,8 @@ +name = Grid +description = Pluggable grid system manager. +package = Core +version = VERSION +core = 8.x +dependencies[] = config +configure = admin/structure/grids + diff --git a/core/modules/grid/grid.module b/core/modules/grid/grid.module new file mode 100644 index 0000000..71b36b4 --- /dev/null +++ b/core/modules/grid/grid.module @@ -0,0 +1,101 @@ +' . t('Grids provide useful guides to place content on your pages. The grid module provides the ability to edit any type of grid and supports fluid and fixed equal column grids by itself. Extend with contributed modules for more grid types.') . '

'; + } +} + +/** + * Implements hook_menu(). + */ +function grid_menu() { + $items['admin/structure/grids'] = array( + 'title' => 'Grids', + 'description' => 'Manage list of grids which can be used with layouts.', + 'page callback' => 'grid_page_list', + 'access callback' => 'user_access', + 'access arguments' => array('administer grids'), + 'file' => 'grid.admin.inc', + ); + $items['admin/structure/grids/add'] = array( + 'title' => 'Add grid', + 'page callback' => 'grid_page_add', + 'access callback' => 'user_access', + 'access arguments' => array('administer grids'), + 'type' => MENU_LOCAL_ACTION, + 'file' => 'grid.admin.inc', + ); + $items['admin/structure/grids/manage/%grid'] = array( + 'title' => 'Edit grid', + 'page callback' => 'grid_page_edit', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer grids'), + 'type' => MENU_CALLBACK, + 'file' => 'grid.admin.inc', + ); + $items['admin/structure/grids/manage/%grid/edit'] = array( + 'title' => 'Edit', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/structure/grids/manage/%grid/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('grid_confirm_delete', 4), + 'access callback' => 'user_access', + 'access arguments' => array('administer grids'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'grid.admin.inc', + ); + return $items; +} + +/** + * Implements hook_permission(). + */ +function grid_permission() { + return array( + 'administer grids' => array( + 'title' => t('Administer grids'), + 'description' => t('Administer grids created with the grid module.'), + ), + ); +} + +/** + * Entity URI callback. + * + * @param Drupal\grid\Grid $grid + * Grid configuration entity instance. + * + * @return array + * Entity URI information. + */ +function grid_uri(Grid $grid) { + return array( + 'path' => 'admin/structure/grids/manage/' . $grid->id(), + ); +} + +/** + * Look up one grid setup based on machine name. + * + * @return Drupal\grid\Grid + * Grid configuration entity instance. + */ +function grid_load($id) { + return entity_load('grid', $id); +} diff --git a/core/modules/grid/lib/Drupal/grid/GridBundle.php b/core/modules/grid/lib/Drupal/grid/GridBundle.php new file mode 100644 index 0000000..eb5b927 --- /dev/null +++ b/core/modules/grid/lib/Drupal/grid/GridBundle.php @@ -0,0 +1,25 @@ +register('plugin.manager.grid', 'Drupal\grid\Plugin\GridManager'); + } +} diff --git a/core/modules/grid/lib/Drupal/grid/GridFormController.php b/core/modules/grid/lib/Drupal/grid/GridFormController.php new file mode 100644 index 0000000..92eee32 --- /dev/null +++ b/core/modules/grid/lib/Drupal/grid/GridFormController.php @@ -0,0 +1,105 @@ +type)) { + $grid->type = 'equal_column'; + $grid->options = array(); + } + $grid->options = $grid->getPlugin()->prepareOptions($grid->options); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::form(). + */ + public function form(array $form, array &$form_state, EntityInterface $grid) { + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#maxlength' => 255, + '#default_value' => $grid->label(), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $grid->id(), + '#machine_name' => array( + 'exists' => 'grid_load', + 'source' => array('label'), + ), + '#disabled' => !$grid->isNew(), + ); + $form['type'] = array( + '#type' => 'value', + '#value' => $grid->type, + ); + $form['grid'] = array( + '#type' => 'value', + '#value' => $grid, + ); + $form += $grid->getPlugin()->form($form, $form_state, $grid->options); + + return parent::form($form, $form_state, $grid); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + $form_state['values']['grid']->getPlugin()->validate($form, $form_state); + } + + /** + * 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::save(). + */ + public function save(array $form, array &$form_state) { + $grid = $this->getEntity($form_state); + $grid->save(); + + watchdog('grid', 'Grid @label saved.', array('@label' => $grid->label()), WATCHDOG_NOTICE); + drupal_set_message(t('Grid %label saved.', array('%label' => $grid->label()))); + + $form_state['redirect'] = 'admin/structure/grids'; + } + +} diff --git a/core/modules/grid/lib/Drupal/grid/Plugin/Core/Entity/Grid.php b/core/modules/grid/lib/Drupal/grid/Plugin/Core/Entity/Grid.php new file mode 100644 index 0000000..cf47320 --- /dev/null +++ b/core/modules/grid/lib/Drupal/grid/Plugin/Core/Entity/Grid.php @@ -0,0 +1,102 @@ +plugin)) { + // Pass on Grid ID as part of the configuration array. + $this->options['id'] = $this->id; + $this->plugin = drupal_container()->get('plugin.manager.grid')->createInstance($this->type, $this->options); + } + return $this->plugin; + } + + /** + * Implements Drupal\grid\Plugin\GridInterface::getGridCss(). + */ + public function getGridCss($wrapper_selector = NULL, $col_selector_prefix = NULL, $skip_spacing = FALSE) { + return $this->getPlugin()->getGridCss($wrapper_selector, $col_selector_prefix, $skip_spacing); + } + +} diff --git a/core/modules/grid/lib/Drupal/grid/Plugin/GridInterface.php b/core/modules/grid/lib/Drupal/grid/Plugin/GridInterface.php new file mode 100644 index 0000000..ca73fd8 --- /dev/null +++ b/core/modules/grid/lib/Drupal/grid/Plugin/GridInterface.php @@ -0,0 +1,30 @@ +discovery = new AnnotatedClassDiscovery('grid', 'grid'); + $this->factory = new ReflectionFactory($this); + } +} diff --git a/core/modules/grid/lib/Drupal/grid/Plugin/grid/grid/EqualColumn.php b/core/modules/grid/lib/Drupal/grid/Plugin/grid/grid/EqualColumn.php new file mode 100644 index 0000000..e29b99f --- /dev/null +++ b/core/modules/grid/lib/Drupal/grid/Plugin/grid/grid/EqualColumn.php @@ -0,0 +1,250 @@ +id = $id; + $this->columns = $columns; + $this->padding = $padding_width; + $this->gutter = $gutter_width; + $this->width = $width; + $this->units = $units; + } + + /** + * Prepare grid entity. + */ + public function prepareOptions(array $options) { + if (empty($options['width'])) { + // Set some defaults for the user if this is a new grid. + $options['units'] = '%'; + $options['width'] = 100; + $options['columns'] = 12; + $options['padding_width'] = 1.5; + $options['gutter_width'] = 2; + } + return $options; + } + + /** + * Form elements for grid editing. + */ + public function form(array $full_form, array &$form_state, array $options) { + // Master grid configuration. + $form['units'] = array( + '#type' => 'radios', + '#title' => t('Grid units'), + '#options' => array('%' => t('Percentages'), 'em' => t('Em-based'), 'px' => t('Pixel based')), + '#default_value' => $options['units'], + ); + $form['width'] = array( + '#type' => 'textfield', + '#title' => t('Grid width'), + '#description' => t('Width of the grid in unit set above. For example 960 (pixels) or 100 (percent).'), + '#default_value' => $options['width'], + ); + + // Grid detail configuration. + $form['columns'] = array( + '#type' => 'textfield', + '#title' => t('Number of grid columns'), + '#default_value' => $options['columns'], + ); + $form['padding_width'] = array( + '#type' => 'textfield', + '#title' => t('Column padding'), + '#description' => t('Column padding in unit set above. For example 10 (pixels) or 1.5 (percent). Enter 0 for no padding.'), + '#default_value' => $options['padding_width'], + ); + + $form['gutter_width'] = array( + '#type' => 'textfield', + '#title' => t('Gutter width'), + '#description' => t('Gutter width in unit set above. For example 20 (pixels) or 1.5 (percent). Enter 0 for no padding.'), + '#default_value' => $options['gutter_width'], + ); + return $form; + } + + /** + * Form validation callback. + */ + public function validate(array $form, array &$form_state) { + if ((intval($form_state['values']['width']) != $form_state['values']['width']) || $form_state['values']['width'] == 0) { + // Width should be a positive integer. + form_set_error('columns', t('The width should be a positive number.')); + } + if ((intval($form_state['values']['columns']) != $form_state['values']['columns']) || $form_state['values']['columns'] == 0) { + // Columns should be a positive integer. + form_set_error('columns', t('The number of columns should be a positive number.')); + } + if (!is_numeric($form_state['values']['padding_width'])) { + // Padding can be float as well (eg. 1.5 for 1.5% for fluid grids). + form_set_error('padding_width', t('The column padding should be a number. Enter 0 (zero) for no padding.' . $form_state['values']['padding_width'])); + } + if (!is_numeric($form_state['values']['gutter_width'])) { + // Gutter can be float too (eg. 1.5 for 1.5% for fluid grids). + form_set_error('gutter_width', t('The gutter width should be a number. Enter 0 (zero) for no gutter.')); + } + } + + /** + * Implements Drupal\grid\Plugin\GridInterface::getGridCss(). + */ + public function getGridCss($wrapper_selector = NULL, $col_selector_prefix = NULL, $skip_spacing = FALSE) { + + // If the wrapper selector was not provided, generate one. This is useful for + // specific administration use cases when we scope the classes by grids. + if (empty($wrapper_selector)) { + $wrapper_selector = '.grid-' . $this->id; + } + + // If the col span selector was not provided, generate one. This is useful + // for the front end to apply varying span widths under different names. + if (empty($col_selector_prefix)) { + $col_selector_prefix = '.grid-col-'; + } + + // If spacing is to be skipped, use 0 instead of the configured values. + if ($skip_spacing) { + $padding = 0; + $gutter = 0; + } + else { + $padding = $this->padding; + $gutter = $this->gutter; + } + + // Because we use the border-box box model, we only need to substract the + // size of margins from the full width and divide the rest by number of + // columns to get a value for column size. + $colwidth = ($this->width - (($this->columns - 1) * $gutter)) / $this->columns; + + $css = array(); + $css[$wrapper_selector . ' .grid-col'] = array( + 'border' => '0px solid rgba(0,0,0,0)', + 'float' => 'left', + '-webkit-box-sizing' => 'border-box', + '-moz-box-sizing' => 'border-box', + 'box-sizing' => 'border-box', + '-moz-background-clip' => 'padding-box', + '-webkit-background-clip' => 'padding-box', + 'background-clip' => 'padding-box', + 'margin-left' => $gutter . $this->units, + 'padding' => '0 ' . $padding . $this->units, + ); + $css[$wrapper_selector . ' .grid-col' . $col_selector_prefix . 'first'] = array( + 'margin-left' => '0', + 'clear' => 'both', + ); + for ($i = 1; $i <= $this->columns; $i++) { + $selector = $wrapper_selector . ' .grid-col' . $col_selector_prefix . $i; + if ($i == 1) { + // Elements that consume 1 grid column. + $css[$selector] = array( + 'width' => ($colwidth * $i) . $this->units, + ); + } + elseif ($i == $this->columns) { + // Elements that consume the entire grid width. + $css[$selector] = array( + 'width' => $this->width . $this->units, + 'margin-left' => 0, + ); + } + else { + // Elements that consume several grid columns (and therefore, also the + // interior gutters). + $css[$selector] = array( + 'width' => (($colwidth * $i) + ($gutter * ($i - 1))) . $this->units, + ); + } + } + + return $this->formatCss($css); + } + + /** + * Returns a CSS string. + * + * @param (array) $css + * A two dimensional array, keyed first on selector, then on property name. + */ + protected function formatCss($css) { + $output = ''; + foreach ($css as $selector => $rules) { + $output .= $selector . "{\n"; + foreach ($rules as $property => $value) { + $output .= ' ' . $property . ': ' . $value . ";\n"; + } + $output .= "}\n"; + } + return $output; + } +} diff --git a/core/modules/grid/lib/Drupal/grid/Tests/GridTest.php b/core/modules/grid/lib/Drupal/grid/Tests/GridTest.php new file mode 100644 index 0000000..6a7f7eb --- /dev/null +++ b/core/modules/grid/lib/Drupal/grid/Tests/GridTest.php @@ -0,0 +1,118 @@ + 'Grid management', + 'description' => 'Tests grid management.', + 'group' => 'Grid', + ); + } + + function setUp() { + parent::setUp(); + + // Create a new user, allow to manage grids. + $admin_user = $this->drupalCreateUser(array('administer grids')); + $this->drupalLogin($admin_user); + } + + /** + * Tests the default grids. + */ + function testDefaultGrids() { + // Check that these grids show up on the user interface. + $test_grids = array( + 'ninesixty_12' => '960px wide, 12 column grid', + 'fluid_6' => 'Six column fluid', + 'fluid_12' => 'Twelve column fluid', + ); + + // Check if the visibility setting is available. + $this->drupalGet('admin/structure/grids'); + foreach($test_grids as $machine_name => $label) { + $this->assertText($machine_name); + $this->assertText($label); + } + } + + /** + * Tests editing a default grid. + */ + function testEditDefaultGrid() { + $this->drupalGet('admin/structure/grids/manage/fluid_6/edit'); + $this->assertResponse(200); + $this->assertPattern('!disabled="disabled"(.+)id="edit-id"(.+)value="fluid_6"!', 'Existing grid machine name field is disabled.'); + + $edit = array('label' => 'Fluid: six column', 'padding_width' => '1'); + $this->drupalPost('admin/structure/grids/manage/fluid_6/edit', $edit, t('Save')); + $this->assertText('Fluid: six column'); + $this->assertNoText('Six column fluid'); + $this->assertRaw(t('Grid %label saved.', array('%label' => 'Fluid: six column'))); + } + + /** + * Tests deleting a default grid. + */ + function testDeleteDefaultGrid() { + $this->drupalGet('admin/structure/grids/manage/fluid_12/delete'); + $this->assertResponse(200); + + $this->drupalPost('admin/structure/grids/manage/fluid_12/delete', array(), t('Delete')); + $this->assertRaw(t('Grid %label has been deleted.', array('%label' => 'Twelve column fluid'))); + $this->assertNoText('fluid_12'); + } + + /** + * Tests adding new grids and all actions on them. + */ + function testNewGrid() { + $edit = array('label' => 'Three column fluid', 'id' => 'fluid_3', 'columns' => '3', 'units' => '%', 'padding_width' => '1.5', 'gutter_width' => '2'); + $this->drupalPost('admin/structure/grids/add', $edit, t('Save')); + $this->assertText('fluid_3'); + $this->assertRaw(t('Grid %label saved.', array('%label' => 'Three column fluid'))); + + $edit = array('label' => '320px wide, three column grid', 'id' => '320px_3col', 'columns' => '3', 'units' => 'px', 'width' => '320', 'padding_width' => '5', 'gutter_width' => '5'); + $this->drupalPost('admin/structure/grids/add', $edit, t('Save')); + $this->assertText('320px_3col'); + $this->assertRaw(t('Grid %label saved.', array('%label' => '320px wide, three column grid'))); + + $edit = array('label' => 'Fluid: three column'); + $this->drupalPost('admin/structure/grids/manage/fluid_3/edit', $edit, t('Save')); + $this->assertNoText('Three column fluid'); + $this->assertText('fluid_3'); + $this->assertRaw(t('Grid %label saved.', array('%label' => 'Fluid: three column'))); + + $edit = array('label' => 'Conflicting grid', 'id' => 'fluid_3'); + $this->drupalPost('admin/structure/grids/add', $edit, t('Save')); + $this->assertText(t('The machine-readable name is already in use. It must be unique.')); + + $this->drupalGet('admin/structure/grids/manage/fluid_3/edit'); + $this->assertPattern('!disabled="disabled"(.+)id="edit-id"(.+)value="fluid_3"!', 'Existing grid machine name field is disabled.'); + + $this->drupalPost('admin/structure/grids/manage/fluid_3/delete', array(), t('Delete')); + $this->assertRaw(t('Grid %label has been deleted.', array('%label' => 'Fluid: three column'))); + $this->assertNoText('fluid_3'); + } + +} diff --git a/core/modules/grid/tests/config/grid.fluid_12.yml b/core/modules/grid/tests/config/grid.fluid_12.yml new file mode 100644 index 0000000..774b630 --- /dev/null +++ b/core/modules/grid/tests/config/grid.fluid_12.yml @@ -0,0 +1,9 @@ +id: fluid_12 +label: Twelve column fluid +type: equal_column +options: + units: '%' + width: 100 + columns: 12 + padding_width: 1.5 + gutter_width: 2 diff --git a/core/modules/grid/tests/config/grid.fluid_6.yml b/core/modules/grid/tests/config/grid.fluid_6.yml new file mode 100644 index 0000000..1d7fbcb --- /dev/null +++ b/core/modules/grid/tests/config/grid.fluid_6.yml @@ -0,0 +1,9 @@ +id: fluid_6 +label: Six column fluid +type: equal_column +options: + units: '%' + width: 100 + columns: 6 + padding_width: 1.5 + gutter_width: 2 diff --git a/core/modules/grid/tests/config/grid.ninesixty_12.yml b/core/modules/grid/tests/config/grid.ninesixty_12.yml new file mode 100644 index 0000000..5409789 --- /dev/null +++ b/core/modules/grid/tests/config/grid.ninesixty_12.yml @@ -0,0 +1,9 @@ +id: ninesixty_12 +label: '960px wide, 12 column grid' +type: equal_column +options: + units: 'px' + width: 960 + columns: 12 + padding_width: 20 + gutter_width: 10 diff --git a/core/modules/grid/tests/grid_test.info b/core/modules/grid/tests/grid_test.info new file mode 100644 index 0000000..ba4b138 --- /dev/null +++ b/core/modules/grid/tests/grid_test.info @@ -0,0 +1,6 @@ +name = Grid test +description = Helps with testing grids. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/grid/tests/grid_test.module b/core/modules/grid/tests/grid_test.module new file mode 100644 index 0000000..a65cd7e --- /dev/null +++ b/core/modules/grid/tests/grid_test.module @@ -0,0 +1,6 @@ +