elements work best with integers. round up to ensure contents fits
+ }
+ function getSectionHasLiquidHeight(props, sectionConfig) {
+ return props.liquid && sectionConfig.liquid; // does the section do liquid-height? (need to have whole scrollgrid liquid-height as well)
+ }
+ function getAllowYScrolling(props, sectionConfig) {
+ return sectionConfig.maxHeight != null || // if its possible for the height to max out, we might need scrollbars
+ getSectionHasLiquidHeight(props, sectionConfig); // if the section is liquid height, it might condense enough to require scrollbars
+ }
+ // TODO: ONLY use `arg`. force out internal function to use same API
+ function renderChunkContent(sectionConfig, chunkConfig, arg) {
+ var expandRows = arg.expandRows;
+ var content = typeof chunkConfig.content === 'function' ?
+ chunkConfig.content(arg) :
+ createElement('table', {
+ className: [
+ chunkConfig.tableClassName,
+ sectionConfig.syncRowHeights ? 'fc-scrollgrid-sync-table' : '',
+ ].join(' '),
+ style: {
+ minWidth: arg.tableMinWidth,
+ width: arg.clientWidth,
+ height: expandRows ? arg.clientHeight : '',
+ },
+ }, arg.tableColGroupNode, createElement('tbody', {}, typeof chunkConfig.rowContent === 'function' ? chunkConfig.rowContent(arg) : chunkConfig.rowContent));
+ return content;
+ }
+ function isColPropsEqual(cols0, cols1) {
+ return isArraysEqual(cols0, cols1, isPropsEqual);
+ }
+ function renderMicroColGroup(cols, shrinkWidth) {
+ var colNodes = [];
+ /*
+ for ColProps with spans, it would have been great to make a single
+ HOWEVER, Chrome was getting messing up distributing the width to
/
elements with colspans.
+ SOLUTION: making individual
elements makes Chrome behave.
+ */
+ for (var _i = 0, cols_1 = cols; _i < cols_1.length; _i++) {
+ var colProps = cols_1[_i];
+ var span = colProps.span || 1;
+ for (var i = 0; i < span; i += 1) {
+ colNodes.push(createElement("col", { style: {
+ width: colProps.width === 'shrink' ? sanitizeShrinkWidth(shrinkWidth) : (colProps.width || ''),
+ minWidth: colProps.minWidth || '',
+ } }));
+ }
+ }
+ return createElement.apply(void 0, __spreadArrays(['colgroup', {}], colNodes));
+ }
+ function sanitizeShrinkWidth(shrinkWidth) {
+ /* why 4? if we do 0, it will kill any border, which are needed for computeSmallestCellWidth
+ 4 accounts for 2 2-pixel borders. TODO: better solution? */
+ return shrinkWidth == null ? 4 : shrinkWidth;
+ }
+ function hasShrinkWidth(cols) {
+ for (var _i = 0, cols_2 = cols; _i < cols_2.length; _i++) {
+ var col = cols_2[_i];
+ if (col.width === 'shrink') {
+ return true;
+ }
+ }
+ return false;
+ }
+ function getScrollGridClassNames(liquid, context) {
+ var classNames = [
+ 'fc-scrollgrid',
+ context.theme.getClass('table'),
+ ];
+ if (liquid) {
+ classNames.push('fc-scrollgrid-liquid');
+ }
+ return classNames;
+ }
+ function getSectionClassNames(sectionConfig, wholeTableVGrow) {
+ var classNames = [
+ 'fc-scrollgrid-section',
+ "fc-scrollgrid-section-" + sectionConfig.type,
+ sectionConfig.className,
+ ];
+ if (wholeTableVGrow && sectionConfig.liquid && sectionConfig.maxHeight == null) {
+ classNames.push('fc-scrollgrid-section-liquid');
+ }
+ if (sectionConfig.isSticky) {
+ classNames.push('fc-scrollgrid-section-sticky');
+ }
+ return classNames;
+ }
+ function renderScrollShim(arg) {
+ return (createElement("div", { className: "fc-scrollgrid-sticky-shim", style: {
+ width: arg.clientWidth,
+ minWidth: arg.tableMinWidth,
+ } }));
+ }
+ function getStickyHeaderDates(options) {
+ var stickyHeaderDates = options.stickyHeaderDates;
+ if (stickyHeaderDates == null || stickyHeaderDates === 'auto') {
+ stickyHeaderDates = options.height === 'auto' || options.viewHeight === 'auto';
+ }
+ return stickyHeaderDates;
+ }
+ function getStickyFooterScrollbar(options) {
+ var stickyFooterScrollbar = options.stickyFooterScrollbar;
+ if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') {
+ stickyFooterScrollbar = options.height === 'auto' || options.viewHeight === 'auto';
+ }
+ return stickyFooterScrollbar;
+ }
+
+ var SimpleScrollGrid = /** @class */ (function (_super) {
+ __extends(SimpleScrollGrid, _super);
+ function SimpleScrollGrid() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.processCols = memoize(function (a) { return a; }, isColPropsEqual); // so we get same `cols` props every time
+ // yucky to memoize VNodes, but much more efficient for consumers
+ _this.renderMicroColGroup = memoize(renderMicroColGroup);
+ _this.scrollerRefs = new RefMap();
+ _this.scrollerElRefs = new RefMap(_this._handleScrollerEl.bind(_this));
+ _this.state = {
+ shrinkWidth: null,
+ forceYScrollbars: false,
+ scrollerClientWidths: {},
+ scrollerClientHeights: {},
+ };
+ // TODO: can do a really simple print-view. dont need to join rows
+ _this.handleSizing = function () {
+ _this.setState(__assign({ shrinkWidth: _this.computeShrinkWidth() }, _this.computeScrollerDims()));
+ };
+ return _this;
+ }
+ SimpleScrollGrid.prototype.render = function () {
+ var _a = this, props = _a.props, state = _a.state, context = _a.context;
+ var sectionConfigs = props.sections || [];
+ var cols = this.processCols(props.cols);
+ var microColGroupNode = this.renderMicroColGroup(cols, state.shrinkWidth);
+ var classNames = getScrollGridClassNames(props.liquid, context);
+ // TODO: make DRY
+ var configCnt = sectionConfigs.length;
+ var configI = 0;
+ var currentConfig;
+ var headSectionNodes = [];
+ var bodySectionNodes = [];
+ var footSectionNodes = [];
+ while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') {
+ headSectionNodes.push(this.renderSection(currentConfig, configI, microColGroupNode));
+ configI += 1;
+ }
+ while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') {
+ bodySectionNodes.push(this.renderSection(currentConfig, configI, microColGroupNode));
+ configI += 1;
+ }
+ while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') {
+ footSectionNodes.push(this.renderSection(currentConfig, configI, microColGroupNode));
+ configI += 1;
+ }
+ // firefox bug: when setting height on table and there is a thead or tfoot,
+ // the necessary height:100% on the liquid-height body section forces the *whole* table to be taller. (bug #5524)
+ // use getCanVGrowWithinCell as a way to detect table-stupid firefox.
+ // if so, use a simpler dom structure, jam everything into a lone tbody.
+ var isBuggy = !getCanVGrowWithinCell();
+ return createElement('table', {
+ className: classNames.join(' '),
+ style: { height: props.height },
+ }, Boolean(!isBuggy && headSectionNodes.length) && createElement.apply(void 0, __spreadArrays(['thead', {}], headSectionNodes)), Boolean(!isBuggy && bodySectionNodes.length) && createElement.apply(void 0, __spreadArrays(['tbody', {}], bodySectionNodes)), Boolean(!isBuggy && footSectionNodes.length) && createElement.apply(void 0, __spreadArrays(['tfoot', {}], footSectionNodes)), isBuggy && createElement.apply(void 0, __spreadArrays(['tbody', {}], headSectionNodes, bodySectionNodes, footSectionNodes)));
+ };
+ SimpleScrollGrid.prototype.renderSection = function (sectionConfig, sectionI, microColGroupNode) {
+ if ('outerContent' in sectionConfig) {
+ return (createElement(Fragment, { key: sectionConfig.key }, sectionConfig.outerContent));
+ }
+ return (createElement("tr", { key: sectionConfig.key, className: getSectionClassNames(sectionConfig, this.props.liquid).join(' ') }, this.renderChunkTd(sectionConfig, sectionI, microColGroupNode, sectionConfig.chunk)));
+ };
+ SimpleScrollGrid.prototype.renderChunkTd = function (sectionConfig, sectionI, microColGroupNode, chunkConfig) {
+ if ('outerContent' in chunkConfig) {
+ return chunkConfig.outerContent;
+ }
+ var props = this.props;
+ var _a = this.state, forceYScrollbars = _a.forceYScrollbars, scrollerClientWidths = _a.scrollerClientWidths, scrollerClientHeights = _a.scrollerClientHeights;
+ var needsYScrolling = getAllowYScrolling(props, sectionConfig); // TODO: do lazily. do in section config?
+ var isLiquid = getSectionHasLiquidHeight(props, sectionConfig);
+ // for `!props.liquid` - is WHOLE scrollgrid natural height?
+ // TODO: do same thing in advanced scrollgrid? prolly not b/c always has horizontal scrollbars
+ var overflowY = !props.liquid ? 'visible' :
+ forceYScrollbars ? 'scroll' :
+ !needsYScrolling ? 'hidden' :
+ 'auto';
+ var content = renderChunkContent(sectionConfig, chunkConfig, {
+ tableColGroupNode: microColGroupNode,
+ tableMinWidth: '',
+ clientWidth: scrollerClientWidths[sectionI] !== undefined ? scrollerClientWidths[sectionI] : null,
+ clientHeight: scrollerClientHeights[sectionI] !== undefined ? scrollerClientHeights[sectionI] : null,
+ expandRows: sectionConfig.expandRows,
+ syncRowHeights: false,
+ rowSyncHeights: [],
+ reportRowHeightChange: function () { },
+ });
+ return (createElement("td", { ref: chunkConfig.elRef },
+ createElement("div", { className: "fc-scroller-harness" + (isLiquid ? ' fc-scroller-harness-liquid' : '') },
+ createElement(Scroller, { ref: this.scrollerRefs.createRef(sectionI), elRef: this.scrollerElRefs.createRef(sectionI), overflowY: overflowY, overflowX: !props.liquid ? 'visible' : 'hidden' /* natural height? */, maxHeight: sectionConfig.maxHeight, liquid: isLiquid, liquidIsAbsolute // because its within a harness
+ : true }, content))));
+ };
+ SimpleScrollGrid.prototype._handleScrollerEl = function (scrollerEl, key) {
+ var sectionI = parseInt(key, 10);
+ var chunkConfig = this.props.sections[sectionI].chunk;
+ setRef(chunkConfig.scrollerElRef, scrollerEl);
+ };
+ SimpleScrollGrid.prototype.componentDidMount = function () {
+ this.handleSizing();
+ this.context.addResizeHandler(this.handleSizing);
+ };
+ SimpleScrollGrid.prototype.componentDidUpdate = function () {
+ // TODO: need better solution when state contains non-sizing things
+ this.handleSizing();
+ };
+ SimpleScrollGrid.prototype.componentWillUnmount = function () {
+ this.context.removeResizeHandler(this.handleSizing);
+ };
+ SimpleScrollGrid.prototype.computeShrinkWidth = function () {
+ return hasShrinkWidth(this.props.cols)
+ ? computeShrinkWidth(this.scrollerElRefs.getAll())
+ : 0;
+ };
+ SimpleScrollGrid.prototype.computeScrollerDims = function () {
+ var scrollbarWidth = getScrollbarWidths();
+ var sectionCnt = this.props.sections.length;
+ var _a = this, scrollerRefs = _a.scrollerRefs, scrollerElRefs = _a.scrollerElRefs;
+ var forceYScrollbars = false;
+ var scrollerClientWidths = {};
+ var scrollerClientHeights = {};
+ for (var sectionI = 0; sectionI < sectionCnt; sectionI += 1) { // along edge
+ var scroller = scrollerRefs.currentMap[sectionI];
+ if (scroller && scroller.needsYScrolling()) {
+ forceYScrollbars = true;
+ break;
+ }
+ }
+ for (var sectionI = 0; sectionI < sectionCnt; sectionI += 1) { // along edge
+ var scrollerEl = scrollerElRefs.currentMap[sectionI];
+ if (scrollerEl) {
+ var harnessEl = scrollerEl.parentNode; // TODO: weird way to get this. need harness b/c doesn't include table borders
+ scrollerClientWidths[sectionI] = Math.floor(harnessEl.getBoundingClientRect().width - (forceYScrollbars
+ ? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future
+ : 0));
+ scrollerClientHeights[sectionI] = Math.floor(harnessEl.getBoundingClientRect().height);
+ }
+ }
+ return { forceYScrollbars: forceYScrollbars, scrollerClientWidths: scrollerClientWidths, scrollerClientHeights: scrollerClientHeights };
+ };
+ return SimpleScrollGrid;
+ }(BaseComponent));
+ SimpleScrollGrid.addStateEquality({
+ scrollerClientWidths: isPropsEqual,
+ scrollerClientHeights: isPropsEqual,
+ });
+
+ var EventRoot = /** @class */ (function (_super) {
+ __extends(EventRoot, _super);
+ function EventRoot() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.elRef = createRef();
+ return _this;
+ }
+ EventRoot.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ var options = context.options;
+ var seg = props.seg;
+ var eventRange = seg.eventRange;
+ var ui = eventRange.ui;
+ var hookProps = {
+ event: new EventApi(context, eventRange.def, eventRange.instance),
+ view: context.viewApi,
+ timeText: props.timeText,
+ textColor: ui.textColor,
+ backgroundColor: ui.backgroundColor,
+ borderColor: ui.borderColor,
+ isDraggable: !props.disableDragging && computeSegDraggable(seg, context),
+ isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context),
+ isEndResizable: !props.disableResizing && computeSegEndResizable(seg),
+ isMirror: Boolean(props.isDragging || props.isResizing || props.isDateSelecting),
+ isStart: Boolean(seg.isStart),
+ isEnd: Boolean(seg.isEnd),
+ isPast: Boolean(props.isPast),
+ isFuture: Boolean(props.isFuture),
+ isToday: Boolean(props.isToday),
+ isSelected: Boolean(props.isSelected),
+ isDragging: Boolean(props.isDragging),
+ isResizing: Boolean(props.isResizing),
+ };
+ var standardClassNames = getEventClassNames(hookProps).concat(ui.classNames);
+ return (createElement(RenderHook, { hookProps: hookProps, classNames: options.eventClassNames, content: options.eventContent, defaultContent: props.defaultContent, didMount: options.eventDidMount, willUnmount: options.eventWillUnmount, elRef: this.elRef }, function (rootElRef, customClassNames, innerElRef, innerContent) { return props.children(rootElRef, standardClassNames.concat(customClassNames), innerElRef, innerContent, hookProps); }));
+ };
+ EventRoot.prototype.componentDidMount = function () {
+ setElSeg(this.elRef.current, this.props.seg);
+ };
+ /*
+ need to re-assign seg to the element if seg changes, even if the element is the same
+ */
+ EventRoot.prototype.componentDidUpdate = function (prevProps) {
+ var seg = this.props.seg;
+ if (seg !== prevProps.seg) {
+ setElSeg(this.elRef.current, seg);
+ }
+ };
+ return EventRoot;
+ }(BaseComponent));
+
+ // should not be a purecomponent
+ var StandardEvent = /** @class */ (function (_super) {
+ __extends(StandardEvent, _super);
+ function StandardEvent() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ StandardEvent.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ var seg = props.seg;
+ var timeFormat = context.options.eventTimeFormat || props.defaultTimeFormat;
+ var timeText = buildSegTimeText(seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd);
+ return (createElement(EventRoot, { seg: seg, timeText: timeText, disableDragging: props.disableDragging, disableResizing: props.disableResizing, defaultContent: props.defaultContent || renderInnerContent, isDragging: props.isDragging, isResizing: props.isResizing, isDateSelecting: props.isDateSelecting, isSelected: props.isSelected, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("a", __assign({ className: props.extraClassNames.concat(classNames).join(' '), style: {
+ borderColor: hookProps.borderColor,
+ backgroundColor: hookProps.backgroundColor,
+ }, ref: rootElRef }, getSegAnchorAttrs(seg)),
+ createElement("div", { className: "fc-event-main", ref: innerElRef, style: { color: hookProps.textColor } }, innerContent),
+ hookProps.isStartResizable &&
+ createElement("div", { className: "fc-event-resizer fc-event-resizer-start" }),
+ hookProps.isEndResizable &&
+ createElement("div", { className: "fc-event-resizer fc-event-resizer-end" }))); }));
+ };
+ return StandardEvent;
+ }(BaseComponent));
+ function renderInnerContent(innerProps) {
+ return (createElement("div", { className: "fc-event-main-frame" },
+ innerProps.timeText && (createElement("div", { className: "fc-event-time" }, innerProps.timeText)),
+ createElement("div", { className: "fc-event-title-container" },
+ createElement("div", { className: "fc-event-title fc-sticky" }, innerProps.event.title || createElement(Fragment, null, "\u00A0")))));
+ }
+ function getSegAnchorAttrs(seg) {
+ var url = seg.eventRange.def.url;
+ return url ? { href: url } : {};
+ }
+
+ var NowIndicatorRoot = function (props) { return (createElement(ViewContextType.Consumer, null, function (context) {
+ var options = context.options;
+ var hookProps = {
+ isAxis: props.isAxis,
+ date: context.dateEnv.toDate(props.date),
+ view: context.viewApi,
+ };
+ return (createElement(RenderHook, { hookProps: hookProps, classNames: options.nowIndicatorClassNames, content: options.nowIndicatorContent, didMount: options.nowIndicatorDidMount, willUnmount: options.nowIndicatorWillUnmount }, props.children));
+ })); };
+
+ var DAY_NUM_FORMAT = createFormatter({ day: 'numeric' });
+ var DayCellContent = /** @class */ (function (_super) {
+ __extends(DayCellContent, _super);
+ function DayCellContent() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ DayCellContent.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ var options = context.options;
+ var hookProps = refineDayCellHookProps({
+ date: props.date,
+ dateProfile: props.dateProfile,
+ todayRange: props.todayRange,
+ showDayNumber: props.showDayNumber,
+ extraProps: props.extraHookProps,
+ viewApi: context.viewApi,
+ dateEnv: context.dateEnv,
+ });
+ return (createElement(ContentHook, { hookProps: hookProps, content: options.dayCellContent, defaultContent: props.defaultContent }, props.children));
+ };
+ return DayCellContent;
+ }(BaseComponent));
+ function refineDayCellHookProps(raw) {
+ var date = raw.date, dateEnv = raw.dateEnv;
+ var dayMeta = getDateMeta(date, raw.todayRange, null, raw.dateProfile);
+ return __assign(__assign(__assign({ date: dateEnv.toDate(date), view: raw.viewApi }, dayMeta), { dayNumberText: raw.showDayNumber ? dateEnv.format(date, DAY_NUM_FORMAT) : '' }), raw.extraProps);
+ }
+
+ var DayCellRoot = /** @class */ (function (_super) {
+ __extends(DayCellRoot, _super);
+ function DayCellRoot() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.refineHookProps = memoizeObjArg(refineDayCellHookProps);
+ _this.normalizeClassNames = buildClassNameNormalizer();
+ return _this;
+ }
+ DayCellRoot.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ var options = context.options;
+ var hookProps = this.refineHookProps({
+ date: props.date,
+ dateProfile: props.dateProfile,
+ todayRange: props.todayRange,
+ showDayNumber: props.showDayNumber,
+ extraProps: props.extraHookProps,
+ viewApi: context.viewApi,
+ dateEnv: context.dateEnv,
+ });
+ var classNames = getDayClassNames(hookProps, context.theme).concat(hookProps.isDisabled
+ ? [] // don't use custom classNames if disabled
+ : this.normalizeClassNames(options.dayCellClassNames, hookProps));
+ var dataAttrs = hookProps.isDisabled ? {} : {
+ 'data-date': formatDayString(props.date),
+ };
+ return (createElement(MountHook, { hookProps: hookProps, didMount: options.dayCellDidMount, willUnmount: options.dayCellWillUnmount, elRef: props.elRef }, function (rootElRef) { return props.children(rootElRef, classNames, dataAttrs, hookProps.isDisabled); }));
+ };
+ return DayCellRoot;
+ }(BaseComponent));
+
+ function renderFill(fillType) {
+ return (createElement("div", { className: "fc-" + fillType }));
+ }
+ var BgEvent = function (props) { return (createElement(EventRoot, { defaultContent: renderInnerContent$1, seg: props.seg /* uselesss i think */, timeText: "", disableDragging: true, disableResizing: true, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent, hookProps) { return (createElement("div", { ref: rootElRef, className: ['fc-bg-event'].concat(classNames).join(' '), style: {
+ backgroundColor: hookProps.backgroundColor,
+ } }, innerContent)); })); };
+ function renderInnerContent$1(props) {
+ var title = props.event.title;
+ return title && (createElement("div", { className: "fc-event-title" }, props.event.title));
+ }
+
+ var WeekNumberRoot = function (props) { return (createElement(ViewContextType.Consumer, null, function (context) {
+ var dateEnv = context.dateEnv, options = context.options;
+ var date = props.date;
+ var format = options.weekNumberFormat || props.defaultFormat;
+ var num = dateEnv.computeWeekNumber(date); // TODO: somehow use for formatting as well?
+ var text = dateEnv.format(date, format);
+ var hookProps = { num: num, text: text, date: date };
+ return (createElement(RenderHook, { hookProps: hookProps, classNames: options.weekNumberClassNames, content: options.weekNumberContent, defaultContent: renderInner$1, didMount: options.weekNumberDidMount, willUnmount: options.weekNumberWillUnmount }, props.children));
+ })); };
+ function renderInner$1(innerProps) {
+ return innerProps.text;
+ }
+
+ // exports
+ // --------------------------------------------------------------------------------------------------
+ var version = '5.5.0'; // important to type it, so .d.ts has generic string
+
+ var Calendar = /** @class */ (function (_super) {
+ __extends(Calendar, _super);
+ function Calendar(el, optionOverrides) {
+ if (optionOverrides === void 0) { optionOverrides = {}; }
+ var _this = _super.call(this) || this;
+ _this.isRendering = false;
+ _this.isRendered = false;
+ _this.currentClassNames = [];
+ _this.customContentRenderId = 0; // will affect custom generated classNames?
+ _this.handleAction = function (action) {
+ // actions we know we want to render immediately
+ switch (action.type) {
+ case 'SET_EVENT_DRAG':
+ case 'SET_EVENT_RESIZE':
+ _this.renderRunner.tryDrain();
+ }
+ };
+ _this.handleData = function (data) {
+ _this.currentData = data;
+ _this.renderRunner.request(data.calendarOptions.rerenderDelay);
+ };
+ _this.handleRenderRequest = function () {
+ if (_this.isRendering) {
+ _this.isRendered = true;
+ var currentData_1 = _this.currentData;
+ render(createElement(CalendarRoot, { options: currentData_1.calendarOptions, theme: currentData_1.theme, emitter: currentData_1.emitter }, function (classNames, height, isHeightAuto, forPrint) {
+ _this.setClassNames(classNames);
+ _this.setHeight(height);
+ return (createElement(CustomContentRenderContext.Provider, { value: _this.customContentRenderId },
+ createElement(CalendarContent, __assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData_1))));
+ }), _this.el);
+ }
+ else if (_this.isRendered) {
+ _this.isRendered = false;
+ unmountComponentAtNode$1(_this.el);
+ _this.setClassNames([]);
+ _this.setHeight('');
+ }
+ flushToDom$1();
+ };
+ _this.el = el;
+ _this.renderRunner = new DelayedRunner(_this.handleRenderRequest);
+ new CalendarDataManager({
+ optionOverrides: optionOverrides,
+ calendarApi: _this,
+ onAction: _this.handleAction,
+ onData: _this.handleData,
+ });
+ return _this;
+ }
+ Object.defineProperty(Calendar.prototype, "view", {
+ get: function () { return this.currentData.viewApi; } // for public API
+ ,
+ enumerable: false,
+ configurable: true
+ });
+ Calendar.prototype.render = function () {
+ var wasRendering = this.isRendering;
+ if (!wasRendering) {
+ this.isRendering = true;
+ }
+ else {
+ this.customContentRenderId += 1;
+ }
+ this.renderRunner.request();
+ if (wasRendering) {
+ this.updateSize();
+ }
+ };
+ Calendar.prototype.destroy = function () {
+ if (this.isRendering) {
+ this.isRendering = false;
+ this.renderRunner.request();
+ }
+ };
+ Calendar.prototype.updateSize = function () {
+ _super.prototype.updateSize.call(this);
+ flushToDom$1();
+ };
+ Calendar.prototype.batchRendering = function (func) {
+ this.renderRunner.pause('batchRendering');
+ func();
+ this.renderRunner.resume('batchRendering');
+ };
+ Calendar.prototype.pauseRendering = function () {
+ this.renderRunner.pause('pauseRendering');
+ };
+ Calendar.prototype.resumeRendering = function () {
+ this.renderRunner.resume('pauseRendering', true);
+ };
+ Calendar.prototype.resetOptions = function (optionOverrides, append) {
+ this.currentDataManager.resetOptions(optionOverrides, append);
+ };
+ Calendar.prototype.setClassNames = function (classNames) {
+ if (!isArraysEqual(classNames, this.currentClassNames)) {
+ var classList = this.el.classList;
+ for (var _i = 0, _a = this.currentClassNames; _i < _a.length; _i++) {
+ var className = _a[_i];
+ classList.remove(className);
+ }
+ for (var _b = 0, classNames_1 = classNames; _b < classNames_1.length; _b++) {
+ var className = classNames_1[_b];
+ classList.add(className);
+ }
+ this.currentClassNames = classNames;
+ }
+ };
+ Calendar.prototype.setHeight = function (height) {
+ applyStyleProp(this.el, 'height', height);
+ };
+ return Calendar;
+ }(CalendarApi));
+
+ config.touchMouseIgnoreWait = 500;
+ var ignoreMouseDepth = 0;
+ var listenerCnt = 0;
+ var isWindowTouchMoveCancelled = false;
+ /*
+ Uses a "pointer" abstraction, which monitors UI events for both mouse and touch.
+ Tracks when the pointer "drags" on a certain element, meaning down+move+up.
+
+ Also, tracks if there was touch-scrolling.
+ Also, can prevent touch-scrolling from happening.
+ Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement.
+
+ emits:
+ - pointerdown
+ - pointermove
+ - pointerup
+ */
+ var PointerDragging = /** @class */ (function () {
+ function PointerDragging(containerEl) {
+ var _this = this;
+ this.subjectEl = null;
+ // options that can be directly assigned by caller
+ this.selector = ''; // will cause subjectEl in all emitted events to be this element
+ this.handleSelector = '';
+ this.shouldIgnoreMove = false;
+ this.shouldWatchScroll = true; // for simulating pointermove on scroll
+ // internal states
+ this.isDragging = false;
+ this.isTouchDragging = false;
+ this.wasTouchScroll = false;
+ // Mouse
+ // ----------------------------------------------------------------------------------------------------
+ this.handleMouseDown = function (ev) {
+ if (!_this.shouldIgnoreMouse() &&
+ isPrimaryMouseButton(ev) &&
+ _this.tryStart(ev)) {
+ var pev = _this.createEventFromMouse(ev, true);
+ _this.emitter.trigger('pointerdown', pev);
+ _this.initScrollWatch(pev);
+ if (!_this.shouldIgnoreMove) {
+ document.addEventListener('mousemove', _this.handleMouseMove);
+ }
+ document.addEventListener('mouseup', _this.handleMouseUp);
+ }
+ };
+ this.handleMouseMove = function (ev) {
+ var pev = _this.createEventFromMouse(ev);
+ _this.recordCoords(pev);
+ _this.emitter.trigger('pointermove', pev);
+ };
+ this.handleMouseUp = function (ev) {
+ document.removeEventListener('mousemove', _this.handleMouseMove);
+ document.removeEventListener('mouseup', _this.handleMouseUp);
+ _this.emitter.trigger('pointerup', _this.createEventFromMouse(ev));
+ _this.cleanup(); // call last so that pointerup has access to props
+ };
+ // Touch
+ // ----------------------------------------------------------------------------------------------------
+ this.handleTouchStart = function (ev) {
+ if (_this.tryStart(ev)) {
+ _this.isTouchDragging = true;
+ var pev = _this.createEventFromTouch(ev, true);
+ _this.emitter.trigger('pointerdown', pev);
+ _this.initScrollWatch(pev);
+ // unlike mouse, need to attach to target, not document
+ // https://stackoverflow.com/a/45760014
+ var targetEl = ev.target;
+ if (!_this.shouldIgnoreMove) {
+ targetEl.addEventListener('touchmove', _this.handleTouchMove);
+ }
+ targetEl.addEventListener('touchend', _this.handleTouchEnd);
+ targetEl.addEventListener('touchcancel', _this.handleTouchEnd); // treat it as a touch end
+ // 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, true);
+ }
+ };
+ this.handleTouchMove = function (ev) {
+ var pev = _this.createEventFromTouch(ev);
+ _this.recordCoords(pev);
+ _this.emitter.trigger('pointermove', pev);
+ };
+ this.handleTouchEnd = function (ev) {
+ if (_this.isDragging) { // done to guard against touchend followed by touchcancel
+ var targetEl = ev.target;
+ targetEl.removeEventListener('touchmove', _this.handleTouchMove);
+ targetEl.removeEventListener('touchend', _this.handleTouchEnd);
+ targetEl.removeEventListener('touchcancel', _this.handleTouchEnd);
+ window.removeEventListener('scroll', _this.handleTouchScroll, true); // useCaptured=true
+ _this.emitter.trigger('pointerup', _this.createEventFromTouch(ev));
+ _this.cleanup(); // call last so that pointerup has access to props
+ _this.isTouchDragging = false;
+ startIgnoringMouse();
+ }
+ };
+ this.handleTouchScroll = function () {
+ _this.wasTouchScroll = true;
+ };
+ this.handleScroll = function (ev) {
+ if (!_this.shouldIgnoreMove) {
+ var pageX = (window.pageXOffset - _this.prevScrollX) + _this.prevPageX;
+ var pageY = (window.pageYOffset - _this.prevScrollY) + _this.prevPageY;
+ _this.emitter.trigger('pointermove', {
+ origEvent: ev,
+ isTouch: _this.isTouchDragging,
+ subjectEl: _this.subjectEl,
+ pageX: pageX,
+ pageY: pageY,
+ deltaX: pageX - _this.origPageX,
+ deltaY: pageY - _this.origPageY,
+ });
+ }
+ };
+ this.containerEl = containerEl;
+ this.emitter = new Emitter();
+ containerEl.addEventListener('mousedown', this.handleMouseDown);
+ containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true });
+ listenerCreated();
+ }
+ PointerDragging.prototype.destroy = function () {
+ this.containerEl.removeEventListener('mousedown', this.handleMouseDown);
+ this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
+ listenerDestroyed();
+ };
+ PointerDragging.prototype.tryStart = function (ev) {
+ var subjectEl = this.querySubjectEl(ev);
+ var downEl = ev.target;
+ if (subjectEl &&
+ (!this.handleSelector || elementClosest(downEl, this.handleSelector))) {
+ this.subjectEl = subjectEl;
+ this.isDragging = true; // do this first so cancelTouchScroll will work
+ this.wasTouchScroll = false;
+ return true;
+ }
+ return false;
+ };
+ PointerDragging.prototype.cleanup = function () {
+ isWindowTouchMoveCancelled = false;
+ this.isDragging = false;
+ this.subjectEl = null;
+ // keep wasTouchScroll around for later access
+ this.destroyScrollWatch();
+ };
+ PointerDragging.prototype.querySubjectEl = function (ev) {
+ if (this.selector) {
+ return elementClosest(ev.target, this.selector);
+ }
+ return this.containerEl;
+ };
+ PointerDragging.prototype.shouldIgnoreMouse = function () {
+ return ignoreMouseDepth || this.isTouchDragging;
+ };
+ // can be called by user of this class, to cancel touch-based scrolling for the current drag
+ PointerDragging.prototype.cancelTouchScroll = function () {
+ if (this.isDragging) {
+ isWindowTouchMoveCancelled = true;
+ }
+ };
+ // Scrolling that simulates pointermoves
+ // ----------------------------------------------------------------------------------------------------
+ PointerDragging.prototype.initScrollWatch = function (ev) {
+ if (this.shouldWatchScroll) {
+ this.recordCoords(ev);
+ window.addEventListener('scroll', this.handleScroll, true); // useCapture=true
+ }
+ };
+ PointerDragging.prototype.recordCoords = function (ev) {
+ if (this.shouldWatchScroll) {
+ this.prevPageX = ev.pageX;
+ this.prevPageY = ev.pageY;
+ this.prevScrollX = window.pageXOffset;
+ this.prevScrollY = window.pageYOffset;
+ }
+ };
+ PointerDragging.prototype.destroyScrollWatch = function () {
+ if (this.shouldWatchScroll) {
+ window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true
+ }
+ };
+ // Event Normalization
+ // ----------------------------------------------------------------------------------------------------
+ PointerDragging.prototype.createEventFromMouse = function (ev, isFirst) {
+ var deltaX = 0;
+ var deltaY = 0;
+ // TODO: repeat code
+ if (isFirst) {
+ this.origPageX = ev.pageX;
+ this.origPageY = ev.pageY;
+ }
+ else {
+ deltaX = ev.pageX - this.origPageX;
+ deltaY = ev.pageY - this.origPageY;
+ }
+ return {
+ origEvent: ev,
+ isTouch: false,
+ subjectEl: this.subjectEl,
+ pageX: ev.pageX,
+ pageY: ev.pageY,
+ deltaX: deltaX,
+ deltaY: deltaY,
+ };
+ };
+ PointerDragging.prototype.createEventFromTouch = function (ev, isFirst) {
+ var touches = ev.touches;
+ var pageX;
+ var pageY;
+ var deltaX = 0;
+ var deltaY = 0;
+ // if touch coords available, prefer,
+ // because FF would give bad ev.pageX ev.pageY
+ if (touches && touches.length) {
+ pageX = touches[0].pageX;
+ pageY = touches[0].pageY;
+ }
+ else {
+ pageX = ev.pageX;
+ pageY = ev.pageY;
+ }
+ // TODO: repeat code
+ if (isFirst) {
+ this.origPageX = pageX;
+ this.origPageY = pageY;
+ }
+ else {
+ deltaX = pageX - this.origPageX;
+ deltaY = pageY - this.origPageY;
+ }
+ return {
+ origEvent: ev,
+ isTouch: true,
+ subjectEl: this.subjectEl,
+ pageX: pageX,
+ pageY: pageY,
+ deltaX: deltaX,
+ deltaY: deltaY,
+ };
+ };
+ return PointerDragging;
+ }());
+ // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
+ function isPrimaryMouseButton(ev) {
+ return ev.button === 0 && !ev.ctrlKey;
+ }
+ // Ignoring fake mouse events generated by touch
+ // ----------------------------------------------------------------------------------------------------
+ function startIgnoringMouse() {
+ ignoreMouseDepth += 1;
+ setTimeout(function () {
+ ignoreMouseDepth -= 1;
+ }, config.touchMouseIgnoreWait);
+ }
+ // We want to attach touchmove as early as possible for Safari
+ // ----------------------------------------------------------------------------------------------------
+ function listenerCreated() {
+ listenerCnt += 1;
+ if (listenerCnt === 1) {
+ window.addEventListener('touchmove', onWindowTouchMove, { passive: false });
+ }
+ }
+ function listenerDestroyed() {
+ listenerCnt -= 1;
+ if (!listenerCnt) {
+ window.removeEventListener('touchmove', onWindowTouchMove, { passive: false });
+ }
+ }
+ function onWindowTouchMove(ev) {
+ if (isWindowTouchMoveCancelled) {
+ ev.preventDefault();
+ }
+ }
+
+ /*
+ An effect in which an element follows the movement of a pointer across the screen.
+ The moving element is a clone of some other element.
+ Must call start + handleMove + stop.
+ */
+ var ElementMirror = /** @class */ (function () {
+ function ElementMirror() {
+ this.isVisible = false; // must be explicitly enabled
+ this.sourceEl = null;
+ this.mirrorEl = null;
+ this.sourceElRect = null; // screen coords relative to viewport
+ // options that can be set directly by caller
+ this.parentNode = document.body;
+ this.zIndex = 9999;
+ this.revertDuration = 0;
+ }
+ ElementMirror.prototype.start = function (sourceEl, pageX, pageY) {
+ this.sourceEl = sourceEl;
+ this.sourceElRect = this.sourceEl.getBoundingClientRect();
+ this.origScreenX = pageX - window.pageXOffset;
+ this.origScreenY = pageY - window.pageYOffset;
+ this.deltaX = 0;
+ this.deltaY = 0;
+ this.updateElPosition();
+ };
+ ElementMirror.prototype.handleMove = function (pageX, pageY) {
+ this.deltaX = (pageX - window.pageXOffset) - this.origScreenX;
+ this.deltaY = (pageY - window.pageYOffset) - this.origScreenY;
+ this.updateElPosition();
+ };
+ // can be called before start
+ ElementMirror.prototype.setIsVisible = function (bool) {
+ if (bool) {
+ if (!this.isVisible) {
+ if (this.mirrorEl) {
+ this.mirrorEl.style.display = '';
+ }
+ this.isVisible = bool; // needs to happen before updateElPosition
+ this.updateElPosition(); // because was not updating the position while invisible
+ }
+ }
+ else if (this.isVisible) {
+ if (this.mirrorEl) {
+ this.mirrorEl.style.display = 'none';
+ }
+ this.isVisible = bool;
+ }
+ };
+ // always async
+ ElementMirror.prototype.stop = function (needsRevertAnimation, callback) {
+ var _this = this;
+ var done = function () {
+ _this.cleanup();
+ callback();
+ };
+ if (needsRevertAnimation &&
+ this.mirrorEl &&
+ this.isVisible &&
+ this.revertDuration && // if 0, transition won't work
+ (this.deltaX || this.deltaY) // if same coords, transition won't work
+ ) {
+ this.doRevertAnimation(done, this.revertDuration);
+ }
+ else {
+ setTimeout(done, 0);
+ }
+ };
+ ElementMirror.prototype.doRevertAnimation = function (callback, revertDuration) {
+ var mirrorEl = this.mirrorEl;
+ var finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened
+ mirrorEl.style.transition =
+ 'top ' + revertDuration + 'ms,' +
+ 'left ' + revertDuration + 'ms';
+ applyStyle(mirrorEl, {
+ left: finalSourceElRect.left,
+ top: finalSourceElRect.top,
+ });
+ whenTransitionDone(mirrorEl, function () {
+ mirrorEl.style.transition = '';
+ callback();
+ });
+ };
+ ElementMirror.prototype.cleanup = function () {
+ if (this.mirrorEl) {
+ removeElement(this.mirrorEl);
+ this.mirrorEl = null;
+ }
+ this.sourceEl = null;
+ };
+ ElementMirror.prototype.updateElPosition = function () {
+ if (this.sourceEl && this.isVisible) {
+ applyStyle(this.getMirrorEl(), {
+ left: this.sourceElRect.left + this.deltaX,
+ top: this.sourceElRect.top + this.deltaY,
+ });
+ }
+ };
+ ElementMirror.prototype.getMirrorEl = function () {
+ var sourceElRect = this.sourceElRect;
+ var mirrorEl = this.mirrorEl;
+ if (!mirrorEl) {
+ mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true
+ // we don't want long taps or any mouse interaction causing selection/menus.
+ // would use preventSelection(), but that prevents selectstart, causing problems.
+ mirrorEl.classList.add('fc-unselectable');
+ mirrorEl.classList.add('fc-event-dragging');
+ applyStyle(mirrorEl, {
+ position: 'fixed',
+ zIndex: this.zIndex,
+ visibility: '',
+ boxSizing: 'border-box',
+ width: sourceElRect.right - sourceElRect.left,
+ height: sourceElRect.bottom - sourceElRect.top,
+ right: 'auto',
+ bottom: 'auto',
+ margin: 0,
+ });
+ this.parentNode.appendChild(mirrorEl);
+ }
+ return mirrorEl;
+ };
+ return ElementMirror;
+ }());
+
+ /*
+ Is a cache for a given element's scroll information (all the info that ScrollController stores)
+ in addition the "client rectangle" of the element.. the area within the scrollbars.
+
+ The cache can be in one of two modes:
+ - doesListening:false - ignores when the container is scrolled by someone else
+ - doesListening:true - watch for scrolling and update the cache
+ */
+ var ScrollGeomCache = /** @class */ (function (_super) {
+ __extends(ScrollGeomCache, _super);
+ function ScrollGeomCache(scrollController, doesListening) {
+ var _this = _super.call(this) || this;
+ _this.handleScroll = function () {
+ _this.scrollTop = _this.scrollController.getScrollTop();
+ _this.scrollLeft = _this.scrollController.getScrollLeft();
+ _this.handleScrollChange();
+ };
+ _this.scrollController = scrollController;
+ _this.doesListening = doesListening;
+ _this.scrollTop = _this.origScrollTop = scrollController.getScrollTop();
+ _this.scrollLeft = _this.origScrollLeft = scrollController.getScrollLeft();
+ _this.scrollWidth = scrollController.getScrollWidth();
+ _this.scrollHeight = scrollController.getScrollHeight();
+ _this.clientWidth = scrollController.getClientWidth();
+ _this.clientHeight = scrollController.getClientHeight();
+ _this.clientRect = _this.computeClientRect(); // do last in case it needs cached values
+ if (_this.doesListening) {
+ _this.getEventTarget().addEventListener('scroll', _this.handleScroll);
+ }
+ return _this;
+ }
+ ScrollGeomCache.prototype.destroy = function () {
+ if (this.doesListening) {
+ this.getEventTarget().removeEventListener('scroll', this.handleScroll);
+ }
+ };
+ ScrollGeomCache.prototype.getScrollTop = function () {
+ return this.scrollTop;
+ };
+ ScrollGeomCache.prototype.getScrollLeft = function () {
+ return this.scrollLeft;
+ };
+ ScrollGeomCache.prototype.setScrollTop = function (top) {
+ this.scrollController.setScrollTop(top);
+ if (!this.doesListening) {
+ // we are not relying on the element to normalize out-of-bounds scroll values
+ // so we need to sanitize ourselves
+ this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0);
+ this.handleScrollChange();
+ }
+ };
+ ScrollGeomCache.prototype.setScrollLeft = function (top) {
+ this.scrollController.setScrollLeft(top);
+ if (!this.doesListening) {
+ // we are not relying on the element to normalize out-of-bounds scroll values
+ // so we need to sanitize ourselves
+ this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0);
+ this.handleScrollChange();
+ }
+ };
+ ScrollGeomCache.prototype.getClientWidth = function () {
+ return this.clientWidth;
+ };
+ ScrollGeomCache.prototype.getClientHeight = function () {
+ return this.clientHeight;
+ };
+ ScrollGeomCache.prototype.getScrollWidth = function () {
+ return this.scrollWidth;
+ };
+ ScrollGeomCache.prototype.getScrollHeight = function () {
+ return this.scrollHeight;
+ };
+ ScrollGeomCache.prototype.handleScrollChange = function () {
+ };
+ return ScrollGeomCache;
+ }(ScrollController));
+
+ var ElementScrollGeomCache = /** @class */ (function (_super) {
+ __extends(ElementScrollGeomCache, _super);
+ function ElementScrollGeomCache(el, doesListening) {
+ return _super.call(this, new ElementScrollController(el), doesListening) || this;
+ }
+ ElementScrollGeomCache.prototype.getEventTarget = function () {
+ return this.scrollController.el;
+ };
+ ElementScrollGeomCache.prototype.computeClientRect = function () {
+ return computeInnerRect(this.scrollController.el);
+ };
+ return ElementScrollGeomCache;
+ }(ScrollGeomCache));
+
+ var WindowScrollGeomCache = /** @class */ (function (_super) {
+ __extends(WindowScrollGeomCache, _super);
+ function WindowScrollGeomCache(doesListening) {
+ return _super.call(this, new WindowScrollController(), doesListening) || this;
+ }
+ WindowScrollGeomCache.prototype.getEventTarget = function () {
+ return window;
+ };
+ WindowScrollGeomCache.prototype.computeClientRect = function () {
+ return {
+ left: this.scrollLeft,
+ right: this.scrollLeft + this.clientWidth,
+ top: this.scrollTop,
+ bottom: this.scrollTop + this.clientHeight,
+ };
+ };
+ // the window is the only scroll object that changes it's rectangle relative
+ // to the document's topleft as it scrolls
+ WindowScrollGeomCache.prototype.handleScrollChange = function () {
+ this.clientRect = this.computeClientRect();
+ };
+ return WindowScrollGeomCache;
+ }(ScrollGeomCache));
+
+ // If available we are using native "performance" API instead of "Date"
+ // Read more about it on MDN:
+ // https://developer.mozilla.org/en-US/docs/Web/API/Performance
+ var getTime = typeof performance === 'function' ? performance.now : Date.now;
+ /*
+ For a pointer interaction, automatically scrolls certain scroll containers when the pointer
+ approaches the edge.
+
+ The caller must call start + handleMove + stop.
+ */
+ var AutoScroller = /** @class */ (function () {
+ function AutoScroller() {
+ var _this = this;
+ // options that can be set by caller
+ this.isEnabled = true;
+ this.scrollQuery = [window, '.fc-scroller'];
+ this.edgeThreshold = 50; // pixels
+ this.maxVelocity = 300; // pixels per second
+ // internal state
+ this.pointerScreenX = null;
+ this.pointerScreenY = null;
+ this.isAnimating = false;
+ this.scrollCaches = null;
+ // protect against the initial pointerdown being too close to an edge and starting the scroll
+ this.everMovedUp = false;
+ this.everMovedDown = false;
+ this.everMovedLeft = false;
+ this.everMovedRight = false;
+ this.animate = function () {
+ if (_this.isAnimating) { // wasn't cancelled between animation calls
+ var edge = _this.computeBestEdge(_this.pointerScreenX + window.pageXOffset, _this.pointerScreenY + window.pageYOffset);
+ if (edge) {
+ var now = getTime();
+ _this.handleSide(edge, (now - _this.msSinceRequest) / 1000);
+ _this.requestAnimation(now);
+ }
+ else {
+ _this.isAnimating = false; // will stop animation
+ }
+ }
+ };
+ }
+ AutoScroller.prototype.start = function (pageX, pageY) {
+ if (this.isEnabled) {
+ this.scrollCaches = this.buildCaches();
+ this.pointerScreenX = null;
+ this.pointerScreenY = null;
+ this.everMovedUp = false;
+ this.everMovedDown = false;
+ this.everMovedLeft = false;
+ this.everMovedRight = false;
+ this.handleMove(pageX, pageY);
+ }
+ };
+ AutoScroller.prototype.handleMove = function (pageX, pageY) {
+ if (this.isEnabled) {
+ var pointerScreenX = pageX - window.pageXOffset;
+ var pointerScreenY = pageY - window.pageYOffset;
+ var yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY;
+ var xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX;
+ if (yDelta < 0) {
+ this.everMovedUp = true;
+ }
+ else if (yDelta > 0) {
+ this.everMovedDown = true;
+ }
+ if (xDelta < 0) {
+ this.everMovedLeft = true;
+ }
+ else if (xDelta > 0) {
+ this.everMovedRight = true;
+ }
+ this.pointerScreenX = pointerScreenX;
+ this.pointerScreenY = pointerScreenY;
+ if (!this.isAnimating) {
+ this.isAnimating = true;
+ this.requestAnimation(getTime());
+ }
+ }
+ };
+ AutoScroller.prototype.stop = function () {
+ if (this.isEnabled) {
+ this.isAnimating = false; // will stop animation
+ for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
+ var scrollCache = _a[_i];
+ scrollCache.destroy();
+ }
+ this.scrollCaches = null;
+ }
+ };
+ AutoScroller.prototype.requestAnimation = function (now) {
+ this.msSinceRequest = now;
+ requestAnimationFrame(this.animate);
+ };
+ AutoScroller.prototype.handleSide = function (edge, seconds) {
+ var scrollCache = edge.scrollCache;
+ var edgeThreshold = this.edgeThreshold;
+ var invDistance = edgeThreshold - edge.distance;
+ var velocity = // the closer to the edge, the faster we scroll
+ ((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic
+ this.maxVelocity * seconds;
+ var sign = 1;
+ switch (edge.name) {
+ case 'left':
+ sign = -1;
+ // falls through
+ case 'right':
+ scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign);
+ break;
+ case 'top':
+ sign = -1;
+ // falls through
+ case 'bottom':
+ scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign);
+ break;
+ }
+ };
+ // left/top are relative to document topleft
+ AutoScroller.prototype.computeBestEdge = function (left, top) {
+ var edgeThreshold = this.edgeThreshold;
+ var bestSide = null;
+ for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
+ var scrollCache = _a[_i];
+ var rect = scrollCache.clientRect;
+ var leftDist = left - rect.left;
+ var rightDist = rect.right - left;
+ var topDist = top - rect.top;
+ var bottomDist = rect.bottom - top;
+ // completely within the rect?
+ if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) {
+ if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() &&
+ (!bestSide || bestSide.distance > topDist)) {
+ bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist };
+ }
+ if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() &&
+ (!bestSide || bestSide.distance > bottomDist)) {
+ bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist };
+ }
+ if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() &&
+ (!bestSide || bestSide.distance > leftDist)) {
+ bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist };
+ }
+ if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() &&
+ (!bestSide || bestSide.distance > rightDist)) {
+ bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist };
+ }
+ }
+ }
+ return bestSide;
+ };
+ AutoScroller.prototype.buildCaches = function () {
+ return this.queryScrollEls().map(function (el) {
+ if (el === window) {
+ return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls
+ }
+ return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls
+ });
+ };
+ AutoScroller.prototype.queryScrollEls = function () {
+ var els = [];
+ for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) {
+ var query = _a[_i];
+ if (typeof query === 'object') {
+ els.push(query);
+ }
+ else {
+ els.push.apply(els, Array.prototype.slice.call(document.querySelectorAll(query)));
+ }
+ }
+ return els;
+ };
+ return AutoScroller;
+ }());
+
+ /*
+ Monitors dragging on an element. Has a number of high-level features:
+ - minimum distance required before dragging
+ - minimum wait time ("delay") before dragging
+ - a mirror element that follows the pointer
+ */
+ var FeaturefulElementDragging = /** @class */ (function (_super) {
+ __extends(FeaturefulElementDragging, _super);
+ function FeaturefulElementDragging(containerEl, selector) {
+ var _this = _super.call(this, containerEl) || this;
+ // options that can be directly set by caller
+ // the caller can also set the PointerDragging's options as well
+ _this.delay = null;
+ _this.minDistance = 0;
+ _this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag
+ _this.mirrorNeedsRevert = false;
+ _this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup
+ _this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation
+ _this.isDelayEnded = false;
+ _this.isDistanceSurpassed = false;
+ _this.delayTimeoutId = null;
+ _this.onPointerDown = function (ev) {
+ if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going
+ _this.isInteracting = true;
+ _this.isDelayEnded = false;
+ _this.isDistanceSurpassed = false;
+ preventSelection(document.body);
+ preventContextMenu(document.body);
+ // prevent links from being visited if there's an eventual drag.
+ // also prevents selection in older browsers (maybe?).
+ // not necessary for touch, besides, browser would complain about passiveness.
+ if (!ev.isTouch) {
+ ev.origEvent.preventDefault();
+ }
+ _this.emitter.trigger('pointerdown', ev);
+ if (_this.isInteracting && // not destroyed via pointerdown handler
+ !_this.pointer.shouldIgnoreMove) {
+ // actions related to initiating dragstart+dragmove+dragend...
+ _this.mirror.setIsVisible(false); // reset. caller must set-visible
+ _this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down
+ _this.startDelay(ev);
+ if (!_this.minDistance) {
+ _this.handleDistanceSurpassed(ev);
+ }
+ }
+ }
+ };
+ _this.onPointerMove = function (ev) {
+ if (_this.isInteracting) {
+ _this.emitter.trigger('pointermove', ev);
+ if (!_this.isDistanceSurpassed) {
+ var minDistance = _this.minDistance;
+ var distanceSq = void 0; // current distance from the origin, squared
+ var deltaX = ev.deltaX, deltaY = ev.deltaY;
+ distanceSq = deltaX * deltaX + deltaY * deltaY;
+ if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
+ _this.handleDistanceSurpassed(ev);
+ }
+ }
+ if (_this.isDragging) {
+ // a real pointer move? (not one simulated by scrolling)
+ if (ev.origEvent.type !== 'scroll') {
+ _this.mirror.handleMove(ev.pageX, ev.pageY);
+ _this.autoScroller.handleMove(ev.pageX, ev.pageY);
+ }
+ _this.emitter.trigger('dragmove', ev);
+ }
+ }
+ };
+ _this.onPointerUp = function (ev) {
+ if (_this.isInteracting) {
+ _this.isInteracting = false;
+ allowSelection(document.body);
+ allowContextMenu(document.body);
+ _this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert
+ if (_this.isDragging) {
+ _this.autoScroller.stop();
+ _this.tryStopDrag(ev); // which will stop the mirror
+ }
+ if (_this.delayTimeoutId) {
+ clearTimeout(_this.delayTimeoutId);
+ _this.delayTimeoutId = null;
+ }
+ }
+ };
+ var pointer = _this.pointer = new PointerDragging(containerEl);
+ pointer.emitter.on('pointerdown', _this.onPointerDown);
+ pointer.emitter.on('pointermove', _this.onPointerMove);
+ pointer.emitter.on('pointerup', _this.onPointerUp);
+ if (selector) {
+ pointer.selector = selector;
+ }
+ _this.mirror = new ElementMirror();
+ _this.autoScroller = new AutoScroller();
+ return _this;
+ }
+ FeaturefulElementDragging.prototype.destroy = function () {
+ this.pointer.destroy();
+ // HACK: simulate a pointer-up to end the current drag
+ // TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire)
+ this.onPointerUp({});
+ };
+ FeaturefulElementDragging.prototype.startDelay = function (ev) {
+ var _this = this;
+ if (typeof this.delay === 'number') {
+ this.delayTimeoutId = setTimeout(function () {
+ _this.delayTimeoutId = null;
+ _this.handleDelayEnd(ev);
+ }, this.delay); // not assignable to number!
+ }
+ else {
+ this.handleDelayEnd(ev);
+ }
+ };
+ FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) {
+ this.isDelayEnded = true;
+ this.tryStartDrag(ev);
+ };
+ FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) {
+ this.isDistanceSurpassed = true;
+ this.tryStartDrag(ev);
+ };
+ FeaturefulElementDragging.prototype.tryStartDrag = function (ev) {
+ if (this.isDelayEnded && this.isDistanceSurpassed) {
+ if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) {
+ this.isDragging = true;
+ this.mirrorNeedsRevert = false;
+ this.autoScroller.start(ev.pageX, ev.pageY);
+ this.emitter.trigger('dragstart', ev);
+ if (this.touchScrollAllowed === false) {
+ this.pointer.cancelTouchScroll();
+ }
+ }
+ }
+ };
+ FeaturefulElementDragging.prototype.tryStopDrag = function (ev) {
+ // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events
+ // that come from the document to fire beforehand. much more convenient this way.
+ this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev));
+ };
+ FeaturefulElementDragging.prototype.stopDrag = function (ev) {
+ this.isDragging = false;
+ this.emitter.trigger('dragend', ev);
+ };
+ // fill in the implementations...
+ FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) {
+ this.pointer.shouldIgnoreMove = bool;
+ };
+ FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) {
+ this.mirror.setIsVisible(bool);
+ };
+ FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) {
+ this.mirrorNeedsRevert = bool;
+ };
+ FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) {
+ this.autoScroller.isEnabled = bool;
+ };
+ return FeaturefulElementDragging;
+ }(ElementDragging));
+
+ /*
+ When this class is instantiated, it records the offset of an element (relative to the document topleft),
+ and continues to monitor scrolling, updating the cached coordinates if it needs to.
+ Does not access the DOM after instantiation, so highly performant.
+
+ Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element
+ and an determine if a given point is inside the combined clipping rectangle.
+ */
+ var OffsetTracker = /** @class */ (function () {
+ function OffsetTracker(el) {
+ this.origRect = computeRect(el);
+ // will work fine for divs that have overflow:hidden
+ this.scrollCaches = getClippingParents(el).map(function (scrollEl) { return new ElementScrollGeomCache(scrollEl, true); });
+ }
+ OffsetTracker.prototype.destroy = function () {
+ for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
+ var scrollCache = _a[_i];
+ scrollCache.destroy();
+ }
+ };
+ OffsetTracker.prototype.computeLeft = function () {
+ var left = this.origRect.left;
+ for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
+ var scrollCache = _a[_i];
+ left += scrollCache.origScrollLeft - scrollCache.getScrollLeft();
+ }
+ return left;
+ };
+ OffsetTracker.prototype.computeTop = function () {
+ var top = this.origRect.top;
+ for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
+ var scrollCache = _a[_i];
+ top += scrollCache.origScrollTop - scrollCache.getScrollTop();
+ }
+ return top;
+ };
+ OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) {
+ var point = { left: pageX, top: pageY };
+ for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
+ var scrollCache = _a[_i];
+ if (!isIgnoredClipping(scrollCache.getEventTarget()) &&
+ !pointInsideRect(point, scrollCache.clientRect)) {
+ return false;
+ }
+ }
+ return true;
+ };
+ return OffsetTracker;
+ }());
+ // certain clipping containers should never constrain interactions, like and
+ // https://github.com/fullcalendar/fullcalendar/issues/3615
+ function isIgnoredClipping(node) {
+ var tagName = node.tagName;
+ return tagName === 'HTML' || tagName === 'BODY';
+ }
+
+ /*
+ Tracks movement over multiple droppable areas (aka "hits")
+ that exist in one or more DateComponents.
+ Relies on an existing draggable.
+
+ emits:
+ - pointerdown
+ - dragstart
+ - hitchange - fires initially, even if not over a hit
+ - pointerup
+ - (hitchange - again, to null, if ended over a hit)
+ - dragend
+ */
+ var HitDragging = /** @class */ (function () {
+ function HitDragging(dragging, droppableStore) {
+ var _this = this;
+ // options that can be set by caller
+ this.useSubjectCenter = false;
+ this.requireInitial = true; // if doesn't start out on a hit, won't emit any events
+ this.initialHit = null;
+ this.movingHit = null;
+ this.finalHit = null; // won't ever be populated if shouldIgnoreMove
+ this.handlePointerDown = function (ev) {
+ var dragging = _this.dragging;
+ _this.initialHit = null;
+ _this.movingHit = null;
+ _this.finalHit = null;
+ _this.prepareHits();
+ _this.processFirstCoord(ev);
+ if (_this.initialHit || !_this.requireInitial) {
+ dragging.setIgnoreMove(false);
+ // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :(
+ _this.emitter.trigger('pointerdown', ev);
+ }
+ else {
+ dragging.setIgnoreMove(true);
+ }
+ };
+ this.handleDragStart = function (ev) {
+ _this.emitter.trigger('dragstart', ev);
+ _this.handleMove(ev, true); // force = fire even if initially null
+ };
+ this.handleDragMove = function (ev) {
+ _this.emitter.trigger('dragmove', ev);
+ _this.handleMove(ev);
+ };
+ this.handlePointerUp = function (ev) {
+ _this.releaseHits();
+ _this.emitter.trigger('pointerup', ev);
+ };
+ this.handleDragEnd = function (ev) {
+ if (_this.movingHit) {
+ _this.emitter.trigger('hitupdate', null, true, ev);
+ }
+ _this.finalHit = _this.movingHit;
+ _this.movingHit = null;
+ _this.emitter.trigger('dragend', ev);
+ };
+ this.droppableStore = droppableStore;
+ dragging.emitter.on('pointerdown', this.handlePointerDown);
+ dragging.emitter.on('dragstart', this.handleDragStart);
+ dragging.emitter.on('dragmove', this.handleDragMove);
+ dragging.emitter.on('pointerup', this.handlePointerUp);
+ dragging.emitter.on('dragend', this.handleDragEnd);
+ this.dragging = dragging;
+ this.emitter = new Emitter();
+ }
+ // sets initialHit
+ // sets coordAdjust
+ HitDragging.prototype.processFirstCoord = function (ev) {
+ var origPoint = { left: ev.pageX, top: ev.pageY };
+ var adjustedPoint = origPoint;
+ var subjectEl = ev.subjectEl;
+ var subjectRect;
+ if (subjectEl !== document) {
+ subjectRect = computeRect(subjectEl);
+ adjustedPoint = constrainPoint(adjustedPoint, subjectRect);
+ }
+ var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top);
+ if (initialHit) {
+ if (this.useSubjectCenter && subjectRect) {
+ var slicedSubjectRect = intersectRects(subjectRect, initialHit.rect);
+ if (slicedSubjectRect) {
+ adjustedPoint = getRectCenter(slicedSubjectRect);
+ }
+ }
+ this.coordAdjust = diffPoints(adjustedPoint, origPoint);
+ }
+ else {
+ this.coordAdjust = { left: 0, top: 0 };
+ }
+ };
+ HitDragging.prototype.handleMove = function (ev, forceHandle) {
+ var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top);
+ if (forceHandle || !isHitsEqual(this.movingHit, hit)) {
+ this.movingHit = hit;
+ this.emitter.trigger('hitupdate', hit, false, ev);
+ }
+ };
+ HitDragging.prototype.prepareHits = function () {
+ this.offsetTrackers = mapHash(this.droppableStore, function (interactionSettings) {
+ interactionSettings.component.prepareHits();
+ return new OffsetTracker(interactionSettings.el);
+ });
+ };
+ HitDragging.prototype.releaseHits = function () {
+ var offsetTrackers = this.offsetTrackers;
+ for (var id in offsetTrackers) {
+ offsetTrackers[id].destroy();
+ }
+ this.offsetTrackers = {};
+ };
+ HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) {
+ var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers;
+ var bestHit = null;
+ for (var id in droppableStore) {
+ var component = droppableStore[id].component;
+ var offsetTracker = offsetTrackers[id];
+ if (offsetTracker && // wasn't destroyed mid-drag
+ offsetTracker.isWithinClipping(offsetLeft, offsetTop)) {
+ var originLeft = offsetTracker.computeLeft();
+ var originTop = offsetTracker.computeTop();
+ var positionLeft = offsetLeft - originLeft;
+ var positionTop = offsetTop - originTop;
+ var origRect = offsetTracker.origRect;
+ var width = origRect.right - origRect.left;
+ var height = origRect.bottom - origRect.top;
+ if (
+ // must be within the element's bounds
+ positionLeft >= 0 && positionLeft < width &&
+ positionTop >= 0 && positionTop < height) {
+ var hit = component.queryHit(positionLeft, positionTop, width, height);
+ var dateProfile = component.context.getCurrentData().dateProfile;
+ if (hit &&
+ (
+ // make sure the hit is within activeRange, meaning it's not a deal cell
+ rangeContainsRange(dateProfile.activeRange, hit.dateSpan.range)) &&
+ (!bestHit || hit.layer > bestHit.layer)) {
+ // TODO: better way to re-orient rectangle
+ hit.rect.left += originLeft;
+ hit.rect.right += originLeft;
+ hit.rect.top += originTop;
+ hit.rect.bottom += originTop;
+ bestHit = hit;
+ }
+ }
+ }
+ }
+ return bestHit;
+ };
+ return HitDragging;
+ }());
+ function isHitsEqual(hit0, hit1) {
+ if (!hit0 && !hit1) {
+ return true;
+ }
+ if (Boolean(hit0) !== Boolean(hit1)) {
+ return false;
+ }
+ return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan);
+ }
+
+ function buildDatePointApiWithContext(dateSpan, context) {
+ var props = {};
+ for (var _i = 0, _a = context.pluginHooks.datePointTransforms; _i < _a.length; _i++) {
+ var transform = _a[_i];
+ __assign(props, transform(dateSpan, context));
+ }
+ __assign(props, buildDatePointApi(dateSpan, context.dateEnv));
+ return props;
+ }
+ function buildDatePointApi(span, dateEnv) {
+ return {
+ date: dateEnv.toDate(span.range.start),
+ dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
+ allDay: span.allDay,
+ };
+ }
+
+ /*
+ Monitors when the user clicks on a specific date/time of a component.
+ A pointerdown+pointerup on the same "hit" constitutes a click.
+ */
+ var DateClicking = /** @class */ (function (_super) {
+ __extends(DateClicking, _super);
+ function DateClicking(settings) {
+ var _this = _super.call(this, settings) || this;
+ _this.handlePointerDown = function (pev) {
+ var dragging = _this.dragging;
+ var downEl = pev.origEvent.target;
+ // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired
+ dragging.setIgnoreMove(!_this.component.isValidDateDownEl(downEl));
+ };
+ // won't even fire if moving was ignored
+ _this.handleDragEnd = function (ev) {
+ var component = _this.component;
+ var pointer = _this.dragging.pointer;
+ if (!pointer.wasTouchScroll) {
+ var _a = _this.hitDragging, initialHit = _a.initialHit, finalHit = _a.finalHit;
+ if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) {
+ var context = component.context;
+ var arg = __assign(__assign({}, buildDatePointApiWithContext(initialHit.dateSpan, context)), { dayEl: initialHit.dayEl, jsEvent: ev.origEvent, view: context.viewApi || context.calendarApi.view });
+ context.emitter.trigger('dateClick', arg);
+ }
+ }
+ };
+ // we DO want to watch pointer moves because otherwise finalHit won't get populated
+ _this.dragging = new FeaturefulElementDragging(settings.el);
+ _this.dragging.autoScroller.isEnabled = false;
+ var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings));
+ hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
+ hitDragging.emitter.on('dragend', _this.handleDragEnd);
+ return _this;
+ }
+ DateClicking.prototype.destroy = function () {
+ this.dragging.destroy();
+ };
+ return DateClicking;
+ }(Interaction));
+
+ /*
+ Tracks when the user selects a portion of time of a component,
+ constituted by a drag over date cells, with a possible delay at the beginning of the drag.
+ */
+ var DateSelecting = /** @class */ (function (_super) {
+ __extends(DateSelecting, _super);
+ function DateSelecting(settings) {
+ var _this = _super.call(this, settings) || this;
+ _this.dragSelection = null;
+ _this.handlePointerDown = function (ev) {
+ var _a = _this, component = _a.component, dragging = _a.dragging;
+ var options = component.context.options;
+ var canSelect = options.selectable &&
+ component.isValidDateDownEl(ev.origEvent.target);
+ // don't bother to watch expensive moves if component won't do selection
+ dragging.setIgnoreMove(!canSelect);
+ // if touch, require user to hold down
+ dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null;
+ };
+ _this.handleDragStart = function (ev) {
+ _this.component.context.calendarApi.unselect(ev); // unselect previous selections
+ };
+ _this.handleHitUpdate = function (hit, isFinal) {
+ var context = _this.component.context;
+ var dragSelection = null;
+ var isInvalid = false;
+ if (hit) {
+ dragSelection = joinHitsIntoSelection(_this.hitDragging.initialHit, hit, context.pluginHooks.dateSelectionTransformers);
+ if (!dragSelection || !_this.component.isDateSelectionValid(dragSelection)) {
+ isInvalid = true;
+ dragSelection = null;
+ }
+ }
+ if (dragSelection) {
+ context.dispatch({ type: 'SELECT_DATES', selection: dragSelection });
+ }
+ else if (!isFinal) { // only unselect if moved away while dragging
+ context.dispatch({ type: 'UNSELECT_DATES' });
+ }
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ _this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging
+ }
+ };
+ _this.handlePointerUp = function (pev) {
+ if (_this.dragSelection) {
+ // selection is already rendered, so just need to report selection
+ triggerDateSelect(_this.dragSelection, pev, _this.component.context);
+ _this.dragSelection = null;
+ }
+ };
+ var component = settings.component;
+ var options = component.context.options;
+ var dragging = _this.dragging = new FeaturefulElementDragging(settings.el);
+ dragging.touchScrollAllowed = false;
+ dragging.minDistance = options.selectMinDistance || 0;
+ dragging.autoScroller.isEnabled = options.dragScroll;
+ var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings));
+ hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
+ hitDragging.emitter.on('dragstart', _this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
+ hitDragging.emitter.on('pointerup', _this.handlePointerUp);
+ return _this;
+ }
+ DateSelecting.prototype.destroy = function () {
+ this.dragging.destroy();
+ };
+ return DateSelecting;
+ }(Interaction));
+ function getComponentTouchDelay(component) {
+ var options = component.context.options;
+ var delay = options.selectLongPressDelay;
+ if (delay == null) {
+ delay = options.longPressDelay;
+ }
+ return delay;
+ }
+ function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) {
+ var dateSpan0 = hit0.dateSpan;
+ var dateSpan1 = hit1.dateSpan;
+ var ms = [
+ dateSpan0.range.start,
+ dateSpan0.range.end,
+ dateSpan1.range.start,
+ dateSpan1.range.end,
+ ];
+ ms.sort(compareNumbers);
+ var props = {};
+ for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) {
+ var transformer = dateSelectionTransformers_1[_i];
+ var res = transformer(hit0, hit1);
+ if (res === false) {
+ return null;
+ }
+ if (res) {
+ __assign(props, res);
+ }
+ }
+ props.range = { start: ms[0], end: ms[3] };
+ props.allDay = dateSpan0.allDay;
+ return props;
+ }
+
+ var EventDragging = /** @class */ (function (_super) {
+ __extends(EventDragging, _super);
+ function EventDragging(settings) {
+ var _this = _super.call(this, settings) || this;
+ // internal state
+ _this.subjectEl = null;
+ _this.subjectSeg = null; // the seg being selected/dragged
+ _this.isDragging = false;
+ _this.eventRange = null;
+ _this.relevantEvents = null; // the events being dragged
+ _this.receivingContext = null;
+ _this.validMutation = null;
+ _this.mutatedRelevantEvents = null;
+ _this.handlePointerDown = function (ev) {
+ var origTarget = ev.origEvent.target;
+ var _a = _this, component = _a.component, dragging = _a.dragging;
+ var mirror = dragging.mirror;
+ var options = component.context.options;
+ var initialContext = component.context;
+ _this.subjectEl = ev.subjectEl;
+ var subjectSeg = _this.subjectSeg = getElSeg(ev.subjectEl);
+ var eventRange = _this.eventRange = subjectSeg.eventRange;
+ var eventInstanceId = eventRange.instance.instanceId;
+ _this.relevantEvents = getRelevantEvents(initialContext.getCurrentData().eventStore, eventInstanceId);
+ dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance;
+ dragging.delay =
+ // only do a touch delay if touch and this event hasn't been selected yet
+ (ev.isTouch && eventInstanceId !== component.props.eventSelection) ?
+ getComponentTouchDelay$1(component) :
+ null;
+ if (options.fixedMirrorParent) {
+ mirror.parentNode = options.fixedMirrorParent;
+ }
+ else {
+ mirror.parentNode = elementClosest(origTarget, '.fc');
+ }
+ mirror.revertDuration = options.dragRevertDuration;
+ var isValid = component.isValidSegDownEl(origTarget) &&
+ !elementClosest(origTarget, '.fc-event-resizer'); // NOT on a resizer
+ dragging.setIgnoreMove(!isValid);
+ // disable dragging for elements that are resizable (ie, selectable)
+ // but are not draggable
+ _this.isDragging = isValid &&
+ ev.subjectEl.classList.contains('fc-event-draggable');
+ };
+ _this.handleDragStart = function (ev) {
+ var initialContext = _this.component.context;
+ var eventRange = _this.eventRange;
+ var eventInstanceId = eventRange.instance.instanceId;
+ if (ev.isTouch) {
+ // need to select a different event?
+ if (eventInstanceId !== _this.component.props.eventSelection) {
+ initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId });
+ }
+ }
+ else {
+ // if now using mouse, but was previous touch interaction, clear selected event
+ initialContext.dispatch({ type: 'UNSELECT_EVENT' });
+ }
+ if (_this.isDragging) {
+ initialContext.calendarApi.unselect(ev); // unselect *date* selection
+ initialContext.emitter.trigger('eventDragStart', {
+ el: _this.subjectEl,
+ event: new EventApi(initialContext, eventRange.def, eventRange.instance),
+ jsEvent: ev.origEvent,
+ view: initialContext.viewApi,
+ });
+ }
+ };
+ _this.handleHitUpdate = function (hit, isFinal) {
+ if (!_this.isDragging) {
+ return;
+ }
+ var relevantEvents = _this.relevantEvents;
+ var initialHit = _this.hitDragging.initialHit;
+ var initialContext = _this.component.context;
+ // states based on new hit
+ var receivingContext = null;
+ var mutation = null;
+ var mutatedRelevantEvents = null;
+ var isInvalid = false;
+ var interaction = {
+ affectedEvents: relevantEvents,
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: true,
+ };
+ if (hit) {
+ var receivingComponent = hit.component;
+ receivingContext = receivingComponent.context;
+ var receivingOptions = receivingContext.options;
+ if (initialContext === receivingContext ||
+ (receivingOptions.editable && receivingOptions.droppable)) {
+ mutation = computeEventMutation(initialHit, hit, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers);
+ if (mutation) {
+ mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.getCurrentData().eventUiBases, mutation, receivingContext);
+ interaction.mutatedEvents = mutatedRelevantEvents;
+ if (!receivingComponent.isInteractionValid(interaction)) {
+ isInvalid = true;
+ mutation = null;
+ mutatedRelevantEvents = null;
+ interaction.mutatedEvents = createEmptyEventStore();
+ }
+ }
+ }
+ else {
+ receivingContext = null;
+ }
+ }
+ _this.displayDrag(receivingContext, interaction);
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ if (initialContext === receivingContext && // TODO: write test for this
+ isHitsEqual(initialHit, hit)) {
+ mutation = null;
+ }
+ _this.dragging.setMirrorNeedsRevert(!mutation);
+ // render the mirror if no already-rendered mirror
+ // TODO: wish we could somehow wait for dispatch to guarantee render
+ _this.dragging.setMirrorIsVisible(!hit || !document.querySelector('.fc-event-mirror'));
+ // assign states based on new hit
+ _this.receivingContext = receivingContext;
+ _this.validMutation = mutation;
+ _this.mutatedRelevantEvents = mutatedRelevantEvents;
+ }
+ };
+ _this.handlePointerUp = function () {
+ if (!_this.isDragging) {
+ _this.cleanup(); // because handleDragEnd won't fire
+ }
+ };
+ _this.handleDragEnd = function (ev) {
+ if (_this.isDragging) {
+ var initialContext_1 = _this.component.context;
+ var initialView = initialContext_1.viewApi;
+ var _a = _this, receivingContext_1 = _a.receivingContext, validMutation = _a.validMutation;
+ var eventDef = _this.eventRange.def;
+ var eventInstance = _this.eventRange.instance;
+ var eventApi = new EventApi(initialContext_1, eventDef, eventInstance);
+ var relevantEvents_1 = _this.relevantEvents;
+ var mutatedRelevantEvents_1 = _this.mutatedRelevantEvents;
+ var finalHit = _this.hitDragging.finalHit;
+ _this.clearDrag(); // must happen after revert animation
+ initialContext_1.emitter.trigger('eventDragStop', {
+ el: _this.subjectEl,
+ event: eventApi,
+ jsEvent: ev.origEvent,
+ view: initialView,
+ });
+ if (validMutation) {
+ // dropped within same calendar
+ if (receivingContext_1 === initialContext_1) {
+ var updatedEventApi = new EventApi(initialContext_1, mutatedRelevantEvents_1.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents_1.instances[eventInstance.instanceId] : null);
+ initialContext_1.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: mutatedRelevantEvents_1,
+ });
+ var eventChangeArg = {
+ oldEvent: eventApi,
+ event: updatedEventApi,
+ relatedEvents: buildEventApis(mutatedRelevantEvents_1, initialContext_1, eventInstance),
+ revert: function () {
+ initialContext_1.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: relevantEvents_1,
+ });
+ },
+ };
+ var transformed = {};
+ for (var _i = 0, _b = initialContext_1.getCurrentData().pluginHooks.eventDropTransformers; _i < _b.length; _i++) {
+ var transformer = _b[_i];
+ __assign(transformed, transformer(validMutation, initialContext_1));
+ }
+ initialContext_1.emitter.trigger('eventDrop', __assign(__assign(__assign({}, eventChangeArg), transformed), { el: ev.subjectEl, delta: validMutation.datesDelta, jsEvent: ev.origEvent, view: initialView }));
+ initialContext_1.emitter.trigger('eventChange', eventChangeArg);
+ // dropped in different calendar
+ }
+ else if (receivingContext_1) {
+ var eventRemoveArg = {
+ event: eventApi,
+ relatedEvents: buildEventApis(relevantEvents_1, initialContext_1, eventInstance),
+ revert: function () {
+ initialContext_1.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: relevantEvents_1,
+ });
+ },
+ };
+ initialContext_1.emitter.trigger('eventLeave', __assign(__assign({}, eventRemoveArg), { draggedEl: ev.subjectEl, view: initialView }));
+ initialContext_1.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: relevantEvents_1,
+ });
+ initialContext_1.emitter.trigger('eventRemove', eventRemoveArg);
+ var addedEventDef = mutatedRelevantEvents_1.defs[eventDef.defId];
+ var addedEventInstance = mutatedRelevantEvents_1.instances[eventInstance.instanceId];
+ var addedEventApi = new EventApi(receivingContext_1, addedEventDef, addedEventInstance);
+ receivingContext_1.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: mutatedRelevantEvents_1,
+ });
+ var eventAddArg = {
+ event: addedEventApi,
+ relatedEvents: buildEventApis(mutatedRelevantEvents_1, receivingContext_1, addedEventInstance),
+ revert: function () {
+ receivingContext_1.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: mutatedRelevantEvents_1,
+ });
+ },
+ };
+ receivingContext_1.emitter.trigger('eventAdd', eventAddArg);
+ if (ev.isTouch) {
+ receivingContext_1.dispatch({
+ type: 'SELECT_EVENT',
+ eventInstanceId: eventInstance.instanceId,
+ });
+ }
+ receivingContext_1.emitter.trigger('drop', __assign(__assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext_1)), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.component.context.viewApi }));
+ receivingContext_1.emitter.trigger('eventReceive', __assign(__assign({}, eventAddArg), { draggedEl: ev.subjectEl, view: finalHit.component.context.viewApi }));
+ }
+ }
+ else {
+ initialContext_1.emitter.trigger('_noEventDrop');
+ }
+ }
+ _this.cleanup();
+ };
+ var component = _this.component;
+ var options = component.context.options;
+ var dragging = _this.dragging = new FeaturefulElementDragging(settings.el);
+ dragging.pointer.selector = EventDragging.SELECTOR;
+ dragging.touchScrollAllowed = false;
+ dragging.autoScroller.isEnabled = options.dragScroll;
+ var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsStore);
+ hitDragging.useSubjectCenter = settings.useEventCenter;
+ hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
+ hitDragging.emitter.on('dragstart', _this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
+ hitDragging.emitter.on('pointerup', _this.handlePointerUp);
+ hitDragging.emitter.on('dragend', _this.handleDragEnd);
+ return _this;
+ }
+ EventDragging.prototype.destroy = function () {
+ this.dragging.destroy();
+ };
+ // render a drag state on the next receivingCalendar
+ EventDragging.prototype.displayDrag = function (nextContext, state) {
+ var initialContext = this.component.context;
+ var prevContext = this.receivingContext;
+ // does the previous calendar need to be cleared?
+ if (prevContext && prevContext !== nextContext) {
+ // does the initial calendar need to be cleared?
+ // if so, don't clear all the way. we still need to to hide the affectedEvents
+ if (prevContext === initialContext) {
+ prevContext.dispatch({
+ type: 'SET_EVENT_DRAG',
+ state: {
+ affectedEvents: state.affectedEvents,
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: true,
+ },
+ });
+ // completely clear the old calendar if it wasn't the initial
+ }
+ else {
+ prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ }
+ if (nextContext) {
+ nextContext.dispatch({ type: 'SET_EVENT_DRAG', state: state });
+ }
+ };
+ EventDragging.prototype.clearDrag = function () {
+ var initialCalendar = this.component.context;
+ var receivingContext = this.receivingContext;
+ if (receivingContext) {
+ receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ // the initial calendar might have an dummy drag state from displayDrag
+ if (initialCalendar !== receivingContext) {
+ initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ };
+ EventDragging.prototype.cleanup = function () {
+ this.subjectSeg = null;
+ this.isDragging = false;
+ this.eventRange = null;
+ this.relevantEvents = null;
+ this.receivingContext = null;
+ this.validMutation = null;
+ this.mutatedRelevantEvents = null;
+ };
+ // TODO: test this in IE11
+ // QUESTION: why do we need it on the resizable???
+ EventDragging.SELECTOR = '.fc-event-draggable, .fc-event-resizable';
+ return EventDragging;
+ }(Interaction));
+ function computeEventMutation(hit0, hit1, massagers) {
+ var dateSpan0 = hit0.dateSpan;
+ var dateSpan1 = hit1.dateSpan;
+ var date0 = dateSpan0.range.start;
+ var date1 = dateSpan1.range.start;
+ var standardProps = {};
+ if (dateSpan0.allDay !== dateSpan1.allDay) {
+ standardProps.allDay = dateSpan1.allDay;
+ standardProps.hasEnd = hit1.component.context.options.allDayMaintainDuration;
+ if (dateSpan1.allDay) {
+ // means date1 is already start-of-day,
+ // but date0 needs to be converted
+ date0 = startOfDay(date0);
+ }
+ }
+ var delta = diffDates(date0, date1, hit0.component.context.dateEnv, hit0.component === hit1.component ?
+ hit0.component.largeUnit :
+ null);
+ if (delta.milliseconds) { // has hours/minutes/seconds
+ standardProps.allDay = false;
+ }
+ var mutation = {
+ datesDelta: delta,
+ standardProps: standardProps,
+ };
+ for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) {
+ var massager = massagers_1[_i];
+ massager(mutation, hit0, hit1);
+ }
+ return mutation;
+ }
+ function getComponentTouchDelay$1(component) {
+ var options = component.context.options;
+ var delay = options.eventLongPressDelay;
+ if (delay == null) {
+ delay = options.longPressDelay;
+ }
+ return delay;
+ }
+
+ var EventResizing = /** @class */ (function (_super) {
+ __extends(EventResizing, _super);
+ function EventResizing(settings) {
+ var _this = _super.call(this, settings) || this;
+ // internal state
+ _this.draggingSegEl = null;
+ _this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg?
+ _this.eventRange = null;
+ _this.relevantEvents = null;
+ _this.validMutation = null;
+ _this.mutatedRelevantEvents = null;
+ _this.handlePointerDown = function (ev) {
+ var component = _this.component;
+ var segEl = _this.querySegEl(ev);
+ var seg = getElSeg(segEl);
+ var eventRange = _this.eventRange = seg.eventRange;
+ _this.dragging.minDistance = component.context.options.eventDragMinDistance;
+ // if touch, need to be working with a selected event
+ _this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) ||
+ (ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId));
+ };
+ _this.handleDragStart = function (ev) {
+ var context = _this.component.context;
+ var eventRange = _this.eventRange;
+ _this.relevantEvents = getRelevantEvents(context.getCurrentData().eventStore, _this.eventRange.instance.instanceId);
+ var segEl = _this.querySegEl(ev);
+ _this.draggingSegEl = segEl;
+ _this.draggingSeg = getElSeg(segEl);
+ context.calendarApi.unselect();
+ context.emitter.trigger('eventResizeStart', {
+ el: segEl,
+ event: new EventApi(context, eventRange.def, eventRange.instance),
+ jsEvent: ev.origEvent,
+ view: context.viewApi,
+ });
+ };
+ _this.handleHitUpdate = function (hit, isFinal, ev) {
+ var context = _this.component.context;
+ var relevantEvents = _this.relevantEvents;
+ var initialHit = _this.hitDragging.initialHit;
+ var eventInstance = _this.eventRange.instance;
+ var mutation = null;
+ var mutatedRelevantEvents = null;
+ var isInvalid = false;
+ var interaction = {
+ affectedEvents: relevantEvents,
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: true,
+ };
+ if (hit) {
+ mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-event-resizer-start'), eventInstance.range, context.pluginHooks.eventResizeJoinTransforms);
+ }
+ if (mutation) {
+ mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context);
+ interaction.mutatedEvents = mutatedRelevantEvents;
+ if (!_this.component.isInteractionValid(interaction)) {
+ isInvalid = true;
+ mutation = null;
+ mutatedRelevantEvents = null;
+ interaction.mutatedEvents = null;
+ }
+ }
+ if (mutatedRelevantEvents) {
+ context.dispatch({
+ type: 'SET_EVENT_RESIZE',
+ state: interaction,
+ });
+ }
+ else {
+ context.dispatch({ type: 'UNSET_EVENT_RESIZE' });
+ }
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ if (mutation && isHitsEqual(initialHit, hit)) {
+ mutation = null;
+ }
+ _this.validMutation = mutation;
+ _this.mutatedRelevantEvents = mutatedRelevantEvents;
+ }
+ };
+ _this.handleDragEnd = function (ev) {
+ var context = _this.component.context;
+ var eventDef = _this.eventRange.def;
+ var eventInstance = _this.eventRange.instance;
+ var eventApi = new EventApi(context, eventDef, eventInstance);
+ var relevantEvents = _this.relevantEvents;
+ var mutatedRelevantEvents = _this.mutatedRelevantEvents;
+ context.emitter.trigger('eventResizeStop', {
+ el: _this.draggingSegEl,
+ event: eventApi,
+ jsEvent: ev.origEvent,
+ view: context.viewApi,
+ });
+ if (_this.validMutation) {
+ var updatedEventApi = new EventApi(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
+ context.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: mutatedRelevantEvents,
+ });
+ var eventChangeArg = {
+ oldEvent: eventApi,
+ event: updatedEventApi,
+ relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance),
+ revert: function () {
+ context.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: relevantEvents,
+ });
+ },
+ };
+ context.emitter.trigger('eventResize', __assign(__assign({}, eventChangeArg), { el: _this.draggingSegEl, startDelta: _this.validMutation.startDelta || createDuration(0), endDelta: _this.validMutation.endDelta || createDuration(0), jsEvent: ev.origEvent, view: context.viewApi }));
+ context.emitter.trigger('eventChange', eventChangeArg);
+ }
+ else {
+ context.emitter.trigger('_noEventResize');
+ }
+ // reset all internal state
+ _this.draggingSeg = null;
+ _this.relevantEvents = null;
+ _this.validMutation = null;
+ // okay to keep eventInstance around. useful to set it in handlePointerDown
+ };
+ var component = settings.component;
+ var dragging = _this.dragging = new FeaturefulElementDragging(settings.el);
+ dragging.pointer.selector = '.fc-event-resizer';
+ dragging.touchScrollAllowed = false;
+ dragging.autoScroller.isEnabled = component.context.options.dragScroll;
+ var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings));
+ hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
+ hitDragging.emitter.on('dragstart', _this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
+ hitDragging.emitter.on('dragend', _this.handleDragEnd);
+ return _this;
+ }
+ EventResizing.prototype.destroy = function () {
+ this.dragging.destroy();
+ };
+ EventResizing.prototype.querySegEl = function (ev) {
+ return elementClosest(ev.subjectEl, '.fc-event');
+ };
+ return EventResizing;
+ }(Interaction));
+ function computeMutation(hit0, hit1, isFromStart, instanceRange, transforms) {
+ var dateEnv = hit0.component.context.dateEnv;
+ var date0 = hit0.dateSpan.range.start;
+ var date1 = hit1.dateSpan.range.start;
+ var delta = diffDates(date0, date1, dateEnv, hit0.component.largeUnit);
+ var props = {};
+ for (var _i = 0, transforms_1 = transforms; _i < transforms_1.length; _i++) {
+ var transform = transforms_1[_i];
+ var res = transform(hit0, hit1);
+ if (res === false) {
+ return null;
+ }
+ if (res) {
+ __assign(props, res);
+ }
+ }
+ if (isFromStart) {
+ if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) {
+ props.startDelta = delta;
+ return props;
+ }
+ }
+ else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) {
+ props.endDelta = delta;
+ return props;
+ }
+ return null;
+ }
+
+ var UnselectAuto = /** @class */ (function () {
+ function UnselectAuto(context) {
+ var _this = this;
+ this.context = context;
+ this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system
+ this.matchesCancel = false;
+ this.matchesEvent = false;
+ this.onSelect = function (selectInfo) {
+ if (selectInfo.jsEvent) {
+ _this.isRecentPointerDateSelect = true;
+ }
+ };
+ this.onDocumentPointerDown = function (pev) {
+ var unselectCancel = _this.context.options.unselectCancel;
+ var downEl = pev.origEvent.target;
+ _this.matchesCancel = !!elementClosest(downEl, unselectCancel);
+ _this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR); // interaction started on an event?
+ };
+ this.onDocumentPointerUp = function (pev) {
+ var context = _this.context;
+ var documentPointer = _this.documentPointer;
+ var calendarState = context.getCurrentData();
+ // touch-scrolling should never unfocus any type of selection
+ if (!documentPointer.wasTouchScroll) {
+ if (calendarState.dateSelection && // an existing date selection?
+ !_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
+ ) {
+ var unselectAuto = context.options.unselectAuto;
+ if (unselectAuto && (!unselectAuto || !_this.matchesCancel)) {
+ context.calendarApi.unselect(pev);
+ }
+ }
+ if (calendarState.eventSelection && // an existing event selected?
+ !_this.matchesEvent // interaction DIDN'T start on an event
+ ) {
+ context.dispatch({ type: 'UNSELECT_EVENT' });
+ }
+ }
+ _this.isRecentPointerDateSelect = false;
+ };
+ var documentPointer = this.documentPointer = new PointerDragging(document);
+ documentPointer.shouldIgnoreMove = true;
+ documentPointer.shouldWatchScroll = false;
+ documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown);
+ documentPointer.emitter.on('pointerup', this.onDocumentPointerUp);
+ /*
+ TODO: better way to know about whether there was a selection with the pointer
+ */
+ context.emitter.on('select', this.onSelect);
+ }
+ UnselectAuto.prototype.destroy = function () {
+ this.context.emitter.off('select', this.onSelect);
+ this.documentPointer.destroy();
+ };
+ return UnselectAuto;
+ }());
+
+ var OPTION_REFINERS = {
+ fixedMirrorParent: identity,
+ };
+ var LISTENER_REFINERS = {
+ dateClick: identity,
+ eventDragStart: identity,
+ eventDragStop: identity,
+ eventDrop: identity,
+ eventResizeStart: identity,
+ eventResizeStop: identity,
+ eventResize: identity,
+ drop: identity,
+ eventReceive: identity,
+ eventLeave: identity,
+ };
+
+ /*
+ Given an already instantiated draggable object for one-or-more elements,
+ Interprets any dragging as an attempt to drag an events that lives outside
+ of a calendar onto a calendar.
+ */
+ var ExternalElementDragging = /** @class */ (function () {
+ function ExternalElementDragging(dragging, suppliedDragMeta) {
+ var _this = this;
+ this.receivingContext = null;
+ this.droppableEvent = null; // will exist for all drags, even if create:false
+ this.suppliedDragMeta = null;
+ this.dragMeta = null;
+ this.handleDragStart = function (ev) {
+ _this.dragMeta = _this.buildDragMeta(ev.subjectEl);
+ };
+ this.handleHitUpdate = function (hit, isFinal, ev) {
+ var dragging = _this.hitDragging.dragging;
+ var receivingContext = null;
+ var droppableEvent = null;
+ var isInvalid = false;
+ var interaction = {
+ affectedEvents: createEmptyEventStore(),
+ mutatedEvents: createEmptyEventStore(),
+ isEvent: _this.dragMeta.create,
+ };
+ if (hit) {
+ receivingContext = hit.component.context;
+ if (_this.canDropElOnCalendar(ev.subjectEl, receivingContext)) {
+ droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingContext);
+ interaction.mutatedEvents = eventTupleToStore(droppableEvent);
+ isInvalid = !isInteractionValid(interaction, receivingContext);
+ if (isInvalid) {
+ interaction.mutatedEvents = createEmptyEventStore();
+ droppableEvent = null;
+ }
+ }
+ }
+ _this.displayDrag(receivingContext, interaction);
+ // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?)
+ // TODO: wish we could somehow wait for dispatch to guarantee render
+ dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror'));
+ if (!isInvalid) {
+ enableCursor();
+ }
+ else {
+ disableCursor();
+ }
+ if (!isFinal) {
+ dragging.setMirrorNeedsRevert(!droppableEvent);
+ _this.receivingContext = receivingContext;
+ _this.droppableEvent = droppableEvent;
+ }
+ };
+ this.handleDragEnd = function (pev) {
+ var _a = _this, receivingContext = _a.receivingContext, droppableEvent = _a.droppableEvent;
+ _this.clearDrag();
+ if (receivingContext && droppableEvent) {
+ var finalHit = _this.hitDragging.finalHit;
+ var finalView = finalHit.component.context.viewApi;
+ var dragMeta = _this.dragMeta;
+ receivingContext.emitter.trigger('drop', __assign(__assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView }));
+ if (dragMeta.create) {
+ var addingEvents_1 = eventTupleToStore(droppableEvent);
+ receivingContext.dispatch({
+ type: 'MERGE_EVENTS',
+ eventStore: addingEvents_1,
+ });
+ if (pev.isTouch) {
+ receivingContext.dispatch({
+ type: 'SELECT_EVENT',
+ eventInstanceId: droppableEvent.instance.instanceId,
+ });
+ }
+ // signal that an external event landed
+ receivingContext.emitter.trigger('eventReceive', {
+ event: new EventApi(receivingContext, droppableEvent.def, droppableEvent.instance),
+ relatedEvents: [],
+ revert: function () {
+ receivingContext.dispatch({
+ type: 'REMOVE_EVENTS',
+ eventStore: addingEvents_1,
+ });
+ },
+ draggedEl: pev.subjectEl,
+ view: finalView,
+ });
+ }
+ }
+ _this.receivingContext = null;
+ _this.droppableEvent = null;
+ };
+ var hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore);
+ hitDragging.requireInitial = false; // will start outside of a component
+ hitDragging.emitter.on('dragstart', this.handleDragStart);
+ hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
+ hitDragging.emitter.on('dragend', this.handleDragEnd);
+ this.suppliedDragMeta = suppliedDragMeta;
+ }
+ ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) {
+ if (typeof this.suppliedDragMeta === 'object') {
+ return parseDragMeta(this.suppliedDragMeta);
+ }
+ if (typeof this.suppliedDragMeta === 'function') {
+ return parseDragMeta(this.suppliedDragMeta(subjectEl));
+ }
+ return getDragMetaFromEl(subjectEl);
+ };
+ ExternalElementDragging.prototype.displayDrag = function (nextContext, state) {
+ var prevContext = this.receivingContext;
+ if (prevContext && prevContext !== nextContext) {
+ prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ if (nextContext) {
+ nextContext.dispatch({ type: 'SET_EVENT_DRAG', state: state });
+ }
+ };
+ ExternalElementDragging.prototype.clearDrag = function () {
+ if (this.receivingContext) {
+ this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
+ }
+ };
+ ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingContext) {
+ var dropAccept = receivingContext.options.dropAccept;
+ if (typeof dropAccept === 'function') {
+ return dropAccept.call(receivingContext.calendarApi, el);
+ }
+ if (typeof dropAccept === 'string' && dropAccept) {
+ return Boolean(elementMatches(el, dropAccept));
+ }
+ return true;
+ };
+ return ExternalElementDragging;
+ }());
+ // Utils for computing event store from the DragMeta
+ // ----------------------------------------------------------------------------------------------------
+ function computeEventForDateSpan(dateSpan, dragMeta, context) {
+ var defProps = __assign({}, dragMeta.leftoverProps);
+ for (var _i = 0, _a = context.pluginHooks.externalDefTransforms; _i < _a.length; _i++) {
+ var transform = _a[_i];
+ __assign(defProps, transform(dateSpan, dragMeta));
+ }
+ var _b = refineEventDef(defProps, context), refined = _b.refined, extra = _b.extra;
+ var def = parseEventDef(refined, extra, dragMeta.sourceId, dateSpan.allDay, context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd
+ context);
+ var start = dateSpan.range.start;
+ // only rely on time info if drop zone is all-day,
+ // otherwise, we already know the time
+ if (dateSpan.allDay && dragMeta.startTime) {
+ start = context.dateEnv.add(start, dragMeta.startTime);
+ }
+ var end = dragMeta.duration ?
+ context.dateEnv.add(start, dragMeta.duration) :
+ getDefaultEventEnd(dateSpan.allDay, start, context);
+ var instance = createEventInstance(def.defId, { start: start, end: end });
+ return { def: def, instance: instance };
+ }
+ // Utils for extracting data from element
+ // ----------------------------------------------------------------------------------------------------
+ function getDragMetaFromEl(el) {
+ var str = getEmbeddedElData(el, 'event');
+ var obj = str ?
+ JSON.parse(str) :
+ { create: false }; // if no embedded data, assume no event creation
+ return parseDragMeta(obj);
+ }
+ config.dataAttrPrefix = '';
+ function getEmbeddedElData(el, name) {
+ var prefix = config.dataAttrPrefix;
+ var prefixedName = (prefix ? prefix + '-' : '') + name;
+ return el.getAttribute('data-' + prefixedName) || '';
+ }
+
+ /*
+ Makes an element (that is *external* to any calendar) draggable.
+ Can pass in data that determines how an event will be created when dropped onto a calendar.
+ Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system.
+ */
+ var ExternalDraggable = /** @class */ (function () {
+ function ExternalDraggable(el, settings) {
+ var _this = this;
+ if (settings === void 0) { settings = {}; }
+ this.handlePointerDown = function (ev) {
+ var dragging = _this.dragging;
+ var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay;
+ dragging.minDistance =
+ minDistance != null ?
+ minDistance :
+ (ev.isTouch ? 0 : BASE_OPTION_DEFAULTS.eventDragMinDistance);
+ dragging.delay =
+ ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
+ (longPressDelay != null ? longPressDelay : BASE_OPTION_DEFAULTS.longPressDelay) :
+ 0;
+ };
+ this.handleDragStart = function (ev) {
+ if (ev.isTouch &&
+ _this.dragging.delay &&
+ ev.subjectEl.classList.contains('fc-event')) {
+ _this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected');
+ }
+ };
+ this.settings = settings;
+ var dragging = this.dragging = new FeaturefulElementDragging(el);
+ dragging.touchScrollAllowed = false;
+ if (settings.itemSelector != null) {
+ dragging.pointer.selector = settings.itemSelector;
+ }
+ if (settings.appendTo != null) {
+ dragging.mirror.parentNode = settings.appendTo; // TODO: write tests
+ }
+ dragging.emitter.on('pointerdown', this.handlePointerDown);
+ dragging.emitter.on('dragstart', this.handleDragStart);
+ new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new
+ }
+ ExternalDraggable.prototype.destroy = function () {
+ this.dragging.destroy();
+ };
+ return ExternalDraggable;
+ }());
+
+ /*
+ Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements.
+ The third-party system is responsible for drawing the visuals effects of the drag.
+ This class simply monitors for pointer movements and fires events.
+ It also has the ability to hide the moving element (the "mirror") during the drag.
+ */
+ var InferredElementDragging = /** @class */ (function (_super) {
+ __extends(InferredElementDragging, _super);
+ function InferredElementDragging(containerEl) {
+ var _this = _super.call(this, containerEl) || this;
+ _this.shouldIgnoreMove = false;
+ _this.mirrorSelector = '';
+ _this.currentMirrorEl = null;
+ _this.handlePointerDown = function (ev) {
+ _this.emitter.trigger('pointerdown', ev);
+ if (!_this.shouldIgnoreMove) {
+ // fire dragstart right away. does not support delay or min-distance
+ _this.emitter.trigger('dragstart', ev);
+ }
+ };
+ _this.handlePointerMove = function (ev) {
+ if (!_this.shouldIgnoreMove) {
+ _this.emitter.trigger('dragmove', ev);
+ }
+ };
+ _this.handlePointerUp = function (ev) {
+ _this.emitter.trigger('pointerup', ev);
+ if (!_this.shouldIgnoreMove) {
+ // fire dragend right away. does not support a revert animation
+ _this.emitter.trigger('dragend', ev);
+ }
+ };
+ var pointer = _this.pointer = new PointerDragging(containerEl);
+ pointer.emitter.on('pointerdown', _this.handlePointerDown);
+ pointer.emitter.on('pointermove', _this.handlePointerMove);
+ pointer.emitter.on('pointerup', _this.handlePointerUp);
+ return _this;
+ }
+ InferredElementDragging.prototype.destroy = function () {
+ this.pointer.destroy();
+ };
+ InferredElementDragging.prototype.setIgnoreMove = function (bool) {
+ this.shouldIgnoreMove = bool;
+ };
+ InferredElementDragging.prototype.setMirrorIsVisible = function (bool) {
+ if (bool) {
+ // restore a previously hidden element.
+ // use the reference in case the selector class has already been removed.
+ if (this.currentMirrorEl) {
+ this.currentMirrorEl.style.visibility = '';
+ this.currentMirrorEl = null;
+ }
+ }
+ else {
+ var mirrorEl = this.mirrorSelector ?
+ document.querySelector(this.mirrorSelector) :
+ null;
+ if (mirrorEl) {
+ this.currentMirrorEl = mirrorEl;
+ mirrorEl.style.visibility = 'hidden';
+ }
+ }
+ };
+ return InferredElementDragging;
+ }(ElementDragging));
+
+ /*
+ Bridges third-party drag-n-drop systems with FullCalendar.
+ Must be instantiated and destroyed by caller.
+ */
+ var ThirdPartyDraggable = /** @class */ (function () {
+ function ThirdPartyDraggable(containerOrSettings, settings) {
+ var containerEl = document;
+ if (
+ // wish we could just test instanceof EventTarget, but doesn't work in IE11
+ containerOrSettings === document ||
+ containerOrSettings instanceof Element) {
+ containerEl = containerOrSettings;
+ settings = settings || {};
+ }
+ else {
+ settings = (containerOrSettings || {});
+ }
+ var dragging = this.dragging = new InferredElementDragging(containerEl);
+ if (typeof settings.itemSelector === 'string') {
+ dragging.pointer.selector = settings.itemSelector;
+ }
+ else if (containerEl === document) {
+ dragging.pointer.selector = '[data-event]';
+ }
+ if (typeof settings.mirrorSelector === 'string') {
+ dragging.mirrorSelector = settings.mirrorSelector;
+ }
+ new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new
+ }
+ ThirdPartyDraggable.prototype.destroy = function () {
+ this.dragging.destroy();
+ };
+ return ThirdPartyDraggable;
+ }());
+
+ var interactionPlugin = createPlugin({
+ componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing],
+ calendarInteractions: [UnselectAuto],
+ elementDraggingImpl: FeaturefulElementDragging,
+ optionRefiners: OPTION_REFINERS,
+ listenerRefiners: LISTENER_REFINERS,
+ });
+
+ /* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells.
+ ----------------------------------------------------------------------------------------------------------------------*/
+ // It is a manager for a Table subcomponent, which does most of the heavy lifting.
+ // It is responsible for managing width/height.
+ var TableView = /** @class */ (function (_super) {
+ __extends(TableView, _super);
+ function TableView() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.headerElRef = createRef();
+ return _this;
+ }
+ TableView.prototype.renderSimpleLayout = function (headerRowContent, bodyContent) {
+ var _a = this, props = _a.props, context = _a.context;
+ var sections = [];
+ var stickyHeaderDates = getStickyHeaderDates(context.options);
+ if (headerRowContent) {
+ sections.push({
+ type: 'header',
+ key: 'header',
+ isSticky: stickyHeaderDates,
+ chunk: {
+ elRef: this.headerElRef,
+ tableClassName: 'fc-col-header',
+ rowContent: headerRowContent,
+ },
+ });
+ }
+ sections.push({
+ type: 'body',
+ key: 'body',
+ liquid: true,
+ chunk: { content: bodyContent },
+ });
+ return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: ['fc-daygrid'].concat(classNames).join(' ') },
+ createElement(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, cols: [] /* TODO: make optional? */, sections: sections }))); }));
+ };
+ TableView.prototype.renderHScrollLayout = function (headerRowContent, bodyContent, colCnt, dayMinWidth) {
+ var ScrollGrid = this.context.pluginHooks.scrollGridImpl;
+ if (!ScrollGrid) {
+ throw new Error('No ScrollGrid implementation');
+ }
+ var _a = this, props = _a.props, context = _a.context;
+ var stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options);
+ var stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options);
+ var sections = [];
+ if (headerRowContent) {
+ sections.push({
+ type: 'header',
+ key: 'header',
+ isSticky: stickyHeaderDates,
+ chunks: [{
+ key: 'main',
+ elRef: this.headerElRef,
+ tableClassName: 'fc-col-header',
+ rowContent: headerRowContent,
+ }],
+ });
+ }
+ sections.push({
+ type: 'body',
+ key: 'body',
+ liquid: true,
+ chunks: [{
+ key: 'main',
+ content: bodyContent,
+ }],
+ });
+ if (stickyFooterScrollbar) {
+ sections.push({
+ type: 'footer',
+ key: 'footer',
+ isSticky: true,
+ chunks: [{
+ key: 'main',
+ content: renderScrollShim,
+ }],
+ });
+ }
+ return (createElement(ViewRoot, { viewSpec: context.viewSpec }, function (rootElRef, classNames) { return (createElement("div", { ref: rootElRef, className: ['fc-daygrid'].concat(classNames).join(' ') },
+ createElement(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, colGroups: [{ cols: [{ span: colCnt, minWidth: dayMinWidth }] }], sections: sections }))); }));
+ };
+ return TableView;
+ }(DateComponent));
+
+ function splitSegsByRow(segs, rowCnt) {
+ var byRow = [];
+ for (var i = 0; i < rowCnt; i += 1) {
+ byRow[i] = [];
+ }
+ for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
+ var seg = segs_1[_i];
+ byRow[seg.row].push(seg);
+ }
+ return byRow;
+ }
+ function splitSegsByFirstCol(segs, colCnt) {
+ var byCol = [];
+ for (var i = 0; i < colCnt; i += 1) {
+ byCol[i] = [];
+ }
+ for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
+ var seg = segs_2[_i];
+ byCol[seg.firstCol].push(seg);
+ }
+ return byCol;
+ }
+ function splitInteractionByRow(ui, rowCnt) {
+ var byRow = [];
+ if (!ui) {
+ for (var i = 0; i < rowCnt; i += 1) {
+ byRow[i] = null;
+ }
+ }
+ else {
+ for (var i = 0; i < rowCnt; i += 1) {
+ byRow[i] = {
+ affectedInstances: ui.affectedInstances,
+ isEvent: ui.isEvent,
+ segs: [],
+ };
+ }
+ for (var _i = 0, _a = ui.segs; _i < _a.length; _i++) {
+ var seg = _a[_i];
+ byRow[seg.row].segs.push(seg);
+ }
+ }
+ return byRow;
+ }
+
+ var TableCellTop = /** @class */ (function (_super) {
+ __extends(TableCellTop, _super);
+ function TableCellTop() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ TableCellTop.prototype.render = function () {
+ var props = this.props;
+ var navLinkAttrs = this.context.options.navLinks
+ ? { 'data-navlink': buildNavLinkData(props.date), tabIndex: 0 }
+ : {};
+ return (createElement(DayCellContent, { date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, extraHookProps: props.extraHookProps, defaultContent: renderTopInner }, function (innerElRef, innerContent) { return ((innerContent || props.forceDayTop) && (createElement("div", { className: "fc-daygrid-day-top", ref: innerElRef },
+ createElement("a", __assign({ className: "fc-daygrid-day-number" }, navLinkAttrs), innerContent || createElement(Fragment, null, "\u00A0"))))); }));
+ };
+ return TableCellTop;
+ }(BaseComponent));
+ function renderTopInner(props) {
+ return props.dayNumberText;
+ }
+
+ var DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'narrow' });
+ var TableCell = /** @class */ (function (_super) {
+ __extends(TableCell, _super);
+ function TableCell() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.handleRootEl = function (el) {
+ _this.rootEl = el;
+ setRef(_this.props.elRef, el);
+ };
+ _this.handleMoreLinkClick = function (ev) {
+ var props = _this.props;
+ if (props.onMoreClick) {
+ var allSegs = props.segsByEachCol;
+ var hiddenSegs = allSegs.filter(function (seg) { return props.segIsHidden[seg.eventRange.instance.instanceId]; });
+ props.onMoreClick({
+ date: props.date,
+ allSegs: allSegs,
+ hiddenSegs: hiddenSegs,
+ moreCnt: props.moreCnt,
+ dayEl: _this.rootEl,
+ ev: ev,
+ });
+ }
+ };
+ return _this;
+ }
+ TableCell.prototype.render = function () {
+ var _this = this;
+ var _a = this.context, options = _a.options, viewApi = _a.viewApi;
+ var props = this.props;
+ var date = props.date, dateProfile = props.dateProfile;
+ var hookProps = {
+ num: props.moreCnt,
+ text: props.buildMoreLinkText(props.moreCnt),
+ view: viewApi,
+ };
+ var navLinkAttrs = options.navLinks
+ ? { 'data-navlink': buildNavLinkData(date, 'week'), tabIndex: 0 }
+ : {};
+ return (createElement(DayCellRoot, { date: date, dateProfile: dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, extraHookProps: props.extraHookProps, elRef: this.handleRootEl }, function (dayElRef, dayClassNames, rootDataAttrs, isDisabled) { return (createElement("td", __assign({ ref: dayElRef, className: ['fc-daygrid-day'].concat(dayClassNames, props.extraClassNames || []).join(' ') }, rootDataAttrs, props.extraDataAttrs),
+ createElement("div", { className: "fc-daygrid-day-frame fc-scrollgrid-sync-inner", ref: props.innerElRef /* different from hook system! RENAME */ },
+ props.showWeekNumber && (createElement(WeekNumberRoot, { date: date, defaultFormat: DEFAULT_WEEK_NUM_FORMAT }, function (weekElRef, weekClassNames, innerElRef, innerContent) { return (createElement("a", __assign({ ref: weekElRef, className: ['fc-daygrid-week-number'].concat(weekClassNames).join(' ') }, navLinkAttrs), innerContent)); })),
+ !isDisabled && (createElement(TableCellTop, { date: date, dateProfile: dateProfile, showDayNumber: props.showDayNumber, forceDayTop: props.forceDayTop, todayRange: props.todayRange, extraHookProps: props.extraHookProps })),
+ createElement("div", { className: "fc-daygrid-day-events", ref: props.fgContentElRef, style: { paddingBottom: props.fgPaddingBottom } },
+ props.fgContent,
+ Boolean(props.moreCnt) && (createElement("div", { className: "fc-daygrid-day-bottom", style: { marginTop: props.moreMarginTop } },
+ createElement(RenderHook, { hookProps: hookProps, classNames: options.moreLinkClassNames, content: options.moreLinkContent, defaultContent: renderMoreLinkInner, didMount: options.moreLinkDidMount, willUnmount: options.moreLinkWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("a", { ref: rootElRef, className: ['fc-daygrid-more-link'].concat(classNames).join(' '), onClick: _this.handleMoreLinkClick }, innerContent)); })))),
+ createElement("div", { className: "fc-daygrid-day-bg" }, props.bgContent)))); }));
+ };
+ return TableCell;
+ }(DateComponent));
+ TableCell.addPropsEquality({
+ onMoreClick: true,
+ });
+ function renderMoreLinkInner(props) {
+ return props.text;
+ }
+
+ var DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: true,
+ meridiem: 'narrow',
+ });
+ function hasListItemDisplay(seg) {
+ var display = seg.eventRange.ui.display;
+ return display === 'list-item' || (display === 'auto' &&
+ !seg.eventRange.def.allDay &&
+ seg.firstCol === seg.lastCol && // can't be multi-day
+ seg.isStart && // "
+ seg.isEnd // "
+ );
+ }
+
+ var TableListItemEvent = /** @class */ (function (_super) {
+ __extends(TableListItemEvent, _super);
+ function TableListItemEvent() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ TableListItemEvent.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ var timeFormat = context.options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT;
+ var timeText = buildSegTimeText(props.seg, timeFormat, context, true, props.defaultDisplayEventEnd);
+ return (createElement(EventRoot, { seg: props.seg, timeText: timeText, defaultContent: renderInnerContent$2, isDragging: props.isDragging, isResizing: false, isDateSelecting: false, isSelected: props.isSelected, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday }, function (rootElRef, classNames, innerElRef, innerContent) { return ( // we don't use styles!
+ createElement("a", __assign({ className: ['fc-daygrid-event', 'fc-daygrid-dot-event'].concat(classNames).join(' '), ref: rootElRef }, getSegAnchorAttrs$1(props.seg)), innerContent)); }));
+ };
+ return TableListItemEvent;
+ }(BaseComponent));
+ function renderInnerContent$2(innerProps) {
+ return (createElement(Fragment, null,
+ createElement("div", { className: "fc-daygrid-event-dot", style: { borderColor: innerProps.borderColor || innerProps.backgroundColor } }),
+ innerProps.timeText && (createElement("div", { className: "fc-event-time" }, innerProps.timeText)),
+ createElement("div", { className: "fc-event-title" }, innerProps.event.title || createElement(Fragment, null, "\u00A0"))));
+ }
+ function getSegAnchorAttrs$1(seg) {
+ var url = seg.eventRange.def.url;
+ return url ? { href: url } : {};
+ }
+
+ var TableBlockEvent = /** @class */ (function (_super) {
+ __extends(TableBlockEvent, _super);
+ function TableBlockEvent() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ TableBlockEvent.prototype.render = function () {
+ var props = this.props;
+ return (createElement(StandardEvent, __assign({}, props, { extraClassNames: ['fc-daygrid-event', 'fc-daygrid-block-event', 'fc-h-event'], defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: props.defaultDisplayEventEnd, disableResizing: !props.seg.eventRange.def.allDay })));
+ };
+ return TableBlockEvent;
+ }(BaseComponent));
+
+ function computeFgSegPlacement(// for one row. TODO: print mode?
+ cellModels, segs, dayMaxEvents, dayMaxEventRows, eventHeights, maxContentHeight, colCnt, eventOrderSpecs) {
+ var colPlacements = []; // if event spans multiple cols, its present in each col
+ var moreCnts = []; // by-col
+ var segIsHidden = {};
+ var segTops = {}; // always populated for each seg
+ var segMarginTops = {}; // simetimes populated for each seg
+ var moreTops = {};
+ var paddingBottoms = {}; // for each cell's inner-wrapper div
+ for (var i = 0; i < colCnt; i += 1) {
+ colPlacements.push([]);
+ moreCnts.push(0);
+ }
+ segs = sortEventSegs(segs, eventOrderSpecs);
+ for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
+ var seg = segs_1[_i];
+ var instanceId = seg.eventRange.instance.instanceId;
+ var eventHeight = eventHeights[instanceId + ':' + seg.firstCol];
+ placeSeg(seg, eventHeight || 0); // will keep colPlacements sorted by top
+ }
+ if (dayMaxEvents === true || dayMaxEventRows === true) {
+ limitByMaxHeight(moreCnts, segIsHidden, colPlacements, maxContentHeight); // populates moreCnts/segIsHidden
+ }
+ else if (typeof dayMaxEvents === 'number') {
+ limitByMaxEvents(moreCnts, segIsHidden, colPlacements, dayMaxEvents); // populates moreCnts/segIsHidden
+ }
+ else if (typeof dayMaxEventRows === 'number') {
+ limitByMaxRows(moreCnts, segIsHidden, colPlacements, dayMaxEventRows); // populates moreCnts/segIsHidden
+ }
+ // computes segTops/segMarginTops/moreTops/paddingBottoms
+ for (var col = 0; col < colCnt; col += 1) {
+ var placements = colPlacements[col];
+ var currentNonAbsBottom = 0;
+ var currentAbsHeight = 0;
+ for (var _a = 0, placements_1 = placements; _a < placements_1.length; _a++) {
+ var placement = placements_1[_a];
+ var seg = placement.seg;
+ if (!segIsHidden[seg.eventRange.instance.instanceId]) {
+ segTops[seg.eventRange.instance.instanceId] = placement.top; // from top of container
+ if (seg.firstCol === seg.lastCol && seg.isStart && seg.isEnd) { // TODO: simpler way? NOT DRY
+ segMarginTops[seg.eventRange.instance.instanceId] =
+ placement.top - currentNonAbsBottom; // from previous seg bottom
+ currentAbsHeight = 0;
+ currentNonAbsBottom = placement.bottom;
+ }
+ else { // multi-col event, abs positioned
+ currentAbsHeight = placement.bottom - currentNonAbsBottom;
+ }
+ }
+ }
+ if (currentAbsHeight) {
+ if (moreCnts[col]) {
+ moreTops[col] = currentAbsHeight;
+ }
+ else {
+ paddingBottoms[col] = currentAbsHeight;
+ }
+ }
+ }
+ function placeSeg(seg, segHeight) {
+ if (!tryPlaceSegAt(seg, segHeight, 0)) {
+ for (var col = seg.firstCol; col <= seg.lastCol; col += 1) {
+ for (var _i = 0, _a = colPlacements[col]; _i < _a.length; _i++) { // will repeat multi-day segs!!!!!!! bad!!!!!!
+ var placement = _a[_i];
+ if (tryPlaceSegAt(seg, segHeight, placement.bottom)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ function tryPlaceSegAt(seg, segHeight, top) {
+ if (canPlaceSegAt(seg, segHeight, top)) {
+ for (var col = seg.firstCol; col <= seg.lastCol; col += 1) {
+ var placements = colPlacements[col];
+ var insertionIndex = 0;
+ while (insertionIndex < placements.length &&
+ top >= placements[insertionIndex].top) {
+ insertionIndex += 1;
+ }
+ placements.splice(insertionIndex, 0, {
+ seg: seg,
+ top: top,
+ bottom: top + segHeight,
+ });
+ }
+ return true;
+ }
+ return false;
+ }
+ function canPlaceSegAt(seg, segHeight, top) {
+ for (var col = seg.firstCol; col <= seg.lastCol; col += 1) {
+ for (var _i = 0, _a = colPlacements[col]; _i < _a.length; _i++) {
+ var placement = _a[_i];
+ if (top < placement.bottom && top + segHeight > placement.top) { // collide?
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ // what does this do!?
+ for (var instanceIdAndFirstCol in eventHeights) {
+ if (!eventHeights[instanceIdAndFirstCol]) {
+ segIsHidden[instanceIdAndFirstCol.split(':')[0]] = true;
+ }
+ }
+ var segsByFirstCol = colPlacements.map(extractFirstColSegs); // operates on the sorted cols
+ var segsByEachCol = colPlacements.map(function (placements, col) {
+ var segsForCols = extractAllColSegs(placements);
+ segsForCols = resliceDaySegs(segsForCols, cellModels[col].date, col);
+ return segsForCols;
+ });
+ return {
+ segsByFirstCol: segsByFirstCol,
+ segsByEachCol: segsByEachCol,
+ segIsHidden: segIsHidden,
+ segTops: segTops,
+ segMarginTops: segMarginTops,
+ moreCnts: moreCnts,
+ moreTops: moreTops,
+ paddingBottoms: paddingBottoms,
+ };
+ }
+ function extractFirstColSegs(oneColPlacements, col) {
+ var segs = [];
+ for (var _i = 0, oneColPlacements_1 = oneColPlacements; _i < oneColPlacements_1.length; _i++) {
+ var placement = oneColPlacements_1[_i];
+ if (placement.seg.firstCol === col) {
+ segs.push(placement.seg);
+ }
+ }
+ return segs;
+ }
+ function extractAllColSegs(oneColPlacements) {
+ var segs = [];
+ for (var _i = 0, oneColPlacements_2 = oneColPlacements; _i < oneColPlacements_2.length; _i++) {
+ var placement = oneColPlacements_2[_i];
+ segs.push(placement.seg);
+ }
+ return segs;
+ }
+ function limitByMaxHeight(hiddenCnts, segIsHidden, colPlacements, maxContentHeight) {
+ limitEvents(hiddenCnts, segIsHidden, colPlacements, true, function (placement) { return placement.bottom <= maxContentHeight; });
+ }
+ function limitByMaxEvents(hiddenCnts, segIsHidden, colPlacements, dayMaxEvents) {
+ limitEvents(hiddenCnts, segIsHidden, colPlacements, false, function (placement, levelIndex) { return levelIndex < dayMaxEvents; });
+ }
+ function limitByMaxRows(hiddenCnts, segIsHidden, colPlacements, dayMaxEventRows) {
+ limitEvents(hiddenCnts, segIsHidden, colPlacements, true, function (placement, levelIndex) { return levelIndex < dayMaxEventRows; });
+ }
+ /*
+ populates the given hiddenCnts/segIsHidden, which are supplied empty.
+ TODO: return them instead
+ */
+ function limitEvents(hiddenCnts, segIsHidden, colPlacements, _moreLinkConsumesLevel, isPlacementInBounds) {
+ var colCnt = hiddenCnts.length;
+ var segIsVisible = {}; // TODO: instead, use segIsHidden with true/false?
+ var visibleColPlacements = []; // will mirror colPlacements
+ for (var col = 0; col < colCnt; col += 1) {
+ visibleColPlacements.push([]);
+ }
+ for (var col = 0; col < colCnt; col += 1) {
+ var placements = colPlacements[col];
+ var level = 0;
+ for (var _i = 0, placements_2 = placements; _i < placements_2.length; _i++) {
+ var placement = placements_2[_i];
+ if (isPlacementInBounds(placement, level)) {
+ recordVisible(placement);
+ }
+ else {
+ recordHidden(placement, level, _moreLinkConsumesLevel);
+ }
+ // only considered a level if the seg had height
+ if (placement.top !== placement.bottom) {
+ level += 1;
+ }
+ }
+ }
+ function recordVisible(placement) {
+ var seg = placement.seg;
+ var instanceId = seg.eventRange.instance.instanceId;
+ if (!segIsVisible[instanceId]) {
+ segIsVisible[instanceId] = true;
+ for (var col = seg.firstCol; col <= seg.lastCol; col += 1) {
+ var destPlacements = visibleColPlacements[col];
+ var newPosition = 0;
+ // insert while keeping top sorted in each column
+ while (newPosition < destPlacements.length &&
+ placement.top >= destPlacements[newPosition].top) {
+ newPosition += 1;
+ }
+ destPlacements.splice(newPosition, 0, placement);
+ }
+ }
+ }
+ function recordHidden(placement, currentLevel, moreLinkConsumesLevel) {
+ var seg = placement.seg;
+ var instanceId = seg.eventRange.instance.instanceId;
+ if (!segIsHidden[instanceId]) {
+ segIsHidden[instanceId] = true;
+ for (var col = seg.firstCol; col <= seg.lastCol; col += 1) {
+ hiddenCnts[col] += 1;
+ var hiddenCnt = hiddenCnts[col];
+ if (moreLinkConsumesLevel && hiddenCnt === 1 && currentLevel > 0) {
+ var doomedLevel = currentLevel - 1;
+ while (visibleColPlacements[col].length > doomedLevel) {
+ recordHidden(visibleColPlacements[col].pop(), // removes
+ visibleColPlacements[col].length, // will execute after the pop. will be the index of the removed placement
+ false);
+ }
+ }
+ }
+ }
+ }
+ }
+ // Given the events within an array of segment objects, reslice them to be in a single day
+ function resliceDaySegs(segs, dayDate, colIndex) {
+ var dayStart = dayDate;
+ var dayEnd = addDays(dayStart, 1);
+ var dayRange = { start: dayStart, end: dayEnd };
+ var newSegs = [];
+ for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
+ var seg = segs_2[_i];
+ var eventRange = seg.eventRange;
+ var origRange = eventRange.range;
+ var slicedRange = intersectRanges(origRange, dayRange);
+ if (slicedRange) {
+ newSegs.push(__assign(__assign({}, seg), { firstCol: colIndex, lastCol: colIndex, eventRange: {
+ def: eventRange.def,
+ ui: __assign(__assign({}, eventRange.ui), { durationEditable: false }),
+ instance: eventRange.instance,
+ range: slicedRange,
+ }, isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf() }));
+ }
+ }
+ return newSegs;
+ }
+
+ var TableRow = /** @class */ (function (_super) {
+ __extends(TableRow, _super);
+ function TableRow() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.cellElRefs = new RefMap(); // the
+ _this.frameElRefs = new RefMap(); // the fc-daygrid-day-frame
+ _this.fgElRefs = new RefMap(); // the fc-daygrid-day-events
+ _this.segHarnessRefs = new RefMap(); // indexed by "instanceId:firstCol"
+ _this.rootElRef = createRef();
+ _this.state = {
+ framePositions: null,
+ maxContentHeight: null,
+ segHeights: {},
+ };
+ return _this;
+ }
+ TableRow.prototype.render = function () {
+ var _this = this;
+ var _a = this, props = _a.props, state = _a.state, context = _a.context;
+ var colCnt = props.cells.length;
+ var businessHoursByCol = splitSegsByFirstCol(props.businessHourSegs, colCnt);
+ var bgEventSegsByCol = splitSegsByFirstCol(props.bgEventSegs, colCnt);
+ var highlightSegsByCol = splitSegsByFirstCol(this.getHighlightSegs(), colCnt);
+ var mirrorSegsByCol = splitSegsByFirstCol(this.getMirrorSegs(), colCnt);
+ var _b = computeFgSegPlacement(props.cells, props.fgEventSegs, props.dayMaxEvents, props.dayMaxEventRows, state.segHeights, state.maxContentHeight, colCnt, context.options.eventOrder), paddingBottoms = _b.paddingBottoms, segsByFirstCol = _b.segsByFirstCol, segsByEachCol = _b.segsByEachCol, segIsHidden = _b.segIsHidden, segTops = _b.segTops, segMarginTops = _b.segMarginTops, moreCnts = _b.moreCnts, moreTops = _b.moreTops;
+ var selectedInstanceHash = // TODO: messy way to compute this
+ (props.eventDrag && props.eventDrag.affectedInstances) ||
+ (props.eventResize && props.eventResize.affectedInstances) ||
+ {};
+ return (createElement("tr", { ref: this.rootElRef },
+ props.renderIntro && props.renderIntro(),
+ props.cells.map(function (cell, col) {
+ var normalFgNodes = _this.renderFgSegs(segsByFirstCol[col], segIsHidden, segTops, segMarginTops, selectedInstanceHash, props.todayRange);
+ var mirrorFgNodes = _this.renderFgSegs(mirrorSegsByCol[col], {}, segTops, // use same tops as real rendering
+ {}, {}, props.todayRange, Boolean(props.eventDrag), Boolean(props.eventResize), false);
+ return (createElement(TableCell, { key: cell.key, elRef: _this.cellElRefs.createRef(cell.key), innerElRef: _this.frameElRefs.createRef(cell.key) /* FF
problem, but okay to use for left/right. TODO: rename prop */, dateProfile: props.dateProfile, date: cell.date, showDayNumber: props.showDayNumbers, showWeekNumber: props.showWeekNumbers && col === 0, forceDayTop: props.showWeekNumbers /* even displaying weeknum for row, not necessarily day */, todayRange: props.todayRange, extraHookProps: cell.extraHookProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, moreCnt: moreCnts[col], buildMoreLinkText: props.buildMoreLinkText, onMoreClick: function (arg) {
+ props.onMoreClick(__assign(__assign({}, arg), { fromCol: col }));
+ }, segIsHidden: segIsHidden, moreMarginTop: moreTops[col] /* rename */, segsByEachCol: segsByEachCol[col], fgPaddingBottom: paddingBottoms[col], fgContentElRef: _this.fgElRefs.createRef(cell.key), fgContent: ( // Fragment scopes the keys
+ createElement(Fragment, null,
+ createElement(Fragment, null, normalFgNodes),
+ createElement(Fragment, null, mirrorFgNodes))), bgContent: ( // Fragment scopes the keys
+ createElement(Fragment, null,
+ _this.renderFillSegs(highlightSegsByCol[col], 'highlight'),
+ _this.renderFillSegs(businessHoursByCol[col], 'non-business'),
+ _this.renderFillSegs(bgEventSegsByCol[col], 'bg-event'))) }));
+ })));
+ };
+ TableRow.prototype.componentDidMount = function () {
+ this.updateSizing(true);
+ };
+ TableRow.prototype.componentDidUpdate = function (prevProps, prevState) {
+ var currentProps = this.props;
+ this.updateSizing(!isPropsEqual(prevProps, currentProps));
+ };
+ TableRow.prototype.getHighlightSegs = function () {
+ var props = this.props;
+ if (props.eventDrag && props.eventDrag.segs.length) { // messy check
+ return props.eventDrag.segs;
+ }
+ if (props.eventResize && props.eventResize.segs.length) { // messy check
+ return props.eventResize.segs;
+ }
+ return props.dateSelectionSegs;
+ };
+ TableRow.prototype.getMirrorSegs = function () {
+ var props = this.props;
+ if (props.eventResize && props.eventResize.segs.length) { // messy check
+ return props.eventResize.segs;
+ }
+ return [];
+ };
+ TableRow.prototype.renderFgSegs = function (segs, segIsHidden, // does NOT mean display:hidden
+ segTops, segMarginTops, selectedInstanceHash, todayRange, isDragging, isResizing, isDateSelecting) {
+ var context = this.context;
+ var eventSelection = this.props.eventSelection;
+ var framePositions = this.state.framePositions;
+ var defaultDisplayEventEnd = this.props.cells.length === 1; // colCnt === 1
+ var nodes = [];
+ if (framePositions) {
+ for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
+ var seg = segs_1[_i];
+ var instanceId = seg.eventRange.instance.instanceId;
+ var isMirror = isDragging || isResizing || isDateSelecting;
+ var isSelected = selectedInstanceHash[instanceId];
+ var isInvisible = segIsHidden[instanceId] || isSelected;
+ // TODO: simpler way? NOT DRY
+ var isAbsolute = segIsHidden[instanceId] || isMirror || seg.firstCol !== seg.lastCol || !seg.isStart || !seg.isEnd;
+ var marginTop = void 0;
+ var top_1 = void 0;
+ var left = void 0;
+ var right = void 0;
+ if (isAbsolute) {
+ top_1 = segTops[instanceId];
+ if (context.isRtl) {
+ right = 0;
+ left = framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol];
+ }
+ else {
+ left = 0;
+ right = framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol];
+ }
+ }
+ else {
+ marginTop = segMarginTops[instanceId];
+ }
+ /*
+ known bug: events that are force to be list-item but span multiple days still take up space in later columns
+ */
+ nodes.push(createElement("div", { className: 'fc-daygrid-event-harness' + (isAbsolute ? ' fc-daygrid-event-harness-abs' : ''), key: instanceId,
+ // in print mode when in mult cols, could collide
+ ref: isMirror ? null : this.segHarnessRefs.createRef(instanceId + ':' + seg.firstCol), style: {
+ visibility: isInvisible ? 'hidden' : '',
+ marginTop: marginTop || '',
+ top: top_1 || '',
+ left: left || '',
+ right: right || '',
+ } }, hasListItemDisplay(seg) ? (createElement(TableListItemEvent, __assign({ seg: seg, isDragging: isDragging, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange)))) : (createElement(TableBlockEvent, __assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange))))));
+ }
+ }
+ return nodes;
+ };
+ TableRow.prototype.renderFillSegs = function (segs, fillType) {
+ var isRtl = this.context.isRtl;
+ var todayRange = this.props.todayRange;
+ var framePositions = this.state.framePositions;
+ var nodes = [];
+ if (framePositions) {
+ for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
+ var seg = segs_2[_i];
+ var leftRightCss = isRtl ? {
+ right: 0,
+ left: framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol],
+ } : {
+ left: 0,
+ right: framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol],
+ };
+ nodes.push(createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-daygrid-bg-harness", style: leftRightCss }, fillType === 'bg-event' ?
+ createElement(BgEvent, __assign({ seg: seg }, getSegMeta(seg, todayRange))) :
+ renderFill(fillType)));
+ }
+ }
+ return createElement.apply(void 0, __spreadArrays([Fragment, {}], nodes));
+ };
+ TableRow.prototype.updateSizing = function (isExternalSizingChange) {
+ var _a = this, props = _a.props, frameElRefs = _a.frameElRefs;
+ if (props.clientWidth !== null) { // positioning ready?
+ if (isExternalSizingChange) {
+ var frameEls = props.cells.map(function (cell) { return frameElRefs.currentMap[cell.key]; });
+ if (frameEls.length) {
+ var originEl = this.rootElRef.current;
+ this.setState({
+ framePositions: new PositionCache(originEl, frameEls, true, // isHorizontal
+ false),
+ });
+ }
+ }
+ var limitByContentHeight = props.dayMaxEvents === true || props.dayMaxEventRows === true;
+ this.setState({
+ segHeights: this.computeSegHeights(),
+ maxContentHeight: limitByContentHeight ? this.computeMaxContentHeight() : null,
+ });
+ }
+ };
+ TableRow.prototype.computeSegHeights = function () {
+ return mapHash(this.segHarnessRefs.currentMap, function (eventHarnessEl) { return (eventHarnessEl.getBoundingClientRect().height); });
+ };
+ TableRow.prototype.computeMaxContentHeight = function () {
+ var firstKey = this.props.cells[0].key;
+ var cellEl = this.cellElRefs.currentMap[firstKey];
+ var fcContainerEl = this.fgElRefs.currentMap[firstKey];
+ return cellEl.getBoundingClientRect().bottom - fcContainerEl.getBoundingClientRect().top;
+ };
+ TableRow.prototype.getCellEls = function () {
+ var elMap = this.cellElRefs.currentMap;
+ return this.props.cells.map(function (cell) { return elMap[cell.key]; });
+ };
+ return TableRow;
+ }(DateComponent));
+ TableRow.addPropsEquality({
+ onMoreClick: true,
+ });
+ TableRow.addStateEquality({
+ segHeights: isPropsEqual,
+ });
+
+ var PADDING_FROM_VIEWPORT = 10;
+ var SCROLL_DEBOUNCE = 10;
+ var Popover = /** @class */ (function (_super) {
+ __extends(Popover, _super);
+ function Popover() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.repositioner = new DelayedRunner(_this.updateSize.bind(_this));
+ _this.handleRootEl = function (el) {
+ _this.rootEl = el;
+ if (_this.props.elRef) {
+ setRef(_this.props.elRef, el);
+ }
+ };
+ // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
+ _this.handleDocumentMousedown = function (ev) {
+ var onClose = _this.props.onClose;
+ // only hide the popover if the click happened outside the popover
+ if (onClose && !_this.rootEl.contains(ev.target)) {
+ onClose();
+ }
+ };
+ _this.handleDocumentScroll = function () {
+ _this.repositioner.request(SCROLL_DEBOUNCE);
+ };
+ _this.handleCloseClick = function () {
+ var onClose = _this.props.onClose;
+ if (onClose) {
+ onClose();
+ }
+ };
+ return _this;
+ }
+ Popover.prototype.render = function () {
+ var theme = this.context.theme;
+ var props = this.props;
+ var classNames = [
+ 'fc-popover',
+ theme.getClass('popover'),
+ ].concat(props.extraClassNames || []);
+ return (createElement("div", __assign({ className: classNames.join(' ') }, props.extraAttrs, { ref: this.handleRootEl }),
+ createElement("div", { className: 'fc-popover-header ' + theme.getClass('popoverHeader') },
+ createElement("span", { className: "fc-popover-title" }, props.title),
+ createElement("span", { className: 'fc-popover-close ' + theme.getIconClass('close'), onClick: this.handleCloseClick })),
+ createElement("div", { className: 'fc-popover-body ' + theme.getClass('popoverContent') }, props.children)));
+ };
+ Popover.prototype.componentDidMount = function () {
+ document.addEventListener('mousedown', this.handleDocumentMousedown);
+ document.addEventListener('scroll', this.handleDocumentScroll);
+ this.updateSize();
+ };
+ Popover.prototype.componentWillUnmount = function () {
+ document.removeEventListener('mousedown', this.handleDocumentMousedown);
+ document.removeEventListener('scroll', this.handleDocumentScroll);
+ };
+ // TODO: adjust on window resize
+ /*
+ NOTE: the popover is position:fixed, so coordinates are relative to the viewport
+ NOTE: the PARENT calls this as well, on window resize. we would have wanted to use the repositioner,
+ but need to ensure that all other components have updated size first (for alignmentEl)
+ */
+ Popover.prototype.updateSize = function () {
+ var _a = this.props, alignmentEl = _a.alignmentEl, topAlignmentEl = _a.topAlignmentEl;
+ var rootEl = this.rootEl;
+ if (!rootEl) {
+ return; // not sure why this was null, but we shouldn't let external components call updateSize() anyway
+ }
+ var dims = rootEl.getBoundingClientRect(); // only used for width,height
+ var alignment = alignmentEl.getBoundingClientRect();
+ var top = topAlignmentEl ? topAlignmentEl.getBoundingClientRect().top : alignment.top;
+ top = Math.min(top, window.innerHeight - dims.height - PADDING_FROM_VIEWPORT);
+ top = Math.max(top, PADDING_FROM_VIEWPORT);
+ var left;
+ if (this.context.isRtl) {
+ left = alignment.right - dims.width;
+ }
+ else {
+ left = alignment.left;
+ }
+ left = Math.min(left, window.innerWidth - dims.width - PADDING_FROM_VIEWPORT);
+ left = Math.max(left, PADDING_FROM_VIEWPORT);
+ applyStyle(rootEl, { top: top, left: left });
+ };
+ return Popover;
+ }(BaseComponent));
+
+ var MorePopover = /** @class */ (function (_super) {
+ __extends(MorePopover, _super);
+ function MorePopover() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.rootElRef = createRef();
+ return _this;
+ }
+ MorePopover.prototype.render = function () {
+ var _a = this.context, options = _a.options, dateEnv = _a.dateEnv;
+ var props = this.props;
+ var date = props.date, hiddenInstances = props.hiddenInstances, todayRange = props.todayRange, dateProfile = props.dateProfile, selectedInstanceId = props.selectedInstanceId;
+ var title = dateEnv.format(date, options.dayPopoverFormat);
+ return (createElement(DayCellRoot, { date: date, dateProfile: dateProfile, todayRange: todayRange, elRef: this.rootElRef }, function (rootElRef, dayClassNames, dataAttrs) { return (createElement(Popover, { elRef: rootElRef, title: title, extraClassNames: ['fc-more-popover'].concat(dayClassNames), extraAttrs: dataAttrs, onClose: props.onCloseClick, alignmentEl: props.alignmentEl, topAlignmentEl: props.topAlignmentEl },
+ createElement(DayCellContent, { date: date, dateProfile: dateProfile, todayRange: todayRange }, function (innerElRef, innerContent) { return (innerContent &&
+ createElement("div", { className: "fc-more-popover-misc", ref: innerElRef }, innerContent)); }),
+ props.segs.map(function (seg) {
+ var instanceId = seg.eventRange.instance.instanceId;
+ return (createElement("div", { className: "fc-daygrid-event-harness", key: instanceId, style: {
+ visibility: hiddenInstances[instanceId] ? 'hidden' : '',
+ } }, hasListItemDisplay(seg) ? (createElement(TableListItemEvent, __assign({ seg: seg, isDragging: false, isSelected: instanceId === selectedInstanceId, defaultDisplayEventEnd: false }, getSegMeta(seg, todayRange)))) : (createElement(TableBlockEvent, __assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === selectedInstanceId, defaultDisplayEventEnd: false }, getSegMeta(seg, todayRange))))));
+ }))); }));
+ };
+ MorePopover.prototype.positionToHit = function (positionLeft, positionTop, originEl) {
+ var rootEl = this.rootElRef.current;
+ if (!originEl || !rootEl) { // why?
+ return null;
+ }
+ var originRect = originEl.getBoundingClientRect();
+ var elRect = rootEl.getBoundingClientRect();
+ var newOriginLeft = elRect.left - originRect.left;
+ var newOriginTop = elRect.top - originRect.top;
+ var localLeft = positionLeft - newOriginLeft;
+ var localTop = positionTop - newOriginTop;
+ var date = this.props.date;
+ if ( // ugly way to detect intersection
+ localLeft >= 0 && localLeft < elRect.width &&
+ localTop >= 0 && localTop < elRect.height) {
+ return {
+ dateSpan: {
+ allDay: true,
+ range: { start: date, end: addDays(date, 1) },
+ },
+ dayEl: rootEl,
+ relativeRect: {
+ left: newOriginLeft,
+ top: newOriginTop,
+ right: elRect.width,
+ bottom: elRect.height,
+ },
+ layer: 1,
+ };
+ }
+ return null;
+ };
+ return MorePopover;
+ }(DateComponent));
+
+ var Table = /** @class */ (function (_super) {
+ __extends(Table, _super);
+ function Table() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.splitBusinessHourSegs = memoize(splitSegsByRow);
+ _this.splitBgEventSegs = memoize(splitSegsByRow);
+ _this.splitFgEventSegs = memoize(splitSegsByRow);
+ _this.splitDateSelectionSegs = memoize(splitSegsByRow);
+ _this.splitEventDrag = memoize(splitInteractionByRow);
+ _this.splitEventResize = memoize(splitInteractionByRow);
+ _this.buildBuildMoreLinkText = memoize(buildBuildMoreLinkText);
+ _this.morePopoverRef = createRef();
+ _this.rowRefs = new RefMap();
+ _this.state = {
+ morePopoverState: null,
+ };
+ _this.handleRootEl = function (rootEl) {
+ _this.rootEl = rootEl;
+ setRef(_this.props.elRef, rootEl);
+ };
+ // TODO: bad names "more link click" versus "more click"
+ _this.handleMoreLinkClick = function (arg) {
+ var context = _this.context;
+ var dateEnv = context.dateEnv;
+ var clickOption = context.options.moreLinkClick;
+ function segForPublic(seg) {
+ var _a = seg.eventRange, def = _a.def, instance = _a.instance, range = _a.range;
+ return {
+ event: new EventApi(context, def, instance),
+ start: dateEnv.toDate(range.start),
+ end: dateEnv.toDate(range.end),
+ isStart: seg.isStart,
+ isEnd: seg.isEnd,
+ };
+ }
+ if (typeof clickOption === 'function') {
+ clickOption = clickOption({
+ date: dateEnv.toDate(arg.date),
+ allDay: true,
+ allSegs: arg.allSegs.map(segForPublic),
+ hiddenSegs: arg.hiddenSegs.map(segForPublic),
+ jsEvent: arg.ev,
+ view: context.viewApi,
+ }); // hack to handle void
+ }
+ if (!clickOption || clickOption === 'popover') {
+ _this.setState({
+ morePopoverState: __assign(__assign({}, arg), { currentFgEventSegs: _this.props.fgEventSegs, fromRow: arg.fromRow, fromCol: arg.fromCol }),
+ });
+ }
+ else if (typeof clickOption === 'string') { // a view name
+ context.calendarApi.zoomTo(arg.date, clickOption);
+ }
+ };
+ _this.handleMorePopoverClose = function () {
+ _this.setState({
+ morePopoverState: null,
+ });
+ };
+ return _this;
+ }
+ Table.prototype.render = function () {
+ var _this = this;
+ var props = this.props;
+ var dateProfile = props.dateProfile, dayMaxEventRows = props.dayMaxEventRows, dayMaxEvents = props.dayMaxEvents, expandRows = props.expandRows;
+ var morePopoverState = this.state.morePopoverState;
+ var rowCnt = props.cells.length;
+ var businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCnt);
+ var bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCnt);
+ var fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCnt);
+ var dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCnt);
+ var eventDragByRow = this.splitEventDrag(props.eventDrag, rowCnt);
+ var eventResizeByRow = this.splitEventResize(props.eventResize, rowCnt);
+ var buildMoreLinkText = this.buildBuildMoreLinkText(this.context.options.moreLinkText);
+ var limitViaBalanced = dayMaxEvents === true || dayMaxEventRows === true;
+ // if rows can't expand to fill fixed height, can't do balanced-height event limit
+ // TODO: best place to normalize these options?
+ if (limitViaBalanced && !expandRows) {
+ limitViaBalanced = false;
+ dayMaxEventRows = null;
+ dayMaxEvents = null;
+ }
+ var classNames = [
+ 'fc-daygrid-body',
+ limitViaBalanced ? 'fc-daygrid-body-balanced' : 'fc-daygrid-body-unbalanced',
+ expandRows ? '' : 'fc-daygrid-body-natural',
+ ];
+ return (createElement("div", { className: classNames.join(' '), ref: this.handleRootEl, style: {
+ // these props are important to give this wrapper correct dimensions for interactions
+ // TODO: if we set it here, can we avoid giving to inner tables?
+ width: props.clientWidth,
+ minWidth: props.tableMinWidth,
+ } },
+ createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { return (createElement(Fragment, null,
+ createElement("table", { className: "fc-scrollgrid-sync-table", style: {
+ width: props.clientWidth,
+ minWidth: props.tableMinWidth,
+ height: expandRows ? props.clientHeight : '',
+ } },
+ props.colGroupNode,
+ createElement("tbody", null, props.cells.map(function (cells, row) { return (createElement(TableRow, { ref: _this.rowRefs.createRef(row), key: cells.length
+ ? cells[0].date.toISOString() /* best? or put key on cell? or use diff formatter? */
+ : row // in case there are no cells (like when resource view is loading)
+ , showDayNumbers: rowCnt > 1, showWeekNumbers: props.showWeekNumbers, todayRange: todayRange, dateProfile: dateProfile, cells: cells, renderIntro: props.renderRowIntro, businessHourSegs: businessHourSegsByRow[row], eventSelection: props.eventSelection, bgEventSegs: bgEventSegsByRow[row].filter(isSegAllDay) /* hack */, fgEventSegs: fgEventSegsByRow[row], dateSelectionSegs: dateSelectionSegsByRow[row], eventDrag: eventDragByRow[row], eventResize: eventResizeByRow[row], dayMaxEvents: dayMaxEvents, dayMaxEventRows: dayMaxEventRows, clientWidth: props.clientWidth, clientHeight: props.clientHeight, buildMoreLinkText: buildMoreLinkText, onMoreClick: function (arg) {
+ _this.handleMoreLinkClick(__assign(__assign({}, arg), { fromRow: row }));
+ } })); }))),
+ (!props.forPrint && morePopoverState && morePopoverState.currentFgEventSegs === props.fgEventSegs) && (createElement(MorePopover, { ref: _this.morePopoverRef, date: morePopoverState.date, dateProfile: dateProfile, segs: morePopoverState.allSegs, alignmentEl: morePopoverState.dayEl, topAlignmentEl: rowCnt === 1 ? props.headerAlignElRef.current : null, onCloseClick: _this.handleMorePopoverClose, selectedInstanceId: props.eventSelection, hiddenInstances: // yuck
+ (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
+ (props.eventResize ? props.eventResize.affectedInstances : null) ||
+ {}, todayRange: todayRange })))); })));
+ };
+ // Hit System
+ // ----------------------------------------------------------------------------------------------------
+ Table.prototype.prepareHits = function () {
+ this.rowPositions = new PositionCache(this.rootEl, this.rowRefs.collect().map(function (rowObj) { return rowObj.getCellEls()[0]; }), // first cell el in each row. TODO: not optimal
+ false, true);
+ this.colPositions = new PositionCache(this.rootEl, this.rowRefs.currentMap[0].getCellEls(), // cell els in first row
+ true, // horizontal
+ false);
+ };
+ Table.prototype.positionToHit = function (leftPosition, topPosition) {
+ var morePopover = this.morePopoverRef.current;
+ var morePopoverHit = morePopover ? morePopover.positionToHit(leftPosition, topPosition, this.rootEl) : null;
+ var morePopoverState = this.state.morePopoverState;
+ if (morePopoverHit) {
+ return __assign({ row: morePopoverState.fromRow, col: morePopoverState.fromCol }, morePopoverHit);
+ }
+ var _a = this, colPositions = _a.colPositions, rowPositions = _a.rowPositions;
+ var col = colPositions.leftToIndex(leftPosition);
+ var row = rowPositions.topToIndex(topPosition);
+ if (row != null && col != null) {
+ return {
+ row: row,
+ col: col,
+ dateSpan: {
+ range: this.getCellRange(row, col),
+ allDay: true,
+ },
+ dayEl: this.getCellEl(row, col),
+ relativeRect: {
+ left: colPositions.lefts[col],
+ right: colPositions.rights[col],
+ top: rowPositions.tops[row],
+ bottom: rowPositions.bottoms[row],
+ },
+ };
+ }
+ return null;
+ };
+ Table.prototype.getCellEl = function (row, col) {
+ return this.rowRefs.currentMap[row].getCellEls()[col]; // TODO: not optimal
+ };
+ Table.prototype.getCellRange = function (row, col) {
+ var start = this.props.cells[row][col].date;
+ var end = addDays(start, 1);
+ return { start: start, end: end };
+ };
+ return Table;
+ }(DateComponent));
+ function buildBuildMoreLinkText(moreLinkTextInput) {
+ if (typeof moreLinkTextInput === 'function') {
+ return moreLinkTextInput;
+ }
+ return function (num) { return "+" + num + " " + moreLinkTextInput; };
+ }
+ function isSegAllDay(seg) {
+ return seg.eventRange.def.allDay;
+ }
+
+ var DayTableSlicer = /** @class */ (function (_super) {
+ __extends(DayTableSlicer, _super);
+ function DayTableSlicer() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.forceDayIfListItem = true;
+ return _this;
+ }
+ DayTableSlicer.prototype.sliceRange = function (dateRange, dayTableModel) {
+ return dayTableModel.sliceRange(dateRange);
+ };
+ return DayTableSlicer;
+ }(Slicer));
+
+ var DayTable = /** @class */ (function (_super) {
+ __extends(DayTable, _super);
+ function DayTable() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.slicer = new DayTableSlicer();
+ _this.tableRef = createRef();
+ _this.handleRootEl = function (rootEl) {
+ if (rootEl) {
+ _this.context.registerInteractiveComponent(_this, { el: rootEl });
+ }
+ else {
+ _this.context.unregisterInteractiveComponent(_this);
+ }
+ };
+ return _this;
+ }
+ DayTable.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ return (createElement(Table, __assign({ ref: this.tableRef, elRef: this.handleRootEl }, this.slicer.sliceProps(props, props.dateProfile, props.nextDayThreshold, context, props.dayTableModel), { dateProfile: props.dateProfile, cells: props.dayTableModel.cells, colGroupNode: props.colGroupNode, tableMinWidth: props.tableMinWidth, renderRowIntro: props.renderRowIntro, dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows, showWeekNumbers: props.showWeekNumbers, expandRows: props.expandRows, headerAlignElRef: props.headerAlignElRef, clientWidth: props.clientWidth, clientHeight: props.clientHeight, forPrint: props.forPrint })));
+ };
+ DayTable.prototype.prepareHits = function () {
+ this.tableRef.current.prepareHits();
+ };
+ DayTable.prototype.queryHit = function (positionLeft, positionTop) {
+ var rawHit = this.tableRef.current.positionToHit(positionLeft, positionTop);
+ if (rawHit) {
+ return {
+ component: this,
+ dateSpan: rawHit.dateSpan,
+ dayEl: rawHit.dayEl,
+ rect: {
+ left: rawHit.relativeRect.left,
+ right: rawHit.relativeRect.right,
+ top: rawHit.relativeRect.top,
+ bottom: rawHit.relativeRect.bottom,
+ },
+ layer: 0,
+ };
+ }
+ return null;
+ };
+ return DayTable;
+ }(DateComponent));
+
+ var DayTableView = /** @class */ (function (_super) {
+ __extends(DayTableView, _super);
+ function DayTableView() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.buildDayTableModel = memoize(buildDayTableModel);
+ _this.headerRef = createRef();
+ _this.tableRef = createRef();
+ return _this;
+ }
+ DayTableView.prototype.render = function () {
+ var _this = this;
+ var _a = this.context, options = _a.options, dateProfileGenerator = _a.dateProfileGenerator;
+ var props = this.props;
+ var dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator);
+ var headerContent = options.dayHeaders && (createElement(DayHeader, { ref: this.headerRef, dateProfile: props.dateProfile, dates: dayTableModel.headerDates, datesRepDistinctDays: dayTableModel.rowCnt === 1 }));
+ var bodyContent = function (contentArg) { return (createElement(DayTable, { ref: _this.tableRef, dateProfile: props.dateProfile, dayTableModel: dayTableModel, businessHours: props.businessHours, dateSelection: props.dateSelection, eventStore: props.eventStore, eventUiBases: props.eventUiBases, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, nextDayThreshold: options.nextDayThreshold, colGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, dayMaxEvents: options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows, showWeekNumbers: options.weekNumbers, expandRows: !props.isHeightAuto, headerAlignElRef: _this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint })); };
+ return options.dayMinWidth
+ ? this.renderHScrollLayout(headerContent, bodyContent, dayTableModel.colCnt, options.dayMinWidth)
+ : this.renderSimpleLayout(headerContent, bodyContent);
+ };
+ return DayTableView;
+ }(TableView));
+ function buildDayTableModel(dateProfile, dateProfileGenerator) {
+ var daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
+ return new DayTableModel(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit));
+ }
+
+ var TableDateProfileGenerator = /** @class */ (function (_super) {
+ __extends(TableDateProfileGenerator, _super);
+ function TableDateProfileGenerator() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ // Computes the date range that will be rendered.
+ TableDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) {
+ var dateEnv = this.props.dateEnv;
+ var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay);
+ var start = renderRange.start;
+ var end = renderRange.end;
+ var endOfWeek;
+ // year and month views should be aligned with weeks. this is already done for week
+ if (/^(year|month)$/.test(currentRangeUnit)) {
+ start = dateEnv.startOfWeek(start);
+ // make end-of-week if not already
+ endOfWeek = dateEnv.startOfWeek(end);
+ if (endOfWeek.valueOf() !== end.valueOf()) {
+ end = addWeeks(endOfWeek, 1);
+ }
+ }
+ // ensure 6 weeks
+ if (this.props.monthMode &&
+ this.props.fixedWeekCount) {
+ var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
+ diffWeeks(start, end));
+ end = addWeeks(end, 6 - rowCnt);
+ }
+ return { start: start, end: end };
+ };
+ return TableDateProfileGenerator;
+ }(DateProfileGenerator));
+
+ var OPTION_REFINERS$1 = {
+ moreLinkClick: identity,
+ moreLinkClassNames: identity,
+ moreLinkContent: identity,
+ moreLinkDidMount: identity,
+ moreLinkWillUnmount: identity,
+ };
+
+ var dayGridPlugin = createPlugin({
+ initialView: 'dayGridMonth',
+ optionRefiners: OPTION_REFINERS$1,
+ views: {
+ dayGrid: {
+ component: DayTableView,
+ dateProfileGeneratorClass: TableDateProfileGenerator,
+ },
+ dayGridDay: {
+ type: 'dayGrid',
+ duration: { days: 1 },
+ },
+ dayGridWeek: {
+ type: 'dayGrid',
+ duration: { weeks: 1 },
+ },
+ dayGridMonth: {
+ type: 'dayGrid',
+ duration: { months: 1 },
+ monthMode: true,
+ fixedWeekCount: true,
+ },
+ },
+ });
+
+ var AllDaySplitter = /** @class */ (function (_super) {
+ __extends(AllDaySplitter, _super);
+ function AllDaySplitter() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ AllDaySplitter.prototype.getKeyInfo = function () {
+ return {
+ allDay: {},
+ timed: {},
+ };
+ };
+ AllDaySplitter.prototype.getKeysForDateSpan = function (dateSpan) {
+ if (dateSpan.allDay) {
+ return ['allDay'];
+ }
+ return ['timed'];
+ };
+ AllDaySplitter.prototype.getKeysForEventDef = function (eventDef) {
+ if (!eventDef.allDay) {
+ return ['timed'];
+ }
+ if (hasBgRendering(eventDef)) {
+ return ['timed', 'allDay'];
+ }
+ return ['allDay'];
+ };
+ return AllDaySplitter;
+ }(Splitter));
+
+ var DEFAULT_SLAT_LABEL_FORMAT = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: true,
+ meridiem: 'short',
+ });
+ function TimeColsAxisCell(props) {
+ var classNames = [
+ 'fc-timegrid-slot',
+ 'fc-timegrid-slot-label',
+ props.isLabeled ? 'fc-scrollgrid-shrink' : 'fc-timegrid-slot-minor',
+ ];
+ return (createElement(ViewContextType.Consumer, null, function (context) {
+ if (!props.isLabeled) {
+ return (createElement("td", { className: classNames.join(' '), "data-time": props.isoTimeStr }));
+ }
+ var dateEnv = context.dateEnv, options = context.options, viewApi = context.viewApi;
+ var labelFormat = // TODO: fully pre-parse
+ options.slotLabelFormat == null ? DEFAULT_SLAT_LABEL_FORMAT :
+ Array.isArray(options.slotLabelFormat) ? createFormatter(options.slotLabelFormat[0]) :
+ createFormatter(options.slotLabelFormat);
+ var hookProps = {
+ level: 0,
+ time: props.time,
+ date: dateEnv.toDate(props.date),
+ view: viewApi,
+ text: dateEnv.format(props.date, labelFormat),
+ };
+ return (createElement(RenderHook, { hookProps: hookProps, classNames: options.slotLabelClassNames, content: options.slotLabelContent, defaultContent: renderInnerContent$3, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-time": props.isoTimeStr },
+ createElement("div", { className: "fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame" },
+ createElement("div", { className: "fc-timegrid-slot-label-cushion fc-scrollgrid-shrink-cushion", ref: innerElRef }, innerContent)))); }));
+ }));
+ }
+ function renderInnerContent$3(props) {
+ return props.text;
+ }
+
+ var TimeBodyAxis = /** @class */ (function (_super) {
+ __extends(TimeBodyAxis, _super);
+ function TimeBodyAxis() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ TimeBodyAxis.prototype.render = function () {
+ return this.props.slatMetas.map(function (slatMeta) { return (createElement("tr", { key: slatMeta.key },
+ createElement(TimeColsAxisCell, __assign({}, slatMeta)))); });
+ };
+ return TimeBodyAxis;
+ }(BaseComponent));
+
+ var DEFAULT_WEEK_NUM_FORMAT$1 = createFormatter({ week: 'short' });
+ var AUTO_ALL_DAY_MAX_EVENT_ROWS = 5;
+ var TimeColsView = /** @class */ (function (_super) {
+ __extends(TimeColsView, _super);
+ function TimeColsView() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.allDaySplitter = new AllDaySplitter(); // for use by subclasses
+ _this.headerElRef = createRef();
+ _this.rootElRef = createRef();
+ _this.scrollerElRef = createRef();
+ _this.state = {
+ slatCoords: null,
+ };
+ _this.handleScrollTopRequest = function (scrollTop) {
+ var scrollerEl = _this.scrollerElRef.current;
+ if (scrollerEl) { // TODO: not sure how this could ever be null. weirdness with the reducer
+ scrollerEl.scrollTop = scrollTop;
+ }
+ };
+ /* Header Render Methods
+ ------------------------------------------------------------------------------------------------------------------*/
+ _this.renderHeadAxis = function (rowKey, frameHeight) {
+ if (frameHeight === void 0) { frameHeight = ''; }
+ var options = _this.context.options;
+ var dateProfile = _this.props.dateProfile;
+ var range = dateProfile.renderRange;
+ var dayCnt = diffDays(range.start, range.end);
+ var navLinkAttrs = (options.navLinks && dayCnt === 1) // only do in day views (to avoid doing in week views that dont need it)
+ ? { 'data-navlink': buildNavLinkData(range.start, 'week'), tabIndex: 0 }
+ : {};
+ if (options.weekNumbers && rowKey === 'day') {
+ return (createElement(WeekNumberRoot, { date: range.start, defaultFormat: DEFAULT_WEEK_NUM_FORMAT$1 }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("th", { ref: rootElRef, className: [
+ 'fc-timegrid-axis',
+ 'fc-scrollgrid-shrink',
+ ].concat(classNames).join(' ') },
+ createElement("div", { className: "fc-timegrid-axis-frame fc-scrollgrid-shrink-frame fc-timegrid-axis-frame-liquid", style: { height: frameHeight } },
+ createElement("a", __assign({ ref: innerElRef, className: "fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner" }, navLinkAttrs), innerContent)))); }));
+ }
+ return (createElement("th", { className: "fc-timegrid-axis" },
+ createElement("div", { className: "fc-timegrid-axis-frame", style: { height: frameHeight } })));
+ };
+ /* Table Component Render Methods
+ ------------------------------------------------------------------------------------------------------------------*/
+ // only a one-way height sync. we don't send the axis inner-content height to the DayGrid,
+ // but DayGrid still needs to have classNames on inner elements in order to measure.
+ _this.renderTableRowAxis = function (rowHeight) {
+ var _a = _this.context, options = _a.options, viewApi = _a.viewApi;
+ var hookProps = {
+ text: options.allDayText,
+ view: viewApi,
+ };
+ return (
+ // TODO: make reusable hook. used in list view too
+ createElement(RenderHook, { hookProps: hookProps, classNames: options.allDayClassNames, content: options.allDayContent, defaultContent: renderAllDayInner, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: [
+ 'fc-timegrid-axis',
+ 'fc-scrollgrid-shrink',
+ ].concat(classNames).join(' ') },
+ createElement("div", { className: 'fc-timegrid-axis-frame fc-scrollgrid-shrink-frame' + (rowHeight == null ? ' fc-timegrid-axis-frame-liquid' : ''), style: { height: rowHeight } },
+ createElement("span", { className: "fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner", ref: innerElRef }, innerContent)))); }));
+ };
+ _this.handleSlatCoords = function (slatCoords) {
+ _this.setState({ slatCoords: slatCoords });
+ };
+ return _this;
+ }
+ // rendering
+ // ----------------------------------------------------------------------------------------------------
+ TimeColsView.prototype.renderSimpleLayout = function (headerRowContent, allDayContent, timeContent) {
+ var _a = this, context = _a.context, props = _a.props;
+ var sections = [];
+ var stickyHeaderDates = getStickyHeaderDates(context.options);
+ if (headerRowContent) {
+ sections.push({
+ type: 'header',
+ key: 'header',
+ isSticky: stickyHeaderDates,
+ chunk: {
+ elRef: this.headerElRef,
+ tableClassName: 'fc-col-header',
+ rowContent: headerRowContent,
+ },
+ });
+ }
+ if (allDayContent) {
+ sections.push({
+ type: 'body',
+ key: 'all-day',
+ chunk: { content: allDayContent },
+ });
+ sections.push({
+ type: 'body',
+ key: 'all-day-divider',
+ outerContent: ( // TODO: rename to cellContent so don't need to define
+
+
+{% endblock %}
+
+{% block js_load %}
+{{ block.super }}
+
+
{% endblock %}
{% block js_ready %}
{{ block.super }}
+$("#sales-order-calendar").hide();
+$("#view-list").hide();
+
+$('#view-calendar').click(function() {
+ // Hide the list view, show the calendar view
+ $("#sales-order-table").hide();
+ $("#view-calendar").hide();
+ $(".fixed-table-pagination").hide();
+ $(".columns-right").hide();
+ $(".search").hide();
+ $('#filter-list-salesorder').hide();
+
+ $("#sales-order-calendar").show();
+ $("#view-list").show();
+});
+
+$("#view-list").click(function() {
+ // Hide the calendar view, show the list view
+ $("#sales-order-calendar").hide();
+ $("#view-list").hide();
+
+ $(".fixed-table-pagination").show();
+ $(".columns-right").show();
+ $(".search").show();
+ $("#sales-order-table").show();
+ $('#filter-list-salesorder').show();
+ $("#view-calendar").show();
+});
+
loadSalesOrderTable("#sales-order-table", {
url: "{% url 'api-so-list' %}",
});
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index d5658909bb..bd758e39fb 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -24,6 +24,8 @@ from company.models import Company, SupplierPart
from stock.models import StockItem, StockLocation
from part.models import Part
+from common.models import InvenTreeSetting
+
from . import forms as order_forms
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
@@ -1359,7 +1361,8 @@ class SalesOrderAllocationCreate(AjaxCreateView):
try:
line = SalesOrderLineItem.objects.get(pk=line_id)
- queryset = form.fields['item'].queryset
+ # Construct a queryset for allowable stock items
+ queryset = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
# Ensure the part reference matches
queryset = queryset.filter(part=line.part)
@@ -1369,6 +1372,10 @@ class SalesOrderAllocationCreate(AjaxCreateView):
queryset = queryset.exclude(pk__in=allocated)
+ # Exclude stock items which have expired
+ if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_SALE'):
+ queryset = queryset.exclude(StockItem.EXPIRED_FILTER)
+
form.fields['item'].queryset = queryset
# Hide the 'line' field
diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml
index f6d9d246db..508a1577bb 100644
--- a/InvenTree/part/fixtures/part.yaml
+++ b/InvenTree/part/fixtures/part.yaml
@@ -74,6 +74,7 @@
level: 0
lft: 0
rght: 0
+ default_expiry: 10
- model: part.part
pk: 50
@@ -134,6 +135,7 @@
fields:
name: 'Red chair'
variant_of: 10000
+ IPN: "R.CH"
trackable: true
category: 7
tree_id: 1
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 1cbbec0b42..1ac62161a5 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -181,6 +181,7 @@ class EditPartForm(HelperForm):
'keywords': 'fa-key',
'link': 'fa-link',
'IPN': 'fa-hashtag',
+ 'default_expiry': 'fa-stopwatch',
}
bom_copy = forms.BooleanField(required=False,
@@ -228,11 +229,16 @@ class EditPartForm(HelperForm):
'link',
'default_location',
'default_supplier',
+ 'default_expiry',
'units',
'minimum_stock',
+ 'component',
+ 'assembly',
+ 'is_template',
'trackable',
'purchaseable',
'salable',
+ 'virtual',
]
diff --git a/InvenTree/part/migrations/0061_auto_20210103_2313.py b/InvenTree/part/migrations/0061_auto_20210103_2313.py
new file mode 100644
index 0000000000..ca0c2a277f
--- /dev/null
+++ b/InvenTree/part/migrations/0061_auto_20210103_2313.py
@@ -0,0 +1,85 @@
+# Generated by Django 3.0.7 on 2021-01-03 12:13
+
+import InvenTree.fields
+import InvenTree.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import markdownx.models
+import mptt.fields
+import part.settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0055_auto_20201117_1453'),
+ ('part', '0060_merge_20201112_1722'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='part',
+ name='IPN',
+ field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, validators=[InvenTree.validators.validate_part_ipn], verbose_name='IPN'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='assembly',
+ field=models.BooleanField(default=part.settings.part_assembly_default, help_text='Can this part be built from other parts?', verbose_name='Assembly'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='category',
+ field=mptt.fields.TreeForeignKey(blank=True, help_text='Part category', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='parts', to='part.PartCategory', verbose_name='Category'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='default_location',
+ field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation', verbose_name='Default Location'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='description',
+ field=models.CharField(help_text='Part description', max_length=250, verbose_name='Description'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='is_template',
+ field=models.BooleanField(default=part.settings.part_template_default, help_text='Is this part a template part?', verbose_name='Is Template'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='keywords',
+ field=models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250, null=True, verbose_name='Keywords'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='link',
+ field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='name',
+ field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name], verbose_name='Name'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='notes',
+ field=markdownx.models.MarkdownxField(blank=True, help_text='Part notes - supports Markdown formatting', null=True, verbose_name='Notes'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='revision',
+ field=models.CharField(blank=True, help_text='Part revision or version number', max_length=100, null=True, verbose_name='Revision'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='variant_of',
+ field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'active': True, 'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.Part', verbose_name='Variant Of'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='virtual',
+ field=models.BooleanField(default=part.settings.part_virtual_default, help_text='Is this a virtual part, such as a software product or license?', verbose_name='Virtual'),
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0061_auto_20210104_2331.py b/InvenTree/part/migrations/0061_auto_20210104_2331.py
new file mode 100644
index 0000000000..c40b611b29
--- /dev/null
+++ b/InvenTree/part/migrations/0061_auto_20210104_2331.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.0.7 on 2021-01-04 12:31
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('company', '0031_auto_20210103_2215'),
+ ('part', '0060_merge_20201112_1722'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='part',
+ name='default_expiry',
+ field=models.PositiveIntegerField(default=0, help_text='Expiry time (in days) for stock items of this part', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Default Expiry'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='default_supplier',
+ field=models.ForeignKey(blank=True, help_text='Default supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='company.SupplierPart', verbose_name='Default Supplier'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='minimum_stock',
+ field=models.PositiveIntegerField(default=0, help_text='Minimum allowed stock level', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Minimum Stock'),
+ ),
+ migrations.AlterField(
+ model_name='part',
+ name='units',
+ field=models.CharField(blank=True, default='', help_text='Stock keeping units for this part', max_length=20, null=True, verbose_name='Units'),
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0062_merge_20210105_0056.py b/InvenTree/part/migrations/0062_merge_20210105_0056.py
new file mode 100644
index 0000000000..4a8f4378f4
--- /dev/null
+++ b/InvenTree/part/migrations/0062_merge_20210105_0056.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.7 on 2021-01-04 13:56
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0061_auto_20210104_2331'),
+ ('part', '0061_auto_20210103_2313'),
+ ]
+
+ operations = [
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index b9d63979e7..8c88adf747 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -291,11 +291,12 @@ class Part(MPTTModel):
keywords: Optional keywords for improving part search results
IPN: Internal part number (optional)
revision: Part revision
- is_template: If True, this part is a 'template' part and cannot be instantiated as a StockItem
+ is_template: If True, this part is a 'template' part
link: Link to an external page with more information about this part (e.g. internal Wiki)
image: Image of this part
default_location: Where the item is normally stored (may be null)
default_supplier: The default SupplierPart which should be used to procure and stock this part
+ default_expiry: The default expiry duration for any StockItem instances of this part
minimum_stock: Minimum preferred quantity to keep in stock
units: Units of measure for this part (default='pcs')
salable: Can this part be sold to customers?
@@ -640,36 +641,69 @@ class Part(MPTTModel):
parent_part.clean()
parent_part.save()
- name = models.CharField(max_length=100, blank=False,
- help_text=_('Part name'),
- validators=[validators.validate_part_name]
- )
+ name = models.CharField(
+ max_length=100, blank=False,
+ help_text=_('Part name'),
+ verbose_name=_('Name'),
+ validators=[validators.validate_part_name]
+ )
- is_template = models.BooleanField(default=False, help_text=_('Is this part a template part?'))
+ is_template = models.BooleanField(
+ default=part_settings.part_template_default,
+ verbose_name=_('Is Template'),
+ help_text=_('Is this part a template part?')
+ )
- variant_of = models.ForeignKey('part.Part', related_name='variants',
- null=True, blank=True,
- limit_choices_to={
- 'is_template': True,
- 'active': True,
- },
- on_delete=models.SET_NULL,
- help_text=_('Is this part a variant of another part?'))
+ variant_of = models.ForeignKey(
+ 'part.Part', related_name='variants',
+ null=True, blank=True,
+ limit_choices_to={
+ 'is_template': True,
+ 'active': True,
+ },
+ on_delete=models.SET_NULL,
+ help_text=_('Is this part a variant of another part?'),
+ verbose_name=_('Variant Of'),
+ )
- description = models.CharField(max_length=250, blank=False, help_text=_('Part description'))
+ description = models.CharField(
+ max_length=250, blank=False,
+ verbose_name=_('Description'),
+ help_text=_('Part description')
+ )
- keywords = models.CharField(max_length=250, blank=True, null=True, help_text=_('Part keywords to improve visibility in search results'))
+ keywords = models.CharField(
+ max_length=250, blank=True, null=True,
+ verbose_name=_('Keywords'),
+ help_text=_('Part keywords to improve visibility in search results')
+ )
- category = TreeForeignKey(PartCategory, related_name='parts',
- null=True, blank=True,
- on_delete=models.DO_NOTHING,
- help_text=_('Part category'))
+ category = TreeForeignKey(
+ PartCategory, related_name='parts',
+ null=True, blank=True,
+ on_delete=models.DO_NOTHING,
+ verbose_name=_('Category'),
+ help_text=_('Part category')
+ )
- IPN = models.CharField(max_length=100, blank=True, null=True, help_text=_('Internal Part Number'), validators=[validators.validate_part_ipn])
+ IPN = models.CharField(
+ max_length=100, blank=True, null=True,
+ verbose_name=_('IPN'),
+ help_text=_('Internal Part Number'),
+ validators=[validators.validate_part_ipn]
+ )
- revision = models.CharField(max_length=100, blank=True, null=True, help_text=_('Part revision or version number'))
+ revision = models.CharField(
+ max_length=100, blank=True, null=True,
+ help_text=_('Part revision or version number'),
+ verbose_name=_('Revision'),
+ )
- link = InvenTreeURLField(blank=True, null=True, help_text=_('Link to external URL'))
+ link = InvenTreeURLField(
+ blank=True, null=True,
+ verbose_name=_('Link'),
+ help_text=_('Link to external URL')
+ )
image = StdImageField(
upload_to=rename_part_image,
@@ -679,10 +713,14 @@ class Part(MPTTModel):
delete_orphans=True,
)
- default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
- blank=True, null=True,
- help_text=_('Where is this item normally stored?'),
- related_name='default_parts')
+ default_location = TreeForeignKey(
+ 'stock.StockLocation',
+ on_delete=models.SET_NULL,
+ blank=True, null=True,
+ help_text=_('Where is this item normally stored?'),
+ related_name='default_parts',
+ verbose_name=_('Default Location'),
+ )
def get_default_location(self):
""" Get the default location for a Part (may be None).
@@ -722,18 +760,37 @@ class Part(MPTTModel):
# Default to None if there are multiple suppliers to choose from
return None
- default_supplier = models.ForeignKey(SupplierPart,
- on_delete=models.SET_NULL,
- blank=True, null=True,
- help_text=_('Default supplier part'),
- related_name='default_parts')
+ default_supplier = models.ForeignKey(
+ SupplierPart,
+ on_delete=models.SET_NULL,
+ blank=True, null=True,
+ verbose_name=_('Default Supplier'),
+ help_text=_('Default supplier part'),
+ related_name='default_parts'
+ )
- minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text=_('Minimum allowed stock level'))
+ default_expiry = models.PositiveIntegerField(
+ default=0,
+ validators=[MinValueValidator(0)],
+ verbose_name=_('Default Expiry'),
+ help_text=_('Expiry time (in days) for stock items of this part'),
+ )
- units = models.CharField(max_length=20, default="", blank=True, null=True, help_text=_('Stock keeping units for this part'))
+ minimum_stock = models.PositiveIntegerField(
+ default=0, validators=[MinValueValidator(0)],
+ verbose_name=_('Minimum Stock'),
+ help_text=_('Minimum allowed stock level')
+ )
+
+ units = models.CharField(
+ max_length=20, default="",
+ blank=True, null=True,
+ verbose_name=_('Units'),
+ help_text=_('Stock keeping units for this part')
+ )
assembly = models.BooleanField(
- default=False,
+ default=part_settings.part_assembly_default,
verbose_name=_('Assembly'),
help_text=_('Can this part be built from other parts?')
)
@@ -765,11 +822,15 @@ class Part(MPTTModel):
help_text=_('Is this part active?'))
virtual = models.BooleanField(
- default=False,
+ default=part_settings.part_virtual_default,
verbose_name=_('Virtual'),
help_text=_('Is this a virtual part, such as a software product or license?'))
- notes = MarkdownxField(blank=True, null=True, help_text=_('Part notes - supports Markdown formatting'))
+ notes = MarkdownxField(
+ blank=True, null=True,
+ verbose_name=_('Notes'),
+ help_text=_('Part notes - supports Markdown formatting')
+ )
bom_checksum = models.CharField(max_length=128, blank=True, help_text=_('Stored BOM checksum'))
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 0eebe6617d..05fc3091f7 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -289,6 +289,7 @@ class PartSerializer(InvenTreeModelSerializer):
'component',
'description',
'default_location',
+ 'default_expiry',
'full_name',
'image',
'in_stock',
diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py
index 8d87cdffe3..801b4dd2ec 100644
--- a/InvenTree/part/settings.py
+++ b/InvenTree/part/settings.py
@@ -8,6 +8,30 @@ from __future__ import unicode_literals
from common.models import InvenTreeSetting
+def part_assembly_default():
+ """
+ Returns the default value for the 'assembly' field of a Part object
+ """
+
+ return InvenTreeSetting.get_setting('PART_ASSEMBLY')
+
+
+def part_template_default():
+ """
+ Returns the default value for the 'is_template' field of a Part object
+ """
+
+ return InvenTreeSetting.get_setting('PART_TEMPLATE')
+
+
+def part_virtual_default():
+ """
+ Returns the default value for the 'is_virtual' field of Part object
+ """
+
+ return InvenTreeSetting.get_setting('PART_VIRTUAL')
+
+
def part_component_default():
"""
Returns the default value for the 'component' field of a Part object
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 9711c9fbc8..f723193abb 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -109,6 +109,13 @@
{{ part.minimum_stock }}
{% endif %}
+ {% if part.default_expiry > 0 %}
+
+
+
{% trans "Stock Expiry Time" %}
+
{{ part.default_expiry }} {% trans "days" %}
+
+ {% endif %}
{% trans "Creation Date" %}
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index c02be211b5..4c08911122 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -235,6 +235,8 @@ class PartSettingsTest(TestCase):
InvenTreeSetting.set_setting('PART_PURCHASEABLE', val, self.user)
InvenTreeSetting.set_setting('PART_SALABLE', val, self.user)
InvenTreeSetting.set_setting('PART_TRACKABLE', val, self.user)
+ InvenTreeSetting.set_setting('PART_ASSEMBLY', val, self.user)
+ InvenTreeSetting.set_setting('PART_TEMPLATE', val, self.user)
self.assertEqual(val, InvenTreeSetting.get_setting('PART_COMPONENT'))
self.assertEqual(val, InvenTreeSetting.get_setting('PART_PURCHASEABLE'))
@@ -247,6 +249,8 @@ class PartSettingsTest(TestCase):
self.assertEqual(part.purchaseable, val)
self.assertEqual(part.salable, val)
self.assertEqual(part.trackable, val)
+ self.assertEqual(part.assembly, val)
+ self.assertEqual(part.is_template, val)
Part.objects.filter(pk=part.pk).delete()
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index ac4925685f..1d76860ac1 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -35,6 +35,8 @@ from .models import PartSellPriceBreak
from common.models import InvenTreeSetting
from company.models import SupplierPart
+import common.settings as inventree_settings
+
from . import forms as part_forms
from .bom import MakeBomTemplate, BomUploadManager, ExportBom, IsValidBOMFormat
@@ -626,6 +628,10 @@ class PartCreate(AjaxCreateView):
"""
form = super(AjaxCreateView, self).get_form()
+ # Hide the "default expiry" field if the feature is not enabled
+ if not inventree_settings.stock_expiry_enabled():
+ form.fields.pop('default_expiry')
+
# Hide the default_supplier field (there are no matching supplier parts yet!)
form.fields['default_supplier'].widget = HiddenInput()
@@ -918,6 +924,10 @@ class PartEdit(AjaxUpdateView):
form = super(AjaxUpdateView, self).get_form()
+ # Hide the "default expiry" field if the feature is not enabled
+ if not inventree_settings.stock_expiry_enabled():
+ form.fields.pop('default_expiry')
+
part = self.get_object()
form.fields['default_supplier'].queryset = SupplierPart.objects.filter(part=part)
diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py
index b3725cadc0..4dd5fbfa5c 100644
--- a/InvenTree/report/models.py
+++ b/InvenTree/report/models.py
@@ -16,7 +16,7 @@ from django.conf import settings
from django.core.validators import FileExtensionValidator
from django.core.exceptions import ValidationError
-from stock.models import StockItem
+import stock.models
from InvenTree.helpers import validateFilterString
@@ -191,7 +191,7 @@ class TestReport(ReportTemplateBase):
filters = validateFilterString(self.filters)
- items = StockItem.objects.filter(**filters)
+ items = stock.models.StockItem.objects.filter(**filters)
# Ensure the provided StockItem object matches the filters
items = items.filter(pk=item.pk)
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index b74ac200f7..9f0a4278f5 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -23,6 +23,9 @@ from part.serializers import PartBriefSerializer
from company.models import SupplierPart
from company.serializers import SupplierPartSerializer
+import common.settings
+import common.models
+
from .serializers import StockItemSerializer
from .serializers import LocationSerializer, LocationBriefSerializer
from .serializers import StockTrackingSerializer
@@ -35,6 +38,8 @@ from InvenTree.api import AttachmentMixin
from decimal import Decimal, InvalidOperation
+from datetime import datetime, timedelta
+
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
@@ -342,10 +347,18 @@ class StockList(generics.ListCreateAPIView):
# A location was *not* specified - try to infer it
if 'location' not in request.data:
location = item.part.get_default_location()
+
if location is not None:
item.location = location
item.save()
+ # An expiry date was *not* specified - try to infer it!
+ if 'expiry_date' not in request.data:
+
+ if item.part.default_expiry > 0:
+ item.expiry_date = datetime.now().date() + timedelta(days=item.part.default_expiry)
+ item.save()
+
# Return a response
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@@ -525,6 +538,38 @@ class StockList(generics.ListCreateAPIView):
# Exclude items which are instaled in another item
queryset = queryset.filter(belongs_to=None)
+ if common.settings.stock_expiry_enabled():
+
+ # Filter by 'expired' status
+ expired = params.get('expired', None)
+
+ if expired is not None:
+ expired = str2bool(expired)
+
+ if expired:
+ queryset = queryset.filter(StockItem.EXPIRED_FILTER)
+ else:
+ queryset = queryset.exclude(StockItem.EXPIRED_FILTER)
+
+ # Filter by 'stale' status
+ stale = params.get('stale', None)
+
+ if stale is not None:
+ stale = str2bool(stale)
+
+ # How many days to account for "staleness"?
+ stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
+
+ if stale_days > 0:
+ stale_date = datetime.now().date() + timedelta(days=stale_days)
+
+ stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date)
+
+ if stale:
+ queryset = queryset.filter(stale_filter)
+ else:
+ queryset = queryset.exclude(stale_filter)
+
# Filter by customer
customer = params.get('customer', None)
diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml
index 719d8a34ce..45a5f5dd7f 100644
--- a/InvenTree/stock/fixtures/stock.yaml
+++ b/InvenTree/stock/fixtures/stock.yaml
@@ -69,7 +69,7 @@
part: 25
batch: 'ABCDE'
location: 7
- quantity: 3
+ quantity: 0
level: 0
tree_id: 0
lft: 0
@@ -220,6 +220,7 @@
tree_id: 0
lft: 0
rght: 0
+ expiry_date: "1990-10-10"
- model: stock.stockitem
pk: 521
@@ -232,6 +233,7 @@
tree_id: 0
lft: 0
rght: 0
+ status: 60
- model: stock.stockitem
pk: 522
@@ -243,4 +245,6 @@
level: 0
tree_id: 0
lft: 0
- rght: 0
\ No newline at end of file
+ rght: 0
+ expiry_date: "1990-10-10"
+ status: 70
\ No newline at end of file
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index 8ab88155e2..ec7cbf7805 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -16,6 +16,7 @@ from mptt.fields import TreeNodeChoiceField
from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
+from InvenTree.fields import DatePickerFormField
from report.models import TestReport
@@ -108,6 +109,10 @@ class ConvertStockItemForm(HelperForm):
class CreateStockItemForm(HelperForm):
""" Form for creating a new StockItem """
+ expiry_date = DatePickerFormField(
+ help_text=('Expiration date for this stock item'),
+ )
+
serial_numbers = forms.CharField(label=_('Serial numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)'))
def __init__(self, *args, **kwargs):
@@ -129,6 +134,7 @@ class CreateStockItemForm(HelperForm):
'batch',
'serial_numbers',
'purchase_price',
+ 'expiry_date',
'link',
'delete_on_deplete',
'status',
@@ -241,7 +247,7 @@ class TestReportFormatForm(HelperForm):
templates = TestReport.objects.filter(enabled=True)
for template in templates:
- if template.matches_stock_item(self.stock_item):
+ if template.enabled and template.matches_stock_item(self.stock_item):
choices.append((template.pk, template))
return choices
@@ -392,6 +398,10 @@ class EditStockItemForm(HelperForm):
part - Cannot be edited after creation
"""
+ expiry_date = DatePickerFormField(
+ help_text=('Expiration date for this stock item'),
+ )
+
class Meta:
model = StockItem
@@ -400,6 +410,7 @@ class EditStockItemForm(HelperForm):
'serial',
'batch',
'status',
+ 'expiry_date',
'purchase_price',
'link',
'delete_on_deplete',
diff --git a/InvenTree/stock/migrations/0056_stockitem_expiry_date.py b/InvenTree/stock/migrations/0056_stockitem_expiry_date.py
new file mode 100644
index 0000000000..f558d615a6
--- /dev/null
+++ b/InvenTree/stock/migrations/0056_stockitem_expiry_date.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2021-01-03 12:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0055_auto_20201117_1453'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='stockitem',
+ name='expiry_date',
+ field=models.DateField(blank=True, help_text='Expiry date for stock item. Stock will be considered expired after this date', null=True, verbose_name='Expiry Date'),
+ ),
+ ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index d1e46c53a7..807d6644ca 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -27,9 +27,12 @@ from mptt.models import MPTTModel, TreeForeignKey
from djmoney.models.fields import MoneyField
from decimal import Decimal, InvalidOperation
-from datetime import datetime
+from datetime import datetime, timedelta
from InvenTree import helpers
+import common.models
+import report.models
+
from InvenTree.status_codes import StockStatus
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField
@@ -125,6 +128,7 @@ class StockItem(MPTTModel):
serial: Unique serial number for this StockItem
link: Optional URL to link to external resource
updated: Date that this stock item was last updated (auto)
+ expiry_date: Expiry date of the StockItem (optional)
stocktake_date: Date of last stocktake for this item
stocktake_user: User that performed the most recent stocktake
review_needed: Flag if StockItem needs review
@@ -149,6 +153,9 @@ class StockItem(MPTTModel):
status__in=StockStatus.AVAILABLE_CODES
)
+ # A query filter which can be used to filter StockItem objects which have expired
+ EXPIRED_FILTER = IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=datetime.now().date())
+
def save(self, *args, **kwargs):
"""
Save this StockItem to the database. Performs a number of checks:
@@ -428,11 +435,19 @@ class StockItem(MPTTModel):
related_name='stock_items',
null=True, blank=True)
- # last time the stock was checked / counted
+ expiry_date = models.DateField(
+ blank=True, null=True,
+ verbose_name=_('Expiry Date'),
+ help_text=_('Expiry date for stock item. Stock will be considered expired after this date'),
+ )
+
stocktake_date = models.DateField(blank=True, null=True)
- stocktake_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
- related_name='stocktake_stock')
+ stocktake_user = models.ForeignKey(
+ User, on_delete=models.SET_NULL,
+ blank=True, null=True,
+ related_name='stocktake_stock'
+ )
review_needed = models.BooleanField(default=False)
@@ -459,6 +474,55 @@ class StockItem(MPTTModel):
help_text=_('Single unit purchase price at time of purchase'),
)
+ def is_stale(self):
+ """
+ Returns True if this Stock item is "stale".
+
+ To be "stale", the following conditions must be met:
+
+ - Expiry date is not None
+ - Expiry date will "expire" within the configured stale date
+ - The StockItem is otherwise "in stock"
+ """
+
+ if self.expiry_date is None:
+ return False
+
+ if not self.in_stock:
+ return False
+
+ today = datetime.now().date()
+
+ stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
+
+ if stale_days <= 0:
+ return False
+
+ expiry_date = today + timedelta(days=stale_days)
+
+ return self.expiry_date < expiry_date
+
+ def is_expired(self):
+ """
+ Returns True if this StockItem is "expired".
+
+ To be "expired", the following conditions must be met:
+
+ - Expiry date is not None
+ - Expiry date is "in the past"
+ - The StockItem is otherwise "in stock"
+ """
+
+ if self.expiry_date is None:
+ return False
+
+ if not self.in_stock:
+ return False
+
+ today = datetime.now().date()
+
+ return self.expiry_date < today
+
def clearAllocations(self):
"""
Clear all order allocations for this StockItem:
@@ -721,36 +785,16 @@ class StockItem(MPTTModel):
@property
def in_stock(self):
"""
- Returns True if this item is in stock
+ Returns True if this item is in stock.
See also: IN_STOCK_FILTER
"""
- # Quantity must be above zero (unless infinite)
- if self.quantity <= 0 and not self.infinite:
- return False
+ query = StockItem.objects.filter(pk=self.pk)
- # Not 'in stock' if it has been installed inside another StockItem
- if self.belongs_to is not None:
- return False
-
- # Not 'in stock' if it has been sent to a customer
- if self.sales_order is not None:
- return False
+ query = query.filter(StockItem.IN_STOCK_FILTER)
- # Not 'in stock' if it has been assigned to a customer
- if self.customer is not None:
- return False
-
- # Not 'in stock' if it is building
- if self.is_building:
- return False
-
- # Not 'in stock' if the status code makes it unavailable
- if self.status in StockStatus.UNAVAILABLE_CODES:
- return False
-
- return True
+ return query.exists()
@property
def tracking_info_count(self):
@@ -1263,6 +1307,41 @@ class StockItem(MPTTModel):
return status['passed'] >= status['total']
+ def available_test_reports(self):
+ """
+ Return a list of TestReport objects which match this StockItem.
+ """
+
+ reports = []
+
+ item_query = StockItem.objects.filter(pk=self.pk)
+
+ for test_report in report.models.TestReport.objects.filter(enabled=True):
+
+ filters = helpers.validateFilterString(test_report.filters)
+
+ if item_query.filter(**filters).exists():
+ reports.append(test_report)
+
+ return reports
+
+ @property
+ def has_test_reports(self):
+ """
+ Return True if there are test reports available for this stock item
+ """
+
+ return len(self.available_test_reports()) > 0
+
+ @property
+ def has_labels(self):
+ """
+ Return True if there are any label templates available for this stock item
+ """
+
+ # TODO - Implement this
+ return True
+
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
def before_delete_stock_item(sender, instance, using, **kwargs):
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index e8675a8fff..70ad1abc18 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -11,10 +11,17 @@ from .models import StockItemTestResult
from django.db.models.functions import Coalesce
+from django.db.models import Case, When, Value
+from django.db.models import BooleanField
+from django.db.models import Q
+
from sql_util.utils import SubquerySum, SubqueryCount
from decimal import Decimal
+from datetime import datetime, timedelta
+
+import common.models
from company.serializers import SupplierPartSerializer
from part.serializers import PartBriefSerializer
from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer
@@ -106,6 +113,30 @@ class StockItemSerializer(InvenTreeModelSerializer):
tracking_items=SubqueryCount('tracking_info')
)
+ # Add flag to indicate if the StockItem has expired
+ queryset = queryset.annotate(
+ expired=Case(
+ When(
+ StockItem.EXPIRED_FILTER, then=Value(True, output_field=BooleanField()),
+ ),
+ default=Value(False, output_field=BooleanField())
+ )
+ )
+
+ # Add flag to indicate if the StockItem is stale
+ stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
+ stale_date = datetime.now().date() + timedelta(days=stale_days)
+ stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date)
+
+ queryset = queryset.annotate(
+ stale=Case(
+ When(
+ stale_filter, then=Value(True, output_field=BooleanField()),
+ ),
+ default=Value(False, output_field=BooleanField()),
+ )
+ )
+
return queryset
status_text = serializers.CharField(source='get_status_display', read_only=True)
@@ -122,6 +153,10 @@ class StockItemSerializer(InvenTreeModelSerializer):
allocated = serializers.FloatField(source='allocation_count', required=False)
+ expired = serializers.BooleanField(required=False, read_only=True)
+
+ stale = serializers.BooleanField(required=False, read_only=True)
+
serial = serializers.CharField(required=False)
required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False)
@@ -155,6 +190,8 @@ class StockItemSerializer(InvenTreeModelSerializer):
'belongs_to',
'build',
'customer',
+ 'expired',
+ 'expiry_date',
'in_stock',
'is_building',
'link',
@@ -168,6 +205,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
'required_tests',
'sales_order',
'serial',
+ 'stale',
'status',
'status_text',
'supplier_part',
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index 74e93fecc0..bea7057351 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -70,7 +70,14 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% block page_data %}
{% trans "Stock Item" %}
+ {% if item.is_expired %}
+ {% trans "Expired" %}
+ {% else %}
{% stock_status_label item.status large=True %}
+ {% if item.is_stale %}
+ {% trans "Stale" %}
+ {% endif %}
+ {% endif %}
+ {{ item.expiry_date }}
+ {% if item.is_expired %}
+ {% trans "Expired" %}
+ {% elif item.is_stale %}
+ {% trans "Stale" %}
+ {% endif %}
+
+
+ {% endif %}
{% trans "Last Updated" %}
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index a34e895ed8..9b3e926456 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -1,11 +1,23 @@
+"""
+Unit testing for the Stock API
+"""
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from datetime import datetime, timedelta
+
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth import get_user_model
from InvenTree.helpers import addUserPermissions
+from InvenTree.status_codes import StockStatus
-from .models import StockLocation
+from common.models import InvenTreeSetting
+
+from .models import StockItem, StockLocation
class StockAPITestCase(APITestCase):
@@ -26,6 +38,9 @@ class StockAPITestCase(APITestCase):
self.user = user.objects.create_user('testuser', 'test@testing.com', 'password')
+ self.user.is_staff = True
+ self.user.save()
+
# Add the necessary permissions to the user
perms = [
'view_stockitemtestresult',
@@ -76,6 +91,177 @@ class StockLocationTest(StockAPITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+class StockItemListTest(StockAPITestCase):
+ """
+ Tests for the StockItem API LIST endpoint
+ """
+
+ list_url = reverse('api-stock-list')
+
+ def get_stock(self, **kwargs):
+ """
+ Filter stock and return JSON object
+ """
+
+ response = self.client.get(self.list_url, format='json', data=kwargs)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ # Return JSON-ified data
+ return response.data
+
+ def test_get_stock_list(self):
+ """
+ List *all* StockItem objects.
+ """
+
+ response = self.get_stock()
+
+ self.assertEqual(len(response), 19)
+
+ def test_filter_by_part(self):
+ """
+ Filter StockItem by Part reference
+ """
+
+ response = self.get_stock(part=25)
+
+ self.assertEqual(len(response), 7)
+
+ response = self.get_stock(part=10004)
+
+ self.assertEqual(len(response), 12)
+
+ def test_filter_by_IPN(self):
+ """
+ Filter StockItem by IPN reference
+ """
+
+ response = self.get_stock(IPN="R.CH")
+ self.assertEqual(len(response), 3)
+
+ def test_filter_by_location(self):
+ """
+ Filter StockItem by StockLocation reference
+ """
+
+ response = self.get_stock(location=5)
+ self.assertEqual(len(response), 1)
+
+ response = self.get_stock(location=1, cascade=0)
+ self.assertEqual(len(response), 0)
+
+ response = self.get_stock(location=1, cascade=1)
+ self.assertEqual(len(response), 2)
+
+ response = self.get_stock(location=7)
+ self.assertEqual(len(response), 16)
+
+ def test_filter_by_depleted(self):
+ """
+ Filter StockItem by depleted status
+ """
+
+ response = self.get_stock(depleted=1)
+ self.assertEqual(len(response), 1)
+
+ response = self.get_stock(depleted=0)
+ self.assertEqual(len(response), 18)
+
+ def test_filter_by_in_stock(self):
+ """
+ Filter StockItem by 'in stock' status
+ """
+
+ response = self.get_stock(in_stock=1)
+ self.assertEqual(len(response), 16)
+
+ response = self.get_stock(in_stock=0)
+ self.assertEqual(len(response), 3)
+
+ def test_filter_by_status(self):
+ """
+ Filter StockItem by 'status' field
+ """
+
+ codes = {
+ StockStatus.OK: 17,
+ StockStatus.DESTROYED: 1,
+ StockStatus.LOST: 1,
+ StockStatus.DAMAGED: 0,
+ StockStatus.REJECTED: 0,
+ }
+
+ for code in codes.keys():
+ num = codes[code]
+
+ response = self.get_stock(status=code)
+ self.assertEqual(len(response), num)
+
+ def test_filter_by_batch(self):
+ """
+ Filter StockItem by batch code
+ """
+
+ response = self.get_stock(batch='B123')
+ self.assertEqual(len(response), 1)
+
+ def test_filter_by_serialized(self):
+ """
+ Filter StockItem by serialized status
+ """
+
+ response = self.get_stock(serialized=1)
+ self.assertEqual(len(response), 12)
+
+ for item in response:
+ self.assertIsNotNone(item['serial'])
+
+ response = self.get_stock(serialized=0)
+ self.assertEqual(len(response), 7)
+
+ for item in response:
+ self.assertIsNone(item['serial'])
+
+ def test_filter_by_expired(self):
+ """
+ Filter StockItem by expiry status
+ """
+
+ # First, we can assume that the 'stock expiry' feature is disabled
+ response = self.get_stock(expired=1)
+ self.assertEqual(len(response), 19)
+
+ # Now, ensure that the expiry date feature is enabled!
+ InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
+
+ response = self.get_stock(expired=1)
+ self.assertEqual(len(response), 1)
+
+ for item in response:
+ self.assertTrue(item['expired'])
+
+ response = self.get_stock(expired=0)
+ self.assertEqual(len(response), 18)
+
+ for item in response:
+ self.assertFalse(item['expired'])
+
+ # Mark some other stock items as expired
+ today = datetime.now().date()
+
+ for pk in [510, 511, 512]:
+ item = StockItem.objects.get(pk=pk)
+ item.expiry_date = today - timedelta(days=pk)
+ item.save()
+
+ response = self.get_stock(expired=1)
+ self.assertEqual(len(response), 4)
+
+ response = self.get_stock(expired=0)
+ self.assertEqual(len(response), 15)
+
+
class StockItemTest(StockAPITestCase):
"""
Series of API tests for the StockItem API
@@ -94,10 +280,6 @@ class StockItemTest(StockAPITestCase):
StockLocation.objects.create(name='B', description='location b', parent=top)
StockLocation.objects.create(name='C', description='location c', parent=top)
- def test_get_stock_list(self):
- response = self.client.get(self.list_url, format='json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
def test_create_default_location(self):
"""
Test the default location functionality,
@@ -198,6 +380,56 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ def test_default_expiry(self):
+ """
+ Test that the "default_expiry" functionality works via the API.
+
+ - If an expiry_date is specified, use that
+ - Otherwise, check if the referenced part has a default_expiry defined
+ - If so, use that!
+ - Otherwise, no expiry
+
+ Notes:
+ - Part <25> has a default_expiry of 10 days
+
+ """
+
+ # First test - create a new StockItem without an expiry date
+ data = {
+ 'part': 4,
+ 'quantity': 10,
+ }
+
+ response = self.client.post(self.list_url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ self.assertIsNone(response.data['expiry_date'])
+
+ # Second test - create a new StockItem with an explicit expiry date
+ data['expiry_date'] = '2022-12-12'
+
+ response = self.client.post(self.list_url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ self.assertIsNotNone(response.data['expiry_date'])
+ self.assertEqual(response.data['expiry_date'], '2022-12-12')
+
+ # Third test - create a new StockItem for a Part which has a default expiry time
+ data = {
+ 'part': 25,
+ 'quantity': 10
+ }
+
+ response = self.client.post(self.list_url, data)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ # Expected expiry date is 10 days in the future
+ expiry = datetime.now().date() + timedelta(10)
+
+ self.assertEqual(response.data['expiry_date'], expiry.isoformat())
+
class StocktakeTest(StockAPITestCase):
"""
diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py
index 1f55d74eec..b842de3836 100644
--- a/InvenTree/stock/test_views.py
+++ b/InvenTree/stock/test_views.py
@@ -5,7 +5,10 @@ from django.urls import reverse
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
+from common.models import InvenTreeSetting
+
import json
+from datetime import datetime, timedelta
class StockViewTestCase(TestCase):
@@ -31,6 +34,9 @@ class StockViewTestCase(TestCase):
password='password'
)
+ self.user.is_staff = True
+ self.user.save()
+
# Put the user into a group with the correct permissions
group = Group.objects.create(name='mygroup')
self.user.groups.add(group)
@@ -135,21 +141,56 @@ class StockItemTest(StockViewTestCase):
self.assertEqual(response.status_code, 200)
def test_create_item(self):
- # Test creation of StockItem
- response = self.client.get(reverse('stock-item-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ """
+ Test creation of StockItem
+ """
+
+ url = reverse('stock-item-create')
+
+ response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
- response = self.client.get(reverse('stock-item-create'), {'part': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ response = self.client.get(url, {'part': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Copy from a valid item, valid location
- response = self.client.get(reverse('stock-item-create'), {'location': 1, 'copy': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ response = self.client.get(url, {'location': 1, 'copy': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Copy from an invalid item, invalid location
- response = self.client.get(reverse('stock-item-create'), {'location': 999, 'copy': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ response = self.client.get(url, {'location': 999, 'copy': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
+ def test_create_stock_with_expiry(self):
+ """
+ Test creation of stock item of a part with an expiry date.
+ The initial value for the "expiry_date" field should be pre-filled,
+ and should be in the future!
+ """
+
+ # First, ensure that the expiry date feature is enabled!
+ InvenTreeSetting.set_setting('STOCK_ENABLE_EXPIRY', True, self.user)
+
+ url = reverse('stock-item-create')
+
+ response = self.client.get(url, {'part': 25}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ self.assertEqual(response.status_code, 200)
+
+ # We are expecting 10 days in the future
+ expiry = datetime.now().date() + timedelta(10)
+
+ expected = f'name=\\\\"expiry_date\\\\" value=\\\\"{expiry.isoformat()}\\\\"'
+
+ self.assertIn(expected, str(response.content))
+
+ # Now check with a part which does *not* have a default expiry period
+ response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ expected = 'name=\\\\"expiry_date\\\\" placeholder=\\\\"\\\\"'
+
+ self.assertIn(expected, str(response.content))
+
def test_serialize_item(self):
# Test the serialization view
diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py
index 3d309c0360..b0b05b6326 100644
--- a/InvenTree/stock/tests.py
+++ b/InvenTree/stock/tests.py
@@ -49,6 +49,40 @@ class StockTest(TestCase):
Part.objects.rebuild()
StockItem.objects.rebuild()
+ def test_expiry(self):
+ """
+ Test expiry date functionality for StockItem model.
+ """
+
+ today = datetime.datetime.now().date()
+
+ item = StockItem.objects.create(
+ location=self.office,
+ part=Part.objects.get(pk=1),
+ quantity=10,
+ )
+
+ # Without an expiry_date set, item should not be "expired"
+ self.assertFalse(item.is_expired())
+
+ # Set the expiry date to today
+ item.expiry_date = today
+ item.save()
+
+ self.assertFalse(item.is_expired())
+
+ # Set the expiry date in the future
+ item.expiry_date = today + datetime.timedelta(days=5)
+ item.save()
+
+ self.assertFalse(item.is_expired())
+
+ # Set the expiry date in the past
+ item.expiry_date = today - datetime.timedelta(days=5)
+ item.save()
+
+ self.assertTrue(item.is_expired())
+
def test_is_building(self):
"""
Test that the is_building flag does not count towards stock.
@@ -143,8 +177,10 @@ class StockTest(TestCase):
# There should be 9000 screws in stock
self.assertEqual(part.total_stock, 9000)
- # There should be 18 widgets in stock
- self.assertEqual(StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 19)
+ # There should be 16 widgets "in stock"
+ self.assertEqual(
+ StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 16
+ )
def test_delete_location(self):
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 582c1b5d89..ab6f64fb44 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -26,7 +26,7 @@ from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import extract_serial_numbers
from decimal import Decimal, InvalidOperation
-from datetime import datetime
+from datetime import datetime, timedelta
from company.models import Company, SupplierPart
from part.models import Part
@@ -1302,6 +1302,10 @@ class StockItemEdit(AjaxUpdateView):
form = super(AjaxUpdateView, self).get_form()
+ # Hide the "expiry date" field if the feature is not enabled
+ if not common.settings.stock_expiry_enabled():
+ form.fields.pop('expiry_date')
+
item = self.get_object()
# If the part cannot be purchased, hide the supplier_part field
@@ -1513,6 +1517,10 @@ class StockItemCreate(AjaxCreateView):
form = super().get_form()
+ # Hide the "expiry date" field if the feature is not enabled
+ if not common.settings.stock_expiry_enabled():
+ form.fields.pop('expiry_date')
+
part = self.get_part(form=form)
if part is not None:
@@ -1596,6 +1604,11 @@ class StockItemCreate(AjaxCreateView):
initials['location'] = part.get_default_location()
initials['supplier_part'] = part.default_supplier
+ # If the part has a defined expiry period, extrapolate!
+ if part.default_expiry > 0:
+ expiry_date = datetime.now().date() + timedelta(days=part.default_expiry)
+ initials['expiry_date'] = expiry_date
+
currency_code = common.settings.currency_code_default()
# SupplierPart field has been specified
diff --git a/InvenTree/templates/InvenTree/expired_stock.html b/InvenTree/templates/InvenTree/expired_stock.html
new file mode 100644
index 0000000000..20e2591c16
--- /dev/null
+++ b/InvenTree/templates/InvenTree/expired_stock.html
@@ -0,0 +1,15 @@
+{% extends "collapse_index.html" %}
+
+{% load i18n %}
+
+{% block collapse_title %}
+
+{% trans "Expired Stock" %}
+{% endblock %}
+
+{% block collapse_content %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html
index f862175920..1b9a492a22 100644
--- a/InvenTree/templates/InvenTree/index.html
+++ b/InvenTree/templates/InvenTree/index.html
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
+{% load inventree_extras %}
{% block page_title %}
InvenTree | {% trans "Index" %}
{% endblock %}
@@ -8,7 +9,7 @@ InvenTree | {% trans "Index" %}
InvenTree
-
+
{% if roles.part.view %}
{% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %}
{% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %}
@@ -19,11 +20,18 @@ InvenTree | {% trans "Index" %}
{% include "InvenTree/build_overdue.html" with collapse_id="build_overdue" %}
{% endif %}
-
+
{% if roles.stock.view %}
{% include "InvenTree/low_stock.html" with collapse_id="order" %}
+ {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
+ {% if expiry %}
+ {% include "InvenTree/expired_stock.html" with collapse_id="expired" %}
+ {% include "InvenTree/stale_stock.html" with collapse_id="stale" %}
+ {% endif %}
{% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %}
{% endif %}
+
+
{% if roles.purchase_order.view %}
{% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %}
{% endif %}
@@ -83,6 +91,23 @@ loadBuildTable("#build-overdue-table", {
disableFilters: true,
});
+loadStockTable($("#expired-stock-table"), {
+ params: {
+ expired: true,
+ location_detail: true,
+ part_detail: true,
+ },
+});
+
+loadStockTable($("#stale-stock-table"), {
+ params: {
+ stale: true,
+ expired: false,
+ location_detail: true,
+ part_detail: true,
+ },
+});
+
loadSimplePartTable("#low-stock-table", "{% url 'api-part-list' %}", {
params: {
low_stock: true,
@@ -121,64 +146,19 @@ loadSalesOrderTable("#so-overdue-table", {
}
});
-$("#latest-parts-table").on('load-success.bs.table', function() {
- var count = $("#latest-parts-table").bootstrapTable('getData').length;
+{% include "InvenTree/index/on_load.html" with label="latest-parts" %}
+{% include "InvenTree/index/on_load.html" with label="starred-parts" %}
+{% include "InvenTree/index/on_load.html" with label="bom-invalid" %}
+{% include "InvenTree/index/on_load.html" with label="build-pending" %}
+{% include "InvenTree/index/on_load.html" with label="build-overdue" %}
- $("#latest-parts-count").html(count);
-});
+{% include "InvenTree/index/on_load.html" with label="expired-stock" %}
+{% include "InvenTree/index/on_load.html" with label="stale-stock" %}
+{% include "InvenTree/index/on_load.html" with label="low-stock" %}
+{% include "InvenTree/index/on_load.html" with label="stock-to-build" %}
-$("#starred-parts-table").on('load-success.bs.table', function() {
- var count = $("#starred-parts-table").bootstrapTable('getData').length;
-
- $("#starred-parts-count").html(count);
-});
-
-$("#bom-invalid-table").on('load-success.bs.table', function() {
- var count = $("#bom-invalid-table").bootstrapTable('getData').length;
-
- $("#bom-invalid-count").html(count);
-});
-
-$("#build-pending-table").on('load-success.bs.table', function() {
- var count = $("#build-pending-table").bootstrapTable('getData').length;
-
- $("#build-pending-count").html(count);
-});
-
-$("#build-overdue-table").on('load-success.bs.table', function() {
- var count = $("#build-overdue-table").bootstrapTable('getData').length;
-
- $("#build-overdue-count").html(count);
-});
-
-$("#low-stock-table").on('load-success.bs.table', function() {
- var count = $("#low-stock-table").bootstrapTable('getData').length;
-
- $("#low-stock-count").html(count);
-});
-
-$("#stock-to-build-table").on('load-success.bs.table', function() {
- var count = $("#stock-to-build-table").bootstrapTable('getData').length;
-
- $("#stock-to-build-count").html(count);
-});
-
-$("#po-outstanding-table").on('load-success.bs.table', function() {
- var count = $("#po-outstanding-table").bootstrapTable('getData').length;
-
- $("#po-outstanding-count").html(count);
-});
-
-$("#so-outstanding-table").on('load-success.bs.table', function() {
- var count = $("#so-outstanding-table").bootstrapTable('getData').length;
-
- $("#so-outstanding-count").html(count);
-});
-
-$("#so-overdue-table").on('load-success.bs.table', function() {
- var count = $("#so-overdue-table").bootstrapTable('getData').length;
-
- $("#so-overdue-count").html(count);
-});
+{% include "InvenTree/index/on_load.html" with label="po-outstanding" %}
+{% include "InvenTree/index/on_load.html" with label="so-outstanding" %}
+{% include "InvenTree/index/on_load.html" with label="so-overdue" %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/InvenTree/index/on_load.html b/InvenTree/templates/InvenTree/index/on_load.html
new file mode 100644
index 0000000000..a63479e60d
--- /dev/null
+++ b/InvenTree/templates/InvenTree/index/on_load.html
@@ -0,0 +1,5 @@
+$("#{{ label }}-table").on('load-success.bs.table', function() {
+ var count = $("#{{ label }}-table").bootstrapTable('getData').length;
+
+ $("#{{ label }}-count").html(count);
+});
\ No newline at end of file
diff --git a/InvenTree/templates/InvenTree/settings/build.html b/InvenTree/templates/InvenTree/settings/build.html
index 781402795b..7d04a8f8b7 100644
--- a/InvenTree/templates/InvenTree/settings/build.html
+++ b/InvenTree/templates/InvenTree/settings/build.html
@@ -13,7 +13,7 @@
{% block settings %}
-
+ {% include "InvenTree/settings/header.html" %}
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %}
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %}
diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html
index 775d30b915..76af68b441 100644
--- a/InvenTree/templates/InvenTree/settings/global.html
+++ b/InvenTree/templates/InvenTree/settings/global.html
@@ -13,11 +13,11 @@
{% block settings %}
-
+ {% include "InvenTree/settings/header.html" %}
- {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" %}
- {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" %}
- {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" %}
+ {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %}
+ {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
+ {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
-
+ {% include "InvenTree/settings/header.html" %}
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
-
- {% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" %}
- {% include "InvenTree/settings/setting.html" with key="PART_PURCHASEABLE" %}
- {% include "InvenTree/settings/setting.html" with key="PART_SALABLE" %}
- {% include "InvenTree/settings/setting.html" with key="PART_TRACKABLE" %}
-
+
+ {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
+ {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %}
+ {% include "InvenTree/settings/setting.html" with key="PART_COMPONENT" icon="fa-th"%}
+ {% include "InvenTree/settings/setting.html" with key="PART_TRACKABLE" icon="fa-directions" %}
+ {% include "InvenTree/settings/setting.html" with key="PART_PURCHASEABLE" icon="fa-shopping-cart" %}
+ {% include "InvenTree/settings/setting.html" with key="PART_SALABLE" icon="fa-dollar-sign" %}
+ {% include "InvenTree/settings/setting.html" with key="PART_VIRTUAL" icon="fa-ghost" %}
+
{% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %}
{% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %}
{% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %}
diff --git a/InvenTree/templates/InvenTree/settings/po.html b/InvenTree/templates/InvenTree/settings/po.html
index a709d40dd3..20e3b0074b 100644
--- a/InvenTree/templates/InvenTree/settings/po.html
+++ b/InvenTree/templates/InvenTree/settings/po.html
@@ -11,7 +11,7 @@
{% block settings %}
-
+ {% include "InvenTree/settings/header.html" %}
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PREFIX" %}
diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html
index ffbb78cbbc..b7932fc30a 100644
--- a/InvenTree/templates/InvenTree/settings/setting.html
+++ b/InvenTree/templates/InvenTree/settings/setting.html
@@ -3,6 +3,11 @@
{% setting_object key as setting %}
+
+ {{ setting.value }} {{ setting.units }}
+
{% else %}
{% trans "No value set" %}
{% endif %}
diff --git a/InvenTree/templates/InvenTree/settings/so.html b/InvenTree/templates/InvenTree/settings/so.html
index 368374532f..4ef1709068 100644
--- a/InvenTree/templates/InvenTree/settings/so.html
+++ b/InvenTree/templates/InvenTree/settings/so.html
@@ -12,7 +12,7 @@
{% block settings %}
-
+ {% include "InvenTree/settings/header.html" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %}
diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html
index c3c40087ff..5ad308decc 100644
--- a/InvenTree/templates/InvenTree/settings/stock.html
+++ b/InvenTree/templates/InvenTree/settings/stock.html
@@ -10,4 +10,15 @@
{% endblock %}
{% block settings %}
+
{% trans "Stock Options" %}
+
+
+ {% include "InvenTree/settings/header.html" %}
+
+ {% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" %}
+ {% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" %}
+ {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" %}
+ {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" %}
+
+
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/InvenTree/stale_stock.html b/InvenTree/templates/InvenTree/stale_stock.html
new file mode 100644
index 0000000000..3cbb74369c
--- /dev/null
+++ b/InvenTree/templates/InvenTree/stale_stock.html
@@ -0,0 +1,15 @@
+{% extends "collapse_index.html" %}
+
+{% load i18n %}
+
+{% block collapse_title %}
+
+{% trans "Stale Stock" %}
+{% endblock %}
+
+{% block collapse_content %}
+
+
diff --git a/InvenTree/templates/js/calendar.js b/InvenTree/templates/js/calendar.js
new file mode 100644
index 0000000000..861bbe1727
--- /dev/null
+++ b/InvenTree/templates/js/calendar.js
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+/**
+ * Helper functions for calendar display
+ */
+
+function startDate(calendar) {
+ // Extract the first displayed date on the calendar
+ return calendar.currentData.dateProfile.activeRange.start.toISOString().split("T")[0];
+}
+
+function endDate(calendar) {
+ // Extract the last display date on the calendar
+ return calendar.currentData.dateProfile.activeRange.end.toISOString().split("T")[0];
+}
+
+function clearEvents(calendar) {
+ // Remove all events from the calendar
+
+ var events = calendar.getEvents();
+
+ events.forEach(function(event) {
+ event.remove();
+ })
+}
\ No newline at end of file
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index f8a7c38d2c..e3f124d252 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -1,4 +1,5 @@
{% load i18n %}
+{% load inventree_extras %}
{% load status_codes %}
/* Stock API functions
@@ -532,6 +533,12 @@ function loadStockTable(table, options) {
html += makeIconBadge('fa-user', '{% trans "Stock item assigned to customer" %}');
}
+ if (row.expired) {
+ html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Stock item has expired" %}');
+ } else if (row.stale) {
+ html += makeIconBadge('fa-stopwatch', '{% trans "Stock item will expire soon" %}');
+ }
+
if (row.allocated) {
html += makeIconBadge('fa-bookmark', '{% trans "Stock item has been allocated" %}');
}
@@ -583,6 +590,14 @@ function loadStockTable(table, options) {
return locationDetail(row);
}
},
+ {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
+ {% if expiry %}
+ {
+ field: 'expiry_date',
+ title: '{% trans "Expiry Date" %}',
+ sortable: true,
+ },
+ {% endif %}
{
field: 'notes',
title: '{% trans "Notes" %}',
@@ -609,8 +624,8 @@ function loadStockTable(table, options) {
if (action == 'move') {
secondary.push({
field: 'destination',
- label: 'New Location',
- title: 'Create new location',
+ label: '{% trans "New Location" %}',
+ title: '{% trans "Create new location" %}',
url: "/stock/location/new/",
});
}
@@ -828,14 +843,25 @@ function createNewStockItem(options) {
}
);
- // Disable serial number field if the part is not trackable
+ // Request part information from the server
inventreeGet(
`/api/part/${value}/`, {},
{
success: function(response) {
-
+
+ // Disable serial number field if the part is not trackable
enableField('serial_numbers', response.trackable);
clearField('serial_numbers');
+
+ // Populate the expiry date
+ if (response.default_expiry <= 0) {
+ // No expiry date
+ clearField('expiry_date');
+ } else {
+ var expiry = moment().add(response.default_expiry, 'days');
+
+ setFieldValue('expiry_date', expiry.format("YYYY-MM-DD"));
+ }
}
}
);
diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js
index d2ea26d8c3..84aa12c139 100644
--- a/InvenTree/templates/js/table_filters.js
+++ b/InvenTree/templates/js/table_filters.js
@@ -106,6 +106,16 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
+ expired: {
+ type: 'bool',
+ title: '{% trans "Expired" %}',
+ description: '{% trans "Show stock items which have expired" %}',
+ },
+ stale: {
+ type: 'bool',
+ title: '{% trans "Stale" %}',
+ description: '{% trans "Show stock which is close to expiring" %}',
+ },
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',