[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/yui/src/dragdrop/js/ -> dragdrop.js (source)

   1  /* eslint-disable no-empty-function */
   2  /**
   3   * The core drag and drop module for Moodle which extends the YUI drag and
   4   * drop functionality with additional features.
   5   *
   6   * @module moodle-core-dragdrop
   7   */
   8  var MOVEICON = {
   9      pix: "i/move_2d",
  10      largepix: "i/dragdrop",
  11      component: 'moodle',
  12      cssclass: 'moodle-core-dragdrop-draghandle'
  13  };
  14  
  15  /**
  16   * General DRAGDROP class, this should not be used directly,
  17   * it is supposed to be extended by your class
  18   *
  19   * @class M.core.dragdrop
  20   * @constructor
  21   * @extends Base
  22   */
  23  var DRAGDROP = function() {
  24      DRAGDROP.superclass.constructor.apply(this, arguments);
  25  };
  26  
  27  Y.extend(DRAGDROP, Y.Base, {
  28      /**
  29       * Whether the item is being moved upwards compared with the last
  30       * location.
  31       *
  32       * @property goingup
  33       * @type Boolean
  34       * @default null
  35       */
  36      goingup: null,
  37  
  38      /**
  39       * Whether the item is being moved upwards compared with the start
  40       * point.
  41       *
  42       * @property absgoingup
  43       * @type Boolean
  44       * @default null
  45       */
  46      absgoingup: null,
  47  
  48      /**
  49       * The class for the object.
  50       *
  51       * @property samenodeclass
  52       * @type String
  53       * @default null
  54       */
  55      samenodeclass: null,
  56  
  57      /**
  58       * The class on the parent of the item being moved.
  59       *
  60       * @property parentnodeclass
  61       * @type String
  62       * @default
  63       */
  64      parentnodeclass: null,
  65  
  66      /**
  67       * The label to use with keyboard drag/drop to describe items of the same Node.
  68       *
  69       * @property samenodelabel
  70       * @type Object
  71       * @default null
  72       */
  73      samenodelabel: null,
  74  
  75      /**
  76       * The label to use with keyboard drag/drop to describe items of the parent Node.
  77       *
  78       * @property samenodelabel
  79       * @type Object
  80       * @default null
  81       */
  82      parentnodelabel: null,
  83  
  84      /**
  85       * The groups for this instance.
  86       *
  87       * @property groups
  88       * @type Array
  89       * @default []
  90       */
  91      groups: [],
  92  
  93      /**
  94       * The previous drop location.
  95       *
  96       * @property lastdroptarget
  97       * @type Node
  98       * @default null
  99       */
 100      lastdroptarget: null,
 101  
 102      /**
 103       * Listeners.
 104       *
 105       * @property listeners
 106       * @type Array
 107       * @default null
 108       */
 109      listeners: null,
 110  
 111      /**
 112       * The initializer which sets up the move action.
 113       *
 114       * @method initializer
 115       * @protected
 116       */
 117      initializer: function() {
 118          this.listeners = [];
 119  
 120          // Listen for all drag:start events.
 121          this.listeners.push(Y.DD.DDM.on('drag:start', this.global_drag_start, this));
 122  
 123          // Listen for all drag:end events.
 124          this.listeners.push(Y.DD.DDM.on('drag:end', this.global_drag_end, this));
 125  
 126          // Listen for all drag:drag events.
 127          this.listeners.push(Y.DD.DDM.on('drag:drag', this.global_drag_drag, this));
 128  
 129          // Listen for all drop:over events.
 130          this.listeners.push(Y.DD.DDM.on('drop:over', this.global_drop_over, this));
 131  
 132          // Listen for all drop:hit events.
 133          this.listeners.push(Y.DD.DDM.on('drop:hit', this.global_drop_hit, this));
 134  
 135          // Listen for all drop:miss events.
 136          this.listeners.push(Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this));
 137  
 138          // Add keybaord listeners for accessible drag/drop
 139          this.listeners.push(Y.one(Y.config.doc.body).delegate('key', this.global_keydown,
 140                  'down:32, enter, esc', '.' + MOVEICON.cssclass, this));
 141  
 142          // Make the accessible drag/drop respond to a single click.
 143          this.listeners.push(Y.one(Y.config.doc.body).delegate('click', this.global_keydown,
 144                  '.' + MOVEICON.cssclass, this));
 145      },
 146  
 147      /**
 148       * The destructor to shut down the instance of the dragdrop system.
 149       *
 150       * @method destructor
 151       * @protected
 152       */
 153      destructor: function() {
 154          new Y.EventHandle(this.listeners).detach();
 155      },
 156  
 157      /**
 158       * Build a new drag handle Node.
 159       *
 160       * @method get_drag_handle
 161       * @param {String} title The title on the drag handle
 162       * @param {String} classname The name of the class to add to the node
 163       * wrapping the drag icon
 164       * @param {String} [iconclass] The class to add to the icon
 165       * @param {Boolean} [large=false] whether to use the larger version of
 166       * the drag icon
 167       * @return Node The built drag handle.
 168       */
 169      get_drag_handle: function(title, classname, iconclass, large) {
 170          var iconname = MOVEICON.pix;
 171          if (large) {
 172              iconname = MOVEICON.largepix;
 173          }
 174          var dragicon = Y.Node.create('<img />')
 175              .setStyle('cursor', 'move')
 176              .setAttrs({
 177                  'src': M.util.image_url(iconname, MOVEICON.component),
 178                  'alt': title
 179              });
 180          if (iconclass) {
 181              dragicon.addClass(iconclass);
 182          }
 183  
 184          var dragelement = Y.Node.create('<span></span>')
 185              .addClass(classname)
 186              .setAttribute('title', title)
 187              .setAttribute('tabIndex', 0)
 188              .setAttribute('data-draggroups', this.groups)
 189              .setAttribute('role', 'button');
 190          dragelement.appendChild(dragicon);
 191          dragelement.addClass(MOVEICON.cssclass);
 192  
 193          return dragelement;
 194      },
 195  
 196      lock_drag_handle: function(drag, classname) {
 197          drag.removeHandle('.' + classname);
 198      },
 199  
 200      unlock_drag_handle: function(drag, classname) {
 201          drag.addHandle('.' + classname);
 202          drag.get('activeHandle').focus();
 203      },
 204  
 205      ajax_failure: function(response) {
 206          var e = {
 207              name: response.status + ' ' + response.statusText,
 208              message: response.responseText
 209          };
 210          return new M.core.exception(e);
 211      },
 212  
 213      in_group: function(target) {
 214          var ret = false;
 215          Y.each(this.groups, function(v) {
 216              if (target._groups[v]) {
 217                  ret = true;
 218              }
 219          }, this);
 220          return ret;
 221      },
 222      /*
 223          * Drag-dropping related functions
 224          */
 225      global_drag_start: function(e) {
 226          // Get our drag object
 227          var drag = e.target;
 228          // Check that drag object belongs to correct group
 229          if (!this.in_group(drag)) {
 230              return;
 231          }
 232          // Store the nodes current style, so we can restore it later.
 233          this.originalstyle = drag.get('node').getAttribute('style');
 234          // Set some general styles here
 235          drag.get('node').setStyle('opacity', '.25');
 236          drag.get('dragNode').setStyles({
 237              opacity: '.75',
 238              borderColor: drag.get('node').getStyle('borderColor'),
 239              backgroundColor: drag.get('node').getStyle('backgroundColor')
 240          });
 241          drag.get('dragNode').empty();
 242          this.drag_start(e);
 243      },
 244  
 245      global_drag_end: function(e) {
 246          var drag = e.target;
 247          // Check that drag object belongs to correct group
 248          if (!this.in_group(drag)) {
 249              return;
 250          }
 251          // Put our general styles back
 252          drag.get('node').setAttribute('style', this.originalstyle);
 253          this.drag_end(e);
 254      },
 255  
 256      global_drag_drag: function(e) {
 257          var drag = e.target,
 258              info = e.info;
 259  
 260          // Check that drag object belongs to correct group
 261          if (!this.in_group(drag)) {
 262              return;
 263          }
 264  
 265          // Note, we test both < and > situations here. We don't want to
 266          // effect a change in direction if the user is only moving side
 267          // to side with no Y position change.
 268  
 269          // Detect changes in the position relative to the start point.
 270          if (info.start[1] < info.xy[1]) {
 271              // We are going up if our final position is higher than our start position.
 272              this.absgoingup = true;
 273  
 274          } else if (info.start[1] > info.xy[1]) {
 275              // Otherwise we're going down.
 276              this.absgoingup = false;
 277          }
 278  
 279          // Detect changes in the position relative to the last movement.
 280          if (info.delta[1] < 0) {
 281              // We are going up if our final position is higher than our start position.
 282              this.goingup = true;
 283  
 284          } else if (info.delta[1] > 0) {
 285              // Otherwise we're going down.
 286              this.goingup = false;
 287          }
 288  
 289          this.drag_drag(e);
 290      },
 291  
 292      global_drop_over: function(e) {
 293          // Check that drop object belong to correct group.
 294          if (!e.drop || !e.drop.inGroup(this.groups)) {
 295              return;
 296          }
 297  
 298          // Get a reference to our drag and drop nodes.
 299          var drag = e.drag.get('node'),
 300              drop = e.drop.get('node');
 301  
 302          // Save last drop target for the case of missed target processing.
 303          this.lastdroptarget = e.drop;
 304  
 305          // Are we dropping within the same parent node?
 306          if (drop.hasClass(this.samenodeclass)) {
 307              var where;
 308  
 309              if (this.goingup) {
 310                  where = "before";
 311              } else {
 312                  where = "after";
 313              }
 314  
 315              // Add the node contents so that it's moved, otherwise only the drag handle is moved.
 316              drop.insert(drag, where);
 317          } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
 318              // We are dropping on parent node and it is empty
 319              if (this.goingup) {
 320                  drop.append(drag);
 321              } else {
 322                  drop.prepend(drag);
 323              }
 324          }
 325          this.drop_over(e);
 326      },
 327  
 328      global_drag_dropmiss: function(e) {
 329          // drag:dropmiss does not have e.drag and e.drop properties
 330          // we substitute them for the ease of use. For e.drop we use,
 331          // this.lastdroptarget (ghost node we use for indicating where to drop)
 332          e.drag = e.target;
 333          e.drop = this.lastdroptarget;
 334          // Check that drag object belongs to correct group
 335          if (!this.in_group(e.drag)) {
 336              return;
 337          }
 338          // Check that drop object belong to correct group
 339          if (!e.drop || !e.drop.inGroup(this.groups)) {
 340              return;
 341          }
 342          this.drag_dropmiss(e);
 343      },
 344  
 345      global_drop_hit: function(e) {
 346          // Check that drop object belong to correct group
 347          if (!e.drop || !e.drop.inGroup(this.groups)) {
 348              return;
 349          }
 350          this.drop_hit(e);
 351      },
 352  
 353      /**
 354       * This is used to build the text for the heading of the keyboard
 355       * drag drop menu and the text for the nodes in the list.
 356       * @method find_element_text
 357       * @param {Node} n The node to start searching for a valid text node.
 358       * @return {string} The text of the first text-like child node of n.
 359       */
 360      find_element_text: function(n) {
 361          // The valid node types to get text from.
 362          var nodes = n.all('h2, h3, h4, h5, span:not(.actions):not(.menu-action-text), p, div.no-overflow, div.dimmed_text');
 363          var text = '';
 364  
 365          nodes.each(function() {
 366              if (text === '') {
 367                  if (Y.Lang.trim(this.get('text')) !== '') {
 368                      text = this.get('text');
 369                  }
 370              }
 371          });
 372  
 373          if (text !== '') {
 374              return text;
 375          }
 376          return M.util.get_string('emptydragdropregion', 'moodle');
 377      },
 378  
 379      /**
 380       * This is used to initiate a keyboard version of a drag and drop.
 381       * A dialog will open listing all the valid drop targets that can be selected
 382       * using tab, tab, tab, enter.
 383       * @method global_start_keyboard_drag
 384       * @param {Event} e The keydown / click event on the grab handle.
 385       * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
 386       * @param {Node} draghandle The node that triggered this action.
 387       */
 388      global_start_keyboard_drag: function(e, draghandle, dragcontainer) {
 389          M.core.dragdrop.keydragcontainer = dragcontainer;
 390          M.core.dragdrop.keydraghandle = draghandle;
 391  
 392          // Get the name of the thing to move.
 393          var nodetitle = this.find_element_text(dragcontainer);
 394          var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
 395  
 396          // Build the list of drop targets.
 397          var droplist = Y.Node.create('<ul></ul>');
 398          droplist.addClass('dragdrop-keyboard-drag');
 399          var listitem, listlink, listitemtext;
 400  
 401          // Search for possible drop targets.
 402          var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
 403  
 404          droptargets.each(function(node) {
 405              var validdrop = false;
 406              var labelroot = node;
 407              if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer) {
 408                  // This is a drag and drop target with the same class as the grabbed node.
 409                  validdrop = true;
 410              } else {
 411                  var elementgroups = node.getAttribute('data-draggroups').split(' ');
 412                  var i, j;
 413                  for (i = 0; i < elementgroups.length; i++) {
 414                      for (j = 0; j < this.groups.length; j++) {
 415                          if (elementgroups[i] === this.groups[j]) {
 416                              // This is a parent node of the grabbed node (used for dropping in empty sections).
 417                              validdrop = true;
 418                              // This node will have no text - so we get the first valid text from the parent.
 419                              labelroot = node.get('parentNode');
 420                              break;
 421                          }
 422                      }
 423                      if (validdrop) {
 424                          break;
 425                      }
 426                  }
 427              }
 428  
 429              if (validdrop) {
 430                  // It is a valid drop target - create a list item for it.
 431                  listitem = Y.Node.create('<li></li>');
 432                  listlink = Y.Node.create('<a></a>');
 433                  nodetitle = this.find_element_text(labelroot);
 434  
 435                  if (this.samenodelabel && node.hasClass(this.samenodeclass)) {
 436                      listitemtext = M.util.get_string(this.samenodelabel.identifier, this.samenodelabel.component, nodetitle);
 437                  } else if (this.parentnodelabel && node.hasClass(this.parentnodeclass)) {
 438                      listitemtext = M.util.get_string(this.parentnodelabel.identifier, this.parentnodelabel.component, nodetitle);
 439                  } else {
 440                      listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle);
 441                  }
 442                  listlink.setContent(listitemtext);
 443  
 444                  // Add a data attribute so we can get the real drop target.
 445                  listlink.setAttribute('data-drop-target', node.get('id'));
 446                  // Allow tabbing to the link.
 447                  listlink.setAttribute('tabindex', '0');
 448  
 449                  // Set the event listeners for enter, space or click.
 450                  listlink.on('click', this.global_keyboard_drop, this);
 451                  listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this);
 452  
 453                  // Add to the list or drop targets.
 454                  listitem.append(listlink);
 455                  droplist.append(listitem);
 456              }
 457          }, this);
 458  
 459          // Create the dialog for the interaction.
 460          M.core.dragdrop.dropui = new M.core.dialogue({
 461              headerContent: dialogtitle,
 462              bodyContent: droplist,
 463              draggable: true,
 464              visible: true,
 465              center: true,
 466              modal: true
 467          });
 468  
 469          M.core.dragdrop.dropui.after('visibleChange', function(e) {
 470              // After the dialogue has been closed, we call the cancel function. This will
 471              // ensure that tidying up happens (e.g. focusing on the start Node).
 472              if (e.prevVal && !e.newVal) {
 473                  this.global_cancel_keyboard_drag();
 474              }
 475          }, this);
 476  
 477          // Focus the first drop target.
 478          if (droplist.one('a')) {
 479              droplist.one('a').focus();
 480          }
 481      },
 482  
 483      /**
 484       * This is used as a simulated drag/drop event in order to prevent any
 485       * subtle bugs from creating a real instance of a drag drop event. This means
 486       * there are no state changes in the Y.DD.DDM and any undefined functions
 487       * will trigger an obvious and fatal error.
 488       * The end result is that we call all our drag/drop handlers but do not bubble the
 489       * event to anyone else.
 490       *
 491       * The functions/properties implemented in the wrapper are:
 492       * e.target
 493       * e.drag
 494       * e.drop
 495       * e.drag.get('node')
 496       * e.drop.get('node')
 497       * e.drag.addHandle()
 498       * e.drag.removeHandle()
 499       *
 500       * @method simulated_drag_drop_event
 501       * @param {Node} dragnode The drag container node
 502       * @param {Node} dropnode The node to initiate the drop on
 503       */
 504      simulated_drag_drop_event: function(dragnode, dropnode) {
 505  
 506          // Subclass for wrapping both drag and drop.
 507          var DragDropWrapper = function(node) {
 508              this.node = node;
 509          };
 510  
 511          // Method e.drag.get() - get the node.
 512          DragDropWrapper.prototype.get = function(param) {
 513              if (param === 'node' || param === 'dragNode' || param === 'dropNode') {
 514                  return this.node;
 515              }
 516              if (param === 'activeHandle') {
 517                  return this.node.one('.editing_move');
 518              }
 519              return null;
 520          };
 521  
 522          // Method e.drag.inGroup() - we have already run the group checks before triggering the event.
 523          DragDropWrapper.prototype.inGroup = function() {
 524              return true;
 525          };
 526  
 527          // Method e.drag.addHandle() - we don't want to run this.
 528          DragDropWrapper.prototype.addHandle = function() {};
 529          // Method e.drag.removeHandle() - we don't want to run this.
 530          DragDropWrapper.prototype.removeHandle = function() {};
 531  
 532          // Create instances of the DragDropWrapper.
 533          this.drop = new DragDropWrapper(dropnode);
 534          this.drag = new DragDropWrapper(dragnode);
 535          this.target = this.drop;
 536      },
 537  
 538      /**
 539       * This is used to complete a keyboard version of a drag and drop.
 540       * A drop event will be simulated based on the drag and drop nodes.
 541       * @method global_keyboard_drop
 542       * @param {Event} e The keydown / click event on the proxy drop node.
 543       */
 544      global_keyboard_drop: function(e) {
 545          // The drag node was saved.
 546          var dragcontainer = M.core.dragdrop.keydragcontainer;
 547          // The real drop node is stored in an attribute of the proxy.
 548          var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
 549  
 550          // Close the dialog.
 551          M.core.dragdrop.dropui.hide();
 552          // Cancel the event.
 553          e.preventDefault();
 554          // Convert to drag drop events.
 555          var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer);
 556          var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget);
 557          // Simulate the full sequence.
 558          this.drag_start(dragevent);
 559          this.global_drop_over(dropevent);
 560  
 561          if (droptarget.hasClass(this.parentnodeclass) && droptarget.contains(dragcontainer)) {
 562              // The global_drop_over function does not handle the case where an item was moved up, without the
 563              // 'goingup' variable being set, as is the case wih keyboard drag/drop. We must detect this case and
 564              // apply it after the drop_over, but before the drop_hit event in order for it to be moved to the
 565              // correct location.
 566              droptarget.prepend(dragcontainer);
 567          }
 568  
 569          this.global_drop_hit(dropevent);
 570      },
 571  
 572      /**
 573       * This is used to cancel a keyboard version of a drag and drop.
 574       *
 575       * @method global_cancel_keyboard_drag
 576       */
 577      global_cancel_keyboard_drag: function() {
 578          if (M.core.dragdrop.keydragcontainer) {
 579              // Focus on the node which was being dragged.
 580              M.core.dragdrop.keydraghandle.focus();
 581              M.core.dragdrop.keydragcontainer = null;
 582          }
 583          if (M.core.dragdrop.dropui) {
 584              M.core.dragdrop.dropui.destroy();
 585          }
 586      },
 587  
 588      /**
 589       * Process key events on the drag handles.
 590       *
 591       * @method global_keydown
 592       * @param {EventFacade} e The keydown / click event on the drag handle.
 593       */
 594      global_keydown: function(e) {
 595          var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true),
 596              dragcontainer,
 597              draggroups;
 598  
 599          if (draghandle === null) {
 600              // The element clicked did not have a a draghandle in it's lineage.
 601              return;
 602          }
 603  
 604          if (e.keyCode === 27) {
 605              // Escape to cancel from anywhere.
 606              this.global_cancel_keyboard_drag();
 607              e.preventDefault();
 608              return;
 609          }
 610  
 611          // Only process events on a drag handle.
 612          if (!draghandle.hasClass(MOVEICON.cssclass)) {
 613              return;
 614          }
 615  
 616          // Do nothing if not space or enter.
 617          if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') {
 618              return;
 619          }
 620  
 621          // Check the drag groups to see if we are the handler for this node.
 622          draggroups = draghandle.getAttribute('data-draggroups').split(' ');
 623          var i, j;
 624          var validgroup = false;
 625  
 626          for (i = 0; i < draggroups.length; i++) {
 627              for (j = 0; j < this.groups.length; j++) {
 628                  if (draggroups[i] === this.groups[j]) {
 629                      validgroup = true;
 630                      break;
 631                  }
 632              }
 633              if (validgroup) {
 634                  break;
 635              }
 636          }
 637          if (!validgroup) {
 638              return;
 639          }
 640  
 641          // Valid event - start the keyboard drag.
 642          dragcontainer = draghandle.ancestor('.yui3-dd-drop');
 643          this.global_start_keyboard_drag(e, draghandle, dragcontainer);
 644  
 645          e.preventDefault();
 646      },
 647  
 648  
 649      // Abstract functions definitions.
 650  
 651      /**
 652       * Callback to use when dragging starts.
 653       *
 654       * @method drag_start
 655       * @param {EventFacade} e
 656       */
 657      drag_start: function() {},
 658  
 659      /**
 660       * Callback to use when dragging ends.
 661       *
 662       * @method drag_end
 663       * @param {EventFacade} e
 664       */
 665      drag_end: function() {},
 666  
 667      /**
 668       * Callback to use during dragging.
 669       *
 670       * @method drag_drag
 671       * @param {EventFacade} e
 672       */
 673      drag_drag: function() {},
 674  
 675      /**
 676       * Callback to use when dragging ends and is not over a drop target.
 677       *
 678       * @method drag_dropmiss
 679       * @param {EventFacade} e
 680       */
 681      drag_dropmiss: function() {},
 682  
 683      /**
 684       * Callback to use when a drop over event occurs.
 685       *
 686       * @method drop_over
 687       * @param {EventFacade} e
 688       */
 689      drop_over: function() {},
 690  
 691      /**
 692       * Callback to use on drop:hit.
 693       *
 694       * @method drop_hit
 695       * @param {EventFacade} e
 696       */
 697      drop_hit: function() {}
 698  }, {
 699      NAME: 'dragdrop',
 700      ATTRS: {}
 701  });
 702  
 703  M.core = M.core || {};
 704  M.core.dragdrop = DRAGDROP;


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1