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.


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).


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:
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