diff --git a/core/modules/layout/includes/layout.admin.inc b/core/modules/layout/includes/layout.admin.inc index 8bc2270..489e8a3 100644 --- a/core/modules/layout/includes/layout.admin.inc +++ b/core/modules/layout/includes/layout.admin.inc @@ -127,7 +127,7 @@ function layout_master_webservice(Display $display, $a=NULL, $b=NULL, $c=NULL) { // stores complete layout case 'layout': $blockInfo = array(); - // Shove the + // Set payload - this *obviously* needs to be validated. foreach ($payload['regions'] as $region) { foreach ($region['blockInstances'] as $blockInstance) { $block = $blockInstance['id']; diff --git a/core/modules/layout/js/collections/collections.js b/core/modules/layout/js/collections/collections.js index b71c783..c41a80a 100644 --- a/core/modules/layout/js/collections/collections.js +++ b/core/modules/layout/js/collections/collections.js @@ -19,8 +19,29 @@ Drupal.layout.BlockInstancesCollection = Backbone.Collection.extend({ model: Drupal.layout.BlockInstanceModel, + initialize: function() { + // Reorder every time a block instance is added or removed. + this.on('add', this.reorder, this); + this.on('remove', this.reorder, this); + }, + /** + * Sorting callback for the collection. + * @param {Drupal.layout.BlockInstanceModel} + * @return {Number} + */ comparator: function(model) { return model.get('weight'); + }, + /** + * Make sure that weight attribute of the models correspond to their index. + */ + reorder: function(options) { + this.each(function (model, index) { + model.set('weight', index); + }); + if (!options || !options.silent) { + this.trigger('reorder'); + } } }); diff --git a/core/modules/layout/js/layout.admin.js b/core/modules/layout/js/layout.admin.js index 79fe84e..43c1563 100755 --- a/core/modules/layout/js/layout.admin.js +++ b/core/modules/layout/js/layout.admin.js @@ -2,14 +2,13 @@ "use strict"; +var appView; + /** * Attach display editor functionality. */ Drupal.behaviors.displayEditor = { - attach: function (context, settings) { - var appModel, appView; - function randomId() { var chars = "abcdefghiklmnopqrstuvwxyz"; var randomString = ''; @@ -52,23 +51,47 @@ Drupal.behaviors.displayEditor = { id: region.id, label: region.label, blockInstances: - new Drupal.layout.BlockInstancesCollection().reset(region.blockInstances, {silent: true}) + new Drupal.layout.BlockInstancesCollection().reset(region.blockInstances) })); }); return regions; } - // Populate the appModel - Drupal.layout.appModel = new Drupal.layout.AppModel({ - id: drupalSettings.layout.id, - regions: generateRegionCollections(drupalSettings.layout.layoutData) - }); - var appView = new Drupal.layout.AppView({ - model: Drupal.layout.appModel, - el: $('#block-system-main'), - locked: drupalSettings.layout.locked - }); - appView.render(); + // Initial attaching. + if (!appView) { + Drupal.layout.appModel = new Drupal.layout.AppModel({ + id: drupalSettings.layout.id, + layout: drupalSettings.layout.layoutData.layout, + regions: generateRegionCollections(drupalSettings.layout.layoutData) + }); + appView = new Drupal.layout.AppView({ + model: Drupal.layout.appModel, + el: $('#block-system-main'), + locked: drupalSettings.layout.locked + }); + // @todo: we need to do this in order to circumvent the merge-behavior of + // Drupal.ajax on drupalSettings (which makes sense, just not here). + drupalSettings.layout.layoutData = {}; + appView.render(); + } else { + // Drupal.ajax has (good) reasons to call the attach function three times + // per response (triggered by layout select menu). But we + // need this only once and we need to make sure that the layout data is + // replaced not merged, that's why we do this stunt. There needs to be + // some form of making this less awkward. + if (drupalSettings.layout.layoutData.id) { + // Updating the model will trigger an rendering as appropriate. + Drupal.layout.appModel.set({ + id: drupalSettings.layout.id, + layout: drupalSettings.layout.layoutData.layout, + regions: generateRegionCollections(drupalSettings.layout.layoutData) + }); + // @todo: we need to do this in order to circumvent the merge-behavior of + // Drupal.ajax on drupalSettings (which makes sense, just not here). + drupalSettings.layout.layoutData = {}; + } + } + } }; diff --git a/core/modules/layout/js/models/app-model.js b/core/modules/layout/js/models/app-model.js index b7535b4..6fd97e7 100644 --- a/core/modules/layout/js/models/app-model.js +++ b/core/modules/layout/js/models/app-model.js @@ -15,7 +15,7 @@ }, defaults: { 'id': null, - 'template': null, + 'layout': null, 'regions': null, 'config': null } diff --git a/core/modules/layout/js/views/app-view.js b/core/modules/layout/js/views/app-view.js index 4b8985a..b9c2814 100644 --- a/core/modules/layout/js/views/app-view.js +++ b/core/modules/layout/js/views/app-view.js @@ -16,7 +16,7 @@ Drupal.layout = Drupal.layout || {}; Drupal.layout.AppView = Backbone.View.extend({ - initialize: function(options) { + initializeRegions: function() { this.regionsView = new Drupal.layout.UpdatingCollectionView({ el: this.$el.find('.layout-display'), collection: this.model.get('regions'), @@ -24,6 +24,17 @@ nestedViewTagName:'div' }); }, + initialize: function(options) { + this.initializeRegions(); + // Listen to changes of the layout-property for a complete repaint. + this.model.on('change:layout', function(m) { + this.remove(); + // Reinitialize region - @todo: find a way of doing this w/o + // reinitializing the view. + this.initializeRegions(); + this.render(); + }, this); + }, render: function() { // @todo: this should move to layout.admin.js and provide better handling. // Do not setup the js app if another user is currently operating on this @@ -32,20 +43,10 @@ return false; } this.regionsView.render(); - this.$el.sortable({ - items: '.block', - connectWith: '.connected-sortable', - cursor: 'move', - stop: function(event, ui) { - ui.item.trigger('drop', ui.item.index()); - } - }); return this; }, remove: function() { - this.$el.sortable('destroy'); this.regionsView.remove(); - this.$el.remove(); } }); diff --git a/core/modules/layout/js/views/blockselectormodal-view.js b/core/modules/layout/js/views/blockselectormodal-view.js index 5e6a3c4..ea2c600 100644 --- a/core/modules/layout/js/views/blockselectormodal-view.js +++ b/core/modules/layout/js/views/blockselectormodal-view.js @@ -19,6 +19,8 @@ selectBlock: function(e, block) { // Model is RegionModel var instance = block.createBlockInstance(); + // Append at the end. + instance.set('weight', this.model.get('blockInstances').length); this.model.get('blockInstances').add(instance); // Remove & close dialog. this.remove(); @@ -26,6 +28,7 @@ tagName: 'ul', render: function() { this.$el.empty(); + // @todo: refactor this to use nestedViewContainerSelector. this._collectionView = new Drupal.layout.UpdatingCollectionView({ collection:this.collection, nestedViewConstructor:Drupal.layout.BlockListItemView, diff --git a/core/modules/layout/js/views/region-view.js b/core/modules/layout/js/views/region-view.js index 8a8834a..d5d1995 100644 --- a/core/modules/layout/js/views/region-view.js +++ b/core/modules/layout/js/views/region-view.js @@ -56,12 +56,10 @@ nestedViewContainerSelector: '.blocks .row' }); - // @todo: be selective about what change-events trigger requests to the - // server. - // "Debounce" rapid sequences of change events to avoid unnecessary requests. - blockInstances.on('change', _.debounce(function() { - this.saveFullLayout(); - }, 100), this); + // @todo: be more selective about what changes trigger requests to the + // server. And let that bubble up to the app-view or only persist the + // region-specific changes here. + blockInstances.on('reorder', this.saveFullLayout, this); blockInstances.on('add', this.saveFullLayout, this); blockInstances.on('remove', this.saveFullLayout, this); }, @@ -69,36 +67,40 @@ render:function () { this.$el.html(Drupal.theme.layoutRegion(this.model.get('id'), this.model.get('label'))); 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. + this.$('.layout-region').sortable({ + items: '.block', + connectWith: '.layout-region', + cursor: 'move', + stop: function(event, ui) { + ui.item.trigger('drop', ui.item.index()); + } + }); return this; }, remove:function () { + this.$el.sortable('destroy'); this.$el.empty(); this._collectionView.remove(); }, reorderInstances:function (event, model, position) { var collection = this.model.get('blockInstances'); - // handle cross collection. + var originCollection; + // Handle cross-collection drag and drop. if (!collection.contains(model)) { - // let's remove it from the other first before adding it here. - model.collection.remove(model); + originCollection = model.collection; + // Let's remove it from the other first before adding it here. + model.collection.remove(model, {silent: true}); + // This is set to silent to avoid potential race condition. + originCollection.reorder({silent: true}); } else { - if (model.get('weight')==position) { - return ; - } - collection.remove(model); + // We'll be re-adding immediately, so no need for rapid-fire events. + collection.remove(model, {silent: true}); } - collection.each(function (model, index) { - var weight = index; - if (index >= position) { - weight += 1; - } - model.set('weight', weight); - }); - model.set('weight', position); collection.add(model, {at:position}); - collection.trigger('reorder'); this.render(); } }); diff --git a/core/modules/layout/js/views/updatingcollection-view.js b/core/modules/layout/js/views/updatingcollection-view.js index 3568a27..fb42ebf 100644 --- a/core/modules/layout/js/views/updatingcollection-view.js +++ b/core/modules/layout/js/views/updatingcollection-view.js @@ -96,9 +96,25 @@ // up-to-date. this.collection.each(function(m) { var nv = this._getViewByModel(m); - $el.append(nv.render().$el); + // Check that a view could be retrieved. + if (nv) { + $el.append(nv.render().$el); + } }, this); return this; + }, + + /** + * Remove all nested views. + * @todo: should we instead remove the models from the collection? Currently + * we leave the collection intact but retrieve each nested view and remove it. + */ + remove: function() { + // Cleanup. + this.collection.each(function(m) { + this._removeModel(m); + }, this); + this._getContainerElement().remove(); } }); diff --git a/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php b/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php index cac0407..e40a6c0 100644 --- a/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php +++ b/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php @@ -107,7 +107,7 @@ public function mapBlocksToLayout(LayoutInterface $layout) { // No need to do anything. } // Then, try to remap using region types. - else if (!empty($types[$info['region-type']])) { + else if (isset($types[$info['region-type']]) && !empty($types[$info['region-type']])) { $info['region'] = reset($types[$info['region-type']]); } // Finally, fall back to dumping everything in the layout's first region. diff --git a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php index 3d0cc3f..802cacb 100755 --- a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php +++ b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php @@ -38,17 +38,6 @@ public function form(array $form, array &$form_state, EntityInterface $display) 'layout' => isset($display->layout) ? $display->layout : reset($layout_keys) ); - // @todo: we need proper lock & dirty handling. - // @todo: this needs to happen in the client to. - if (isset($display->dirty) && $display->dirty) { - $form['dirty'] = array( - '#type' => 'container', - '#attributes' => array('class' => array('view-locked', 'messages', 'warning')), - '#children' => t('This display has been edited by you and the changes have not been saved yet.'), - '#weight' => -10, - ); - } - $locked = isset($display->locked) && is_object($display->locked) && $display->locked->owner != $GLOBALS['user']->uid; // Copied from ViewsEditFormController if ($locked) { @@ -80,8 +69,6 @@ public function form(array $form, array &$form_state, EntityInterface $display) } } - - $form['layout'] = array( '#type' => 'select', '#title' => t('Template'), @@ -98,6 +85,16 @@ public function form(array $form, array &$form_state, EntityInterface $display) // To support the Ajax interaction, remap the display to the newly selected // layout. This will reorganize the blocks as appropriate. 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. + + // But if i *don't* reload the blockInfo property of the Display, it can + // be "stale" and overwrite changes made via the webservice. + $reloaded_display = layout_master_cache_load($display->id); + // Need to copy selectively (because just setting $display = $reloaded_display + // breaks other things). + $display->set('blockInfo', $reloaded_display->get('blockInfo')); + // Now remap. $layout = layout_manager()->createInstance($form_state['values']['layout'], array()); $display->remapToLayout($layout); // Store changes in TempStore. @@ -141,14 +138,6 @@ private function layoutDemonstration(EntityInterface $display) { } /** - * Temporary implementation of block markup generation. - */ - private function layoutBlock($block_id) { - $block_id = str_replace('block.', '', $block_id); - return '
M' . $block_id . ' block
'; - } - - /** * Overrides Drupal\Core\Entity\EntityFormController::actions(). */ protected function actions(array $form, array &$form_state) {