Browse Source

beginnings of new dnd utils

Adam Shaw 7 years ago
parent
commit
62bda021cc

+ 0 - 44
src/View.ts

@@ -1,6 +1,4 @@
 import { assignTo } from './util/object'
 import { assignTo } from './util/object'
-import { elementClosest } from './util/dom-manip'
-import { isPrimaryMouseButton } from './util/dom-event'
 import { parseFieldSpecs } from './util/misc'
 import { parseFieldSpecs } from './util/misc'
 import Calendar from './Calendar'
 import Calendar from './Calendar'
 import { default as DateProfileGenerator, DateProfile } from './DateProfileGenerator'
 import { default as DateProfileGenerator, DateProfile } from './DateProfileGenerator'
@@ -432,48 +430,6 @@ export default abstract class View extends InteractiveDateComponent {
   }
   }
 
 
 
 
-  /* Mouse / Touch Unselecting (time range & event unselection)
-  ------------------------------------------------------------------------------------------------------------------*/
-  // TODO: move consistently to down/start or up/end?
-  // TODO: don't kill previous selection if touch scrolling
-
-
-  handleDocumentMousedown(ev) {
-    if (isPrimaryMouseButton(ev)) {
-      this.processUnselect(ev)
-    }
-  }
-
-
-  processUnselect(ev) {
-    this.processRangeUnselect(ev)
-    this.processEventUnselect(ev)
-  }
-
-
-  processRangeUnselect(ev) {
-    let ignore
-
-    // is there a time-range selection?
-    if (this.isSelected && this.opt('unselectAuto')) {
-      // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
-      ignore = this.opt('unselectCancel')
-      if (!ignore || !elementClosest(ev.target, ignore)) {
-        this.unselect(ev)
-      }
-    }
-  }
-
-
-  processEventUnselect(ev) {
-    if (this.selectedEventInstance) {
-      if (!elementClosest(ev.target, '.fc-selected')) {
-        // TODO: use dispatch to change selectedEventInstanceId
-      }
-    }
-  }
-
-
   /* Date Utils
   /* Date Utils
   ------------------------------------------------------------------------------------------------------------------*/
   ------------------------------------------------------------------------------------------------------------------*/
 
 

+ 0 - 216
src/common/MouseFollower.ts

@@ -1,216 +0,0 @@
-import {
-  getEvY,
-  getEvX,
-  getEvIsTouch,
-  whenTransitionDone
-} from '../util/dom-event'
-import { removeElement, applyStyle } from '../util/dom-manip'
-
-export interface MouseFollowerOptions {
-  parentEl?: HTMLElement
-  revertDuration?: number
-  additionalClass?: string
-  opacity?: number
-  zIndex?: number
-}
-
-/* Creates a clone of an element and lets it track the mouse as it moves
-----------------------------------------------------------------------------------------------------------------------*/
-
-export default class MouseFollower {
-
-  options: MouseFollowerOptions
-
-  sourceEl: HTMLElement // the element that will be cloned and made to look like it is dragging
-  el: HTMLElement // the clone of `sourceEl` that will track the mouse
-  parentEl: HTMLElement // the element that `el` (the clone) will be attached to
-
-  // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
-  top0: any
-  left0: any
-
-  // the absolute coordinates of the initiating touch/mouse action
-  y0: any
-  x0: any
-
-  // the number of pixels the mouse has moved from its initial position
-  topDelta: any
-  leftDelta: any
-
-  isFollowing: boolean = false
-  isHidden: boolean = false
-  isAnimating: boolean = false // doing the revert animation?
-
-
-  constructor(sourceEl: HTMLElement, options: MouseFollowerOptions) {
-    this.options = options = options || {}
-    this.sourceEl = sourceEl
-    this.parentEl = options.parentEl || (sourceEl.parentNode as HTMLElement) // default to sourceEl's parent
-  }
-
-
-  // Causes the element to start following the mouse
-  start(ev) {
-    if (!this.isFollowing) {
-      this.isFollowing = true
-
-      this.y0 = getEvY(ev)
-      this.x0 = getEvX(ev)
-      this.topDelta = 0
-      this.leftDelta = 0
-
-      if (!this.isHidden) {
-        this.updatePosition()
-      }
-
-      // if (getEvIsTouch(ev)) {
-      //   this.listenTo(document, 'touchmove', this.handleMove)
-      // } else {
-      //   this.listenTo(document, 'mousemove', this.handleMove)
-      // }
-    }
-  }
-
-
-  // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
-  // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
-  stop(shouldRevert, callback) {
-    let { el } = this
-    let revertDuration = this.options.revertDuration
-
-    const complete = () => { // might be called by .animate(), which might change `this` context
-      this.isAnimating = false
-      this.removeElement()
-
-      this.top0 = this.left0 = null // reset state for future updatePosition calls
-
-      if (callback) {
-        callback()
-      }
-    }
-
-    if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
-      this.isFollowing = false
-
-      // this.stopListeningTo(document)
-
-      if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
-        this.isAnimating = true
-
-        // do before position change
-        el.style.transition = 'top ' + revertDuration + 'ms, left ' + revertDuration + 'ms'
-
-        applyStyle(el, {
-          top: this.top0,
-          left: this.left0
-        })
-
-        whenTransitionDone(el, function() {
-          el.style.transition = ''
-          complete()
-        })
-      } else {
-        complete()
-      }
-    }
-  }
-
-
-  // Gets the tracking element. Create it if necessary
-  getEl() {
-    let el = this.el
-
-    if (!el) {
-      el = this.el = this.sourceEl.cloneNode(true) as HTMLElement // cloneChildren=true
-
-      // we don't want long taps or any mouse interaction causing selection/menus.
-      // would use preventSelection(), but that prevents selectstart, causing problems.
-      el.classList.add('fc-unselectable')
-
-      if (this.options.additionalClass) {
-        el.classList.add(this.options.additionalClass)
-      }
-
-      applyStyle(el, {
-        position: 'absolute',
-        visibility: '', // in case original element was hidden (commonly through hideEvents())
-        display: this.isHidden ? 'none' : '', // for when initially hidden
-        margin: 0,
-        right: 'auto', // erase and set width instead
-        bottom: 'auto', // erase and set height instead
-        width: this.sourceEl.offsetWidth, // explicit height in case there was a 'right' value
-        height: this.sourceEl.offsetHeight, // explicit width in case there was a 'bottom' value
-        opacity: this.options.opacity || '',
-        zIndex: this.options.zIndex
-      })
-
-      this.parentEl.appendChild(el)
-    }
-
-    return el
-  }
-
-
-  // Removes the tracking element if it has already been created
-  removeElement() {
-    if (this.el) {
-      removeElement(this.el)
-      this.el = null
-    }
-  }
-
-
-  // Update the CSS position of the tracking element
-  updatePosition() {
-    let sourceRect
-    let origin
-    let el = this.getEl() // ensure this.el
-
-    // make sure origin info was computed
-    if (this.top0 == null) {
-      sourceRect = this.sourceEl.getBoundingClientRect()
-      origin = el.offsetParent.getBoundingClientRect()
-      this.top0 = sourceRect.top - origin.top
-      this.left0 = sourceRect.left - origin.left
-    }
-
-    applyStyle(el, {
-      top: this.top0 + this.topDelta,
-      left: this.left0 + this.leftDelta
-    })
-
-  }
-
-
-  // Gets called when the user moves the mouse
-  handleMove(ev) {
-    this.topDelta = getEvY(ev) - this.y0
-    this.leftDelta = getEvX(ev) - this.x0
-
-    if (!this.isHidden) {
-      this.updatePosition()
-    }
-  }
-
-
-  // Temporarily makes the tracking element invisible. Can be called before following starts
-  hide() {
-    if (!this.isHidden) {
-      this.isHidden = true
-      if (this.el) {
-        this.el.style.display = 'none'
-      }
-    }
-  }
-
-
-  // Show the tracking element after it has been temporarily hidden
-  show() {
-    if (this.isHidden) {
-      this.isHidden = false
-      this.updatePosition()
-      this.getEl().style.display = ''
-    }
-  }
-
-}

+ 171 - 0
src/dnd/IntentfulDragListener.ts

@@ -0,0 +1,171 @@
+import { default as EmitterMixin } from '../common/EmitterMixin'
+import { default as PointerDragListener, PointerEvent } from './PointerDragListener'
+import { preventSelection, allowSelection, preventContextMenu, allowContextMenu } from '../util/misc'
+
+export interface IntentfulDragOptions {
+  touchMinDistance?: number
+  mouseMinDistance?: number
+  touchDelay?: number | [() => number]
+  mouseDelay?: number | [() => number]
+  touchScrollAllowed?: boolean
+}
+
+export default class IntentfulDragListener {
+
+  pointer: PointerDragListener
+  emitter: EmitterMixin
+
+  options: IntentfulDragOptions
+
+  isDragging: boolean = false // is it INTENTFULLY dragging?
+  isDelayEnded: boolean = false
+  isDistanceSurpassed: boolean = false
+  isTouchScroll: boolean = false
+
+  delay: number
+  delayTimeoutId: number
+
+  minDistance: number
+  origX: number
+  origY: number
+
+  constructor(options) {
+    this.options = options
+    this.pointer = new PointerDragListener(options.containerEl, options.selector)
+    this.emitter = new EmitterMixin()
+
+    this.pointer.on('down', this.handleDown)
+    this.pointer.on('move', this.handleMove)
+    this.pointer.on('up', this.handleUp)
+  }
+
+  destroy() {
+    this.pointer.destroy()
+  }
+
+  on(name, handler) {
+    this.emitter.on(name, handler)
+  }
+
+  handleDown = (ev: PointerEvent) => {
+    preventSelection(document.body)
+    preventContextMenu(document.body)
+
+    let minDistance = this.options[ev.isTouch ? 'touchMinDistance' : 'mouseMinDistance']
+    let delay = this.options[ev.isTouch ? 'touchDelay' : 'mouseDelay']
+
+    this.minDistance = minDistance
+    this.delay = typeof delay === 'function' ? (delay as any)() : delay
+
+    this.origX = ev.pageX
+    this.origY = ev.pageY
+
+    this.isDelayEnded = false
+    this.isDistanceSurpassed = false
+    this.isTouchScroll = false
+
+    this.emitter.trigger('pointerdown', ev)
+    this.startDelay(ev)
+
+    if (!this.minDistance) {
+      this.handleDistanceSurpassed(ev)
+    }
+
+    if (ev.isTouch) {
+      // attach a handler to get called when ANY scroll action happens on the page.
+      // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
+      // http://stackoverflow.com/a/32954565/96342
+      window.addEventListener(
+        'scroll',
+        this.handleTouchScroll, // always bound to `this`
+        true // useCapture
+      )
+    }
+  }
+
+  handleMove = (ev: PointerEvent) => {
+    this.emitter.trigger('pointermove', ev)
+
+    if (!this.isDistanceSurpassed) {
+      let dx = ev.pageX - this.origX
+      let dy = ev.pageY - this.origY
+      let minDistance = this.minDistance
+      let distanceSq // current distance from the origin, squared
+
+      distanceSq = dx * dx + dy * dy
+      if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
+        this.handleDistanceSurpassed(ev)
+      }
+    }
+
+    if (this.isDragging) {
+      this.emitter.trigger('dragmove', ev)
+    }
+  }
+
+  handleUp = (ev: PointerEvent) => {
+    if (this.isDragging) {
+      this.stopDrag(ev)
+    }
+
+    allowSelection(document.body)
+    allowContextMenu(document.body)
+
+    if (this.delayTimeoutId) {
+      clearTimeout(this.delayTimeoutId)
+      this.delayTimeoutId = null
+    }
+
+    if (ev.isTouch) {
+      window.removeEventListener('scroll', this.handleTouchScroll)
+    }
+
+    this.emitter.trigger('pointerup', ev)
+  }
+
+  handleTouchScroll = () => {
+    this.isTouchScroll = true
+  }
+
+  startDelay(ev: PointerEvent) {
+    if (typeof this.delay === 'number') {
+      this.delayTimeoutId = setTimeout(() => {
+        this.delayTimeoutId = null
+        this.handleDelayEnd(ev)
+      }, this.delay)
+    } else {
+      this.handleDelayEnd(ev)
+    }
+  }
+
+  handleDelayEnd(ev: PointerEvent) {
+    this.isDelayEnded = true
+    this.startDrag(ev)
+  }
+
+  handleDistanceSurpassed(ev: PointerEvent) {
+    this.isDistanceSurpassed = true
+    this.startDrag(ev)
+  }
+
+  startDrag(ev: PointerEvent) { // will only start if appropriate
+    if (
+      this.isDelayEnded &&
+      this.isDistanceSurpassed &&
+      (!this.isTouchScroll || this.options.touchScrollAllowed !== false)
+    ) {
+      this.emitter.trigger('dragstart', ev)
+      this.isDragging = true
+
+      if (this.options.touchScrollAllowed === false) {
+        this.pointer.cancelTouchScroll()
+      }
+    }
+  }
+
+  stopDrag(ev) {
+    this.emitter.trigger('dragend', ev)
+    this.isDragging = false
+  }
+
+}

+ 179 - 0
src/dnd/PointerDragListener.ts

@@ -0,0 +1,179 @@
+import * as exportHooks from '../exports'
+import { elementClosest } from '../util/dom-manip'
+import { default as EmitterMixin } from '../common/EmitterMixin'
+import { isPrimaryMouseButton } from '../util/dom-event'
+
+(exportHooks as any).touchMouseIgnoreWait = 500
+
+export interface PointerEvent {
+  origEvent: UIEvent
+  isTouch: boolean
+  el: HTMLElement
+  pageX: number
+  pageY: number
+}
+
+export type PointerEventHandler = (ev: PointerEvent) => void
+
+export default class PointerDragListener {
+
+  containerEl: HTMLElement
+  selector: string
+  subjectEl: HTMLElement
+  emitter: EmitterMixin
+
+  isDragging: boolean = false
+  isDraggingTouch: boolean = false
+  ignoreMouseDepth: number = 0
+
+  constructor(containerEl: HTMLElement, selector?: string) {
+    this.containerEl = containerEl
+    this.selector = selector
+    this.emitter = new EmitterMixin()
+    containerEl.addEventListener('mousedown', this.onMouseDown)
+    containerEl.addEventListener('touchstart', this.onTouchStart)
+    listenerCreated()
+  }
+
+  destroy() {
+    listenerDestroyed()
+  }
+
+  on(name, handler: PointerEventHandler) {
+    this.emitter.on(name, handler)
+  }
+
+  onMouseDown = (ev: MouseEvent) => {
+    if (
+      !this.shouldIgnoreMouse() &&
+      isPrimaryMouseButton(ev) &&
+      (this.subjectEl = this.queryValidSubjectEl(ev))
+    ) {
+      this.isDragging = true // do this first so cancelTouchScroll will work
+      this.emitter.trigger('down', this.createMouseEvent(ev))
+      document.addEventListener('mousemove', this.onMouseMove)
+      document.addEventListener('mouseup', this.onMouseUp)
+    }
+  }
+
+  onMouseMove = (ev: MouseEvent) => {
+    this.emitter.trigger('move', this.createMouseEvent(ev))
+  }
+
+  onMouseUp = (ev: MouseEvent) => {
+    isWindowTouchMoveCancelled = false
+    this.isDragging = false
+    document.removeEventListener('mousemove', this.onMouseMove)
+    document.removeEventListener('mouseup', this.onMouseUp)
+    this.emitter.trigger('up', this.createMouseEvent(ev))
+    this.subjectEl = null // clear afterwards, so handlers have access
+  }
+
+  onTouchStart = (ev: TouchEvent) => {
+    if ((this.subjectEl = this.queryValidSubjectEl(ev))) {
+      this.isDragging = true // do this first so cancelTouchScroll will work
+      this.isDraggingTouch = true
+      this.emitter.trigger('down', this.createTouchEvent(ev))
+      document.addEventListener('touchmove', this.onTouchMove)
+      document.addEventListener('touchend', this.onTouchEnd)
+      document.addEventListener('touchcancel', this.onTouchEnd) // treat it as a touch end
+    }
+  }
+
+  onTouchMove = (ev: TouchEvent) => {
+    this.emitter.trigger('move', this.createTouchEvent(ev))
+  }
+
+  onTouchEnd = (ev: TouchEvent) => {
+    isWindowTouchMoveCancelled = false
+    this.isDragging = false
+    this.isDraggingTouch = false
+    document.removeEventListener('touchmove', this.onTouchMove)
+    document.removeEventListener('touchend', this.onTouchEnd)
+    document.removeEventListener('touchcancel', this.onTouchEnd)
+    this.emitter.trigger('up', this.createTouchEvent(ev))
+    this.subjectEl = null // clear afterwards, so handlers have access
+    this.startIgnoringMouse()
+  }
+
+  queryValidSubjectEl(ev: UIEvent): HTMLElement {
+    if (this.selector) {
+      return elementClosest(ev.target as HTMLElement, this.selector)
+    } else {
+      return this.containerEl
+    }
+  }
+
+  shouldIgnoreMouse() {
+    return this.ignoreMouseDepth || this.isDraggingTouch
+  }
+
+  startIgnoringMouse() {
+    this.ignoreMouseDepth++
+
+    setTimeout(() => {
+      this.ignoreMouseDepth--
+    }, (exportHooks as any).touchMouseIgnoreWait)
+  }
+
+  cancelTouchScroll() {
+    if (this.isDragging) {
+      isWindowTouchMoveCancelled = true
+    }
+  }
+
+  createMouseEvent(ev): PointerEvent {
+    return {
+      origEvent: ev,
+      isTouch: false,
+      el: this.subjectEl,
+      pageX: ev.pageX,
+      pageY: ev.pageY
+    }
+  }
+
+  createTouchEvent(ev): PointerEvent {
+    let touches = ev.touches
+    let obj = {
+      origEvent: ev,
+      isTouch: true,
+      el: this.subjectEl
+    } as PointerEvent
+
+    // if touch coords available, prefer,
+    // because FF would give bad ev.pageX ev.pageY
+    if (touches && touches.length) {
+      obj.pageX = touches[0].pageX
+      obj.pageY = touches[0].pageY
+    } else {
+      obj.pageX = ev.pageX
+      obj.pageY = ev.pageY
+    }
+
+    return obj
+  }
+
+}
+
+// we want to attach touchmove as early as possible for safari
+
+let listenerCnt = 0
+let isWindowTouchMoveCancelled = false
+
+function listenerCreated() {
+  if (!(listenerCnt++)) {
+    window.addEventListener('touchmove', onWindowTouchMove, { passive: false })
+  }
+}
+
+function listenerDestroyed() {
+  if (!(--listenerCnt)) {
+    window.removeEventListener('touchmove', onWindowTouchMove)
+  }
+}
+
+function onWindowTouchMove(ev) {
+  if (isWindowTouchMoveCancelled) {
+    ev.preventDefault()
+  }
+}

+ 3 - 0
src/exports.ts

@@ -110,3 +110,6 @@ export { parse as parseMarker } from './datelib/parsing'
 
 
 export { registerSourceType } from './reducers/event-sources'
 export { registerSourceType } from './reducers/event-sources'
 export { refineProps } from './reducers/utils'
 export { refineProps } from './reducers/utils'
+
+export { default as PointerDragListener } from './dnd/PointerDragListener'
+export { default as IntentfulDragListener } from './dnd/IntentfulDragListener'

+ 0 - 31
src/util/dom-event.ts

@@ -7,37 +7,6 @@ export function isPrimaryMouseButton(ev: MouseEvent) {
 }
 }
 
 
 
 
-export function getEvX(ev: UIEvent) {
-  let touches = (ev as TouchEvent).touches
-
-  // on mobile FF, pageX for touch events is present, but incorrect,
-  // so, look at touch coordinates first.
-  if (touches && touches.length) {
-    return touches[0].pageX
-  }
-
-  return (ev as MouseEvent).pageX
-}
-
-
-export function getEvY(ev) {
-  let touches = (ev as TouchEvent).touches
-
-  // on mobile FF, pageX for touch events is present, but incorrect,
-  // so, look at touch coordinates first.
-  if (touches && touches.length) {
-    return touches[0].pageY
-  }
-
-  return (ev as MouseEvent).pageY
-}
-
-
-export function getEvIsTouch(ev) {
-  return /^touch/.test(ev.type)
-}
-
-
 // Stops a mouse/touch event from doing it's native browser action
 // Stops a mouse/touch event from doing it's native browser action
 export function preventDefault(ev) {
 export function preventDefault(ev) {
   ev.preventDefault()
   ev.preventDefault()

+ 14 - 0
src/util/misc.ts

@@ -176,6 +176,20 @@ export function allowSelection(el: HTMLElement) {
 }
 }
 
 
 
 
+/* Context Menu
+----------------------------------------------------------------------------------------------------------------------*/
+
+
+export function preventContextMenu(el: HTMLElement) {
+  el.addEventListener('contextmenu', preventDefault)
+}
+
+
+export function allowContextMenu(el: HTMLElement) {
+  el.removeEventListener('contextmenu', preventDefault)
+}
+
+
 /* Object Ordering by Field
 /* Object Ordering by Field
 ----------------------------------------------------------------------------------------------------------------------*/
 ----------------------------------------------------------------------------------------------------------------------*/
 
 

+ 73 - 0
tests/manual/dnd.html

@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset='utf-8' />
+<script src='../../dist/fullcalendar.js'></script>
+<script>
+
+  document.addEventListener('DOMContentLoaded', function() {
+
+    // var PointerDragListener = FullCalendar.PointerDragListener;
+
+    // var listener = new PointerDragListener(document.getElementById('box'))
+    // listener.on('down', function(ev) {
+    //   console.log('down', ev)
+    //   listener.cancelTouchScroll()
+    // })
+    // listener.on('move', function(ev) {
+    //   console.log('move', ev)
+    // })
+    // listener.on('up', function(ev) {
+    //   console.log('up', ev)
+    // })
+
+    var IntentfulDragListener = FullCalendar.IntentfulDragListener;
+
+    var listener = new IntentfulDragListener({
+      containerEl: document.getElementById('box'),
+      selector: 'a',
+      touchDelay: 1000,
+      mouseMinDistance: 20,
+      touchScrollAllowed: false
+    })
+    listener.on('pointerdown', function(ev) {
+      console.log('pointerdown', ev)
+    })
+    listener.on('dragstart', function(ev) {
+      console.log('dragstart', ev)
+    })
+    listener.on('dragmove', function(ev) {
+      console.log('dragmove', ev)
+    })
+    listener.on('dragend', function(ev) {
+      console.log('dragend', ev)
+    })
+    listener.on('pointerup', function(ev) {
+      console.log('pointerup', ev)
+    })
+  });
+
+</script>
+</head>
+<body>
+
+  <div style='width:400px; height:600px; margin:0 auto; overflow:auto; background:yellow'>
+    <p>
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra.
+    </p>
+    <p>
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra.
+    </p>
+    <div id='box' style='width: 100px; height: 100px; background: red'>
+      <a>stuff in here</a>
+    </div>
+    <p>
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra.
+    </p>
+    <p>
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin semper pretium auctor. Sed condimentum scelerisque pellentesque. Sed blandit dui ut diam blandit faucibus. Mauris tincidunt ac leo quis semper. Integer condimentum neque ac feugiat imperdiet. Nullam eget lorem vel erat pulvinar elementum. Proin elit dolor, tincidunt auctor posuere eu, faucibus tempus eros. Cras at vestibulum augue, non tempus sapien. Donec neque lectus, commodo suscipit erat nec, rhoncus maximus felis. Etiam ut nisi interdum, malesuada erat ac, tempor lectus. Phasellus hendrerit orci vitae nisi finibus sodales. Vestibulum eleifend arcu vel semper fringilla. Ut tincidunt massa a aliquet tincidunt. Donec molestie vitae nulla quis pharetra.
+    </p>
+  </div>
+
+</body>
+</html>