3
\$\begingroup\$

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)
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Okay, I think I got it. Yay! This one has haunted me for awhile. The new gravity function is completely fluid, way more concise, and accurate.

Instead of identifying specific points (top-left, bottom-right, middle-left, etc...) the new code pushes every possible point in steps (set to 20 but could be higher for performance purposes) into an array. Then the array is sorted by the combined x and y difference of the point to page center. Giving us the closest possible points to page center while remaining attached to the element.

One note, be careful not to set the steps too low as that will impact performance. Even in 20px steps the movement is relatively fluid.

gravity = (coords, height, width) ->
    center = x: (page_width = testEl().width) / 2, y: (page_height = testEl().height) / 2
    box_center = x: width / 2, y: height / 2
    points = []

    for x in [coords.left..(coords.right + width)] by 30
      points.push([x - width, coords.top - height])
      points.push([x - width, coords.bottom])

    for y in [coords.top..(coords.bottom + height)] by 30
      points.push([coords.left - width, y - height])
      points.push([coords.right, y - height])

    sort = (a, b) ->
      for ary in [[a, obja = {}], [b, objb = {}]]
        x = ary[0][0]
        y = ary[0][1]
        ary[1].diffx = if (dax = (x + box_center.x)) > center.x then dax - center.x else center.x - dax
        ary[1].diffy = if (day = (y + box_center.y)) > center.y then day - center.y else center.y - day
        ary[1].diff = ary[1].diffx + ary[1].diffy
        if x < 0 || x + width > page_width then ary[1].diff =+ 10000
        if y < 0 || y + height > page_height then ary[1].diff =+ 10000
      obja.diff - objb.diff

    points.sort(sort)
    x: if (x = points[0][0]) < 0 || x + width > page_width then center.x - box_center.x else x
    y: if (y = points[0][1]) < 0 || y + height > page_height then center.y - box_center.y else y
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.