table: improve moving column UI & UX (2)
Tags: webdev, javascript, tutorial, beginners
Published: 2022-12-24 09:49:49

We added a ghost column representing the moving column previously. It always follows our cursor in the entire window although the swapping functionality only happens in the header section.  
![ghost column follows cursor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vf70jmxmdwvbcubsnfj7.gif)

While there is nothing wrong about it, we can still improve this feature to make it easier for use. 

The [code](https://github.com/ga676005/table/tree/2b81b8d087d63a5a3c62926111a07ccd8b51b7b1) we have so far.


We can limit the ghost within the header area which tells users that the boundary of moving. In the meantime, we don't want to force users to control their pointers to stay within the header area. It might sound complicated to do this, but in fact we already did most of the job. We just need to modify something.
 
Currently, we set ghost initial position to overlap the column inside `createGhost` function, and we update ghost position in `handleMove` function on pointer move. 

makeColumnsSwappable.js
```js
columnsContainer.addEventListener('pointerdown', e => {]
  // ...

  function createGhost() {
    const ghost = firstTarget.cloneNode(true)
    copyElementStyleToAnother(firstTarget, ghost)
    ghost.style.position = 'fixed'
    ghost.style.pointerEvents = 'none'

    // we set ghost initial position to overlap the column
    ghost.style.left = e.clientX + pointerOffset.x + 'px'
    ghost.style.top = e.clientY + pointerOffset.y + 'px'

    document.body.appendChild(ghost)
    return ghost
  }

  const ghost = createGhost()

  function handleMove(e) {
    // we update ghost position on pointer move
    ghost.style.left = e.clientX + pointerOffset.x + 'px'
    ghost.style.top = e.clientY + pointerOffset.y + 'px'
    // ...
  } 

 // ...
}
```

Now we only want ghost column to stay in the header section. It should be able to move horizontally but not vertically. We have set ghost column initial `x` and `y` position correctly, which means that we only need to update its `x` on pointer move. Let's remove the code that change its `y`.

```js
function handleMove(e) {
  ghost.style.left = e.clientX + pointerOffset.x + 'px'
  
  // this line change ghost vertical position
  // ghost.style.top = e.clientY + pointerOffset.y + 'px'

  // ...
}
```

Notice that now ghost column only moves within the header section even though our cursor doesn't stay precisely in the header section. 
![now ghost column only moves within the header section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fr1ycpf67yzg294gn13i.gif)


After this change, we would expect columns to swap when ghost column moves over another column. Currently, columns only swap if our cursor is in the header section. Because we use `e.target` from `"pointermove"` to get the `secondTarget` and we made sure we only swap columns if `secondTarget` is one of the `columnElements`. So we need to change how we get the `secondTarget`.

The logic is similar to how we move ghost column. We want to be able to move our cursor freely, but we use new `x` with a locked `y` to get the `secondTarget`. This means that we need a way to use `x` and `y` to get an element, and there is an API for it, which is [Document.elementFromPoint()](https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint). If you have never seen `Document.elementFromPoint()`, I would encourage you to try to google what keywords you need to search to find this API. It is a good exercise that when you kind of know what to do but not exactly how to do. 

We can get the locked `y` from `"pointerdown"`. 
```js
columnsContainer.addEventListener('pointerdown', e => {
  // If `e.target` is able to get the correct `firstTarget`
  // `lockedY` should be able to get `secondTarget` 
  // because the columns stay on the same horizontal level
  const lockedY = e.clientY

  // ...

  // we a check here for `firstTarget`
  if (firstTargetIndex === -1)
    return

  // ...
}
```



Change how we get `secondTarget` from directly using `e.target` from `"pointermove"` to using the new `e.clientX` with locked `y` to get the target on that position.
```js
function handleMove(e) {
  // ...

  const secondTarget = document.elementFromPoint(e.clientX, lockedY)

  // ...
```

It should work like this. We can swap columns without making cursor stay in the header section.
![swap columns without making cursor stay in the header section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9na1pxdisilkm1ue9d2c.gif)

To quickly recap, We successfully make this change by removing the code that update ghost column `y` position, so ghost column stays on the same horizontal level.
```js
// remove from `handleMove` function
ghost.style.top = e.clientY + pointerOffset.y + 'px'
```

We get `lockedY` on `"pointerdown"`, so we can use `elementFromPoint` with this value to get a element from a point.
```js
// added at the top of   
// columnsContainer.addEventListener('pointerdown', e => {
const lockedY = e.clientY
```

Lastly, we change how we get `secondTarget`.
```js
// it was secondTarget = e.target
const secondTarget = document.elementFromPoint(e.clientX, lockedY)
```

It's not a surprise that with this small amount of change can make  a huge difference. Because we have designed pretty clear how to swap columns by getting `firstTarget` and `secondTarget`. In this exmaple, we only change how we get the `secondTarget`. The value of `secondTarget` doesn't change, so everything should still work.


Currently, ghost column can move out of header section. It makes more sense to make ghost column only move within the area. To do that, we need to calculate the boundary that ghost column can move.
```js
columnsContainer.addEventListener('pointerdown', e => {
  // get the position information of columns container
  const columnsContainerRect = columnsContainer.getBoundingClientRect()

  // ...

  function handleMove(e) {
    const newCursorX = e.clientX

    // new ghost `x` value
    let newGhostX = newCursorX + pointerOffset.x
   
    // if new `x` is over the left boundary of `columnsContainerRect`
    if (newGhostX < columnsContainerRect.x) {
      // we want to use the `x` value of `columnsContainerRect`
      // so ghost column doesn't move pass the left boundary
      newGhostX = columnsContainerRect.x
    }

    ghost.style.left = newGhostX + 'px'

    // ...
  }
}
```

Ghost column can no longer move pass the left boundary. 
![limit ghost left boundary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30an1kyb0ktrggxx9d0c.gif)

Next, let's deal with the right boundary. At first glance, we might want to calculate if the right edge of ghost column is over the right edge of `columnsContainerRect`. It's intuitive to think that way, but that will require more work. Since we use `ghost.style.left` to update ghost position, we can instead calculate a max value for left which can represents the limit that ghost column can move toward right. 

Create a variable `ghostXBoundary` to store the information of ghost `x` boundary and use it to conditionally limit ghost position in `handleMove`.
```js
const ghostXBoundary = {
  // the min value that ghost.style.left can set
  min: columnsContainerRect.x,

  // the max value that ghost.style.left can set
  // ghost column's width equals to `firstTargetRect.width` 
  // if you don't know how this work
  // try removing `- firstTargetRect.width` to see the difference
  max: columnsContainerRect.right - firstTargetRect.width
}

function handleMove(e) {
  const newCursorX = e.clientX
  let newGhostX = newCursorX + pointerOffset.x

  // conditions to change `newGhostX`
  // to limit ghost to stay in the boundary
  if (newGhostX < ghostXBoundary.min)
    newGhostX = ghostXBoundary.min
  else if (newGhostX > ghostXBoundary.max) {
    newGhostX = ghostXBoundary.max
  }

  ghost.style.left = newGhostX + 'px'

  // ...
}
```

Now ghost stays in the boundary.
![Ghost stays in the boundary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4os88goy0f046792041m.gif)

One thing you might have noticed is that the ghost column position is slightly off. It's not perfectly stay in the header section. 
![ghost column position is slightly off](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ui8x5mbwvowhqhrv4b0z.gif)

When we see something like this, we can try to exaggerate the problem to help us inspect it. First, I want to enlarge the border size from `1px` to `10px`.

myTables.css
```css
.my-table,
.my-table th,
.my-table td {
  border: 10px solid lightgray;
  border-collapse: collapse;
  padding: 8px;
}
```

We can see that the bug is exaggerated a little bit. Although it is not obviously what's going on, we can guess that border has some impact to this bug. 
![the bug is exaggerated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ofjblm7ifc01q9bdsjo2.gif)

So next we can keep modifying other stuff related to borders. Let's comment out `border-collapse: collapse;`. 
![no bug after comment out `border-collapse: collapse;`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k380uc8zlwywap776bpi.gif)

It seems like when we clone the styles from a column to ghost. It doesn't take that border collapse into account. We need to do that ourselves. 

```js
function createGhost() {
  const ghost = firstTarget.cloneNode(true)
  copyElementStyleToAnother(firstTarget, ghost)

  // cut borderWidth to half when ghost has border collapse style
  if (ghost.style.borderCollapse === 'collapse') {
    const halfBorderWidth = (parseFloat(ghost.style.borderWidth) / 2)
    ghost.style.borderWidth = halfBorderWidth + 'px'
  }

  ghost.style.position = 'fixed'
  ghost.style.pointerEvents = 'none'
  ghost.style.left = e.clientX + pointerOffset.x + 'px'
  ghost.style.top = e.clientY + pointerOffset.y + 'px'
  document.body.appendChild(ghost)
  return ghost
}
```
Note that we cannot directly set `borderWidth` to half without making that condition check, because it will break other situations. It's an edge case so we should handle it separately.

Nice! Ghost column now fits in the container.
![Ghost column now fits in the container](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mlffxjio0c94l79aa0bd.gif)
Finally, don't forget to clean up what we did for exaggerating the bug! Depend on devices, You might still see that bug if the border width is set to `1px`, but I experienced that as a hardware issue related to how pixel are drawn on screen which we have no control.   It will be safer to set a value to 2px or above if possible.