<h1>Undo/Redo</h1>
<h2>Use the arrow keys to move the circle.</h2>
<h2>Use "&lt;" to undo. Use "&gt;" to redo.</h2>
<div id="container">
  <div id="circle"></div>
</div>
body {
  font-family: Arial;
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #eee;
}

h1 {
  margin: 25px 0;
}

h2 {
  margin: 0;
}

#container {
  position: relative;
  margin-top: 25px;
  width: 500px;
  height: 500px;
  box-shadow: 0 0 10px black inset;
}

#circle {
  position: absolute;
  top: 0;
  left: 0;
  width: 50px;
  height: 50px;
  border-radius: 50px;
  background: radial-gradient(red, maroon);
}
console.clear();

// declare state
const stateWithClosure = (() => {
  let state = {
    stack: [],
    count: 0
  };
  
  // define and return stack methods
  return {
    pushDirection: direction => {
      if (state.count !== state.stack.length) {
        state.stack = state.stack.slice(0, state.count);
      }
      
      state.stack.push(direction);
      state.count++;
    },
    
    undoDirection: () => {
      if (state.count > 0) {
        state.count--;
        return state.stack[state.count];
      }
    },
    
    redoDirection: () => {
      if (state.count < state.stack.length) {
        state.count++;
        return state.stack[state.count - 1];
      }
    }
  };
})();

// destructure methods
const { pushDirection, undoDirection, redoDirection } = stateWithClosure;

// DOM
// circle
const circle = document.getElementById('circle');
// get height and width of container
const containerBoundingRect = document.getElementById('container').getBoundingClientRect();
const height = containerBoundingRect.bottom - containerBoundingRect.top;
const width = containerBoundingRect.right - containerBoundingRect.left;

// arrow key events
window.onkeydown = e => {
  switch (e.key) {
    case 'ArrowUp':
      moveUp();
      break;
    case 'ArrowRight':
      moveRight();
      break;
    case 'ArrowDown':
      moveDown();
      break;
    case 'ArrowLeft':
      moveLeft();
      break;
    case ',':
      undo();
      break;
    case '.':
      redo();
  }
};

const moveUp = () => {
  // get current top position
  const pixels = Number(circle.style.top.slice(0, -2));
  
  // if already at top, return
  if (circle.style.top === '0px' || circle.style.top === '') return;
  
  // move div
  if (pixels - 50 > 0) {
    circle.style.top = `${pixels - 50}px`;
  } else {
    circle.style.top = '0px';
  }
  
  // add to stack
  pushDirection({ moveUp: 'top', pixels });
};

const moveRight = () => {
  // get current left position
  const pixels = Number(circle.style.left.slice(0, -2));
  
  // if already at right, return
  if (circle.style.left === `${width - 50}px`) return;
  
  // move div
  if (pixels + 50 < width - 50) {
    circle.style.left = `${pixels + 50}px`;
  } else {
    circle.style.left = `${width - 50}px`;
  }
  
  // add to stack
  pushDirection({ moveRight: 'left', pixels });
};

const moveDown = () => {
  // get current top position
  const pixels = Number(circle.style.top.slice(0, -2));
  
  // if already at bottom, return
  if (circle.style.top === `${height - 50}px`) return;
  
  // move div
  if (pixels + 50 < height - 50) {
    circle.style.top = `${pixels + 50}px`;
  } else {
    circle.style.top = `${height - 50}px`;
  }
  // add to stack
  pushDirection({ moveDown: 'top', pixels });
};

const moveLeft = () => {
  // get current left position
  const pixels = Number(circle.style.left.slice(0, -2));
  
  // if already at left, return
  if (circle.style.left === '0px' || circle.style.left === '') return;
  
  // move div
  if (pixels - 50 > 0) {
    circle.style.left = `${pixels - 50}px`;
  } else {
    circle.style.left = '0px';
  }
  // add to stack
  pushDirection({ moveLeft: 'left', pixels });
};

const undo = () => {
  // invoke stack method, regardless of stack length
  const last = undoDirection();
  
  // if not undefined
  if (last) {
    // update circle
    circle.style[ last.moveUp || last.moveRight || last.moveDown || last.moveLeft ] = `${last.pixels}px`;
  }
};

const redo = () => {
  // invoke stack method, regardless of stack length
  const next = redoDirection();
  
  // if not undefined
  if (next) {
    // update circle
    if (next.moveUp) circle.style[next.moveUp] = `${next.pixels - 50}px`;
    else if (next.moveRight) circle.style[next.moveRight] = `${next.pixels + 50}px`;
    else if (next.moveDown) circle.style[next.moveDown] = `${next.pixels + 50}px`;
    else if (next.moveLeft) circle.style[next.moveLeft] = `${next.pixels - 50}px`;
  }
};

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/emmet/2.1.1/emmet.cjs.js