Преглед на файлове

Merge remote-tracking branch 'origin/master' into example-projects-improvement

Naguumo преди 4 години
родител
ревизия
241b49b0fc

+ 1 - 1
packages-premium

@@ -1 +1 @@
-Subproject commit e6afd334e6de0150027bb9a145c44697afedf24d
+Subproject commit 0fddac2a86bdc24b8b25ef667bbc3e817f514040

+ 1 - 1
packages/common/src/component/DateComponent.ts

@@ -99,7 +99,7 @@ export abstract class DateComponent<Props=Dictionary, State=Dictionary> extends
 
   isValidDateDownEl(el: HTMLElement) {
     return !elementClosest(el, '.fc-event:not(.fc-bg-event)') &&
-      !elementClosest(el, '.fc-daygrid-more-link') && // a "more.." link
+      !elementClosest(el, '.fc-event-more') && // a "more.." link
       !elementClosest(el, 'a[data-navlink]') && // a clickable nav link
       !elementClosest(el, '.fc-popover') // hack
   }

+ 37 - 0
packages/common/src/event-placement.ts

@@ -25,6 +25,12 @@ export interface SegInsertion {
   stackCnt: number
 }
 
+export interface SegEntryGroup {
+  spanStart: number
+  spanEnd: number
+  entries: SegEntry[]
+}
+
 export class SegHierarchy {
   // settings
   allowReslicing: boolean = false
@@ -197,6 +203,37 @@ export function buildEntryKey(entry: SegEntry) {
   return entry.segInput.index + ':' + entry.spanStart
 }
 
+// returns in no specific order
+export function groupIntersectingEntries(entries: SegEntry[]): SegEntryGroup[] {
+  let groups: SegEntryGroup[] = []
+
+  for (let entry of entries) {
+    let filteredMerges: SegEntryGroup[] = []
+    let hungryMerge: SegEntryGroup = { // the merge that will eat what is collides with
+      spanStart: entry.spanStart,
+      spanEnd: entry.spanEnd,
+      entries: [entry],
+    }
+
+    for (let merge of groups) {
+      if (merge.spanStart < hungryMerge.spanEnd && merge.spanEnd > hungryMerge.spanStart) { // collides?
+        hungryMerge = {
+          spanStart: Math.min(merge.spanStart, hungryMerge.spanStart),
+          spanEnd: Math.max(merge.spanEnd, hungryMerge.spanEnd),
+          entries: merge.entries.concat(hungryMerge.entries),
+        }
+      } else {
+        filteredMerges.push(merge)
+      }
+    }
+
+    filteredMerges.push(hungryMerge)
+    groups = filteredMerges
+  }
+
+  return groups
+}
+
 // general util
 // ---------------------------------------------------------------------------------------------------------------------
 

+ 4 - 1
packages/common/src/main.ts

@@ -162,7 +162,10 @@ export { EventSourceDef } from './structs/event-source-def'
 export { EventSource, EventSourceHash } from './structs/event-source'
 export { EventSourceRefiners, EventSourceRefined } from './structs/event-source-parse'
 
-export { SegInput, SegRect, SegHierarchy, SegEntry, SegInsertion, buildEntryKey, getEntrySpanEnd, binarySearch } from './event-placement'
+export {
+  SegInput, SegRect, SegHierarchy, SegEntry, SegInsertion, buildEntryKey,
+  getEntrySpanEnd, binarySearch, SegEntryGroup, groupIntersectingEntries,
+} from './event-placement'
 
 export {
   Interaction,

+ 1 - 1
packages/daygrid/src/TableCell.tsx

@@ -156,7 +156,7 @@ export class TableCell extends DateComponent<TableCellProps> {
                       {(rootElRef, classNames, innerElRef, innerContent) => (
                         <a
                           ref={rootElRef}
-                          className={['fc-daygrid-more-link'].concat(classNames).join(' ')}
+                          className={['fc-daygrid-more-link', 'fc-event-more'].concat(classNames).join(' ')}
                           onClick={this.handleMoreLinkClick}
                         >
                           {innerContent}

+ 69 - 46
packages/timegrid/src/TimeCol.tsx

@@ -1,7 +1,7 @@
 import {
   Ref, DateMarker, BaseComponent, createElement, EventSegUiInteractionState, Seg, getSegMeta,
   DateRange, Fragment, DayCellRoot, NowIndicatorRoot, BgEvent, renderFill,
-  DateProfile, config, buildEventRangeKey, sortEventSegs, SegInput,
+  DateProfile, config, buildEventRangeKey, sortEventSegs, SegInput, memoize, SegEntryGroup,
 } from '@fullcalendar/common'
 import { TimeColsSeg } from './TimeColsSeg'
 import { TimeColsSlatsCoords } from './TimeColsSlatsCoords'
@@ -33,6 +33,9 @@ export interface TimeColProps {
 config.timeGridEventCondensedHeight = 30
 
 export class TimeCol extends BaseComponent<TimeColProps> {
+  sortEventSegs = memoize(sortEventSegs)
+  computeFgSegPlacements = memoize(computeFgSegPlacements) // only for non-print, non-mirror
+
   render() {
     let { props, context } = this
     let isSelectMirror = context.options.selectMirror
@@ -48,6 +51,8 @@ export class TimeCol extends BaseComponent<TimeColProps> {
       (props.eventResize && props.eventResize.affectedInstances) ||
       {}
 
+    let sortedFgSegs = this.sortEventSegs(props.fgEventSegs, context.options.eventOrder) as TimeColsSeg[]
+
     return (
       <DayCellRoot
         elRef={props.elRef}
@@ -71,7 +76,7 @@ export class TimeCol extends BaseComponent<TimeColProps> {
               </div>
               <div className="fc-timegrid-col-events">
                 {this.renderFgSegs(
-                  props.fgEventSegs,
+                  sortedFgSegs,
                   interactionAffectedInstances,
                 )}
               </div>
@@ -102,7 +107,7 @@ export class TimeCol extends BaseComponent<TimeColProps> {
   }
 
   renderFgSegs(
-    segs: TimeColsSeg[],
+    sortedFgSegs: TimeColsSeg[],
     segIsInvisible: { [instanceId: string]: any },
     isDragging?: boolean,
     isResizing?: boolean,
@@ -111,20 +116,20 @@ export class TimeCol extends BaseComponent<TimeColProps> {
     let { props } = this
 
     if (props.forPrint) {
-      return this.renderPrintFgSegs(segs)
+      return this.renderPrintFgSegs(sortedFgSegs)
     }
 
     if (props.slatCoords) {
-      return this.renderPositionedFgSegs(segs, segIsInvisible, isDragging, isResizing, isDateSelecting)
+      return this.renderPositionedFgSegs(sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting)
     }
 
     return null
   }
 
-  renderPrintFgSegs(segs: TimeColsSeg[]) {
-    let { props } = this
-    segs = sortEventSegs(segs, this.context.options.eventOrder) as TimeColsSeg[] // not DRY
-    return segs.map((seg) => (
+  renderPrintFgSegs(sortedFgSegs: TimeColsSeg[]) {
+    let { todayRange, nowDate } = this.props
+
+    return sortedFgSegs.map((seg) => (
       <div
         className="fc-timegrid-event-harness"
         key={seg.eventRange.instance.instanceId}
@@ -136,54 +141,72 @@ export class TimeCol extends BaseComponent<TimeColProps> {
           isDateSelecting={false}
           isSelected={false}
           isCondensed={false}
-          {...getSegMeta(seg, props.todayRange, props.nowDate)}
+          {...getSegMeta(seg, todayRange, nowDate)}
         />
       </div>
     ))
   }
 
   renderPositionedFgSegs(
-    segs: TimeColsSeg[],
+    segs: TimeColsSeg[], // if not mirror, needs to be sorted
     segIsInvisible: { [instanceId: string]: any },
     isDragging?: boolean,
     isResizing?: boolean,
     isDateSelecting?: boolean,
   ) {
-    let { props } = this
-
-    segs = sortEventSegs(segs, this.context.options.eventOrder) as TimeColsSeg[] // not DRY
+    let { eventSelection, todayRange, nowDate } = this.props
+    let isMirror = isDragging || isResizing || isDateSelecting
     let segInputs = this.buildSegInputs(segs)
-    let segRects = computeFgSegPlacements(segInputs)
-
-    return segRects.map((segRect) => {
-      let seg = segs[segRect.segInput.index] as TimeColsSeg
-      let instanceId = seg.eventRange.instance.instanceId
-      let isMirror = isDragging || isResizing || isDateSelecting
-      let positionCss = {
-        ...this.computeSegTopBottomCss(segRect.segInput),
-        // mirrors will span entire column width
-        // also, won't assign z-index, which is good, fc-event-mirror will overpower other harnesses
-        ...(isMirror ? { left: 0, right: 0 } : this.computeSegLeftRightCss(segRect)),
-      }
+    let { segRects, hiddenGroups } = isMirror ? computeFgSegPlacements(segInputs) : // don't use memoized
+      this.computeFgSegPlacements(segInputs, this.context.options.timeGridEventMaxStack)
+
+    return (
+      <Fragment>
+        {this.renderHiddenGroups(hiddenGroups, segs)}
+        {segRects.map((segRect) => {
+          let seg = segs[segRect.segInput.index] as TimeColsSeg
+          let instanceId = seg.eventRange.instance.instanceId
+          let positionCss = {
+            ...this.computeSegTopBottomCss(segRect.segInput),
+            // mirrors will span entire column width
+            // also, won't assign z-index, which is good, fc-event-mirror will overpower other harnesses
+            ...(isMirror ? { left: 0, right: 0 } : this.computeSegLeftRightCss(segRect)),
+          }
+
+          return (
+            <div
+              className={'fc-timegrid-event-harness' + (segRect.stackForward > 0 ? ' fc-timegrid-event-harness-inset' : '')}
+              key={instanceId}
+              style={{
+                visibility: segIsInvisible[instanceId] ? 'hidden' : ('' as any),
+                ...positionCss,
+              }}
+            >
+              <TimeColEvent
+                seg={seg}
+                isDragging={isDragging}
+                isResizing={isResizing}
+                isDateSelecting={isDateSelecting}
+                isSelected={instanceId === eventSelection}
+                isCondensed={(seg.bottom - seg.top) < config.timeGridEventCondensedHeight}
+                {...getSegMeta(seg, todayRange, nowDate)}
+              />
+            </div>
+          )
+        })}
+      </Fragment>
+    )
+  }
+
+  // will already have eventMinHeight applied because segInputs already had it
+  renderHiddenGroups(hiddenGroups: SegEntryGroup[], segs: TimeColsSeg[]) {
+    return hiddenGroups.map((hiddenGroup) => {
+      let positionCss = this.computeSegTopBottomCss(hiddenGroup)
 
       return (
-        <div
-          className={'fc-timegrid-event-harness' + (segRect.stackForward > 0 ? ' fc-timegrid-event-harness-inset' : '')}
-          key={instanceId}
-          style={{
-            visibility: segIsInvisible[instanceId] ? 'hidden' : ('' as any),
-            ...positionCss,
-          }}
-        >
-          <TimeColEvent
-            seg={seg}
-            isDragging={isDragging}
-            isResizing={isResizing}
-            isDateSelecting={isDateSelecting}
-            isSelected={instanceId === props.eventSelection}
-            isCondensed={(seg.bottom - seg.top) < config.timeGridEventCondensedHeight}
-            {...getSegMeta(seg, props.todayRange, props.nowDate)}
-          />
+        <div className="fc-event-more fc-timegrid-event-more" style={positionCss}>
+          {'+' + hiddenGroup.entries.length}
+          {/* TODO: more customizable way to build this text. search buildMoreLinkText */}
         </div>
       )
     })
@@ -258,10 +281,10 @@ export class TimeCol extends BaseComponent<TimeColProps> {
     ))
   }
 
-  computeSegTopBottomCss(segInput: SegInput) {
+  computeSegTopBottomCss(segLike: { spanStart: number, spanEnd: number }) {
     return {
-      top: segInput.spanStart,
-      bottom: -segInput.spanEnd,
+      top: segLike.spanStart,
+      bottom: -segLike.spanEnd,
     }
   }
 

+ 16 - 42
packages/timegrid/src/event-placement.ts

@@ -6,6 +6,8 @@ import {
   getEntrySpanEnd,
   binarySearch,
   SegInput,
+  SegEntryGroup,
+  groupIntersectingEntries,
 } from '@fullcalendar/common'
 
 interface SegNode extends SegEntry {
@@ -26,12 +28,23 @@ export interface TimeColSegRect extends SegRect {
 }
 
 // segInputs assumed sorted
-export function computeFgSegPlacements(segInputs: SegInput[]): TimeColSegRect[] {
+export function computeFgSegPlacements(
+  segInputs: SegInput[],
+  maxStack?: number,
+): { segRects: TimeColSegRect[], hiddenGroups: SegEntryGroup[] } {
   let hierarchy = new SegHierarchy()
-  hierarchy.addSegs(segInputs)
+  if (maxStack != null) {
+    hierarchy.maxStackCnt = maxStack
+  }
+
+  let hiddenEntries = hierarchy.addSegs(segInputs)
+  let hiddenGroups = groupIntersectingEntries(hiddenEntries)
+
   let web = buildWeb(hierarchy)
   web = stretchWeb(web, 1) // all levelCoords/thickness will have 0.0-1.0
-  return webToRects(web)
+  let segRects = webToRects(web)
+
+  return { segRects, hiddenGroups }
 }
 
 function buildWeb(hierarchy: SegHierarchy): SegNode[] {
@@ -192,45 +205,6 @@ function webToRects(topLevelNodes: SegNode[]): TimeColSegRect[] {
   return rects // TODO: sort rects by levelCoord to be consistent with toRects?
 }
 
-/* TODO: for event-limit display
-interface SegEntryGroup {
-  spanStart: number
-  spanEnd: number
-  entries: SegEntry[]
-}
-
-// returns in no specific order
-function groupIntersectingEntries(entries: SegEntry[]): SegEntryGroup[] {
-  let groups: SegEntryGroup[] = []
-
-  for (let entry of entries) {
-    let filteredMerges: SegEntryGroup[] = []
-    let hungryMerge: SegEntryGroup = { // the merge that will eat what is collides with
-      spanStart: entry.spanStart,
-      spanEnd: entry.spanEnd,
-      entries: [entry]
-    }
-
-    for (let merge of groups) {
-      if (merge.spanStart < hungryMerge.spanEnd && merge.spanEnd > hungryMerge.spanStart) { // collides?
-        hungryMerge = {
-          spanStart: Math.min(merge.spanStart, hungryMerge.spanStart),
-          spanEnd: Math.max(merge.spanEnd, hungryMerge.spanEnd),
-          entries: merge.entries.concat(hungryMerge.entries)
-        }
-      } else {
-        filteredMerges.push(merge)
-      }
-    }
-
-    filteredMerges.push(hungryMerge)
-    groups = filteredMerges
-  }
-
-  return groups
-}
-*/
-
 // TODO: move to general util
 
 function cacheable<Args extends any[], Res>(

+ 1 - 0
packages/timegrid/src/options.ts

@@ -1,3 +1,4 @@
 export const OPTION_REFINERS = {
   allDaySlot: Boolean,
+  timeGridEventMaxStack: Number,
 }

+ 28 - 6
packages/timegrid/src/styles/timegrid-event.css

@@ -1,14 +1,17 @@
 
 .fc-timegrid-event-harness-inset .fc-timegrid-event,
-.fc-timegrid-event.fc-event-mirror {
+.fc-timegrid-event.fc-event-mirror,
+.fc-timegrid-event-more {
   box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color);
 }
 
-.fc-timegrid-event { // events need to be root
-
+.fc-timegrid-event,
+.fc-timegrid-event-more { // events need to be root
   font-size: var(--fc-small-font-size);
   border-radius: 3px;
+}
 
+.fc-timegrid-event { // events need to be root
   & .fc-event-main {
     padding: 1px 1px 0;
   }
@@ -18,7 +21,28 @@
     font-size: var(--fc-small-font-size);
     margin-bottom: 1px;
   }
+}
 
+.fc-timegrid-event-more {
+  position: absolute; // TODO: converge with fc-timegrid-event below?
+  top: 0; // for when not yet positioned
+  z-index: 999; // hack
+  background: #ccc; // TODO: variable?
+  padding: 2px;
+  margin-bottom: 1px; // match the space below each event
+  cursor: pointer;
+}
+
+
+.fc-direction-ltr {
+  & .fc-timegrid-event-more {
+    right: 0;
+  }
+}
+.fc-direction-rtl {
+  & .fc-timegrid-event-more {
+    left: 0;
+  }
 }
 
 .fc-timegrid-event-condensed {
@@ -35,17 +59,15 @@
   & .fc-event-title {
     font-size: var(--fc-small-font-size)
   }
-
 }
 
 .fc-media-screen {
 
   & .fc-timegrid-event {
     position: absolute; // absolute WITHIN the harness
-    top: 0;
+    top: 0; // for when not yet positioned
     bottom: 1px; // stay away from bottom slot line
     left: 0;
     right: 0;
   }
-
 }