I've authored a couple of plugins which show popovers on a given element. One of the objectives for both was no specification of plugin direction. So instead of the implementing dev having to specify direction: left
the direction would automatically gravitate towards the center of the page while staying "attached" to the target element.
I've gone through several iterations of the gravity function, but it still feels unnecessarily clunky. Not only is it allot of code for what it's doing it operates in stepped positions rather than fluid. I feel like there is an algorithm that I am overlooking.
The only other requirements are that the popover does not overlap the target element and it does not go off page.
I'm definitely not asking for someone to re-write this, but if you look at it and see some obvious ways to improve it or have an idea on what kind of algorithm could be used to get a fluid movement rather than steps it is much appreciated. Also of note, I understand it is not highly readable as I did a good bit of refactoring to make the footprint as small as possible (though I know it is still not very efficient). So, I've tried to add comments to explain it as best I could, but let me know if you still have questions.
Any suggestions are much appreciated.
It would be called like this:
target = document.getElementById('target')
popover = functionToBuildPopoverReturnsWithWidthAndHeightProps()
gravitate = gravity(coords(target), popover.height, popover.width)
functionToSetPopoverPosition(gravitate.x, gravitate.y)
This is what the related functions look like:
# Get the target element coordinates, used for determining bounds and size of target
coords = (el) ->
rect = el.getBoundingClientRect()
hl_border = if miss.global.highlight then miss.global.highlight_width else 0
top: rect.top - hl_border
right: rect.right + hl_border
bottom: rect.bottom + hl_border
left: rect.left - hl_border
width: rect.width || rect.right - rect.left
height: rect.height || rect.bottom - rect.top
#Build test element for getting screen dimensions
testEl = () ->
unless test = document.getElementById('miss-size-test')
test = document.createElement("div")
test.id = 'miss-size-test'
test.style.cssText = "position: fixed;top: 0;left: 0;bottom: 0;right: 0; visibility: hidden;"
document.body.appendChild(test)
height: test.offsetHeight
width: test.offsetWidth
# Gravitate to center, this is the meat and potatoes
gravity = (coords, height, width) ->
# get dimensions of page, target, and popover
center =
x: testEl().height / 2
y: testEl().width / 2
el_center =
x: coords.height / 2
y: coords.width / 2
box_center =
x: height / 2
y: width / 2
# setup objects to iterate through for x and y coordinates
mapping_x =
plane: 'x',
metric: height,
mstr: 'height',
array: map_x = [],
optimal: optimal_x = [],
diffs: ary_x = [],
setup: {top: null, middle: [el_center.x, 'top'], bottom: null}
mapping_y =
plane: 'y',
metric: width,
mstr: 'width',
array: map_y = [],
optimal: optimal_y = [],
diffs: ary_y = [],
setup: {left: null, middle: [el_center.y, 'left'], right: null}
# sort function
sort = (a,b) -> a - b
# loop through x and y mapping objects
for args in [mapping_x, mapping_y]
# loop through mappings.setup
for pos, arg of args.setup
# if position is middle (which has two array values) set add to
# the first value and loc to the second, else add is 0 and loc is key
# add is used to add the middle position to the calculation
if arg then add = arg[0]; loc = arg[1] else add = 0; loc = pos
diff = {}; val = {}
# calculate the difference between each position and the page center
# and store it as diff.position
diff[Object.keys(args.setup)[0]] = Math.abs(coords[loc] - box_center[args.plane] - center[args.plane] + add)
diff[Object.keys(args.setup)[1]] = Math.abs(coords[loc] - center[args.plane] + add)
diff[Object.keys(args.setup)[2]] = Math.abs(coords[loc] + box_center[args.plane] - center[args.plane] + add)
# calculate the value of that position's coordinate
# and store it as val.position
# (i.e. x.middle is 400px from left: 0, so val.middle = 400)
val[Object.keys(args.setup)[0]] = coords[loc] - args.metric + add
val[Object.keys(args.setup)[1]] = coords[loc] - box_center[args.plane] + add
val[Object.keys(args.setup)[2]] = coords[loc] + add
position = pos
args.array.push({diff, val, position})
# sort diffs lowest to highest
args.diffs.push(value) for key, value of v.diff for k, v of args.array
args.diffs.sort(sort)
pos_ref = args.setup.middle[1]
# there are 9 possible positions for each plane, so we loop over 9 times
for i in [0..8]
# we'll break once we find the nearest position that doesn't overlap
# the target
break_loop = false
# loop over the array of position objects (contains diff, value, position)
for k, v of args.array
# loop over diffs
for dk, dv of v.diff
# if value doesn't place popover off page continue
if dv == args.diffs[i] && v.val[dk] >= 0 && (v.val[dk] + args.metric) < testEl().width
# boolean var, does popover overlap target element
overlap = ((val = v.val[dk]) < coords[pos_ref] + coords[args.mstr] && val + args.metric > coords[pos_ref])
# create collection sorted closest to page center without
# being off page
args.optimal.push({val: val, diff: dv, position: "#{v.position}_#{dk}", overlap: overlap})
break_loop = true; break
break if break_loop
# finally, set x and y to the first instances that would not overlap the element
break for i in [0..8] when (x = optimal_x[i]) && (y = optimal_y[i]) && !(x.overlap && y.overlap)
# return x and y values if any met requirements of not being off page
# and not overlapping the target element. else return centered
x: if x then x.val else (center.x - box_center.x)
y: if y then y.val else (center.y - box_center.y)