diff --git a/core/modules/layout/grunt.js b/core/modules/layout/grunt.js new file mode 100644 index 0000000..f566742 --- /dev/null +++ b/core/modules/layout/grunt.js @@ -0,0 +1,59 @@ +module.exports = function(grunt) { + // Project configuration. + grunt.initConfig({ + lint: { + all: [ + 'js/collections/*.js', + 'js/models/*.js', + 'js/views/*.js', + 'js/routers/*.js' + ] + }, + concat: { + dist: { + src: [ + 'js/models/*.js', + 'js/collections/*.js', + 'js/views/*.js', + 'js/routers/*.js', + 'js/*.js' + ], + // For simplicity now + dest: 'js/app.js' + }, + }, + watch: { + files: '', + tasks: 'default' + }, + min: { + dist: { + src: ['js/app.js'], + dest: 'js/app.min.js' + } + }, + jshint: { + options: { + curly: true, + immed: false, + undef: true, + browser: true, + laxbreak: true + }, + globals: { + jQuery: true, + Backbone: true, + Drupal: true, + VIE: true, + _: true + } + } + }); + + // Load local tasks; we should add local tasks later. + // grunt.loadTasks("tasks"); + + // Set default + grunt.registerTask('default', 'lint concat min'); + +}; diff --git a/core/modules/layout/js/collections/collections.js b/core/modules/layout/js/collections/collections.js new file mode 100644 index 0000000..9020725 --- /dev/null +++ b/core/modules/layout/js/collections/collections.js @@ -0,0 +1,24 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal, drupalSettings) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionsCollection = Backbone.Collection.extend({ + model: Drupal.layout.RegionModel + }); + + Drupal.layout.BlocksCollection = Backbone.Collection.extend({ + model: Drupal.layout.BlockModel + }); + + Drupal.layout.BlockInstancesCollection = Backbone.Collection.extend({ + model: Drupal.layout.BlockInstanceModel, + comparator: function(model) { + return model.get('weight'); + } + }); + +})(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/layout/js/models/app-model.js b/core/modules/layout/js/models/app-model.js new file mode 100644 index 0000000..8830e6c --- /dev/null +++ b/core/modules/layout/js/models/app-model.js @@ -0,0 +1,20 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.AppModel = Backbone.Model.extend({ + url: function() { + return drupalSettings.layout.webserviceURL + '/layout'; + }, + defaults: { + 'id': null, + 'template': null, + 'regions': null, + 'config': null + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/models/block-model.js b/core/modules/layout/js/models/block-model.js new file mode 100644 index 0000000..167b030 --- /dev/null +++ b/core/modules/layout/js/models/block-model.js @@ -0,0 +1,30 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal, drupalSettings) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockModel = Backbone.Model.extend({ + defaults: { + /* CMI name */ + 'id': null, + 'label': '', + 'description': '', + 'config': {} + }, + createBlockInstance: function() { + return new Drupal.layout.BlockInstanceModel({ + // in real life this would need to come from somewhere else .. + // maybe time for UUID.js + 'id': this.get('id'), + 'label': this.get('id'), + 'blockId': this.get('id'), + 'config': this.get('config') + }); + } + }); + + +})(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/layout/js/models/blockinstance-model.js b/core/modules/layout/js/models/blockinstance-model.js new file mode 100644 index 0000000..26d3587 --- /dev/null +++ b/core/modules/layout/js/models/blockinstance-model.js @@ -0,0 +1,24 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal, drupalSettings) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockInstanceModel = Backbone.Model.extend({ + url: function() { + return drupalSettings.layout.webserviceURL + '/block'; + }, + defaults: { + 'id': null, + 'weight': null, + 'blockId': null, + 'region': '', + 'config': {} + } + }); + + + +})(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/layout/js/models/region-model.js b/core/modules/layout/js/models/region-model.js new file mode 100644 index 0000000..6e0b22d --- /dev/null +++ b/core/modules/layout/js/models/region-model.js @@ -0,0 +1,21 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionModel = Backbone.Model.extend({ + url: function() { + return drupalSettings.layout.webserviceURL + '/region/' + this.get('id'); + }, + defaults: { + 'id': null, + 'blockInstances': null, + 'config': null + } + }); + + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/theme.js b/core/modules/layout/js/theme.js new file mode 100644 index 0000000..f4af36c --- /dev/null +++ b/core/modules/layout/js/theme.js @@ -0,0 +1,48 @@ +/** + * @file + */ +(function ($) { + /** + * Theme function for a region. + * @param id + * @param label + * @return {String} + */ + Drupal.theme.layoutRegion = function (id, label) { + var html = + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + label + '
' + + '
' + + '
' + + '
' + '
' + + '
' + + '
'; + return html; + } + + /** + * Theme function to get the html for a block instance. + * Poor man's _.template ... + * @return + * The corresponding HTML. + */ + Drupal.theme.layoutBlock = function (id, label, attributes) { + return '
' + + '
' + + '
' + + 'M' + + '' + id + ' block' + + '
' + + '
' + + '
' + + '
'; + }; + +})(jQuery); diff --git a/core/modules/layout/js/views/app-view.js b/core/modules/layout/js/views/app-view.js new file mode 100644 index 0000000..01d4897 --- /dev/null +++ b/core/modules/layout/js/views/app-view.js @@ -0,0 +1,44 @@ +(function ($, _, Backbone, Drupal) { + + "use strict"; + // sad panda. + Backbone.emulateJSON = true; + Backbone.emulateHTTP = true; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.AppView = Backbone.View.extend({ + events: { + 'change': 'onChange' + }, + initialize: function() { + this.regionsView = new Drupal.layout.RegionsView({ + collection: this.model.get('regions'), + el: this.$el.find('.layout-display') + }); + this.model.get('regions').on('change', function() { + console.log('Change!'); + }); + }, + 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(); + }, + onChange: function() { + + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/blockinstance-view.js b/core/modules/layout/js/views/blockinstance-view.js new file mode 100644 index 0000000..a407c3e --- /dev/null +++ b/core/modules/layout/js/views/blockinstance-view.js @@ -0,0 +1,34 @@ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockInstanceView = Backbone.View.extend({ + events:{ + 'click':'onClick', + 'drop':'onDrop' + }, + onDrop:function (event, index) { + // Trigger reorder, will be handle in Drupal.layout.RegionView + this.$el.trigger('reorder', [this.model, index]); + // @todo: handle dropping onto enpty region. + event.preventDefault(); + event.stopPropagation(); + return ; + }, + onClick:function () { + this.dialogView = new Drupal.layout.BlockInstanceModalView({ + model: this.model, + title: this.model.get('label') + }); + this.dialogView.render(); + }, + render:function () { + this.setElement($(Drupal.theme('layoutBlock', this.model.get('id'), this.model.get('label'), this.model.attributes))); + return this; + } + }); + + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/blockinstancemodal-view.js b/core/modules/layout/js/views/blockinstancemodal-view.js new file mode 100644 index 0000000..ee3a521 --- /dev/null +++ b/core/modules/layout/js/views/blockinstancemodal-view.js @@ -0,0 +1,33 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.BlockInstanceModalView = Drupal.layout.ModalView.extend({ + events: { + 'click button.delete': 'removeBlockInstance' + }, + removeBlockInstance: function() { + // Remove model. + this.model.collection.remove(this.model); + // Destroy model on server. + this.model.destroy(); + // Close and remove dialog. + this.remove(); + }, + render: function() { + this.$el.html( + ' ' + + '' + ); + this.show(); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); + + diff --git a/core/modules/layout/js/views/blockselectormodal-view.js b/core/modules/layout/js/views/blockselectormodal-view.js new file mode 100644 index 0000000..56d12d3 --- /dev/null +++ b/core/modules/layout/js/views/blockselectormodal-view.js @@ -0,0 +1,60 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.BlockSelectorModalView = Drupal.layout.ModalView.extend({ + events: { + 'select': 'selectBlock' + }, + selectBlock: function(e, block) { + // Model is RegionModel + var instance = block.createBlockInstance(); + this.model.get('blockInstances').add(instance); + // Remove & close dialog. + this.remove(); + }, + tagName: 'ul', + render: function() { + this.$el.empty(); + this._collectionView = new Drupal.layout.UpdatingCollectionView({ + collection:this.collection, + nestedViewConstructor:Drupal.layout.BlockListItemView, + nestedViewTagName:'li' + }); + this._collectionView.setElement(this.$el); + this._collectionView.render(); + this.show(); + return this; + }, + remove: function() { + if (this._collectionView) { + this._collectionView.remove(); + } + // Apparently no need to call this.dialog.close(); remove this.$el + // closes the jQueryUI Dialog, oh jqueryui magic ... + this.$el.remove(); + } + }); + + Drupal.layout.BlockListItemView = Backbone.View.extend({ + events: { + 'click a': 'selectBlock' + }, + selectBlock: function(e) { + // Pass this click & model to the parent view. + this.$el.trigger('select', [this.model]); + e.preventDefault(); + e.stopPropagation(); + return ; + }, + render: function() { + this.$el.html('Add Block ' + this.model.get('label') + ''); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/modal-view.js b/core/modules/layout/js/views/modal-view.js new file mode 100644 index 0000000..ea38807 --- /dev/null +++ b/core/modules/layout/js/views/modal-view.js @@ -0,0 +1,27 @@ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.ModalView = Backbone.View.extend({ + dialog: null, + callback: null, + initialize: function(options) { + this.callback = options.callback || null; + this.dialog = Drupal.dialog(this.$el, {title: this.options.title}); + }, + show: function() { + this.dialog.showModal(); + }, + close: function() { + this.dialog.close(); + }, + remove: function() { + // Apparently no need to call this.dialog.close(); remove this.$el + // closes the jQueryUI Dialog, oh jqueryui magic ... + this.$el.remove(); + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/region-view.js b/core/modules/layout/js/views/region-view.js new file mode 100644 index 0000000..fcf934f --- /dev/null +++ b/core/modules/layout/js/views/region-view.js @@ -0,0 +1,95 @@ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionView = Backbone.View.extend({ + events:{ + 'click .plus-icon':'onClickAdd', + 'click .gear-icon':'onClickConfigure', + 'reorder':'reorderInstances' + }, + + onClickConfigure:function (e) { + this.dialogView = new Drupal.layout.RegionModalView({ + model: this.model, + title: this.model.get('label') + }); + this.dialogView.render(); + }, + + onClickAdd:function (e) { + var collection = Drupal.layout.getBlocksCollection(); + this.modalView = new Drupal.layout.BlockSelectorModalView({ + model: this.model, + collection: collection, + title: Drupal.t('Please select a block to place in the regions %region', {'%region': this.model.get('label')}) + }); + this.modalView.render(); + e.preventDefault(); + e.stopPropagation(); + return ; + }, + // @todo: this should be on app-view.js but for now ... + saveFullLayout: function() { + Drupal.layout.appModel.save(); + }, + + initialize:function () { + var blockInstances = this.model.get('blockInstances'); + this._collectionView = new Drupal.layout.UpdatingCollectionView({ + collection:blockInstances, + nestedViewConstructor:Drupal.layout.BlockInstanceView, + nestedViewTagName:'div' + }); + + + // make sure we gather a bunch (300ms) of change events before we push the + // last to the server. @todo: make sure that the submit button is disabled + // so that we don't have crazy race condition thingies. + blockInstances.on('change', _.debounce(function() { + Drupal.layout.appModel.save(); + }, 100), this); + blockInstances.on('add', this.saveFullLayout, this); + blockInstances.on('remove', this.saveFullLayout, 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 { + if (model.get('weight')==position) { + return ; + } + 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}); + collection.trigger('reorder'); + this.render(); + } + }); + + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/regionmodal-view.js b/core/modules/layout/js/views/regionmodal-view.js new file mode 100644 index 0000000..4eca478 --- /dev/null +++ b/core/modules/layout/js/views/regionmodal-view.js @@ -0,0 +1,23 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.RegionModalView = Drupal.layout.ModalView.extend({ + events: { + }, + render: function() { + this.$el.html( + '
Configure region type? Load some FAPI form here.
' + + '' + ); + this.show(); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); + diff --git a/core/modules/layout/js/views/regions-view.js b/core/modules/layout/js/views/regions-view.js new file mode 100644 index 0000000..234d0c8 --- /dev/null +++ b/core/modules/layout/js/views/regions-view.js @@ -0,0 +1,30 @@ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + 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(); + } + }); + + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/updatingcollection-view.js b/core/modules/layout/js/views/updatingcollection-view.js new file mode 100644 index 0000000..c62929e --- /dev/null +++ b/core/modules/layout/js/views/updatingcollection-view.js @@ -0,0 +1,70 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + 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); + }, + + _getViewByModel: function(model) { + // @todo this probably should be cached/tracked. + var vs = _(this._nestedViews).select(function (nv) { + return nv.model === model; + }); + return vs.length ? vs[0] : false; + }, + + 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._getViewByModel(model); + this._nestedViews = _(this._nestedViews).without(viewToRemove); + if (this._rendered) { + viewToRemove.$el.remove(); + } + }, + + render:function () { + this._rendered = true; + this.$el.empty(); + // Use the collection to make sure the order of the rendered views is + // up-to-date. + this.collection.each(function(m) { + var nv = this._getViewByModel(m); + this.$el.append(nv.render().$el); + }, this); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/layout.admin.css b/core/modules/layout/layout.admin.css index 474564f..ee33789 100644 --- a/core/modules/layout/layout.admin.css +++ b/core/modules/layout/layout.admin.css @@ -49,7 +49,7 @@ z#display-blocks { box-shadow:0px 0px 1px 1px rgba(0,0,0,0.1); } -#display-blocks .region-information { +#display-blocks .region-header { clear:both; display: block; width: 100%; @@ -59,6 +59,14 @@ z#display-blocks { overflow: hidden; } +#display-blocks .region-actions { + float: left; +} + +#display-blocks .region-information { + float: right; +} + #display-blocks .region-label { font-size:11px; text-transform: uppercase; @@ -126,8 +134,8 @@ z#display-blocks .page-block { #display-blocks .add-block { display: table-cell; border: 1px solid #ddd; - width: 60px !important; - height: 60px; + width: 20px !important; + height: 20px; background-image:-moz-linear-gradient(rgb(254,254,254) 0%,rgb(225,225,225) 100%); background-image:-webkit-gradient(linear,color-stop(0, rgb(254,254,254)),color-stop(1, rgb(225,225,225))); background-image:-webkit-linear-gradient(rgb(254,254,254) 0%,rgb(225,225,225) 100%); @@ -158,6 +166,15 @@ z#display-blocks .page-block { background-repeat:no-repeat; } +#display-blocks .gear-icon { + display: inline-block; + width: 100%; + height: 100%; + background-image:url(gear_fff.png); + background-position:center; + background-repeat:no-repeat; +} + z#display-blocks .rowspan { width: 100%; position: relative; diff --git a/core/modules/layout/layout.admin.inc b/core/modules/layout/layout.admin.inc index aa37c85..c6188b5 100644 --- a/core/modules/layout/layout.admin.inc +++ b/core/modules/layout/layout.admin.inc @@ -95,3 +95,39 @@ function layout_master_edit(Display $display) { drupal_set_title(t('Edit layout @label', array('@label' => $display->label())), PASS_THROUGH); return entity_get_form($display); } + +function layout_master_webservice(Display $display, $a=NULL, $b=NULL, $c=NULL) { + // This is all evil, evil, evil - hacking this blindly bottom-up. + $payload = isset($_POST['model']) ? drupal_json_decode($_POST['model']) : FALSE; + $method = isset($_POST['_method']) ? $_POST['_method'] : FALSE; + switch ($a) { + // stores complete layout + case 'layout': + $blockInfo = array(); + // Shove the + foreach ($payload['regions'] as $region) { + foreach ($region['blockInstances'] as $blockInstance) { + $block = $blockInstance['id']; + $blockInfo['block.' . $block] = array( + 'region' => $region['id'], + 'weight' => $blockInstance['weight'] * 100 + ); + } + } + $display->set('blockInfo', $blockInfo); + // Store changes in TempStore. + layout_master_cache_set($display); + drupal_exit(); + break; + // Update single region + case 'region': + $region_id = $b; + die(); + break; + // Update single block + case 'block': + $block_id = $b; + break; + } + return ; +} diff --git a/core/modules/layout/layout.admin.js b/core/modules/layout/layout.admin.js index aea15c1..398b885 100644 --- a/core/modules/layout/layout.admin.js +++ b/core/modules/layout/layout.admin.js @@ -1,4 +1,4 @@ -(function ($, window) { +(function ($, window, Drupal, drupalSettings) { "use strict"; @@ -6,62 +6,81 @@ * 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; + // 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(' '); + var region_id = _.filter(classes, function(v) { + return pattern.test(v); + }).toString().replace('layout-region-', ''); - // 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; - } - }; + $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(), + region: region_id, + weight: weight + }); + weight++; + blockInstances.add(block, {silent: true}); + }); + 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; } - // 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); - } + function randomId() { + 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; + } - // Add HTML code for block demonstration. - var block = '
M' + randomString + ' block
'; - $(this).parent().prepend(block); + Drupal.layout.getBlocksCollection = function() { + var blocks = []; + // Generate a bunch of randomly named blocks that. + for (var i = 0; i<10; i++) { + var id = randomId(); + var b = new Drupal.layout.BlockModel({ + 'id': id, + 'label': 'Label ' + id + }); + blocks.push(b); + } + return new Drupal.layout.BlocksCollection(blocks); + } - serializeBlocks(); - }); + // @todo: make this work on template change, i.e. Drupal.ajax/behaviour compatible. + Drupal.layout.appModel = new Drupal.layout.AppModel({ + id: drupalSettings.layout.id }); - - // 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, - }); - $(this).disableSelection(); + // let's grab the stuff from DOM - it would be useful to have JSON ... + Drupal.layout.appModel.set('regions', ExtractModelsFromDOM() ); + var appView = new Drupal.layout.AppView({ + model: Drupal.layout.appModel, + el: $('#block-system-main') }); + appView.render(); } }; -})(jQuery, window); +})(jQuery, window, Drupal, drupalSettings); diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module index e2341b8..8b695fa 100644 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -28,7 +28,6 @@ function layout_menu() { 'access arguments' => array(4), 'file' => 'layout.admin.inc', ); - // Master layout editing. $items['admin/structure/layouts'] = array( 'title' => 'Layouts', @@ -38,7 +37,7 @@ function layout_menu() { 'access arguments' => array('administer layouts'), 'file' => 'layout.admin.inc', ); - $items['admin/structure/layouts/manage/%layout_master'] = array( + $items['admin/structure/layouts/manage/%layout_master_cache'] = array( 'title' => 'Edit layout', 'page callback' => 'layout_master_edit', 'page arguments' => array(4), @@ -46,6 +45,16 @@ function layout_menu() { 'access arguments' => array('administer layouts'), 'file' => 'layout.admin.inc', ); + + $items['admin/structure/layouts/manage/%layout_master_cache/webservice'] = array( + 'title' => 'Webservice', + 'page callback' => 'layout_master_webservice', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'file' => 'layout.admin.inc', + ); + return $items; } @@ -130,6 +139,39 @@ function layout_master_load($id) { } /** + * Specialized menu callback to load a display. + * + * @param $name + * The machine name of the display. + * + * @return + * The display object. + */ +function layout_master_cache_load($name) { + $display_temp_store = drupal_container()->get('user.tempstore')->get('layout'); + $display = $display_temp_store->get($name); + if (empty($display)) { + $display = entity_load('display', $name); + $display->dirty = FALSE; + } + + if (empty($display)) { + return FALSE; + } + // @todo: complete "lock" flag à la views. + return $display; +} + +/** + * Specialized cache function. + */ +function layout_master_cache_set(Display $display) { + // @todo: add "lock" flag etc. + $display->dirty = TRUE; + drupal_container()->get('user.tempstore')->get('layout')->set($display->id, $display); +} + +/** * Implements hook_entity_info_alter(). * * Add URI callback to Display config entities to support listing API. @@ -148,3 +190,48 @@ function layout_entity_info_alter(&$entity_info) { function layout_ajax_block_placement_callback($form, &$form_state) { return $form['blocks']; } + +/** + * Implements hook_library_info(). + */ +function layout_library_info() { + $libraries = array(); + $path = drupal_get_path('module', 'layout'); + $libraries['layout.admin'] = array( + 'title' => 'Layout admin', + 'version' => NULL, + 'js' => array( + // Drupal's pseudo-templates + $path . '/js/theme.js' => array('defer' => TRUE), + + // Models + $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), + $path . '/js/models/region-model.js' => array('defer' => TRUE), + // Collections bundled for the time being. + $path . '/js/collections/collections.js' => array('defer' => TRUE), + + // Views other Views extend first + $path . '/js/views/updatingcollection-view.js' => array('defer' => TRUE), + $path . '/js/views/modal-view.js' => array('defer' => TRUE), + + $path . '/js/views/region-view.js' => array('defer' => TRUE), + $path . '/js/views/regions-view.js' => array('defer' => TRUE), + $path . '/js/views/blockinstance-view.js' => array('defer' => TRUE), + $path . '/js/views/blockinstancemodal-view.js' => array('defer' => TRUE), + + $path . '/js/views/regionmodal-view.js' => array('defer' => TRUE), + $path . '/js/views/blockinstancemodal-view.js' => array('defer' => TRUE), + $path . '/js/views/blockselectormodal-view.js' => array('defer' => TRUE), + + $path . '/js/views/app-view.js' => array('defer' => TRUE), + ), + 'dependencies' => array( + array('system', 'backbone'), + array('system', 'drupal.dialog'), + array('system', 'jquery.ui.sortable') + ), + ); + return $libraries; +} diff --git a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php index 940e7f5..ca330ee 100644 --- a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php +++ b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php @@ -33,20 +33,22 @@ public function form(array $form, array &$form_state, EntityInterface $display) $form_state['values'] = array(); } - // Recreate simple region structure for form communication. - $regions = array(); - $existing_blocks = $display->getAllSortedBlocks(); - foreach ($existing_blocks as $region => $blocks) { - $regions[$region] = array(); - foreach ($blocks as $block_id => $block_info) { - $regions[$region][] = str_replace('block.', '', $block_id); - } - } + $form_state['values'] += array( - 'layout' => isset($display->layout) ? $display->layout : reset($layout_keys), - 'regions' => $regions, + '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, + ); + } + $form['layout'] = array( '#type' => 'select', '#title' => t('Template'), @@ -65,6 +67,8 @@ public function form(array $form, array &$form_state, EntityInterface $display) if (!isset($display->layout) || ($form_state['values']['layout'] != $display->layout)) { $layout = layout_manager()->createInstance($form_state['values']['layout'], array()); $display->remapToLayout($layout); + // Store changes in TempStore. + layout_master_cache_set($display); } // Add block editing interface wrapper for Ajax operation. @@ -73,11 +77,6 @@ public function form(array $form, array &$form_state, EntityInterface $display) '#suffix' => '', ); $form['blocks']['demonstration'] = $this->layoutDemonstration($display); - $form['blocks']['regions'] = array( - '#type' => 'textarea', - '#default_value' => drupal_json_encode($form_state['values']['regions']), - ); - return parent::form($form, $form_state, $display); } @@ -105,7 +104,19 @@ 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('layout', 'layout.admin'); $build['#attached']['library'][] = array('system', 'jquery.ui.sortable'); + // Add the webservice URL and display id. + $build['#attached']['js'][] =array( + 'data' => array( + 'layout' => array( + 'webserviceURL' => url('admin/structure/layouts/manage/' . $display->id . '/webservice'), + 'id' => $display->id, + ) + ), + 'type' => 'setting', + ); return $build; } @@ -140,38 +151,20 @@ protected function actions(array $form, array &$form_state) { * Overrides Drupal\Core\Entity\EntityFormController::save(). */ public function save(array $form, array &$form_state) { - - // Create the block information array needed for the display object. - $blockInfo = array(); - $new_blocks = drupal_json_decode($form_state['values']['regions']); - - // Region type information is derived from the selected layout. - $layout_regions = layout_manager()->createInstance($form_state['values']['layout'], array())->getRegions(); - - foreach ($new_blocks as $region => $blocks) { - $weight = 0; - foreach ($blocks as $block) { - $blockInfo['block.' . $block] = array( - 'region' => $region, - // Increase weights by 100 so they are far enough apart for - // page block placement. - 'weight' => ($weight += 100), - 'region-type' => $layout_regions[$region]['type'], - ); - } - } - unset($form_state['values']['regions']); - - // Build and save the new display with the new block information. $display = $this->getEntity($form_state); - $display->set('blockInfo', $blockInfo); - $display->save(); + $cached_display = layout_master_cache_load($display->id); + // @todo: properly handle storing template / other form values + // this is a all bohemian villages to me ... + $cached_display->set('layout', $form_state['values']['layout']); + + // As the changes are cached in TempStore loading the cached instance and + // saving it should be enough. + $cached_display->save(); + // Cache busting. Remove this view from cache so we can edit it properly. + drupal_container()->get('user.tempstore')->get('layout')->delete($cached_display->id); watchdog('display', 'Layout @label saved.', array('@label' => $display->label()), WATCHDOG_NOTICE); drupal_set_message(t('Layout %label saved.', array('%label' => $display->label()))); - - $form_state['redirect'] = 'admin/structure/layouts'; } } -