PolyAnno: DragonDrop UI Design

This is part of my series of posts about the PolyAnno project – more here

Basics

To make development easier, I separated the development of the front-end Javascript and CSS package to allow me to generate, adjust the size of, drag and drop, adjust responsively to screen size, minimise, and then delete, boxes across the screen.

The Github repo and the documentation for quick usage is here fo now: https://github.com/BluePigeons/dragon_drop .

However, this post is intended to discuss the development of the package in more detail, especially in relation to the overall development of the PolyAnno project.

To see some basic JSfiddle examples:

  • a single popup added and no minimising see this demo.
  • popups with minimising enabled see this demo.
  • dynamically generated (in response to a button press) popups with minimising enabled see this demo.

Functions

There are two main functions you can use from the library:

initialise_dragondrop( parent_id, the_options )

Because dragondrop has several library dependencies but not all of them may be relevant depending on the intended application, I decided to allow the developer to specify the functionality required under the_options parameter in the function, and then only the relevant code will load in browser for optimal usage.

The options take the format as follows:

  {
    "minimise" : Boolean,
    "initialise_min_bar": "min bar parent id",
    "beforemin": "funcname",
    "aftermin": "funcname",
    "beforereopen": "funcname",
    "afterreopen": "funcname",
    "beforeclose": "funcname",
    "afterclose": "funcname"
  }

There are two options detailing the behaviour around minimising popups:

  • minimise (Boolean) (default: false) – If true then the dragon pops will have a minimise functions enabled.
  • initialise_min_bar (String) (Optional) – Defines the element then a bar will be generated inside for the minimised pops to be displayed in. If not defined whilst minimise is set to true then a bar will need to be manually present in the page with the class “dragondrop-min-bar” for the minimised pops to be added to.

Additionally, I decided to try and allow for ‘not-quite-events’ to trigger functions by allowing functions to be the values of the optional fields:

  • beforemin
  • aftermin
  • beforereopen
  • aftereopen
  • beforeclose
  • afterclose

add_dragondrop_pop()

This enables manual addition of a pop box within a dragon_drop framework.

Structure: Basic HTML

The HTML describing the basic components of the boxes, and their minimised bars, are described in variables at the beginning of the code:

  • dragondrop_min_bar_HTML1
  • dragondrop_min_bar_HTML2
  • dragondrop_popup_HTML

Structure: Resizing

The resizing is the most complex component of dragon_drop as the size is determined by both:

  • display screen size
  • user adjustments

This is done by using the Bootstrap grid as a basis, dividing the screen into 12 columns, with a specified number of rows.

bootstrapgrid
Bootstrap Grid via http://getbootstrap.com/css/
bootstrapgridoverflow
Bootstrap Grid via http://getbootstrap.com/css/

Bootstrap automatically updates the sizes of these and ensures consistent alignment – so if the contents of a column are too large to fit adjacent to the others then it is pushed to a new row for a cleaner UI.

Therefore the user size adjustments are effectively just adjusting the size of the contents that Bootstrap ‘sees’ and works with within that framework – you are just adjusting the heights of the rows and then the width snaps to match the width of the nearest whole number of Bootstrap columns (as determined by the screen size).

dragondrop_snappingcolumns
Images by Erin Nolan 2016 || pigeonsblue.com
dragondrop_snappingrows
Images by Erin Nolan 2016 || pigeonsblue.com

The adjustment is done through the using the JQuery UI “draggable” and “resizeable” properties which are set for a pop box when it is added in the add_dragondrop_pop function by the following code, whereby the “popupIDstring” is unique ID to the new box generated:

  $(popupIDstring).draggable();
  $(popupIDstring).draggable({
    addClasses: false,
    handle: ".ui-draggable-handle",
    revert: function(theObject) {
      return adjustDragBootstrapGrid($(this));
    },
    revertDuration: 0,
    snap: ".annoPopup",
    snapMode: "outer"  
  });

  $(popupIDstring).resizable();
  $(popupIDstring).resizable( "enable" );

Handle Options

Within the options specified for the JQuery UI “draggable” are enabling of a “handle” by which the box can be dragged, specified by the class “.ui-draggable-handle” which in this library is only ever added to the HTML describing the bar set across the top of the boxes:

draggablehandlebar

The number of button icons added to these might make UI less clean and so needs to be looked at it further detail at some later time.

Snap Options

Then the “snap” options are set to snap the box into place alongside the “outer” edges of the nearest div with the class “.annoPopup” (only assigned to the outside div of the pop boxes HTML).

snap: ".annoPopup",
snapMode: "outer"

This ensures consistent and easier alignment – I had experimented with no snapping, and allowing entirely free placement of the boxes across the page but the complexities of decisions of how to rearrange boxes when the screen size changed became too complicated, and so I made the decision to restrict placements to no gaps by enabling the snapping.

Revert Options

revert: function(theObject) {
      return adjustDragBootstrapGrid($(this));
},
revertDuration: 0,

The revert option is decided by the result of adjustResizeBootstrapGrid when given the pop box being handled. If the revert is true then **** but if false then ****.

adjustResizeBootstrapGrid

var adjustResizeBootstrapGrid = function(parentDOM, popupDOM, theUI) {
  var gridwidth = Math.round(parentDOM.width() / 12 );
  var newWidth = theUI.size.width;
  var colwidth = Math.round(newWidth/gridwidth);
  return updateGridCols(colwidth, popupDOM);
};

This defined the grid column width by measuring the parent DOM’s width and dividing by 12 , and then determining the nearest number of columns that the pop box width should be rounded to. Then uses the updateGridCols function to change the width.

updateGridCols

var updateGridCols = function(newcol, popupDOM) {
  var newName = "col-md-"+newcol;
  var theClasses = popupDOM.attr("class").toString();
  var theStartIndex = theClasses.indexOf("col-md-");
  var theEndIndex;
  var spaceIndex = theClasses.indexOf(" ", theStartIndex);
  var finishingIndex = theClasses.length;
  if (spaceIndex == -1) {  theEndIndex = finishingIndex;  }
  else {  theEndIndex = spaceIndex;  };
  var theClassName = theClasses.substring(theStartIndex, theEndIndex);
  if ((theStartIndex != -1) && (theClassName != newName)) {
    popupDOM.removeClass(theClassName).addClass(newName+" ");
    return newcol;
  }
  else {  return 0  };
};

In Bootstrap, the column width is defined within the class of the form “col-md-n” attached to an object, where the sum of n in a row must equal 12 or less.

Therefore this function updates the column width by removing the existing class of that format and replacing it with the correct corresponding n class name.

findCornerArray

Returns an array of the DOM position coordinates in the format:[Left, Right, Top, Bottom].

isBetween

If the number given is between the first and last number input then it returns the difference between the number and the first but otherwise returns zero.

formJumpArray

var formJumpArray = function(theChecks, popupDOM, nDOM, theNSiblings) {
  if ( ((theChecks[2] != 0) && (theChecks[3] != 0)) || ((theChecks[2] == 0) && (theChecks[3] == 0)) ){ ///if top and bottom are not overlapping or both are
    if (theChecks[0] != 0) {  theNSiblings[0] = nDOM; return theNSiblings; }
    else if (theChecks[1] != 0) {  theNSiblings[1] = nDOM; return theNSiblings; };
  }
  else if ( ((theChecks[0] != 0) && (theChecks[1] != 0)) || ((theChecks[0] == 0) && (theChecks[1] == 0)) ){ ///if left and right are inside neighbour and needs to jump above or below
    if (theChecks[2] != 0) {  return theNSiblings;  }
    else if (theChecks[3] != 0) { return theNSiblings;  };
  }
  else if (theChecks[0] != 0) {  theNSiblings[0] = nDOM; return theNSiblings;  }
  else if (theChecks[1] != 0) {  theNSiblings[1] = nDOM; return theNSiblings; };

};

 

loopBetweenSides

var loopBetweenSides = function(popupCorners, otherCorners){
  return [
    isBetween(popupCorners[0], otherCorners[0], otherCorners[1]), ///needs to add to left this far to clear left side
    isBetween(popupCorners[1], otherCorners[0], otherCorners[1]), ///needs to remove from left this far to clear right side
    isBetween(popupCorners[2], otherCorners[2], otherCorners[3]),
    isBetween(popupCorners[3], otherCorners[2], otherCorners[3])
  ];
};

Checks where each side of the DOM corners are relative to another DOM’s corners

hasCornerInside

var hasCornerInside = function(popupCorners, otherCorners) {
  var theChecks = loopBetweenSides(popupCorners, otherCorners);
  if (((theChecks[0] != 0) || (theChecks[1] != 0)) && ((theChecks[2] != 0)|| (theChecks[3] != 0))) { 
    return theChecks;
  }
  else {  return false  };
};

If a DOM corner is inside another DOM then it returns the relative positioning, otherwise false.

isEditorOverlap

var isEditorOverlap = function(popupDOM, popupCorners, nDOM, theNSiblings) {
  var otherCorners = findCornerArray(nDOM);
  var theChecks = hasCornerInside(popupCorners, otherCorners);
  var outsideChecks = hasCornerInside(otherCorners, popupCorners);

  if ( (theChecks == false) && (outsideChecks == false) ) {
    return theNSiblings;
  }
  else {
    return formJumpArray(theChecks, popupDOM, nDOM, theNSiblings);
  };
  
};

Checks the new suggested box position relative to each other box on the page in turn to identify relative positioning. Not necessarily the most efficient algorithm but I am assuming a fairly limited number of boxes will be present on a page at any given time. The “editor” in the naming is legacy from development within the context of PolyAnno project where all the boxes are for editing.

getDist

Returns the horizontal distance between two sides.

checkSiblingSides

var checkSiblingSides = function(mainSides, checkSides, theNearestSiblings, nDOM) {
  var checkLeft = getDist(mainSides, checkSides);
  var checkRight = getDist(checkSides, mainSides);
  if ( ((theNearestSiblings[0] == -1)&&( checkLeft >= 0)) || ( (theNearestSiblings[0] != -1) && (checkLeft < getDist(mainSides, findCornerArray(theNearestSiblings[0]) ) )) ) {
    theNearestSiblings[0] = nDOM;
    theNearestSiblings[2] = checkLeft;
    return theNearestSiblings;
  }
  else if ( ((theNearestSiblings[1] == -1)&&( checkRight >= 0)) || ( (theNearestSiblings[1] != -1) && (checkRight < getDist(findCornerArray(theNearestSiblings[1]), mainSides) )) ) {
    theNearestSiblings[1] = nDOM;
    theNearestSiblings[2] = checkRight;
    return theNearestSiblings;
  }
  else { return theNearestSiblings; };
};

Identifying the most relevant nearest geographical siblings and updating its DOM tree listings so its DOM siblings are now the geographical one.

nearestSiblings

var nearestSiblings = function(popupCorners, nDOM, theNearestSiblings) {
  var otherCorners = findCornerArray(nDOM);
  if ((popupCorners[2] < otherCorners[3]) && (popupCorners[3] > otherCorners[2])) { ///higher value for top means lower down hence operators' directions
    return checkSiblingSides(popupCorners, otherCorners, theNearestSiblings, nDOM);
  }
  else {
    return theNearestSiblings;
  };
};

 

updateIsReverting

var isReverting = false;

var updateIsReverting = function(theNearestSiblings, popupDOM) {
  if ((theNearestSiblings[0] != -1) && (theNearestSiblings[0] != popupDOM.prev())) {
    isReverting = ["insertAfter", theNearestSiblings[0]];
    popupDOM.addClass("dragondrop-was-reverted");
    return true;
  }
  else if ((theNearestSiblings[1] != -1) && (theNearestSiblings[1] != popupDOM.next())) {
    isReverting = ["insertBefore", theNearestSiblings[1] ];
    popupDOM.addClass("dragondrop-was-reverted");
    return true;
  }
  else {
    return true;
  };
};

Decides whether or not to trigger the “revert” option on JQueryUI draggable property.

adjustDragBootstrapGrid

var adjustDragBootstrapGrid = function(popupDOM) {

  var popupCorners = findCornerArray(popupDOM);
  var theNearestSiblings = [-1, -1];
  if (!isUseless(popupDOM.prev())) { theNearestSiblings == popupDOM.prev(); };
  if (!isUseless(popupDOM.next())) { theNearestSiblings == popupDOM.next(); };

  popupDOM.siblings().each(function(i) {
    theNearestSiblings = isEditorOverlap(popupDOM, popupCorners, $(this), theNearestSiblings);
  });

  if ( ((theNearestSiblings[0] == -1) || (theNearestSiblings[0] == popupDOM.prev())) && ((theNearestSiblings[0] == -1) || (theNearestSiblings[0] == popupDOM.prev()))  ) {
    popupDOM.siblings().each(function(i) {
      theNearestSiblings = nearestSiblings(popupCorners, $(this), theNearestSiblings);
    });
    return updateIsReverting(theNearestSiblings, popupDOM);
  }
  else {
    return updateIsReverting(theNearestSiblings, popupDOM);
  };
  
};

Checking the Bootstrap grid structure is up to date and valid.

Structure: Dragging

add_dragondrop_pop

var add_dragondrop_pop = function(popupClass, contentHTML, parentID, minOption, handlebarHTML) {

  var popupBoxDiv = document.createElement("div");
  popupBoxDiv.classList.add(popupClass);
  popupBoxDiv.classList.add("annoPopup"); /////"dragonpop"
  popupBoxDiv.classList.add("col-md-4");

  popupBoxDiv.id = "-" + Math.random().toString().substring(2);
  var popupIDstring = "#" + popupBoxDiv.id;

  var pageBody = document.getElementById(parentID);
  pageBody.insertBefore(popupBoxDiv, pageBody.childNodes[0]); 

  document.getElementById(popupBoxDiv.id).innerHTML = dragondrop_popup_HTML;
  if (!isUseless(handlebarHTML)) { document.getElementById(popupBoxDiv.id).children[0].children[0].children[0].innerHTML += handlebarHTML };
  if (!isUseless(contentHTML)) { document.getElementById(popupBoxDiv.id).children[0].children[0].innerHTML += contentHTML };

  drag_drop_parent_id = parentID;

  if (isUseless(minOption)) {
    $(popupIDstring).find(".dragondrop-min").detach();
  };

  $(popupIDstring).draggable();
  $(popupIDstring).draggable({
    addClasses: false,
    handle: ".ui-draggable-handle",
    revert: function(theObject) {
      return adjustDragBootstrapGrid($(this));
    },
    revertDuration: 0,
    snap: ".annoPopup",
    snapMode: "outer"  
  });

  $(popupIDstring).resizable();
  $(popupIDstring).resizable( "enable" );

  return popupIDstring;
};

dragondrop_remove_pop

To remove a pop box entirely, fairly standard code is used:

var dragondrop_remove_pop = function(thispop) {
  var toRemove = document.getElementById(thispop);
  var theParent = document.getElementById(toRemove.parentElement.id);
  theParent.removeChild(toRemove);
  if (  isUseless(toRemove) != true ) {  return thispop;  };
};

Structure: Minimising

The code handling the minimising is mostly defined within the following two functions:

dragondrop_minimise_pop

var dragondrop_minimise_pop = function (thisEditorWithoutHash) {
  var dragondrop_min_bar_HTML = dragondrop_min_bar_HTML1 + thisEditorWithoutHash + dragondrop_min_bar_HTML2;

  $(".dragondrop-min-bar").append(dragondrop_min_bar_HTML);

  $(".dragondrop-min-bar").find("span:contains("+thisEditorWithoutHash+")").addClass(thisEditorWithoutHash);
  $("#"+thisEditorWithoutHash).css("display", "none");
};

 

dragondrop_reopen_min

var dragondrop_reopen_min = function (thisEditorWithoutHash) {
  $("#"+thisEditorWithoutHash).css("display", "block");
  $(".dragondrop-min-bar").find("."+thisEditorWithoutHash).closest(".dragondrop-min-pop").remove(); ///
};

 

Structure: Setting Up

To allow flexibility of usage for the package, options are taken into the function initialise_dragondrop along with the id of the DOM that the setup is to be placed inside.

var initialise_dragondrop = function(parent_id, the_options) {

  $('#'+parent_id).on("click", ".dragondrop-close-pop-btn", function(){
    var thisPopID = $(event.target).closest(".annoPopup").attr("id");
    if (!isUseless(the_options.beforeclose)) {  the_options.beforeclose(thisPopID) };
    dragondrop_remove_pop(thisPopID);
    if (!isUseless(the_options.afterclose)) {  the_options.afterclose(thisPopID) };   

  });

  $( "#"+parent_id ).on( "resizestop", ".annoPopup", function( event, ui ) {
    adjustResizeBootstrapGrid($("#"+parent_id), $(event.target), ui);
  } );

  $( "#"+parent_id ).on( "dragstop", ".annoPopup", function( event, ui ) {
    if ($(event.target).hasClass("dragondrop-was-reverted") && (isReverting[0] == "insertAfter") ) {
      var theRest = isReverting[1].nextAll();
      if( !isUseless(theRest) ) { 
        theRest.insertAfter($(event.target));
      };
      $(event.target).insertAfter(isReverting[1]);
      $(event.target).removeClass("dragondrop-was-reverted");
      isReverting = false;
    }
    else if ($(event.target).hasClass("dragondrop-was-reverted") && (isReverting[0] == "insertBefore") ) {
      var theRest = isReverting[1].prevAll();
      if( !isUseless(theRest) ) { 
        theRest.insertBefore($(event.target));
      };
      $(event.target).insertBefore(isReverting[1]);
      $(event.target).removeClass("dragondrop-was-reverted");
      isReverting = false;
    };
  } );

  if (!isUseless(the_options.minimise)) {

    $( "#"+parent_id).on( "click", ".dragondrop-min", function(event) {
      event.stopPropagation();
      var thisPopID = $(event.target).closest(".annoPopup").attr("id");
      if (!isUseless(the_options.beforemin)) {  the_options.beforemin(thisPopID) };

      dragondrop_minimise_pop(thisPopID);

      if (!isUseless(the_options.aftermin)) {  the_options.aftermin(thisPopID) };
    });

    ///default to true
    if (!isUseless(the_options.initialise_min_bar)) {

      document.getElementById(the_options.initialise_min_bar).innerHTML = `

` ; $(“.dragondrop-min-bar”).on(“click”, “.dragondrop-min-pop”, function(event) { var thisPopID = $(this).find(“.dragondrop-min-pop-title”).html(); if (!isUseless(the_options.beforereopen)) { the_options.beforereopen(thisPopID) }; dragondrop_reopen_min(thisPopID); if (!isUseless(the_options.afterreopen)) { the_options.afterreopen(thisPopID) }; }); } else { $(“.dragondrop-min-bar”).on(“click”, “.dragondrop-min-pop”, function(event) { var thisPopID = $(this).find(“.dragondrop-min-pop-title”).html(); if (!isUseless(the_options.beforereopen)) { the_options.beforereopen(thisPopID) }; dragondrop_reopen_min(thisPopID); if (!isUseless(the_options.afterreopen)) { the_options.afterreopen(thisPopID) }; }); }; }; };

Next: All The Unicode

This is part of my series of posts about the PolyAnno project – more here

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s