commit 426778987791dd4c39f05018d8ffc04cfe28c808 Author: frega Date: Fri Nov 30 15:13:26 2012 +0100 1841584-37 diff --git a/core/modules/layout/js/app.js b/core/modules/layout/js/app.js new file mode 100644 index 0000000..bf03597 --- /dev/null +++ b/core/modules/layout/js/app.js @@ -0,0 +1,234 @@ +/** + * @file + * A Backbone View that is the central app controller. + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.AppView = Backbone.View.extend({ + initialize: function() { + this.regionsView = new Drupal.layout.RegionsView({ + collection: this.model.get('regions'), + el: this.$el.find('.layout-display') + }); + }, + render: function() { + 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(); + } + }); + + Drupal.layout.AppModel = Backbone.Model.extend({ + defaults: { + 'template': null, + 'regions': null, + 'config': null + } + }); + Drupal.layout.RegionModel = Backbone.Model.extend({ + defaults: { + 'id': null, + 'blockInstances': null, + 'config': null + } + }); + Drupal.layout.RegionsCollection = Backbone.Collection.extend({ + model: Drupal.layout.RegionModel + }); + Drupal.layout.BlockInstanceModel = Backbone.Model.extend({ + defaults: { + 'id': null, + 'weight': null, + 'blockId': null, + 'config': {} + } + }); + Drupal.layout.BlockInstancesCollection = Backbone.Collection.extend({ + model: Drupal.layout.BlockInstanceModel, + comparator: function(model) { + return model.get('weight'); + } + }); + + Drupal.layout.UpdatingCollectionView = Backbone.View.extend({ + initialize:function (options) { + if (!options.nestedViewConstructor) { + throw "no child view constructor provided"; + } + if (!options.nestedViewTagName) { + throw "no child view tag name provided"; + } + + this._nestedViewConstructor = options.nestedViewConstructor; + this._nestedViewTagName = options.nestedViewTagName; + + this._nestedViews = []; + + this.collection.each(this.addModel, this); + + this.collection.bind('add', this.addModel, this); + this.collection.bind('remove', this.removeModel, this); + }, + + addModel:function (model) { + var nv = new this._nestedViewConstructor({ + tagName:this._nestedViewTagName, + model:model + }); + + this._nestedViews.push(nv); + if (this._rendered) { + this.$el.append(nv.render().$el); + } + }, + + removeModel:function (model) { + var viewToRemove = _(this._nestedViews).select(function (cv) { + return cv.model === model; + })[0]; + this._nestedViews = _(this._nestedViews).without(viewToRemove); + + if (this._rendered) { + viewToRemove.$el.remove(); + } + }, + + render:function () { + var that = this; + this._rendered = true; + + $(this.el).empty(); + + _(this._nestedViews).each(function (nv) { + that.$el.append(nv.render().$el); + }); + + return this; + } + }); + + Drupal.layout.RegionsView = Backbone.View.extend({ + initialize: function() { + this._collectionView = new Drupal.layout.UpdatingCollectionView({ + collection:this.collection, + nestedViewConstructor:Drupal.layout.RegionView, + nestedViewTagName:'div' + }); + }, + + render : function() { + this.$el.empty(); + this._collectionView.setElement(this.$el); + this._collectionView.render(); + return this; + }, + + remove: function() { + this._collectionView.remove(); + this.$el.empty(); + } + }); + + Drupal.layout.RegionView = Backbone.View.extend({ + events: { + 'click .add-block': 'onClickAdd', + 'reorder': 'reorderInstances' + }, + + onClickAdd: function() { + var collection = this.model.get('blockInstances'); + collection.add(new Drupal.layout.BlockInstanceModel({ + id: Drupal.layout.getRandomId() + })); + }, + + onCollectionChange: function() { + Drupal.layout.serializeRegions(); + }, + + initialize: function() { + var blockInstances = this.model.get('blockInstances'); + this._collectionView = new Drupal.layout.UpdatingCollectionView({ + collection:blockInstances, + nestedViewConstructor:Drupal.layout.BlockInstanceView, + nestedViewTagName:'div' + }); + + blockInstances.on('change', this.onCollectionChange, this); + blockInstances.on('add', this.onCollectionChange, this); + blockInstances.on('remove', this.onCollectionChange, this); + }, + render : function() { + this.$el.html(Drupal.theme.layoutRegion(this.model.get('id'), this.model.get('label'))); + this._collectionView.setElement(this.$el.find('.region-blocks')); + this._collectionView.render(); + + return this; + }, + remove: function() { + this.$el.empty(); + this._collectionView.remove(); + }, + reorderInstances: function(event, model, position) { + var collection = this.model.get('blockInstances'); + // handle cross collection. + if (!collection.contains(model)) { + // let's remove it from the other first before adding it here. + model.collection.remove(model); + } else { + collection.remove(model); + } + 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}); + var ids = collection.pluck('id'); + this.render(); + } + }); + + Drupal.layout.BlockInstanceView = Backbone.View.extend({ + events: { + 'click': 'onClickOperations', + 'drop' : 'onDrop' + }, + onDrop: function(event, index) { + this.$el.trigger('reorder', [this.model, index]); + }, + onClickOperations: function() { + if (confirm('This would be modal for configuration - click ok now to remove it!')) { + this.model.collection.remove(this.model); + } + }, + render: function() { + this.setElement($(Drupal.theme('layoutBlock', this.model.get('id'), this.model.get('label')))); + return this; + } + }); + // @todo: the user interaction stuff. + Drupal.layout.ModalView = Backbone.View.extend({}); + Drupal.layout.BlockSelectorModalView = Drupal.layout.ModalView.extend({}); + Drupal.layout.BlockInstanceConfigurationModalView = Drupal.layout.ModalView.extend({}); + Drupal.layout.RegionConfigurationView = Drupal.layout.ModalView.extend({}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/layout.admin.css b/core/modules/layout/layout.admin.css index 474564f..fa42eff 100644 --- a/core/modules/layout/layout.admin.css +++ b/core/modules/layout/layout.admin.css @@ -17,7 +17,8 @@ } #display-blocks .form-textarea { - display: none; + /* temporarily commented out so that we can *see* what payload is being generated */ + /* display: none; */ } z#display-blocks { diff --git a/core/modules/layout/layout.admin.js b/core/modules/layout/layout.admin.js index aea15c1..37f3d0e 100644 --- a/core/modules/layout/layout.admin.js +++ b/core/modules/layout/layout.admin.js @@ -6,60 +6,101 @@ * Attach display editor functionality. */ Drupal.behaviors.displayEditor = { + attach: function (context) { + var appModel; - function serializeBlocks() { - var regionBlocks = {}; - $('.layout-region', context).each(function() { + function ExtractModelsFromDOM() { + var regions = new Drupal.layout.RegionsCollection(); + // Retrieve regions from DOM. + $('.layout-region').each(function() { + var $region = $(this); + var blockInstances = new Drupal.layout.BlockInstancesCollection(); + var weight = 0; + $region.find('.block').each(function() { + var block = new Drupal.layout.BlockInstanceModel({ + id: $(this).attr('id').replace(/block-/, ''), + blockId: 'default', + label: $(this).find('.block-label').text(), + weight: weight + }); + weight++; + blockInstances.add(block, {silent: true}); + }); + // wow, this is awkward but will go away. + var classes = $region.attr('class').split(' '); var pattern = new RegExp('layout-region-.+'); - var classes = $(this).attr('class').split(' '); - - // Look for the class with the region name. - for (var i = 0; i < classes.length; i++) { - if (pattern.test(classes[i])) { - // Store the block order for this region. - var regionName = classes[i].replace('layout-region-', ''); - regionBlocks[regionName] = []; - var blocks = $(this).find('.region-blocks').sortable('toArray'); - for (var j = 0; j < blocks.length; j++) { - regionBlocks[regionName].push(blocks[j].replace('block-', '')); - }; - break; - } - }; + var region_id = _.filter(classes, function(v) { + return pattern.test(v); + }).toString().replace('layout-region-', ''); + var region = new Drupal.layout.RegionModel({ + id: region_id, + label: $region.find('.region-label').text(), + blockInstances: blockInstances + }); + regions.add(region); }); - $('#edit-regions', context).val(JSON.stringify(regionBlocks)); + return regions; + } + + Drupal.layout.getRandomId = function() { + var chars = "abcdefghiklmnopqrstuvwxyz"; + var randomString = ''; + for (var i=0; i < 8; i++) { + var rnum = Math.floor(Math.random() * chars.length); + randomString += chars.substring(rnum,rnum+1); + } + return randomString; } - // Attach click handler to add block button. - $('.region-table .add-block', context).once('display-add-block', function() { - $(this).click(function () { - // Generate a random block title until we can fire blocks as plugins - // in a modal that will give us a machine name and a title to place. - var chars = "abcdefghiklmnopqrstuvwxyz"; - var randomString = ''; - for (var i=0; i < 8; i++) { - var rnum = Math.floor(Math.random() * chars.length); - randomString += chars.substring(rnum,rnum+1); - } + // just a quick hack to get this working + Drupal.layout.serializeRegions = function() { + var regionBlocks = {}; + appModel.get('regions').each(function(region) { + regionBlocks[region.get('id')] = region.get('blockInstances').pluck('id'); + }, this); + $('#edit-regions', context).val(JSON.stringify(regionBlocks)); + } - // Add HTML code for block demonstration. - var block = '
M' + randomString + ' block
'; - $(this).parent().prepend(block); + /** + * Theme function for a region. + * @param id + * @param label + * @return {String} + */ + Drupal.theme.layoutRegion = function(id, label) { + var html = + '
' + + '
' + + '
' + label + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + return html; + } - serializeBlocks(); - }); - }); + /** + * Theme function to get the html for a block. + * Poor man's _.template ... + * @return + * The corresponding HTML. + */ + Drupal.theme.layoutBlock = function(id, label, settings) { + return '
M' + id+ ' block
'; + }; - // Apply drag and drop behavior to all blocks in all regions. - $('.region-blocks', context).once('display-sort-blocks', function() { - $(this).sortable({ - items: '.block', - connectWith: '.connected-sortable', - cursor: 'move', - update: serializeBlocks, + $('body').once(function() { + appModel = new Drupal.layout.AppModel(); + // let's grab the stuff from DOM - it would be useful to have JSON ... + appModel.set('regions', ExtractModelsFromDOM() ); + var appView = new Drupal.layout.AppView({ + model: appModel, + el: $('#block-system-main') }); - $(this).disableSelection(); + appView.render(); }); } }; diff --git a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php index 940e7f5..bf1b6e7 100644 --- a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php +++ b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php @@ -105,6 +105,11 @@ private function layoutDemonstration(EntityInterface $display) { ); $build['#attached']['css'][] = drupal_get_path('module', 'layout') . '/layout.admin.css'; $build['#attached']['js'][] = drupal_get_path('module', 'layout') . '/layout.admin.js'; + // Add the backbone app. + $build['#attached']['library'][] = array('system', 'backbone'); + $build['#attached']['js'][] = drupal_get_path('module', 'layout') . '/js/app.js'; + + $build['#attached']['library'][] = array('system', 'jquery.ui.sortable'); return $build; }