Adam Shaw 8 лет назад
Родитель
Сommit
9c3f304a1f

+ 2 - 6
src/Calendar.ts

@@ -1,7 +1,7 @@
 import * as $ from 'jquery'
 import * as moment from 'moment'
 import { capitaliseFirstLetter, debounce } from './util'
-import { listenBySelector, toggleClassName, makeElement, removeElement, applyStyle, prependWithinEl } from './util/dom'
+import { listenBySelector, toggleClassName, makeElement, removeElement, applyStyle, prependWithinEl, computeHeightAndMargins } from './util/dom'
 import { globalDefaults, englishDefaults, rtlDefaults } from './options'
 import Iterator from './common/Iterator'
 import GlobalEmitter from './common/GlobalEmitter'
@@ -774,11 +774,7 @@ export default class Calendar {
       let toolbarHeight
 
       if (toolbar.el) {
-        let computed = window.getComputedStyle(toolbar.el)
-        toolbarHeight =
-          toolbar.el.offsetHeight +
-          parseInt(computed.marginTop, 10) +
-          parseInt(computed.marginBottom, 10)
+        toolbarHeight = computeHeightAndMargins(toolbar.el)
       } else {
         toolbarHeight = 0
       }

+ 5 - 5
src/View.ts

@@ -587,7 +587,7 @@ export default abstract class View extends InteractiveDateComponent {
   // Triggers event-drop handlers that have subscribed via the API
   triggerEventDrop(eventInstance, dateDelta, undoFunc, el, ev) {
     this.publiclyTrigger('eventDrop', {
-      context: el[0],
+      context: el,
       args: [
         eventInstance.toLegacy(),
         dateDelta,
@@ -622,7 +622,7 @@ export default abstract class View extends InteractiveDateComponent {
 
     // trigger 'drop' regardless of whether element represents an event
     this.publiclyTrigger('drop', {
-      context: el[0],
+      context: el,
       args: [
         singleEventDef.dateProfile.start.clone(),
         ev,
@@ -674,7 +674,7 @@ export default abstract class View extends InteractiveDateComponent {
   // Triggers event-resize handlers that have subscribed via the API
   triggerEventResize(eventInstance, durationDelta, undoFunc, el, ev) {
     this.publiclyTrigger('eventResize', {
-      context: el[0],
+      context: el,
       args: [
         eventInstance.toLegacy(),
         durationDelta,
@@ -767,7 +767,7 @@ export default abstract class View extends InteractiveDateComponent {
           seg.footprint.eventInstance === eventInstance &&
           seg.el // necessary?
         ) {
-          seg.el.addClass('fc-selected')
+          seg.el.classList.add('fc-selected')
         }
       })
 
@@ -781,7 +781,7 @@ export default abstract class View extends InteractiveDateComponent {
 
       this.getEventSegs().forEach(function(seg) {
         if (seg.el) { // necessary?
-          seg.el.removeClass('fc-selected')
+          seg.el.classList.remove('fc-selected')
         }
       })
 

+ 3 - 3
src/agenda/TimeGrid.ts

@@ -1,7 +1,7 @@
 import * as $ from 'jquery'
 import * as moment from 'moment'
 import { isInt, divideDurationByDuration, htmlEscape } from '../util'
-import { htmlToElement, findElsWithin, makeElement, removeElement } from '../util/dom'
+import { htmlToElement, findElsWithin, makeElement, removeElement, applyStyle } from '../util/dom'
 import InteractiveDateComponent from '../component/InteractiveDateComponent'
 import BusinessHourRenderer from '../component/renderers/BusinessHourRenderer'
 import StandardInteractionsMixin from '../component/interactions/StandardInteractionsMixin'
@@ -395,7 +395,7 @@ export default class TimeGrid extends InteractiveDateComponent {
       segs = segsByCol[col]
 
       for (i = 0; i < segs.length; i++) {
-        containerEls[col].appendChild(segs[i].el[0])
+        containerEls[col].appendChild(segs[i].el)
       }
     }
   }
@@ -556,7 +556,7 @@ export default class TimeGrid extends InteractiveDateComponent {
 
     for (i = 0; i < segs.length; i++) {
       seg = segs[i]
-      seg.el.css(this.generateSegVerticalCss(seg))
+      applyStyle(seg.el, this.generateSegVerticalCss(seg))
     }
   }
 

+ 4 - 3
src/agenda/TimeGridEventRenderer.ts

@@ -1,4 +1,5 @@
 import { htmlEscape, cssToStr, proxy } from '../util'
+import { removeElement, applyStyle } from '../util/dom'
 import EventRenderer from '../component/renderers/EventRenderer'
 
 /*
@@ -40,7 +41,7 @@ export default class TimeGridEventRenderer extends EventRenderer {
   unrenderFgSegs() {
     if (this.fgSegs) { // hack
       this.fgSegs.forEach(function(seg) {
-        seg.el.remove()
+        removeElement(seg.el)
       })
     }
   }
@@ -238,11 +239,11 @@ export default class TimeGridEventRenderer extends EventRenderer {
 
     for (i = 0; i < segs.length; i++) {
       seg = segs[i]
-      seg.el.css(this.generateFgSegHorizontalCss(seg))
+      applyStyle(seg.el, this.generateFgSegHorizontalCss(seg))
 
       // if the height is short, add a className for alternate styling
       if (seg.bottom - seg.top < 30) {
-        seg.el.addClass('fc-short')
+        seg.el.classList.add('fc-short')
       }
     }
   }

+ 1 - 1
src/agenda/TimeGridFillRenderer.ts

@@ -20,7 +20,7 @@ export default class TimeGridFillRenderer extends FillRenderer {
     timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls)
 
     return segs.map(function(seg) {
-      return seg.el[0]
+      return seg.el
     })
   }
 

+ 10 - 8
src/agenda/TimeGridHelperRenderer.ts

@@ -1,4 +1,4 @@
-import * as $ from 'jquery'
+import { applyStyle } from '../util/dom'
 import HelperRenderer from '../component/renderers/HelperRenderer'
 
 
@@ -9,6 +9,7 @@ export default class TimeGridHelperRenderer extends HelperRenderer {
     let i
     let seg
     let sourceEl
+    let computedStyle
 
     // TODO: not good to call eventRenderer this way
     this.eventRenderer.renderFgSegsIntoContainers(
@@ -22,18 +23,19 @@ export default class TimeGridHelperRenderer extends HelperRenderer {
 
       if (sourceSeg && sourceSeg.col === seg.col) {
         sourceEl = sourceSeg.el
-        seg.el.css({
-          left: sourceEl.css('left'),
-          right: sourceEl.css('right'),
-          'margin-left': sourceEl.css('margin-left'),
-          'margin-right': sourceEl.css('margin-right')
+        computedStyle = window.getComputedStyle(sourceEl)
+        applyStyle(seg.el, {
+          left: computedStyle.left,
+          right: computedStyle.right,
+          marginLeft: computedStyle.marginLeft,
+          marginRight: computedStyle.marginRight
         })
       }
 
-      helperNodes.push(seg.el[0])
+      helperNodes.push(seg.el)
     }
 
-    return $(helperNodes) // must return the elements rendered
+    return helperNodes // must return the elements rendered
   }
 
 }

+ 2 - 2
src/basic/DayGrid.ts

@@ -693,7 +693,7 @@ export default class DayGrid extends InteractiveDateComponent {
 
     // the popover doesn't live within the grid's container element, and thus won't get the event
     // delegated-handlers for free. attach event-related handlers to the popover.
-    this.bindAllSegHandlersToEl($(this.segPopover.el))
+    this.bindAllSegHandlersToEl(this.segPopover.el)
 
     this.triggerAfterEventSegsRendered(segs)
   }
@@ -731,7 +731,7 @@ export default class DayGrid extends InteractiveDateComponent {
       segs[i].hit = this.getCellHit(row, col)
       this.hitsNotNeeded()
 
-      segContainer.appendChild(segs[i].el[0])
+      segContainer.appendChild(segs[i].el)
     }
 
     return content

+ 5 - 5
src/basic/DayGridFillRenderer.ts

@@ -11,7 +11,7 @@ export default class DayGridFillRenderer extends FillRenderer {
 
 
   attachSegEls(type, segs) {
-    let nodes = []
+    let els = []
     let i
     let seg
     let skeletonEl: HTMLElement
@@ -20,10 +20,10 @@ export default class DayGridFillRenderer extends FillRenderer {
       seg = segs[i]
       skeletonEl = this.renderFillRow(type, seg)
       this.component.rowEls[seg.row].appendChild(skeletonEl)
-      nodes.push(skeletonEl)
+      els.push(skeletonEl)
     }
 
-    return nodes
+    return els
   }
 
 
@@ -53,8 +53,8 @@ export default class DayGridFillRenderer extends FillRenderer {
       trEl.appendChild(makeElement('td', { colSpan: startCol }))
     }
 
-    (seg.el[0] as HTMLTableCellElement).colSpan = endCol - startCol
-    trEl.appendChild(seg.el[0])
+    (seg.el as HTMLTableCellElement).colSpan = endCol - startCol
+    trEl.appendChild(seg.el)
 
     if (endCol < colCnt) {
       trEl.appendChild(makeElement('td', { colSpan: colCnt - endCol }))

+ 6 - 6
src/basic/DayGridHelperRenderer.ts

@@ -1,4 +1,3 @@
-import * as $ from 'jquery'
 import HelperRenderer from '../component/renderers/HelperRenderer'
 import DayGrid from './DayGrid'
 import { htmlToElement } from '../util/dom'
@@ -25,24 +24,25 @@ export default class DayGridHelperRenderer extends HelperRenderer {
 
       // If there is an original segment, match the top position. Otherwise, put it at the row's top level
       if (sourceSeg && sourceSeg.row === row) {
-        skeletonTop = sourceSeg.el[0].getBoundingClientRect().top
+        skeletonTopEl = sourceSeg.el
       } else {
         skeletonTopEl = rowNode.querySelector('.fc-content-skeleton tbody')
         if (!skeletonTopEl) { // when no events
           skeletonTopEl = rowNode.querySelector('.fc-content-skeleton table')
         }
-
-        skeletonTop = skeletonTopEl.getBoundingClientRect().top
       }
 
+      skeletonTop = skeletonTopEl.getBoundingClientRect().top -
+        rowNode.getBoundingClientRect().top // the offsetParent origin
+
       skeletonEl.style.top = skeletonTop + 'px'
-      skeletonEl.getElementsByTagName('table')[0].appendChild(rowStructs[row].tbodyEl)
+      skeletonEl.querySelector('table').appendChild(rowStructs[row].tbodyEl)
 
       rowNode.appendChild(skeletonEl)
       helperNodes.push(skeletonEl)
     })
 
-    return $(helperNodes) // must return the elements rendered
+    return helperNodes // must return the elements rendered
   }
 
 }

+ 6 - 3
src/common/common.scss

@@ -55,7 +55,7 @@ body .fc { /* extra precedence to overcome jqui */
 
 /* Firefox has an annoying inner border */
 .fc button::-moz-focus-inner { margin: 0; padding: 0; }
-  
+
 .fc-state-default { /* non-theme */
   border: 1px solid;
 }
@@ -78,7 +78,7 @@ body .fc { /* extra precedence to overcome jqui */
   margin: 0 .2em;
   vertical-align: middle;
 }
-  
+
 /*
   button states
   borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/)
@@ -281,7 +281,7 @@ a[data-goto]:hover {
   border-right: 0 hidden transparent;
 
   /* no bottom borders on rows */
-  border-bottom: 0 hidden transparent; 
+  border-bottom: 0 hidden transparent;
 }
 
 .fc-row:first-child table {
@@ -571,6 +571,9 @@ be a descendant of the grid when it is being dragged.
 tr:first-child > td > .fc-day-grid-event {
   margin-top: 2px; /* a little bit more space before the first event */
 }
+.fc-helper-skeleton tr:first-child > td > .fc-day-grid-event {
+  margin-top: 0; /* except for helper skeleton */
+}
 
 .fc-day-grid-event.fc-selected:after {
   content: "";

+ 2 - 2
src/component/DateComponent.ts

@@ -339,7 +339,7 @@ export default abstract class DateComponent extends Component {
         seg.footprint.eventDef.id === eventDefId &&
         seg.el // necessary?
       ) {
-        seg.el.css('visibility', '')
+        seg.el.style.visibility = ''
       }
     })
 
@@ -356,7 +356,7 @@ export default abstract class DateComponent extends Component {
         seg.footprint.eventDef.id === eventDefId &&
         seg.el // necessary?
       ) {
-        seg.el.css('visibility', 'hidden')
+        seg.el.style.visibility = 'hidden'
       }
     })
 

+ 30 - 9
src/component/InteractiveDateComponent.ts

@@ -1,6 +1,7 @@
 import * as $ from 'jquery'
 import * as moment from 'moment'
 import { getEvIsTouch, diffByUnit, diffDayTime } from '../util'
+import { listenBySelector, listenToHoverBySelector } from '../util/dom'
 import DateComponent from './DateComponent'
 import GlobalEmitter from '../common/GlobalEmitter'
 
@@ -65,14 +66,14 @@ export default abstract class InteractiveDateComponent extends DateComponent {
     super.setElement(el)
 
     if (this.dateClicking) {
-      this.dateClicking.bindToEl($(el))
+      this.dateClicking.bindToEl(el)
     }
 
     if (this.dateSelecting) {
-      this.dateSelecting.bindToEl($(el))
+      this.dateSelecting.bindToEl(el)
     }
 
-    this.bindAllSegHandlersToEl($(el))
+    this.bindAllSegHandlersToEl(el)
   }
 
 
@@ -109,9 +110,10 @@ export default abstract class InteractiveDateComponent extends DateComponent {
 
 
   bindDateHandlerToEl(el, name, handler) {
+    // TODO: use event delegation for this? to prevent multiple calls because of bubbling?
     // attach a handler to the grid's root element.
     // jQuery will take care of unregistering them when removeElement gets called.
-    this.el.addEventListener(name, (ev) => {
+    el.addEventListener(name, (ev) => {
       if (
         !$(ev.target).is(
           this.segSelector + ':not(.fc-helper),' + // directly on an event element
@@ -140,15 +142,34 @@ export default abstract class InteractiveDateComponent extends DateComponent {
 
 
   bindSegHandlerToEl(el, name, handler) {
-    el.on(name, this.segSelector, (ev) => {
-      let segEl = $(ev.currentTarget)
-      if (!segEl.is('.fc-helper')) {
-        let seg = segEl.data('fc-seg') // grab segment data. put there by View::renderEventsPayload
+    listenBySelector(
+      el,
+      name,
+      this.segSelector,
+      this.makeSegMouseHandler(handler)
+    )
+  }
+
+
+  bindSegHoverHandlersToEl(el, onMouseEnter, onMouseLeave) {
+    listenToHoverBySelector(
+      el,
+      this.segSelector,
+      this.makeSegMouseHandler(onMouseEnter),
+      this.makeSegMouseHandler(onMouseLeave)
+    )
+  }
+
+
+  makeSegMouseHandler(handler) {
+    return (ev, segEl) => {
+      if (!segEl.classList.contains('fc-helper')) {
+        let seg = (segEl as any).fcSeg // grab segment data. put there by View::renderEventsPayload
         if (seg && !this.shouldIgnoreEventPointing()) {
           return handler.call(this, seg, ev) // context will be the Grid
         }
       }
-    })
+    }
   }
 
 

+ 1 - 1
src/component/interactions/DateSelecting.ts

@@ -59,7 +59,7 @@ export default class DateSelecting extends Interaction {
       }
     })
 
-    preventSelection(el)
+    preventSelection($(el))
   }
 
 

+ 4 - 4
src/component/interactions/EventDragging.ts

@@ -134,12 +134,12 @@ export default class EventDragging extends Interaction {
     // of the view.
     let dragListener = this.dragListener = new HitDragListener(view, {
       scroll: this.opt('dragScroll'),
-      subjectEl: el[0],
+      subjectEl: el,
       subjectCenter: true,
       interactionStart: (ev) => {
         seg.component = component // for renderDrag
         isDragging = false
-        mouseFollower = new MouseFollower(seg.el[0], {
+        mouseFollower = new MouseFollower(seg.el, {
           additionalClass: 'fc-dragging',
           parentEl: view.el,
           opacity: dragListener.isTouch ? null : this.opt('dragOpacity'),
@@ -261,7 +261,7 @@ export default class EventDragging extends Interaction {
   segDragStart(seg, ev) {
     this.isDragging = true
     this.component.publiclyTrigger('eventDragStart', {
-      context: seg.el[0],
+      context: seg.el,
       args: [
         seg.footprint.getEventLegacy(),
         ev,
@@ -276,7 +276,7 @@ export default class EventDragging extends Interaction {
   segDragStop(seg, ev) {
     this.isDragging = false
     this.component.publiclyTrigger('eventDragStop', {
-      context: seg.el[0],
+      context: seg.el,
       args: [
         seg.footprint.getEventLegacy(),
         ev,

+ 11 - 7
src/component/interactions/EventPointing.ts

@@ -17,14 +17,18 @@ export default class EventPointing extends Interaction {
     let component = this.component
 
     component.bindSegHandlerToEl(el, 'click', this.handleClick.bind(this))
-    component.bindSegHandlerToEl(el, 'mouseenter', this.handleMouseover.bind(this))
-    component.bindSegHandlerToEl(el, 'mouseleave', this.handleMouseout.bind(this))
+
+    component.bindSegHoverHandlersToEl(
+      el,
+      this.handleMouseover.bind(this),
+      this.handleMouseout.bind(this)
+    )
   }
 
 
   handleClick(seg, ev) {
     let res = this.component.publiclyTrigger('eventClick', { // can return `false` to cancel
-      context: seg.el[0],
+      context: seg.el,
       args: [ seg.footprint.getEventLegacy(), ev, this.view ]
     })
 
@@ -44,11 +48,11 @@ export default class EventPointing extends Interaction {
 
       // TODO: move to EventSelecting's responsibility
       if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
-        seg.el.addClass('fc-allow-mouse-resize')
+        seg.el.classList.add('fc-allow-mouse-resize')
       }
 
       this.component.publiclyTrigger('eventMouseover', {
-        context: seg.el[0],
+        context: seg.el,
         args: [ seg.footprint.getEventLegacy(), ev, this.view ]
       })
     }
@@ -63,11 +67,11 @@ export default class EventPointing extends Interaction {
 
       // TODO: move to EventSelecting's responsibility
       if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
-        seg.el.removeClass('fc-allow-mouse-resize')
+        seg.el.classList.remove('fc-allow-mouse-resize')
       }
 
       this.component.publiclyTrigger('eventMouseout', {
-        context: seg.el[0],
+        context: seg.el,
         args: [
           seg.footprint.getEventLegacy(),
           ev || {}, // if given no arg, make a mock mouse event

+ 3 - 3
src/component/interactions/EventResizing.ts

@@ -77,7 +77,7 @@ export default class EventResizing extends Interaction {
     // Tracks mouse movement over the *grid's* coordinate map
     let dragListener = this.dragListener = new HitDragListener(component, {
       scroll: this.opt('dragScroll'),
-      subjectEl: el[0],
+      subjectEl: el,
       interactionStart: () => {
         isDragging = false
       },
@@ -161,7 +161,7 @@ export default class EventResizing extends Interaction {
   segResizeStart(seg, ev) {
     this.isResizing = true
     this.component.publiclyTrigger('eventResizeStart', {
-      context: seg.el[0],
+      context: seg.el,
       args: [
         seg.footprint.getEventLegacy(),
         ev,
@@ -176,7 +176,7 @@ export default class EventResizing extends Interaction {
   segResizeStop(seg, ev) {
     this.isResizing = false
     this.component.publiclyTrigger('eventResizeStop', {
-      context: seg.el[0],
+      context: seg.el,
       args: [
         seg.footprint.getEventLegacy(),
         ev,

+ 29 - 13
src/component/interactions/ExternalDropping.ts

@@ -1,8 +1,8 @@
-import * as $ from 'jquery'
 import * as moment from 'moment'
 import * as exportHooks from '../../exports'
 import { disableCursor, enableCursor } from '../../util'
 import { assignTo } from '../../util/object'
+import { elementMatches } from '../../util/dom'
 import momentExt from '../../moment-ext'
 import { default as ListenerMixin, ListenerInterface } from '../../common/ListenerMixin'
 import HitDragListener from '../../common/HitDragListener'
@@ -65,12 +65,12 @@ export default class ExternalDropping extends Interaction {
     let accept
 
     if (this.opt('droppable')) { // only listen if this setting is on
-      el = $((ui ? ui.item : null) || ev.target)
+      el = ((ui && ui.item) ? ui.item[0] : null) || ev.target
 
       // Test that the dragged element passes the dropAccept selector or filter function.
       // FYI, the default is "*" (matches all)
       accept = this.opt('dropAccept')
-      if (typeof accept === 'function' ? accept.call(el[0], el) : el.is(accept)) {
+      if (typeof accept === 'function' ? accept.call(el, el) : elementMatches(el, accept)) {
         if (!this.isDragging) { // prevent double-listening if fired twice
           this.listenToExternalDrag(el, ev, ui)
         }
@@ -210,19 +210,17 @@ ListenerMixin.mixInto(ExternalDropping);
 // to be used for Event Object creation.
 // A defined `.eventProps`, even when empty, indicates that an event should be created.
 function getDraggedElMeta(el) {
-  let prefix = (exportHooks as any).dataAttrPrefix
   let eventProps // properties for creating the event, not related to date/time
   let startTime // a Duration
   let duration
   let stick
 
-  if (prefix) { prefix += '-' }
-  eventProps = el.data(prefix + 'event') || null
+  eventProps = getEmbeddedElData(el, 'event', true)
 
   if (eventProps) {
-    if (typeof eventProps === 'object' && eventProps) { // non-null object
-      eventProps = assignTo({}, eventProps) // make a copy
-    } else { // something like 1 or true. still signal event creation
+
+    // something like 1 or true. still signal event creation
+    if (typeof eventProps !== 'object') {
       eventProps = {}
     }
 
@@ -238,10 +236,10 @@ function getDraggedElMeta(el) {
   }
 
   // fallback to standalone attribute values for each of the date/time properties
-  if (startTime == null) { startTime = el.data(prefix + 'start') }
-  if (startTime == null) { startTime = el.data(prefix + 'time') } // accept 'time' as well
-  if (duration == null) { duration = el.data(prefix + 'duration') }
-  if (stick == null) { stick = el.data(prefix + 'stick') }
+  if (startTime == null) { startTime = getEmbeddedElData(el, 'start') }
+  if (startTime == null) { startTime = getEmbeddedElData(el, 'time') } // accept 'time' as well
+  if (duration == null) { duration = getEmbeddedElData(el, 'duration') }
+  if (stick == null) { stick = getEmbeddedElData(el, 'stick') }
 
   // massage into correct data types
   startTime = startTime != null ? moment.duration(startTime) : null
@@ -250,3 +248,21 @@ function getDraggedElMeta(el) {
 
   return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }
 }
+
+
+function getEmbeddedElData(el, name, shouldParseJson = false) {
+  let prefix = (exportHooks as any).dataAttrPrefix
+  let prefixedName = (prefix ? prefix + '-' : '') + name
+
+  let data = el.getAttribute('data-' + prefixedName) || null
+  if (data && shouldParseJson) {
+    data = JSON.parse(data)
+  }
+
+  if (data === null && window['jQuery']) {
+    // jQuery will automatically parse JSON
+    data = window['jQuery'](el).data(prefixedName)
+  }
+
+  return data
+}

+ 4 - 5
src/component/renderers/EventRenderer.ts

@@ -1,5 +1,5 @@
-import * as $ from 'jquery'
 import { compareByFieldSpecs, proxy } from '../../util'
+import { htmlToElements } from '../../util/dom'
 
 export default class EventRenderer {
 
@@ -188,16 +188,15 @@ export default class EventRenderer {
 
       // Grab individual elements from the combined HTML string. Use each as the default rendering.
       // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
-      $(html).each((i, node) => {
+      htmlToElements(html).forEach((el, i) => {
         let seg = segs[i]
-        let el = $(node)
 
         if (hasEventRenderHandlers) { // optimization
           el = this.filterEventRenderEl(seg.footprint, el)
         }
 
         if (el) {
-          el.data('fc-seg', seg) // used by handlers
+          (el as any).fcSeg = seg // used by handlers
           seg.el = el
           renderedSegs.push(seg)
         }
@@ -255,7 +254,7 @@ export default class EventRenderer {
     if (custom === false) { // means don't render at all
       el = null
     } else if (custom && custom !== true) {
-      el = $(custom)
+      el = custom
     }
 
     return el

+ 14 - 21
src/component/renderers/FillRenderer.ts

@@ -1,5 +1,5 @@
-import * as $ from 'jquery'
 import { cssToStr } from '../../util'
+import { htmlToElements, removeElement } from '../../util/dom'
 
 
 export default class FillRenderer { // use for highlight, background events, business hours
@@ -40,10 +40,9 @@ export default class FillRenderer { // use for highlight, background events, bus
 
   // Unrenders a specific type of fill that is currently rendered on the grid
   unrender(type) {
-    let el = this.elsByFill[type]
-
-    if (el) {
-      el.remove()
+    let els = this.elsByFill[type]
+    if (els) {
+      els.forEach(removeElement)
       delete this.elsByFill[type]
     }
   }
@@ -65,23 +64,20 @@ export default class FillRenderer { // use for highlight, background events, bus
 
       // Grab individual elements from the combined HTML string. Use each as the default rendering.
       // Then, compute the 'el' for each segment.
-      $(html).each((i, node) => {
+      htmlToElements(html).forEach((el, i) => {
         let seg = segs[i]
-        let el = $(node)
 
         // allow custom filter methods per-type
         if (props.filterEl) {
           el = props.filterEl(seg, el)
         }
 
-        if (el) { // custom filters did not cancel the render
-          el = $(el) // allow custom filter to return raw DOM node
-
-          // correct element type? (would be bad if a non-TD were inserted into a table for example)
-          if (el.is(this.fillSegTag)) {
-            seg.el = el
-            renderedSegs.push(seg)
-          }
+        if (
+          el instanceof HTMLElement && // non-null (from filter func) and correct object type
+          el.nodeName.toLocaleLowerCase() === this.fillSegTag // correct element type? (would be bad if a non-TD were inserted into a table for example)
+        ) {
+          seg.el = el
+          renderedSegs.push(seg)
         }
       })
     }
@@ -109,12 +105,9 @@ export default class FillRenderer { // use for highlight, background events, bus
   }
 
 
-  reportEls(type, nodes) {
-    if (this.elsByFill[type]) {
-      this.elsByFill[type] = this.elsByFill[type].add(nodes)
-    } else {
-      this.elsByFill[type] = $(nodes)
-    }
+  reportEls(type, els) {
+    (this.elsByFill[type] || (this.elsByFill[type] = []))
+      .push(...els)
   }
 
 }

+ 11 - 7
src/component/renderers/HelperRenderer.ts

@@ -1,3 +1,4 @@
+import { removeElement } from '../../util/dom'
 import SingleEventDef from '../../models/event/SingleEventDef'
 import EventFootprint from '../../models/event/EventFootprint'
 import EventSource from '../../models/event-source/EventSource'
@@ -8,7 +9,7 @@ export default abstract class HelperRenderer {
   view: any
   component: any
   eventRenderer: any
-  helperEls: JQuery
+  helperEls: HTMLElement[]
 
 
   constructor(component, eventRenderer) {
@@ -44,21 +45,24 @@ export default abstract class HelperRenderer {
   }
 
 
-  renderEventFootprints(eventFootprints, sourceSeg?, extraClassNames?, opacity?) {
+  renderEventFootprints(eventFootprints, sourceSeg?, extraClassName?, opacity?) {
     let segs = this.component.eventFootprintsToSegs(eventFootprints)
-    let classNames = 'fc-helper ' + (extraClassNames || '')
     let i
 
     // assigns each seg's el and returns a subset of segs that were rendered
     segs = this.eventRenderer.renderFgSegEls(segs)
 
     for (i = 0; i < segs.length; i++) {
-      segs[i].el.addClass(classNames)
+      let classList = segs[i].el.classList
+      classList.add('fc-helper')
+      if (extraClassName) {
+        classList.add(extraClassName)
+      }
     }
 
     if (opacity != null) {
       for (i = 0; i < segs.length; i++) {
-        segs[i].el.css('opacity', opacity)
+        segs[i].el.style.opacity = opacity
       }
     }
 
@@ -69,12 +73,12 @@ export default abstract class HelperRenderer {
   /*
   Must return all mock event elements
   */
-  abstract renderSegs(segs, sourceSeg?): JQuery
+  abstract renderSegs(segs, sourceSeg?): HTMLElement[]
 
 
   unrender() {
     if (this.helperEls) {
-      this.helperEls.remove()
+      this.helperEls.forEach(removeElement)
       this.helperEls = null
     }
   }

+ 2 - 1
src/exports.ts

@@ -54,7 +54,8 @@ export {
   listenViaDelegation,
   appendContentTo,
   toggleClassName,
-  applyStyle
+  applyStyle,
+  computeHeightAndMargins
 } from './util/dom'
 
 export {

+ 24 - 27
src/list/ListView.ts

@@ -1,6 +1,6 @@
 import * as $ from 'jquery'
 import { htmlEscape, subtractInnerElHeight } from '../util'
-import { htmlToElement } from '../util/dom'
+import { htmlToElement, makeElement } from '../util/dom'
 import UnzonedRange from '../models/UnzonedRange'
 import View from '../View'
 import Scroller from '../common/Scroller'
@@ -170,7 +170,7 @@ export default class ListView extends View {
         this.eventRenderer.sortEventSegs(daySegs)
 
         for (i = 0; i < daySegs.length; i++) {
-          tbodyEl.appendChild(daySegs[i].el[0]) // append event row
+          tbodyEl.appendChild(daySegs[i].el) // append event row
         }
       }
     }
@@ -201,31 +201,28 @@ export default class ListView extends View {
     let mainFormat = this.opt('listDayFormat')
     let altFormat = this.opt('listDayAltFormat')
 
-    let tr = document.createElement('tr')
-    tr.classList.add('fc-list-heading')
-    tr.setAttribute('data-date', dayDate.format('YYYY-MM-DD'))
-    tr.innerHTML =
-      '<td class="' + (
-        this.calendar.theme.getClass('tableListHeading') ||
-        this.calendar.theme.getClass('widgetHeader')
-      ) + '" colspan="3">' +
-        (mainFormat ?
-          this.buildGotoAnchorHtml(
-            dayDate,
-            { 'class': 'fc-list-heading-main' },
-            htmlEscape(dayDate.format(mainFormat)) // inner HTML
-          ) :
-          '') +
-        (altFormat ?
-          this.buildGotoAnchorHtml(
-            dayDate,
-            { 'class': 'fc-list-heading-alt' },
-            htmlEscape(dayDate.format(altFormat)) // inner HTML
-          ) :
-          '') +
-      '</td>'
-
-    return tr
+    return makeElement('tr', {
+      className: 'fc-list-heading',
+      'data-date': dayDate.format('YYYY-MM-DD')
+    }, '<td class="' + (
+      this.calendar.theme.getClass('tableListHeading') ||
+      this.calendar.theme.getClass('widgetHeader')
+    ) + '" colspan="3">' +
+      (mainFormat ?
+        this.buildGotoAnchorHtml(
+          dayDate,
+          { 'class': 'fc-list-heading-main' },
+          htmlEscape(dayDate.format(mainFormat)) // inner HTML
+        ) :
+        '') +
+      (altFormat ?
+        this.buildGotoAnchorHtml(
+          dayDate,
+          { 'class': 'fc-list-heading-alt' },
+          htmlEscape(dayDate.format(altFormat)) // inner HTML
+        ) :
+        '') +
+    '</td>') as HTMLTableRowElement
   }
 
 }

+ 70 - 10
src/util/dom.ts

@@ -16,7 +16,9 @@ export function makeElement(tagName, attrs, content?): HTMLElement {
     }
   }
 
-  if (content != null) {
+  if (typeof content === 'string') {
+    el.innerHTML = content
+  } else if (content != null) {
     appendContentTo(el, content)
   }
 
@@ -36,6 +38,7 @@ export function applyStyle(el: HTMLElement, props: object | string, propVal?: an
   }
 }
 
+// TODO: just expose this?
 function applyStyleProp(el, name, val) {
   if (val == null) {
     el.style[name] = ''
@@ -56,17 +59,34 @@ export function appendContentTo(el: HTMLElement, content: ElementContent) {
 }
 
 export function htmlToElement(htmlString): HTMLElement {
-  let div = document.createElement('div')
-  div.innerHTML = htmlString.trim()
-  return div.firstChild as HTMLElement
+  htmlString = htmlString.trim()
+  let container = document.createElement(computeContainerTag(htmlString))
+  container.innerHTML = htmlString
+  return container.firstChild as HTMLElement
+}
+
+export function htmlToElements(htmlString): HTMLElement[] {
+  htmlString = htmlString.trim()
+  let container = document.createElement(computeContainerTag(htmlString))
+  container.innerHTML = htmlString
+  return Array.prototype.slice.call(container.childNodes)
 }
 
-export function htmlToElements(htmlString): HTMLElement {
-  let div = document.createElement('div')
-  div.innerHTML = htmlString.trim()
-  return Array.prototype.slice.call(div.childNodes)
+// assumes html already trimmed
+// assumes html tags are lowercase
+// TODO: use hash?
+function computeContainerTag(html: string) {
+  let first3 = html.substr(0, 3) // faster than using regex
+  if (first3 === '<tr') {
+    return 'tbody'
+  } else if (first3 === '<td') {
+    return 'tr'
+  } else {
+    return 'div'
+  }
 }
 
+
 // TODO: rename to listenByClassName
 export function listenViaDelegation(container: HTMLElement, eventType, childClassName, handler) {
   container.addEventListener(eventType, function(ev: Event) {
@@ -95,8 +115,16 @@ const closestMethod = Element.prototype.closest || function(selector) {
   return null;
 }
 
-export function listenBySelector(container: HTMLElement, eventType: string, selector: string, handler: (ev: Event, matchedTarget: Element) => void) {
+export function elementMatches(el: HTMLElement, selector: string) {
+  return matchesMethod.call(el, selector)
+}
 
+export function listenBySelector(
+  container: HTMLElement,
+  eventType: string,
+  selector: string,
+  handler: (ev: Event, matchedTarget: HTMLElement) => void
+) {
   function realHandler(ev: Event) {
     let matchedChild = closestMethod.call(ev.target, selector)
     if (matchedChild) {
@@ -111,6 +139,31 @@ export function listenBySelector(container: HTMLElement, eventType: string, sele
   }
 }
 
+export function listenToHoverBySelector(
+  container: HTMLElement,
+  selector: string,
+  onMouseEnter: (ev: Event, matchedTarget: HTMLElement) => void,
+  onMouseLeave: (ev: Event, matchedTarget: HTMLElement) => void
+) {
+  let currentMatchedChild
+
+  return listenBySelector(container, 'mouseover', selector, function(ev, matchedChild) {
+    if (matchedChild !== currentMatchedChild) {
+      currentMatchedChild = matchedChild
+      onMouseEnter(ev, matchedChild)
+
+      let realOnMouseLeave = (ev) => {
+        currentMatchedChild = null
+        onMouseLeave(ev, matchedChild)
+        matchedChild.removeEventListener('mouseleave', realOnMouseLeave)
+      }
+
+      matchedChild.addEventListener('mouseleave', realOnMouseLeave)
+    }
+  })
+}
+
+
 // TODO: user new signature in other places
 export function findElsWithin(containers: HTMLElement[] | HTMLElement, selector: string): HTMLElement[] {
   if (containers instanceof HTMLElement) {
@@ -120,7 +173,6 @@ export function findElsWithin(containers: HTMLElement[] | HTMLElement, selector:
 
   for (let i = 0; i < containers.length; i++) {
     let childEls = containers[i].querySelectorAll(selector)
-
     for (let j = 0; j < childEls.length; j++) {
       allChildEls.push(childEls[j] as HTMLElement)
     }
@@ -165,6 +217,7 @@ function normalizeContent(content: ElementContent): NodeList | Node[] {
   return els
 }
 
+// TODO: switch to tokenList.toggle
 export function toggleClassName(el, className, bool) {
   if (bool) {
     el.classList.add(className)
@@ -172,3 +225,10 @@ export function toggleClassName(el, className, bool) {
     el.classList.remove(className)
   }
 }
+
+export function computeHeightAndMargins(el: HTMLElement) {
+  let computed = window.getComputedStyle(el)
+  return el.offsetHeight +
+    parseInt(computed.marginTop, 10) +
+    parseInt(computed.marginBottom, 10)
+}

+ 1 - 1
tests/automated/event-render/eventOrder.js

@@ -9,7 +9,7 @@ describe('eventOrder', function() {
       { id: 'x', title: 'c', start: '2018-01-01T09:00:00', myOrder: 2 }
     ],
     eventRender: function(eventObj, el) {
-      el.data('event-id', eventObj.id)
+      el.setAttribute('data-event-id', eventObj.id)
     }
   })
 

+ 1 - 1
tests/automated/legacy/ListView.js

@@ -40,7 +40,7 @@ describe('ListView rendering', function() {
       it('filters events through eventRender', function() {
         var options = {}
         options.eventRender = function(event, el) {
-          el.find('.fc-event-dot').replaceWith('<span class="custom-icon" />')
+          $(el).find('.fc-event-dot').replaceWith('<span class="custom-icon" />')
         }
 
         initCalendar(options)

+ 1 - 1
tests/automated/legacy/event-resize.js

@@ -372,7 +372,7 @@ describe('eventResize', function() {
       it('should run the temporarily rendered event through eventRender', function(done) {
         var options = {}
         options.eventRender = function(event, element) {
-          element.addClass('didEventRender')
+          $(element).addClass('didEventRender')
         }
 
         options.eventAfterAllRender = function() {

+ 17 - 15
tests/automated/legacy/eventRender.js

@@ -26,10 +26,9 @@ describe('eventRender', function() {
               expect(typeof event).toBe('object')
               expect(event.rendering).toBeUndefined()
               expect(event.start).toBeDefined()
-              expect(typeof element).toBe('object')
-              expect(element.length).toBe(1)
+              expect(element instanceof HTMLElement).toBe(true)
               expect(typeof view).toBe('object')
-              element.css('font-size', '20px')
+              $(element).css('font-size', '20px')
             },
             eventAfterAllRender: function() {
               expect($(gridSelector).find('.fc-event').css('font-size')).toBe('20px')
@@ -58,7 +57,7 @@ describe('eventRender', function() {
       it('can return a new element', function(done) {
         var options = {
           eventRender: function(event, element, view) {
-            return $('<div class="fc-event sup" style="background-color:green">sup g</div>')
+            return $('<div class="fc-event sup" style="background-color:green">sup g</div>')[0]
           },
           eventAfterAllRender: function() {
             expect($('.fc-day-grid .sup').length).toBe(1)
@@ -101,10 +100,9 @@ describe('eventRender', function() {
             expect(typeof event).toBe('object')
             expect(event.rendering).toBe('background')
             expect(event.start).toBeDefined()
-            expect(typeof element).toBe('object')
-            expect(element.length).toBe(1)
+            expect(element instanceof HTMLElement).toBe(true)
             expect(typeof view).toBe('object')
-            element.css('font-size', '20px')
+            $(element).css('font-size', '20px')
           },
           eventAfterAllRender: function() {
             expect($('.fc-day-grid .fc-bgevent').css('font-size')).toBe('20px')
@@ -119,7 +117,7 @@ describe('eventRender', function() {
       it('can return a new element', function(done) {
         var options = {
           eventRender: function(event, element, view) {
-            return $('<td class="sup" style="background-color:green">sup g</td>')
+            return $('<td class="sup" style="background-color:green">sup g</td>')[0]
           },
           eventAfterAllRender: function() {
             expect($('.fc-day-grid .sup').length).toBe(1)
@@ -130,10 +128,11 @@ describe('eventRender', function() {
         spyOn(options, 'eventRender').and.callThrough()
         initCalendar(options)
       })
+
       it('won\'t rendering when returning a new element of the wrong type', function(done) {
         var options = {
           eventRender: function(event, element, view) {
-            return $('<div class="sup" style="background-color:green">sup g</div>')
+            return $('<div class="sup" style="background-color:green">sup g</div>')[0]
           },
           eventAfterAllRender: function() {
             expect($('.fc-day-grid .sup').length).toBe(0)
@@ -144,6 +143,7 @@ describe('eventRender', function() {
         spyOn(options, 'eventRender').and.callThrough()
         initCalendar(options)
       })
+
       it('can return false and cancel rendering', function(done) {
         var options = {
           eventRender: function(event, element, view) {
@@ -203,7 +203,7 @@ describe('eventRender', function() {
       it('can return a new element', function(done) {
         var options = {
           eventRender: function(event, element, view) {
-            return $('<div class="fc-event sup" style="background-color:green">sup g</div>')
+            return $('<div class="fc-event sup" style="background-color:green">sup g</div>')[0]
           },
           eventAfterAllRender: function() {
             expect($('.fc-time-grid .sup').length).toBe(1)
@@ -214,6 +214,7 @@ describe('eventRender', function() {
         spyOn(options, 'eventRender').and.callThrough()
         initCalendar(options)
       })
+
       it('can return false and cancel rendering', function(done) {
         var options = {
           eventRender: function(event, element, view) {
@@ -246,10 +247,9 @@ describe('eventRender', function() {
             expect(typeof event).toBe('object')
             expect(event.rendering).toBe('background')
             expect(event.start).toBeDefined()
-            expect(typeof element).toBe('object')
-            expect(element.length).toBe(1)
+            expect(element instanceof HTMLElement).toBe(true)
             expect(typeof view).toBe('object')
-            element.css('font-size', '20px')
+            $(element).css('font-size', '20px')
           },
           eventAfterAllRender: function() {
             expect($('.fc-time-grid .fc-bgevent').css('font-size')).toBe('20px')
@@ -260,10 +260,11 @@ describe('eventRender', function() {
         spyOn(options, 'eventRender').and.callThrough()
         initCalendar(options)
       })
+
       it('can return a new element', function(done) {
         var options = {
           eventRender: function(event, element, view) {
-            return $('<div class="fc-bgevent sup" style="background-color:green">sup g</div>')
+            return $('<div class="fc-bgevent sup" style="background-color:green">sup g</div>')[0]
           },
           eventAfterAllRender: function() {
             expect($('.fc-time-grid .sup').length).toBe(1)
@@ -274,10 +275,11 @@ describe('eventRender', function() {
         spyOn(options, 'eventRender').and.callThrough()
         initCalendar(options)
       })
+
       it('won\'t rendering when returning a new element of the wrong type', function(done) {
         var options = {
           eventRender: function(event, element, view) {
-            return $('<p class="fc-bgevent sup" style="background-color:green">sup g</p>')
+            return $('<p class="fc-bgevent sup" style="background-color:green">sup g</p>')[0]
           },
           eventAfterAllRender: function() {
             expect($('.fc-time-grid .sup').length).toBe(0)

+ 8 - 12
tests/automated/legacy/external-dnd.js

@@ -128,9 +128,8 @@ describe('external drag and drop', function() {
           })
 
           it('works with a filter function that returns true', function(done) {
-            options.dropAccept = function(jqEl) {
-              expect(typeof jqEl).toBe('object')
-              expect(jqEl.length).toBe(1)
+            options.dropAccept = function(el) {
+              expect(el instanceof HTMLElement).toBe(true)
               return true
             }
             options.drop = function() { }
@@ -150,9 +149,8 @@ describe('external drag and drop', function() {
           })
 
           it('prevents a drop with a filter function that returns false', function(done) {
-            options.dropAccept = function(jqEl) {
-              expect(typeof jqEl).toBe('object')
-              expect(jqEl.length).toBe(1)
+            options.dropAccept = function(el) {
+              expect(el instanceof HTMLElement).toBe(true)
               return false
             }
             options.drop = function() { }
@@ -285,9 +283,8 @@ describe('external drag and drop', function() {
           })
 
           it('works with a filter function that returns true', function(done) {
-            options.dropAccept = function(jqEl) {
-              expect(typeof jqEl).toBe('object')
-              expect(jqEl.length).toBe(1)
+            options.dropAccept = function(el) {
+              expect(el instanceof HTMLElement).toBe(true)
               return true
             }
             options.drop = function() { }
@@ -307,9 +304,8 @@ describe('external drag and drop', function() {
           })
 
           it('prevents a drop with a filter function that returns false', function(done) {
-            options.dropAccept = function(jqEl) {
-              expect(typeof jqEl).toBe('object')
-              expect(jqEl.length).toBe(1)
+            options.dropAccept = function(el) {
+              expect(el instanceof HTMLElement).toBe(true)
               return false
             }
             options.drop = function() { }

+ 1 - 1
tests/automated/legacy/selectHelper.js

@@ -10,7 +10,7 @@ describe('selectHelper', function() {
   it('goes through eventRender', function() {
     initCalendar({
       eventRender: function(event, element, view) {
-        element.addClass('didEventRender')
+        $(element).addClass('didEventRender')
       }
     })
     currentCalendar.select('2014-08-04T01:00:00', '2014-08-04T04:00:00')