MDL-41511 blocks: improved how custom block regions were being rendered.
[moodle.git] / lib / yui / src / blocks / js / blocks.js
1 /**
2  * Provides drag and drop functionality for blocks.
3  *
4  * @module moodle-core-blockdraganddrop
5  */
7 var AJAXURL = '/lib/ajax/blocks.php',
8 CSS = {
9     BLOCK : 'block',
10     BLOCKREGION : 'block-region',
11     BLOCKADMINBLOCK : 'block_adminblock',
12     EDITINGMOVE : 'editing_move',
13     HEADER : 'header',
14     LIGHTBOX : 'lightbox',
15     REGIONCONTENT : 'region-content',
16     SKIPBLOCK : 'skip-block',
17     SKIPBLOCKTO : 'skip-block-to',
18     MYINDEX : 'page-my-index',
19     REGIONMAIN : 'region-main'
20 };
22 var SELECTOR = {
23     DRAGHANDLE : '.' + CSS.HEADER + ' .commands .moodle-core-dragdrop-draghandle'
24 };
26 /**
27  * Legacy drag and drop manager.
28  * This drag and drop manager is specifically designed for themes using side-pre and side-post
29  * that do not make use of the block output methods introduced by MDL-39824.
30  *
31  * @namespace M.core.blockdraganddrop
32  * @class LegacyManager
33  * @constructor
34  * @extends M.core.dragdrop
35  */
36 var DRAGBLOCK = function() {
37     DRAGBLOCK.superclass.constructor.apply(this, arguments);
38 };
39 Y.extend(DRAGBLOCK, M.core.dragdrop, {
40     skipnodetop : null,
41     skipnodebottom : null,
42     dragsourceregion : null,
43     initializer : function() {
44         // Set group for parent class
45         this.groups = ['block'];
46         this.samenodeclass = CSS.BLOCK;
47         this.parentnodeclass = CSS.REGIONCONTENT;
49         // Add relevant classes and ID to 'content' block region on My Home page.
50         var myhomecontent = Y.Node.all('body#'+CSS.MYINDEX+' #'+CSS.REGIONMAIN+' > .'+CSS.REGIONCONTENT);
51         if (myhomecontent.size() > 0) {
52             var contentregion = myhomecontent.item(0);
53             contentregion.addClass(CSS.BLOCKREGION);
54             contentregion.set('id', CSS.REGIONCONTENT);
55             contentregion.one('div').addClass(CSS.REGIONCONTENT);
56         }
58         // Initialise blocks dragging
59         // Find all block regions on the page
60         var blockregionlist = Y.Node.all('div.'+CSS.BLOCKREGION);
62         if (blockregionlist.size() === 0) {
63             return false;
64         }
66         // See if we are missing either of block regions,
67         // if yes we need to add an empty one to use as target
68         if (blockregionlist.size() !== this.get('regions').length) {
69             var blockregion = Y.Node.create('<div></div>')
70                 .addClass(CSS.BLOCKREGION);
71             var regioncontent = Y.Node.create('<div></div>')
72                 .addClass(CSS.REGIONCONTENT);
73             blockregion.appendChild(regioncontent);
74             var pre = blockregionlist.filter('#region-pre');
75             var post = blockregionlist.filter('#region-post');
77             if (pre.size() === 0 && post.size() === 1) {
78                 // pre block is missing, instert it before post
79                 blockregion.setAttrs({id : 'region-pre'});
80                 post.item(0).insert(blockregion, 'before');
81                 blockregionlist.unshift(blockregion);
82             } else if (post.size() === 0 && pre.size() === 1) {
83                 // post block is missing, instert it after pre
84                 blockregion.setAttrs({id : 'region-post'});
85                 pre.item(0).insert(blockregion, 'after');
86                 blockregionlist.push(blockregion);
87             }
88         }
90         blockregionlist.each(function(blockregionnode) {
92             // Setting blockregion as droptarget (the case when it is empty)
93             // The region-post (the right one)
94             // is very narrow, so add extra padding on the left to drop block on it.
95             new Y.DD.Drop({
96                 node: blockregionnode.one('div.'+CSS.REGIONCONTENT),
97                 groups: this.groups,
98                 padding: '40 240 40 240'
99             });
101             // Make each div element in the list of blocks draggable
102             var del = new Y.DD.Delegate({
103                 container: blockregionnode,
104                 nodes: '.'+CSS.BLOCK,
105                 target: true,
106                 handles: [SELECTOR.DRAGHANDLE],
107                 invalid: '.block-hider-hide, .block-hider-show, .moveto',
108                 dragConfig: {groups: this.groups}
109             });
110             del.dd.plug(Y.Plugin.DDProxy, {
111                 // Don't move the node at the end of the drag
112                 moveOnEnd: false
113             });
114             del.dd.plug(Y.Plugin.DDWinScroll);
116             var blocklist = blockregionnode.all('.'+CSS.BLOCK);
117             blocklist.each(function(blocknode) {
118                 var move = blocknode.one('a.'+CSS.EDITINGMOVE);
119                 if (move) {
120                     move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'iconsmall', true));
121                     blocknode.one(SELECTOR.DRAGHANDLE).setStyle('cursor', 'move');
122                 }
123             }, this);
124         }, this);
125     },
127     get_block_id : function(node) {
128         return Number(node.get('id').replace(/inst/i, ''));
129     },
131     get_block_region : function(node) {
132         var region = node.ancestor('div.'+CSS.BLOCKREGION).get('id').replace(/region-/i, '');
133         if (Y.Array.indexOf(this.get('regions'), region) === -1) {
134             // Must be standard side-X
135             if (right_to_left()) {
136                 if (region === 'post') {
137                     region = 'pre';
138                 } else if (region === 'pre') {
139                     region = 'post';
140                 }
141             }
142             return 'side-' + region;
143         }
144         // Perhaps custom region
145         return region;
146     },
148     get_region_id : function(node) {
149         return node.get('id').replace(/region-/i, '');
150     },
152     drag_start : function(e) {
153         // Get our drag object
154         var drag = e.target;
156         // Store the parent node of original drag node (block)
157         // we will need it later for show/hide empty regions
158         this.dragsourceregion = drag.get('node').ancestor('div.'+CSS.BLOCKREGION);
160         // Determine skipnodes and store them
161         if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) {
162             this.skipnodetop = drag.get('node').previous();
163         }
164         if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) {
165             this.skipnodebottom = drag.get('node').next();
166         }
167     },
169     drop_over : function(e) {
170         // Get a reference to our drag and drop nodes
171         var drag = e.drag.get('node');
172         var drop = e.drop.get('node');
174         // We need to fix the case when parent drop over event has determined
175         // 'goingup' and appended the drag node after admin-block.
176         if (drop.hasClass(this.parentnodeclass) && drop.one('.'+CSS.BLOCKADMINBLOCK) && drop.one('.'+CSS.BLOCKADMINBLOCK).next('.'+CSS.BLOCK)) {
177             drop.prepend(drag);
178         }
180         // Block is moved within the same region
181         // stop here, no need to modify anything.
182         if (this.dragsourceregion.contains(drop)) {
183             return false;
184         }
186         // TODO: Hiding-displaying block region only works for base theme blocks
187         // (region-pre, region-post) at the moment. It should be improved
188         // to work with custom block regions as well.
190         // TODO: Fix this for the case when user drag block towards empty section,
191         // then the section appears, then user chnages his mind and moving back to
192         // original section. The opposite section remains opened and empty.
194         var documentbody = Y.one('body');
195         // Moving block towards hidden region-content, display it
196         var regionname = this.get_region_id(this.dragsourceregion);
197         if (documentbody.hasClass('side-'+regionname+'-only')) {
198             documentbody.removeClass('side-'+regionname+'-only');
199         }
201         // Moving from empty region-content towards the opposite one,
202         // hide empty one (only for region-pre, region-post areas at the moment).
203         regionname = this.get_region_id(drop.ancestor('div.'+CSS.BLOCKREGION));
204         if (this.dragsourceregion.all('.'+CSS.BLOCK).size() === 0 && this.dragsourceregion.get('id').match(/(region-pre|region-post)/i)) {
205             if (!documentbody.hasClass('side-'+regionname+'-only')) {
206                 documentbody.addClass('side-'+regionname+'-only');
207             }
208         }
209     },
211     drop_end : function() {
212         // clear variables
213         this.skipnodetop = null;
214         this.skipnodebottom = null;
215         this.dragsourceregion = null;
216     },
218     drag_dropmiss : function(e) {
219         // Missed the target, but we assume the user intended to drop it
220         // on the last last ghost node location, e.drag and e.drop should be
221         // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
222         this.drop_hit(e);
223     },
225     drop_hit : function(e) {
226         var drag = e.drag;
227         // Get a reference to our drag node
228         var dragnode = drag.get('node');
229         var dropnode = e.drop.get('node');
231         // Amend existing skipnodes
232         if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) {
233             // the one that belongs to block below move below
234             dragnode.insert(dragnode.previous(), 'after');
235         }
236         // Move original skipnodes
237         if (this.skipnodetop) {
238             dragnode.insert(this.skipnodetop, 'before');
239         }
240         if (this.skipnodebottom) {
241             dragnode.insert(this.skipnodebottom, 'after');
242         }
244         // Add lightbox if it not there
245         var lightbox = M.util.add_lightbox(Y, dragnode);
247         // Prepare request parameters
248         var params = {
249             sesskey : M.cfg.sesskey,
250             courseid : this.get('courseid'),
251             pagelayout : this.get('pagelayout'),
252             pagetype : this.get('pagetype'),
253             subpage : this.get('subpage'),
254             contextid : this.get('contextid'),
255             action : 'move',
256             bui_moveid : this.get_block_id(dragnode),
257             bui_newregion : this.get_block_region(dropnode)
258         };
260         if (this.get('cmid')) {
261             params.cmid = this.get('cmid');
262         }
264         if (dragnode.next('.'+this.samenodeclass) && !dragnode.next('.'+this.samenodeclass).hasClass(CSS.BLOCKADMINBLOCK)) {
265             params.bui_beforeid = this.get_block_id(dragnode.next('.'+this.samenodeclass));
266         }
268         // Do AJAX request
269         Y.io(M.cfg.wwwroot+AJAXURL, {
270             method: 'POST',
271             data: params,
272             on: {
273                 start : function() {
274                     lightbox.show();
275                 },
276                 success: function(tid, response) {
277                     window.setTimeout(function() {
278                         lightbox.hide();
279                     }, 250);
280                     try {
281                         var responsetext = Y.JSON.parse(response.responseText);
282                         if (responsetext.error) {
283                             new M.core.ajaxException(responsetext);
284                         }
285                     } catch (e) {}
286                 },
287                 failure: function(tid, response) {
288                     this.ajax_failure(response);
289                     lightbox.hide();
290                 }
291             },
292             context:this
293         });
294     }
295 }, {
296     NAME : 'core-blocks-dragdrop',
297     ATTRS : {
298         courseid : {
299             value : null
300         },
301         cmid : {
302             value : null
303         },
304         contextid : {
305             value : null
306         },
307         pagelayout : {
308             value : null
309         },
310         pagetype : {
311             value : null
312         },
313         subpage : {
314             value : null
315         },
316         regions : {
317             value : null
318         }
319     }
320 });
322 M.core = M.core || {};
323 M.core.blockdraganddrop = M.core.blockdraganddrop || {};
325 /**
326  * True if the page is using the new blocks methods.
327  * @private
328  * @static
329  * @property M.core.blockdraganddrop._isusingnewblocksmethod
330  * @type Boolean
331  * @default null
332  */
333 M.core.blockdraganddrop._isusingnewblocksmethod = null;
335 /**
336  * Returns true if the page is using the new blocks methods.
337  * @static
338  * @method M.core.blockdraganddrop.is_using_blocks_render_method
339  * @return Boolean
340  */
341 M.core.blockdraganddrop.is_using_blocks_render_method = function() {
342     if (this._isusingnewblocksmethod === null) {
343         var goodregions = Y.all('.block-region[data-blockregion]').size();
344         var allregions = Y.all('.block-region').size();
345         this._isusingnewblocksmethod = (allregions === goodregions);
346         if (goodregions > 0 && allregions > 0) {
347             Y.log('Both core_renderer::blocks and core_renderer::blocks_for_region have been used.', 'warn', 'moodle-core_blocks');
348         }
349     }
350     return this._isusingnewblocksmethod;
351 };
353 /**
354  * Initialises a drag and drop manager.
355  * This should only ever be called once for a page.
356  * @static
357  * @method M.core.blockdraganddrop.init
358  * @param {Object} params
359  * @return Manager
360  */
361 M.core.blockdraganddrop.init = function(params) {
362     if (this.is_using_blocks_render_method()) {
363         Y.log('Block drag and drop initialised for the blocks method.', 'info', 'moodle-core_blocks');
364         new MANAGER(params);
365     } else {
366         Y.log('Block drag and drop initialised with the legacy manager (blocks_for_region used).', 'info', 'moodle-core_blocks');
367         new DRAGBLOCK(params);
368     }
369 };
371 /*
372  * Legacy code to keep things working.
373  */
374 M.core_blocks = M.core_blocks || {};
375 M.core_blocks.init_dragdrop = function(params) {
376     M.core.blockdraganddrop.init(params);
377 };