diff --git a/core/modules/layout/includes/layout.admin.inc b/core/modules/layout/includes/layout.admin.inc index 489e8a3..2f137e9 100644 --- a/core/modules/layout/includes/layout.admin.inc +++ b/core/modules/layout/includes/layout.admin.inc @@ -6,6 +6,10 @@ */ use Drupal\layout\Plugin\Core\Entity\Display; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\layout\Form\BlockInstanceForm; +use Drupal\layout\Ajax\OpenDialogCommand; + /** * Page callback: Presents a list of layouts. @@ -133,7 +137,8 @@ function layout_master_webservice(Display $display, $a=NULL, $b=NULL, $c=NULL) { $block = $blockInstance['id']; $blockInfo['block.' . $block] = array( 'region' => $region['id'], - 'weight' => $blockInstance['weight'] * 100 + 'weight' => $blockInstance['weight'] * 100, + 'label' => $blockInstance['label'], ); } } @@ -193,3 +198,35 @@ function layout_master_break_lock_confirm_submit(&$form, &$form_state) { $form_state['redirect'] = 'admin/structure/layouts/manage/' . $display->get('id') . '/edit'; drupal_set_message(t('The lock has been broken and you may now edit this display.')); } + + +/** + * Page callback for BlockInstance Configuration Form + * + * THIS IS ONLY FOR DEMONSTRATION PURPOSES! + * + * @param Drupal\layout\Plugin\Core\Entity\Display $display + * @param $region + * @param $block_id + * @return Drupal\Core\Ajax\AjaxResponse + */ +function layout_master_blockinstance_configure(Display $display, $block_id) { + $response = new AjaxResponse(); + $form_state = array( + 'display' => $display, + 'block_id' => $block_id + ); + if (!$display->getBlockInstanceInfo($block_id)) { + return MENU_NOT_FOUND; + } + + $form = drupal_build_form('layout_master_blockinstance_configure_form', $form_state); + $html = drupal_render($form); + $response->addCommand(new OpenDialogCommand( t('Configure %block_id in layout %name', array('%block_id' => $block_id, '%name' => $display->get('label'))), $html)); + return $response; +} + +function layout_master_blockinstance_configure_form(array $form, array &$form_state) { + $form_handler = new BlockInstanceForm(); + return $form_handler->build($form, $form_state); +} diff --git a/core/modules/layout/js/layout.admin.js b/core/modules/layout/js/layout.admin.js index 43c1563..e91b5ab 100755 --- a/core/modules/layout/js/layout.admin.js +++ b/core/modules/layout/js/layout.admin.js @@ -4,6 +4,15 @@ var appView; +Drupal.ajax.prototype.commands.layoutBlockReload = function (ajax, response, status) { + // Find the appropriate model by its id. + var m = Drupal.layout.getBlockInstanceModelById(response.data.id); + if (m) { + // The views will take care of all necessary updates (unbinding, rebinding). + m.set(response.data.info); + } +}; + /** * Attach display editor functionality. */ @@ -20,6 +29,55 @@ Drupal.behaviors.displayEditor = { } /** + * Bind links to open in a dialog using Drupal.ajax. + * @param el + */ + Drupal.layout.ajaxify = function(el) { + // Bind Ajax behaviors to all items showing the class. + $(el).find('.ajax').once('ajax', function () { + var element_settings = {}; + element_settings.progress = { 'type': null }; + element_settings.dialog = { + modal: true + }; + + if ($(this).attr('href')) { + element_settings.url = $(this).attr('href'); + element_settings.event = 'click.layout'; + } + var base = $(this).attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings); + }); + } + + /** + * Unbind links bound to Drupal.ajax. + * @param el + */ + Drupal.layout.deajaxify = function(el) { + $(el).find('.ajax').each(function() { + var base = $(this).attr('id'); + if (Drupal.ajax[base]) { + $(this).off('click.layout'); + delete Drupal.ajax[base] + } + }); + } + + /** + * Retrieve the BlockInstanceModel + * @param id + * @return {*} + */ + Drupal.layout.getBlockInstanceModelById = function(id) { + var m; + Drupal.layout.appModel.get('regions').each(function(region) { + m = m || region.get('blockInstances').get(id); + }, this); + return m; + } + + /** * Helper function generating a BlocksCollection populated with randomly * named items. * diff --git a/core/modules/layout/js/theme.js b/core/modules/layout/js/theme.js index b0e24b1..4a5623b 100644 --- a/core/modules/layout/js/theme.js +++ b/core/modules/layout/js/theme.js @@ -9,7 +9,7 @@ * @param label * @return {String} */ - Drupal.theme.layoutRegion = function (id, label) { + Drupal.theme.layoutRegion = function (id, label, attributes) { var html = '
' + '
' + @@ -39,9 +39,13 @@ '
' + '
' + 'M' + - '' + id + ' block' + + '' + label + ' block' + + '
' + + '
' + + '' + + // this is only for demonstration purpose and so that we can have full blockInstance-CRUD. + '' '
' + - '
' + '
' + '
'; }; diff --git a/core/modules/layout/js/views/blockinstance-view.js b/core/modules/layout/js/views/blockinstance-view.js index aded8a8..ec8f15c 100644 --- a/core/modules/layout/js/views/blockinstance-view.js +++ b/core/modules/layout/js/views/blockinstance-view.js @@ -10,27 +10,50 @@ Drupal.layout.BlockInstanceView = Backbone.View.extend({ events:{ - 'click':'onClick', + 'click [name="block"][value="delete"]': 'onClickDelete', 'drop':'onDrop' }, + initialize: function() { + this.model.on('change', this.render, this); + }, onDrop:function (event, index) { // Trigger reorder, will be handled in Drupal.layout.RegionView. this.$el.trigger('reorder', [this.model, index]); - // @todo: handle dropping onto enpty region. event.preventDefault(); event.stopPropagation(); return ; }, - onClick:function () { + onClickDelete:function (event) { this.dialogView = new Drupal.layout.BlockInstanceModalView({ model: this.model, title: this.model.get('label') }); this.dialogView.render(); + event.preventDefault(); + event.stopPropagation(); + return ; + }, + getConfigureURL: function() { + return Drupal.url('admin/structure/layouts/manage/' + drupalSettings.layout.id + '/blockinstances/' + this.model.get('id') + '/configure' ); }, render:function () { - this.setElement($(Drupal.theme('layoutBlock', this.model.get('id'), this.model.get('label'), this.model.attributes))); + // Remove any existent Drupal.ajax. + Drupal.layout.deajaxify(this.$el); + // If you want to have the template render the "top" element of your view + // you need to do this. + // @see http://stackoverflow.com/questions/11594961/backbone-not-this-el-wrapping + var old = this.$el; + this.setElement(Drupal.theme('layoutBlock', this.model.get('id'), this.model.get('label'), { + 'configureURL': this.getConfigureURL() + })); + old.replaceWith(this.$el); + // Rewire Drupal.ajax. + Drupal.layout.ajaxify(this.$el); return this; + }, + remove: function() { + Drupal.layout.deajaxify(this.$el); + this.$el.remove(); } }); diff --git a/core/modules/layout/js/views/region-view.js b/core/modules/layout/js/views/region-view.js index d5d1995..2e765ab 100644 --- a/core/modules/layout/js/views/region-view.js +++ b/core/modules/layout/js/views/region-view.js @@ -65,7 +65,7 @@ }, render:function () { - this.$el.html(Drupal.theme.layoutRegion(this.model.get('id'), this.model.get('label'))); + this.$el.html(Drupal.theme.layoutRegion(this.model.get('id'), this.model.get('label'), this.model.toJSON())); this._collectionView.render(); // Making the whole layout-region-element sortable provides a larger area // to drop block instances on and allows for dropping on empty regions. diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module index 73861d4..5df14b8 100755 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -64,6 +64,15 @@ function layout_menu() { 'file' => 'includes/layout.admin.inc', ); + $items['admin/structure/layouts/manage/%layout_master_cache/blockinstances/%/configure'] = array( + 'title' => 'Configure', + 'page callback' => 'layout_master_blockinstance_configure', + 'page arguments' => array(4, 6), + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'file' => 'includes/layout.admin.inc', + ); + return $items; } @@ -221,8 +230,11 @@ function layout_library_info() { // Drupal's pseudo-templates $path . '/js/theme.js' => array('defer' => TRUE), + // Has to be JS_DEFAULT otherwise Drupal.ajax.prototype.commands can't + // be added to apparently. + $path . '/js/layout.admin.js' => array('group' => JS_DEFAULT), + // Models - $path . '/js/layout.admin.js' => array('defer' => TRUE), $path . '/js/models/app-model.js' => array('defer' => TRUE), $path . '/js/models/block-model.js' => array('defer' => TRUE), $path . '/js/models/blockinstance-model.js' => array('defer' => TRUE), diff --git a/core/modules/layout/lib/Drupal/layout/Ajax/OpenDialogCommand.php b/core/modules/layout/lib/Drupal/layout/Ajax/OpenDialogCommand.php new file mode 100644 index 0000000..4a8362d --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Ajax/OpenDialogCommand.php @@ -0,0 +1,52 @@ +title = $title; + $this->html = $html; + $this->settings = $settings; + } + + /** + * Implements Drupal\Core\Ajax\CommandInterface:render(). + */ + public function render() { + return array( + 'command' => 'insert', + 'method' => NULL, + 'title' => $this->title, + 'selector' => '#drupal-modal', + 'data' => $this->html, + 'settings' => $this->settings, + ); + } +} diff --git a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php index 802cacb..d826557 100755 --- a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php +++ b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php @@ -87,6 +87,10 @@ public function form(array $form, array &$form_state, EntityInterface $display) if (!isset($display->layout) || ($form_state['values']['layout'] != $display->layout)) { // @todo: clean this up - this is highly likely to be the wrong place // to alter the TempStore. + // Figured out where this problem comes from form cache and tempstore + // compete obviously - see also ViewEditFormController where they use + // $form_state['no_cache'] = TRUE; but that again requires going through + // views_ui_ajax_get_form instead of ajax_get_form. // But if i *don't* reload the blockInfo property of the Display, it can // be "stale" and overwrite changes made via the webservice. @@ -127,6 +131,7 @@ private function layoutDemonstration(EntityInterface $display) { $build['#attached']['js'][] =array( 'data' => array( 'layout' => array( + 'id' => $display->id, 'webserviceURL' => url('admin/structure/layouts/manage/' . $display->id . '/webservice'), 'locked' => $locked, 'layoutData' => $display->exportGroupedByRegion() @@ -134,6 +139,7 @@ private function layoutDemonstration(EntityInterface $display) { ), 'type' => 'setting', ); + return $build; } diff --git a/core/modules/layout/lib/Drupal/layout/Form/BlockInstanceForm.php b/core/modules/layout/lib/Drupal/layout/Form/BlockInstanceForm.php new file mode 100644 index 0000000..9c45750 --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Form/BlockInstanceForm.php @@ -0,0 +1,112 @@ +display = $form_state['display']; + $this->blockId = $form_state['block_id']; + $block_info = $this->display->getBlockInstanceInfo($this->blockId); + + $form = array(); + $form['error-placeholder'] = array( + '#markup' => '
', + ); + $form['label'] = array( + '#title' => t('Enter a label'), + '#type' => 'textfield', + '#default_value' => isset($block_info['label']) ? $block_info['label'] : NULL + ); + $form['save'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#ajax' => array( + 'dialog' => array(), + 'callback' => array($this, 'ajaxSave') + ), + ); + $form['cancel'] = array( + '#type' => 'link', + '#title' => t('Cancel'), + '#href' => '#', + '#attributes' => array( + // This is a special class to which JavaScript assigns dialog closing + // behavior. + 'class' => array('dialog-cancel'), + ), + ); + // Add validation and submission handlers. + $form['#validate'][] = array($this, 'validate'); + $form['#submit'][] = array($this, 'submit'); + return $form; + } + + function ajaxSave($form, $form_state) { + // @todo: use proper AjaxResponse commands. + if ($form_state['executed'] && $form_state['submitted']) { + $commands = array(); + $commands[] = array('command'=>'layoutBlockReload', 'data' => array( + 'info' => $this->display->getBlockInstanceInfo($this->blockId), + 'id' => $this->blockId + )); + $commands[] = array('command'=>'remove', 'selector' => '#drupal-modal', 'data' => NULL); + return array('#type' => 'ajax', '#commands' => $commands); + } + $commands = array(); + $errors = form_get_errors(); + if (count($errors)) { + $commands[] = array( + 'command'=>'insert', + 'method' => 'replaceWith', + 'selector' => '#drupal-modal .error-placeholder', + 'data' => theme('status_messages', $errors) + ); + } + return array('#type' => 'ajax', '#commands' => $commands); + } + + /** + * Validates the form. + */ + public function validate(array $form, array &$form_state) { + $values = & $form_state['values']; + if (strlen($values['label'])>10) { + form_set_error('label', t('Please chose a shorter label (for the sake testing validation!)')); + } + } + + /** + * Store the values by updating the TempStored Display. + */ + public function submit(array $form, array &$form_state) { + $values = $form_state['values']; + // Update the instance info + $this->display->setBlockInstanceInfo($this->blockId, array( + 'label'=>$values['label'] + )); + // Store changes in TempStore. + layout_master_cache_set($this->display); + } +} diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php b/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php index 7a2daca..a461479 100644 --- a/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php +++ b/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php @@ -186,7 +186,7 @@ public function getLayoutInstance() { /** * Returns an array representation grouped for json-serialisation. - * @todo: this is a bad name (and should probably) + * @todo: this is a bad name (and should probably change) * * @return array */ @@ -202,7 +202,7 @@ public function exportGroupedByRegion() { $region_data = array( 'id' => $region, 'label' => $info['label'], - 'blockInstances' => array() + 'blockInstances' => array(), ); $existing_blocks = $this->getSortedBlocksByRegion($region); foreach ($existing_blocks as $block => $placement) { @@ -211,7 +211,8 @@ public function exportGroupedByRegion() { $block_id = str_replace('block.', '', $block); $region_data['blockInstances'][] = array( 'id' => $block_id, - 'label' => $block_id, + // we are adding a rather meaningless label for CRUD-demo purposes. + 'label' => isset($placement['label']) ? $placement['label'] : $block_id, 'blockId' => 'default', 'weight' => $placement['weight'], 'region' => $placement['region'], @@ -221,4 +222,24 @@ public function exportGroupedByRegion() { } return $data; } + + public function getBlockInstanceInfo($block_id) { + $block_info = $this->getAllBlockInfo(); + return isset($block_info['block.' . $block_id]) ? $block_info['block.' . $block_id] : NULL; + } + + public function setBlockInstanceInfo($block_id, array $info, $merge=TRUE) { + $block_info = $this->getBlockInstanceInfo($block_id); + if (!isset($block_info)) { + return FALSE; + } + // @todo: does it make sense to invalidate this? + unset($this->blocksInRegions); + if ($merge) { + $info = array_merge($block_info, $info); + } + $this->blockInfo['block.' . $block_id] = $info; + return $this->blockInfo['block.' . $block_id]; + } + }