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..5ce1c4c --- /dev/null +++ b/core/modules/layout/js/collections/collections.js @@ -0,0 +1,26 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "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); + + 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..2e3052f --- /dev/null +++ b/core/modules/layout/js/models/app-model.js @@ -0,0 +1,16 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.AppModel = Backbone.Model.extend({ + defaults: { + '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..0952029 --- /dev/null +++ b/core/modules/layout/js/models/block-model.js @@ -0,0 +1,29 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "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'), + 'blockId': this.get('id'), + 'config': this.get('config') + }); + } + }); + + +})(jQuery, _, Backbone, Drupal); 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..d25e2a9 --- /dev/null +++ b/core/modules/layout/js/models/blockinstance-model.js @@ -0,0 +1,20 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockInstanceModel = Backbone.Model.extend({ + defaults: { + 'id': null, + 'weight': null, + 'blockId': null, + 'config': {} + } + }); + + + +})(jQuery, _, Backbone, Drupal); 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..3a8dc75 --- /dev/null +++ b/core/modules/layout/js/models/region-model.js @@ -0,0 +1,18 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionModel = Backbone.Model.extend({ + 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..d37a366 --- /dev/null +++ b/core/modules/layout/js/theme.js @@ -0,0 +1,39 @@ +/** + * @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. + * Poor man's _.template ... + * @return + * The corresponding HTML. + */ + Drupal.theme.layoutBlock = function (id, label, settings) { + 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..5a298db --- /dev/null +++ b/core/modules/layout/js/views/app-view.js @@ -0,0 +1,32 @@ +(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(); + } + }); + +})(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..e5068c6 --- /dev/null +++ b/core/modules/layout/js/views/blockinstance-view.js @@ -0,0 +1,30 @@ +(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]); + }, + 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')))); + 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..4c19c6b --- /dev/null +++ b/core/modules/layout/js/views/blockinstancemodal-view.js @@ -0,0 +1,28 @@ +/** + * @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); + // Close and remove dialog ... + this.remove(); + }, + render: function() { + this.$el.html(' or '); + 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..eb12f08 --- /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) { + console.log('selectBlock', 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() { + this._collectionView && this._collectionView.remove(); + // replace by calling _super.remove(); + this.dialog.close(); + this.$el.remove(); + } + }); + + Drupal.layout.BlockListItemView = Backbone.View.extend({ + events: { + 'click a': 'selectBlock' + }, + selectBlock: function(e) { + console.log('selectBlock', this.model); + // 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..b5ef098 --- /dev/null +++ b/core/modules/layout/js/views/modal-view.js @@ -0,0 +1,26 @@ +(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() { + this.dialog.close(); + 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..6c309f3 --- /dev/null +++ b/core/modules/layout/js/views/region-view.js @@ -0,0 +1,78 @@ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionView = Backbone.View.extend({ + events:{ + 'click .add-block':'onClickAdd', + 'reorder':'reorderInstances' + }, + + onClickAdd:function (e) { + var collection = Drupal.layout.getBlocksCollection(); + console.log('onClickAdd', collection); + 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 ; + }, + + 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(); + } + }); + + +})(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..4291aca --- /dev/null +++ b/core/modules/layout/js/views/regionmodal-view.js @@ -0,0 +1,19 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.RegionModalView = Drupal.layout.ModalView.extend({ + render: function() { + this.$el.html( + 'Configure the region' + ); + 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..5afe4b2 --- /dev/null +++ b/core/modules/layout/js/views/updatingcollection-view.js @@ -0,0 +1,66 @@ +/** + * @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); + }, + + 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; + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/layout.admin.css b/core/modules/layout/layout.admin.css index 474564f..e423061 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 { @@ -49,7 +50,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 +60,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 +135,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%); diff --git a/core/modules/layout/layout.admin.js b/core/modules/layout/layout.admin.js index aea15c1..b762f0f 100644 --- a/core/modules/layout/layout.admin.js +++ b/core/modules/layout/layout.admin.js @@ -6,60 +6,85 @@ * 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; } - // 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(); - }); - }); + // 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)); + } - // 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/layout.module b/core/modules/layout/layout.module index e2341b8..97b59a0 100644 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -148,3 +148,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..b9c8945 100644 --- a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php +++ b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php @@ -105,6 +105,8 @@ 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'); return $build; }