1/*! 2FullCalendar Scheduler (Vanilla JS) v7.0.2 3Docs & License: https://fullcalendar.io/docs/premium 4(c) 2026 Adam Shaw 5*/ 6(function (protectedApi, dayGridPlugin, jsxRuntime, publicApi, classNames, compat, timeGridPlugin) { 7 'use strict'; 8 9 function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 10 11 var dayGridPlugin__default = /*#__PURE__*/_interopDefaultLegacy(dayGridPlugin); 12 var classNames__default = /*#__PURE__*/_interopDefaultLegacy(classNames); 13 var timeGridPlugin__default = /*#__PURE__*/_interopDefaultLegacy(timeGridPlugin); 14 15 function identity(raw) { 16 return raw; 17 } 18 19 const PRIVATE_ID_PREFIX = '_fc:'; 20 const RESOURCE_REFINERS = { 21 id: String, 22 parentId: String, 23 children: identity, 24 title: String, 25 businessHours: identity, 26 extendedProps: identity, 27 // event-ui 28 eventEditable: Boolean, 29 eventStartEditable: Boolean, 30 eventDurationEditable: Boolean, 31 eventConstraint: identity, 32 eventOverlap: Boolean, // can NOT be a func, different from OptionsInput 33 eventAllow: identity, 34 eventClass: protectedApi.refineClassName, 35 eventColor: String, 36 eventContrastColor: String, 37 }; 38 /* 39 needs a full store so that it can populate children too 40 */ 41 function parseResource(raw, parentId = '', store, context) { 42 let { refined, extra } = protectedApi.refineProps(raw, RESOURCE_REFINERS); 43 let resource = { 44 id: refined.id || (PRIVATE_ID_PREFIX + protectedApi.guid()), 45 parentId: refined.parentId || parentId, 46 title: refined.title || '', 47 businessHours: refined.businessHours ? protectedApi.parseBusinessHours(refined.businessHours, context) : null, 48 ui: protectedApi.createEventUi({ 49 editable: refined.eventEditable, 50 startEditable: refined.eventStartEditable, 51 durationEditable: refined.eventDurationEditable, 52 constraint: refined.eventConstraint, 53 overlap: refined.eventOverlap, 54 allow: refined.eventAllow, 55 className: refined.eventClass, 56 color: refined.eventColor, 57 contrastColor: refined.eventContrastColor, 58 }, context), 59 extendedProps: { 60 ...extra, 61 ...refined.extendedProps, 62 }, 63 }; 64 // help out ResourceApi from having user modify props 65 Object.freeze(resource.ui.className); // might be simple string, but freeze still works 66 Object.freeze(resource.extendedProps); 67 if (!store[resource.id]) { // no duplicate resource ID? 68 store[resource.id] = resource; 69 if (refined.children) { 70 for (let childInput of refined.children) { 71 parseResource(childInput, resource.id, store, context); 72 } 73 } 74 } 75 return resource; 76 } 77 /* 78 TODO: use this in more places 79 */ 80 function getPublicId(id) { 81 if (id.indexOf(PRIVATE_ID_PREFIX) === 0) { 82 return ''; 83 } 84 return id; 85 } 86 87 class ResourceApi { 88 constructor(_context, _resource) { 89 this._context = _context; 90 this._resource = _resource; 91 } 92 setProp(name, value) { 93 let oldResource = this._resource; 94 this._context.dispatch({ 95 type: 'SET_RESOURCE_PROP', 96 resourceId: oldResource.id, 97 propName: name, 98 propValue: value, 99 }); 100 this.sync(oldResource); 101 } 102 setExtendedProp(name, value) { 103 let oldResource = this._resource; 104 this._context.dispatch({ 105 type: 'SET_RESOURCE_EXTENDED_PROP', 106 resourceId: oldResource.id, 107 propName: name, 108 propValue: value, 109 });
vendor: 10,365 bytes, lines 110-363
110 this.sync(oldResource); 111 } 112 sync(oldResource) { 113 let context = this._context; 114 let resourceId = oldResource.id; 115 // TODO: what if dispatch didn't complete synchronously? 116 this._resource = context.getCurrentData().resourceStore[resourceId]; 117 context.emitter.trigger('resourceChange', { 118 oldResource: new ResourceApi(context, oldResource), 119 resource: this, 120 revert() { 121 context.dispatch({ 122 type: 'ADD_RESOURCE', // function as a merge. TODO: rename 123 resourceHash: { 124 [resourceId]: oldResource, 125 }, 126 }); 127 }, 128 }); 129 } 130 remove() { 131 let context = this._context; 132 let internalResource = this._resource; 133 let resourceId = internalResource.id; 134 context.dispatch({ 135 type: 'REMOVE_RESOURCE', 136 resourceId, 137 }); 138 context.emitter.trigger('resourceRemove', { 139 resource: this, 140 revert() { 141 context.dispatch({ 142 type: 'ADD_RESOURCE', // function as a merge. TODO: rename 143 resourceHash: { 144 [resourceId]: internalResource, 145 }, 146 }); 147 }, 148 }); 149 } 150 getParent() { 151 let context = this._context; 152 let parentId = this._resource.parentId; 153 if (parentId) { 154 return new ResourceApi(context, context.getCurrentData().resourceStore[parentId]); 155 } 156 return null; 157 } 158 getChildren() { 159 let thisResourceId = this._resource.id; 160 let context = this._context; 161 let { resourceStore } = context.getCurrentData(); 162 let childApis = []; 163 for (let resourceId in resourceStore) { 164 if (resourceStore[resourceId].parentId === thisResourceId) { 165 childApis.push(new ResourceApi(context, resourceStore[resourceId])); 166 } 167 } 168 return childApis; 169 } 170 /* 171 this is really inefficient! 172 TODO: make EventApi::resourceIds a hash or keep an index in the Calendar's state 173 */ 174 getEvents() { 175 let thisResourceId = this._resource.id; 176 let context = this._context; 177 let { defs, instances } = context.getCurrentData().eventStore; 178 let eventApis = []; 179 for (let instanceId in instances) { 180 let instance = instances[instanceId]; 181 let def = defs[instance.defId]; 182 if (def.resourceIds.indexOf(thisResourceId) !== -1) { // inefficient!!! 183 eventApis.push(new protectedApi.EventImpl(context, def, instance)); 184 } 185 } 186 return eventApis; 187 } 188 get id() { return getPublicId(this._resource.id); } 189 get title() { return this._resource.title; } 190 get eventConstraint() { return this._resource.ui.constraints[0] || null; } // TODO: better type 191 get eventOverlap() { return this._resource.ui.overlap; } 192 get eventAllow() { return this._resource.ui.allows[0] || null; } // TODO: better type 193 get eventColor() { return this._resource.ui.color; } 194 get eventContrastColor() { return this._resource.ui.contrastColor; } 195 // NOTE: user can't modify these because Object.freeze was called in event-def parsing 196 get eventClass() { return this._resource.ui.className; } 197 get extendedProps() { return this._resource.extendedProps; } 198 toPlainObject(settings = {}) { 199 let internal = this._resource; 200 let { ui } = internal; 201 let publicId = this.id; 202 let res = {}; 203 if (publicId) { 204 res.id = publicId; 205 } 206 if (internal.title) { 207 res.title = internal.title; 208 } 209 if (ui.contrastColor) { 210 res.eventContrastColor = ui.contrastColor; 211 } 212 if (ui.contrastColor) { 213 res.eventContrastColor = ui.contrastColor; 214 } 215 if (ui.className) { 216 res.eventClass = ui.className; 217 } 218 if (Object.keys(internal.extendedProps).length) { 219 if (settings.collapseExtendedProps) { 220 Object.assign(res, internal.extendedProps); 221 } 222 else { 223 res.extendedProps = internal.extendedProps; 224 } 225 } 226 return res; 227 } 228 toJSON() { 229 return this.toPlainObject(); 230 } 231 } 232 function buildResourceApis(resourceStore, context) { 233 let resourceApis = []; 234 for (let resourceId in resourceStore) { 235 resourceApis.push(new ResourceApi(context, resourceStore[resourceId])); 236 } 237 return resourceApis; 238 } 239 240 protectedApi.EventImpl.prototype.getResources = function () { 241 let { calendarApi } = this._context; 242 return this._def.resourceIds.map((resourceId) => calendarApi.getResourceById(resourceId)); 243 }; 244 protectedApi.EventImpl.prototype.setResources = function (resources) { 245 let resourceIds = []; 246 // massage resources -> resourceIds 247 for (let resource of resources) { 248 let resourceId = null; 249 if (typeof resource === 'string') { 250 resourceId = resource; 251 } 252 else if (typeof resource === 'number') { 253 resourceId = String(resource); 254 } 255 else if (resource instanceof ResourceApi) { 256 resourceId = resource.id; // guaranteed to always have an ID. hmmm 257 } 258 else { 259 protectedApi.warn(`Invalid resource value \`${resource}\`. Expected a resource ID or ResourceApi object.`); 260 } 261 if (resourceId) { 262 resourceIds.push(resourceId); 263 } 264 } 265 this.mutate({ 266 standardProps: { 267 resourceIds, 268 }, 269 }); 270 }; 271 272 protectedApi.CalendarApiImpl.prototype.addResource = function (input, scrollTo = true) { 273 let currentState = this.getCurrentData(); 274 let resourceHash; 275 let resource; 276 if (input instanceof ResourceApi) { 277 resource = input._resource; 278 resourceHash = { [resource.id]: resource }; 279 } 280 else { 281 resourceHash = {}; 282 resource = parseResource(input, '', resourceHash, currentState); 283 } 284 this.dispatch({ 285 type: 'ADD_RESOURCE', 286 resourceHash, 287 }); 288 if (scrollTo) { 289 // TODO: wait til dispatch completes somehow 290 this.trigger('_resourceScrollRequest', resource.id); 291 } 292 let resourceApi = new ResourceApi(currentState, resource); 293 currentState.emitter.trigger('resourceAdd', { 294 resource: resourceApi, 295 revert: () => { 296 this.dispatch({ 297 type: 'REMOVE_RESOURCE', 298 resourceId: resource.id, 299 }); 300 }, 301 }); 302 return resourceApi; 303 }; 304 protectedApi.CalendarApiImpl.prototype.getResourceById = function (id) { 305 id = String(id); 306 let currentState = this.getCurrentData(); // eslint-disable-line react/no-this-in-sfc 307 if (currentState.resourceStore) { // guard against calendar with no resource functionality 308 let rawResource = currentState.resourceStore[id]; 309 if (rawResource) { 310 return new ResourceApi(currentState, rawResource); 311 } 312 } 313 return null; 314 }; 315 protectedApi.CalendarApiImpl.prototype.getResources = function () { 316 let currentState = this.getCurrentData(); 317 let { resourceStore } = currentState; 318 let resourceApis = []; 319 if (resourceStore) { // guard against calendar with no resource functionality 320 for (let resourceId in resourceStore) { 321 resourceApis.push(new ResourceApi(currentState, resourceStore[resourceId])); 322 } 323 } 324 return resourceApis; 325 }; 326 protectedApi.CalendarApiImpl.prototype.getTopLevelResources = function () { 327 let currentState = this.getCurrentData(); 328 let { resourceStore } = currentState; 329 let resourceApis = []; 330 if (resourceStore) { // guard against calendar with no resource functionality 331 for (let resourceId in resourceStore) { 332 if (!resourceStore[resourceId].parentId) { 333 resourceApis.push(new ResourceApi(currentState, resourceStore[resourceId])); 334 } 335 } 336 } 337 return resourceApis; 338 }; 339 protectedApi.CalendarApiImpl.prototype.refetchResources = function () { 340 this.dispatch({ 341 type: 'REFETCH_RESOURCES', 342 }); 343 }; 344 function transformDatePoint(dateSpan, context) { 345 return dateSpan.resourceId ? 346 { resource: context.calendarApi.getResourceById(dateSpan.resourceId) } : 347 {}; 348 } 349 function transformDateSpan(dateSpan, context) { 350 return dateSpan.resourceId ? 351 { resource: context.calendarApi.getResourceById(dateSpan.resourceId) } : 352 {}; 353 } 354 355 const UPGRADE_WINDOW = 365 + 7; // days. 1 week leeway, for tz shift reasons too 356 const INVALID_LICENSE_URL = 'https://fullcalendar.io/docs/schedulerLicenseKey#invalid'; 357 const OUTDATED_LICENSE_URL = 'https://fullcalendar.io/docs/schedulerLicenseKey#outdated'; 358 const PRESET_LICENSE_KEYS = [ 359 'AGPL-My-Frontend-And-Backend-Are-Open-Source', 360 'CC-Attribution-NonCommercial-NoDerivatives', 361 ]; 362 function buildLicenseWarning(context) { 363 const { options, pluginHooks } = context;
364 const key = options.schedulerLicenseKey; 365 const currentUrl = typeof window !== 'undefined' ? window.location.href : ''; 366 if (!isImmuneUrl(currentUrl)) { 367 const status = processLicenseKey(key, pluginHooks.premiumReleaseDate); 368 let statusText; 369 let statusUrl; 370 if (status === 'outdated') { 371 statusText = 'Your license key is too old to work with this version.'; 372 statusUrl = OUTDATED_LICENSE_URL; 373 } 374 else { 375 statusText = 'Your license key is invalid.'; 376 statusUrl = INVALID_LICENSE_URL; 377 } 378 if (status !== 'valid') { 379 return (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.popoverClass, classNames__default["default"].popoverZ, classNames__default["default"].abs, classNames__default["default"].noPadding), style: { 380 bottom: 0, 381 left: 0, // TODO: use direction-aware start 382 minWidth: 'auto', 383 }, children: jsxRuntime.jsxs("div", { style: { 384 padding: '5px', 385 fontSize: '12px', 386 lineHeight: '1em', 387 }, children: [statusText, ' ', jsxRuntime.jsx("a", { href: statusUrl, target: '_blank', style: { 388 textDecoration: 'underline', 389 }, children: "More info" })] }) })); 390 } 391 } 392 return null; 393 } 394 /* 395 This decryption is not meant to be bulletproof. Just a way to remind about an upgrade. 396 */ 397 function processLicenseKey(key, premiumReleaseDate) { 398 if (PRESET_LICENSE_KEYS.indexOf(key) !== -1) { 399 return 'valid'; 400 } 401 const parts = (key || '').match(/^(\d+)-fcs-(\d+)$/); 402 if (parts && (parts[1].length === 10)) { 403 const purchaseDate = new Date(parseInt(parts[2], 10) * 1000); 404 const releaseDate = protectedApi.config.mockSchedulerReleaseDate || premiumReleaseDate; 405 if (protectedApi.isValidDate(releaseDate)) { // token won't be replaced in dev mode 406 const minPurchaseDate = protectedApi.addDays(releaseDate, -UPGRADE_WINDOW); 407 if (minPurchaseDate < purchaseDate) { 408 return 'valid'; 409 } 410 return 'outdated'; 411 } 412 } 413 return 'invalid'; 414 } 415 function isImmuneUrl(url) { 416 return /\w+:\/\/fullcalendar\.io\/|\/examples\/[\w-]+\.html$/.test(url); 417 } 418 419 const OPTION_REFINERS = { 420 // Premium Common 421 // -------------- 422 schedulerLicenseKey: String, 423 virtualization: Boolean, 424 // Resource 425 // -------- 426 initialResources: protectedApi.identity, 427 resources: protectedApi.identity, 428 eventResourceEditable: Boolean, 429 refetchResourcesOnNavigate: Boolean, 430 resourceOrder: protectedApi.parseFieldSpecs, 431 filterResourcesWithEvents: Boolean, 432 resourceGroupField: String, 433 resourcesInitiallyExpanded: Boolean, 434 datesAboveResources: Boolean, 435 needsResourceData: Boolean, // internal-only 436 // Resource DayGrid 437 // ---------------- 438 resourceDayHeaderClass: protectedApi.refineClassNameGenerator, 439 resourceDayHeaderInnerClass: protectedApi.refineClassNameGenerator, 440 resourceDayHeaderContent: protectedApi.identity, 441 resourceDayHeaderDidMount: protectedApi.identity, 442 resourceDayHeaderWillUnmount: protectedApi.identity, 443 resourceDayHeaderAlign: protectedApi.identity, 444 // stickiness for cell-inner-contents laterally. experimental settings 445 _resourceDayHeaderSticky: protectedApi.identity, 446 // Resource Timeline 447 // ----------------- 448 printMaxRows: Number, 449 resourceColumnsWidth: protectedApi.identity, 450 resourceColumns: protectedApi.identity, 451 resourceColumnDividerClass: protectedApi.refineClassName, 452 // datagrid super-header & normal column headers 453 resourceColumnHeaderClass: protectedApi.refineClassNameGenerator, 454 resourceColumnHeaderInnerClass: protectedApi.refineClassNameGenerator, 455 resourceColumnResizerClass: protectedApi.refineClassNameGenerator, 456 resourceColumnHeaderContent: protectedApi.identity, 457 resourceColumnHeaderDidMount: protectedApi.identity, 458 resourceColumnHeaderWillUnmount: protectedApi.identity, 459 resourceHeaderRowClass: protectedApi.refineClassName, 460 // For both resources & resource groups 461 resourceRowClass: protectedApi.refineClassName, 462 // datagrid cells, for both resources & resource-GROUP 463 resourceCellClass: protectedApi.refineClassNameGenerator, 464 resourceCellInnerClass: protectedApi.refineClassNameGenerator, 465 resourceCellContent: protectedApi.identity, 466 resourceCellDidMount: protectedApi.identity, 467 resourceCellWillUnmount: protectedApi.identity, 468 // datagrid, for resource-GROUP entire row 469 resourceGroupHeaderClass: protectedApi.refineClassNameGenerator, 470 resourceGroupHeaderInnerClass: protectedApi.refineClassNameGenerator, 471 resourceGroupHeaderContent: protectedApi.identity, 472 resourceGroupHeaderDidMount: protectedApi.identity, 473 resourceGroupHeaderWillUnmount: protectedApi.identity, 474 // timeline lane, for resource-GROUP 475 resourceGroupLaneClass: protectedApi.refineClassNameGenerator, 476 resourceGroupLaneInnerClass: protectedApi.refineClassNameGenerator, 477 resourceGroupLaneContent: protectedApi.identity, 478 resourceGroupLaneDidMount: protectedApi.identity, 479 resourceGroupLaneWillUnmount: protectedApi.identity, 480 // timeline, lane, for resource 481 resourceLaneClass: protectedApi.refineClassNameGenerator, 482 resourceLaneDidMount: protectedApi.identity, 483 resourceLaneWillUnmount: protectedApi.identity, 484 resourceLaneTopClass: protectedApi.refineClassNameGenerator, 485 resourceLaneTopContent: protectedApi.identity, 486 resourceLaneBottomClass: protectedApi.refineClassNameGenerator, 487 resourceLaneBottomContent: protectedApi.identity, 488 resourceIndentClass: protectedApi.refineClassName, 489 resourceExpanderClass: protectedApi.refineClassNameGenerator, 490 resourceExpanderContent: protectedApi.identity, 491 // Timeline 492 // -------- 493 timelineTopClass: protectedApi.refineClassName, 494 timelineBottomClass: protectedApi.refineClassName, 495 }; 496 const OPTION_DEFAULTS = { 497 // Resource Timeline 498 // ----------------- 499 printMaxRows: 1000, 500 // Resource DayGrid 501 // ---------------- 502 // resourceDayHeaderAlign: 'start' as const, --- this default was inlined due to plugin ordering problems 503 _resourceDayHeaderSticky: true, 504 }; 505 const LISTENER_REFINERS = { 506 // Resource 507 // -------- 508 resourcesSet: protectedApi.identity, 509 resourceAdd: protectedApi.identity, 510 resourceChange: protectedApi.identity, 511 resourceRemove: protectedApi.identity, 512 // internal 513 _resourceScrollRequest: protectedApi.identity, 514 }; 515 516 var premiumCommonPlugin = { 517 name: 'common', 518 premiumReleaseDate: '2026-07-24', 519 optionRefiners: OPTION_REFINERS, 520 optionDefaults: OPTION_DEFAULTS, 521 listenerRefiners: LISTENER_REFINERS, 522 viewContainerAppends: [buildLicenseWarning], 523 }; 524 525 function massageEventDragMutation(eventMutation, hit0, hit1) { 526 let resource0 = hit0.dateSpan.resourceId;
vendor: 5,702 bytes, lines 527-660
527 let resource1 = hit1.dateSpan.resourceId; 528 if (resource0 && resource1 && 529 resource0 !== resource1) { 530 eventMutation.resourceMutation = { 531 matchResourceId: resource0, 532 setResourceId: resource1, 533 }; 534 } 535 } 536 /* 537 TODO: all this would be much easier if we were using a hash! 538 */ 539 function applyEventDefMutation(eventDef, mutation, context) { 540 let resourceMutation = mutation.resourceMutation; 541 if (resourceMutation && computeResourceEditable(eventDef, context)) { 542 let index = eventDef.resourceIds.indexOf(resourceMutation.matchResourceId); 543 if (index !== -1) { 544 let resourceIds = eventDef.resourceIds.slice(); // copy 545 resourceIds.splice(index, 1); // remove 546 if (resourceIds.indexOf(resourceMutation.setResourceId) === -1) { // not already in there 547 resourceIds.push(resourceMutation.setResourceId); // add 548 } 549 eventDef.resourceIds = resourceIds; 550 } 551 } 552 } 553 /* 554 HACK 555 TODO: use EventUi system instead of this 556 */ 557 function computeResourceEditable(eventDef, context) { 558 let { resourceEditable } = eventDef; 559 if (resourceEditable == null) { 560 let source = eventDef.sourceId && context.getCurrentData().eventSources[eventDef.sourceId]; 561 if (source) { 562 resourceEditable = source.extendedProps.resourceEditable; // used the Source::extendedProps hack 563 } 564 if (resourceEditable == null) { 565 resourceEditable = context.options.eventResourceEditable; 566 if (resourceEditable == null) { 567 resourceEditable = context.options.editable; // TODO: use defaults system instead 568 } 569 } 570 } 571 return resourceEditable; 572 } 573 function transformEventDrop(mutation, context) { 574 let { resourceMutation } = mutation; 575 if (resourceMutation) { 576 let { calendarApi } = context; 577 return { 578 oldResource: calendarApi.getResourceById(resourceMutation.matchResourceId), 579 newResource: calendarApi.getResourceById(resourceMutation.setResourceId), 580 }; 581 } 582 return { 583 oldResource: null, 584 newResource: null, 585 }; 586 } 587 588 class ResourceDataAdder { 589 constructor() { 590 this.filterResources = protectedApi.memoize(filterResources); 591 } 592 transform(viewProps, calendarProps) { 593 if (calendarProps.viewSpec.optionDefaults.needsResourceData) { 594 return { 595 resourceStore: this.filterResources(calendarProps.resourceStore, calendarProps.options.filterResourcesWithEvents, calendarProps.eventStore, calendarProps.dateProfile.activeRange), 596 resourceEntityExpansions: calendarProps.resourceEntityExpansions, 597 }; 598 } 599 return null; 600 } 601 } 602 function filterResources(resourceStore, doFilterResourcesWithEvents, eventStore, activeRange) { 603 if (doFilterResourcesWithEvents) { 604 let instancesInRange = filterEventInstancesInRange(eventStore.instances, activeRange); 605 let hasEvents = computeHasEvents(instancesInRange, eventStore.defs); 606 Object.assign(hasEvents, computeAncestorHasEvents(hasEvents, resourceStore)); 607 return protectedApi.filterHash(resourceStore, (resource, resourceId) => hasEvents[resourceId]); 608 } 609 return resourceStore; 610 } 611 function filterEventInstancesInRange(eventInstances, activeRange) { 612 return protectedApi.filterHash(eventInstances, (eventInstance) => protectedApi.rangesIntersect(eventInstance.range, activeRange)); 613 } 614 function computeHasEvents(eventInstances, eventDefs) { 615 let hasEvents = {}; 616 for (let instanceId in eventInstances) { 617 let instance = eventInstances[instanceId]; 618 for (let resourceId of eventDefs[instance.defId].resourceIds) { 619 hasEvents[resourceId] = true; 620 } 621 } 622 return hasEvents; 623 } 624 /* 625 mark resources as having events if any of their ancestors have them 626 NOTE: resourceStore might not have all the resources that hasEvents{} has keyed 627 */ 628 function computeAncestorHasEvents(hasEvents, resourceStore) { 629 let res = {}; 630 for (let resourceId in hasEvents) { 631 let resource; 632 while ((resource = resourceStore[resourceId])) { 633 resourceId = resource.parentId; // now functioning as the parentId 634 if (resourceId) { 635 res[resourceId] = true; 636 } 637 else { 638 break; 639 } 640 } 641 } 642 return res; 643 } 644 /* 645 for making sure events that have editable resources are always draggable in resource views 646 */ 647 function transformIsDraggable(val, eventDef, eventUi, context) { 648 if (!val) { 649 let state = context.getCurrentData(); 650 let viewSpec = state.viewSpecs[state.currentViewType]; 651 if (viewSpec.optionDefaults.needsResourceData) { 652 if (computeResourceEditable(eventDef, context)) { 653 return true; 654 } 655 } 656 } 657 return val; 658 } 659 660 // for when non-resource view should be given EventUi info (for event coloring/constraints based off of resource data)
661 class ResourceEventConfigAdder { 662 constructor() { 663 this.buildResourceEventUis = protectedApi.memoize(buildResourceEventUis, protectedApi.isPropsEqualShallow); 664 this.injectResourceEventUis = protectedApi.memoize(injectResourceEventUis); 665 } 666 transform(viewProps, calendarProps) { 667 if (!calendarProps.viewSpec.optionDefaults.needsResourceData) { 668 return { 669 eventUiBases: this.injectResourceEventUis(viewProps.eventUiBases, viewProps.eventStore.defs, this.buildResourceEventUis(calendarProps.resourceStore)), 670 }; 671 } 672 return null; 673 } 674 } 675 function buildResourceEventUis(resourceStore) { 676 return protectedApi.mapHash(resourceStore, (resource) => resource.ui); 677 } 678 function injectResourceEventUis(eventUiBases, eventDefs, resourceEventUis) { 679 return protectedApi.mapHash(eventUiBases, (eventUi, defId) => { 680 if (defId) { // not the '' key 681 return injectResourceEventUi(eventUi, eventDefs[defId], resourceEventUis); 682 } 683 return eventUi; 684 }); 685 } 686 function injectResourceEventUi(origEventUi, eventDef, resourceEventUis) { 687 let parts = []; 688 // first resource takes precedence, which fights with the ordering of combineEventUis, thus the unshifts 689 for (let resourceId of eventDef.resourceIds) { 690 if (resourceEventUis[resourceId]) { 691 parts.unshift(resourceEventUis[resourceId]); 692 } 693 } 694 parts.unshift(origEventUi); 695 return protectedApi.combineEventUis(parts); 696 } 697 698 const arrayResourceSource = { 699 ignoreRange: true, 700 parseMeta(refined) { 701 if (Array.isArray(refined.resources)) { 702 return refined.resources; 703 } 704 return null; 705 }, 706 fetch(arg, successCallback) { 707 successCallback({ 708 rawResources: arg.resourceSource.meta, 709 }); 710 }, 711 }; 712 713 const jsonFeedResourceSource = { 714 parseMeta(refined) { 715 if (refined.url) { 716 return { 717 url: refined.url, 718 method: (refined.method || 'GET').toUpperCase(), 719 extraParams: refined.extraParams, 720 }; 721 } 722 return null; 723 }, 724 fetch(arg, successCallback, errorCallback) { 725 const meta = arg.resourceSource.meta; 726 const requestParams = buildRequestParams(meta, arg.range, arg.context); 727 protectedApi.requestJson(meta.method, meta.url, requestParams).then(([rawResources, response]) => { 728 successCallback({ rawResources, response }); 729 }, errorCallback); 730 }, 731 }; 732 // TODO: somehow consolidate with event json feed 733 function buildRequestParams(meta, range, context) { 734 let { dateEnv, options } = context; 735 let startParam; 736 let endParam; 737 let timeZoneParam;
738 let customRequestParams; 739 let params = {}; 740 if (range) { 741 startParam = meta.startParam; 742 if (startParam == null) { 743 startParam = options.startParam; 744 } 745 endParam = meta.endParam; 746 if (endParam == null) { 747 endParam = options.endParam; 748 } 749 timeZoneParam = meta.timeZoneParam; 750 if (timeZoneParam == null) { 751 timeZoneParam = options.timeZoneParam; 752 } 753 params[startParam] = dateEnv.formatIso(range.start); 754 params[endParam] = dateEnv.formatIso(range.end); 755 if (dateEnv.timeZone !== 'local') { 756 params[timeZoneParam] = dateEnv.timeZone; 757 } 758 } 759 // retrieve any outbound GET/POST data from the options 760 if (typeof meta.extraParams === 'function') { 761 // supplied as a function that returns a key/value object 762 customRequestParams = meta.extraParams(); 763 } 764 else { 765 // probably supplied as a straight key/value object
766 customRequestParams = meta.extraParams || {}; 767 } 768 Object.assign(params, customRequestParams); 769 return params; 770 } 771 772 const funcResourceSource = { 773 parseMeta(refined) { 774 if (typeof refined.resources === 'function') { 775 return refined.resources; 776 } 777 return null; 778 }, 779 fetch(info, successCallback, errorCallback) { 780 const dateEnv = info.context.dateEnv; 781 const func = info.resourceSource.meta; 782 const publicData = info.range ? { 783 start: dateEnv.toDate(info.range.start), 784 end: dateEnv.toDate(info.range.end), 785 startStr: dateEnv.formatIso(info.range.start), 786 endStr: dateEnv.formatIso(info.range.end), 787 timeZone: dateEnv.timeZone, 788 } : {}; 789 protectedApi.unpromisify(func.bind(null, publicData), (rawResources) => successCallback({ rawResources }), errorCallback); 790 }, 791 }; 792 793 let defs = [ 794 arrayResourceSource, 795 jsonFeedResourceSource, 796 funcResourceSource, 797 ]; 798 function getResourceSourceDef(id) { 799 return defs[id]; 800 } 801 function getResourceSourceDefs() { 802 return defs; 803 } 804 805 // TODO: make this a plugin-able parser 806 // TODO: success/failure 807 const RESOURCE_SOURCE_REFINERS = { 808 id: String, 809 // for array. TODO: move to resource-array 810 resources: protectedApi.identity, 811 // for json feed. TODO: move to resource-json-feed 812 url: String, 813 method: String, 814 startParam: String, 815 endParam: String, 816 timeZoneParam: String, 817 extraParams: protectedApi.identity, 818 }; 819 function parseResourceSource(input) { 820 let inputObj; 821 if (typeof input === 'string') { 822 inputObj = { url: input }; 823 } 824 else if (typeof input === 'function' || Array.isArray(input)) { 825 inputObj = { resources: input }; 826 } 827 else if (typeof input === 'object' && input) { // non-null object 828 inputObj = input; 829 } 830 if (inputObj) { 831 let { refined, extra } = protectedApi.refineProps(inputObj, RESOURCE_SOURCE_REFINERS); 832 warnUnknownProps(extra); 833 let metaRes = buildResourceSourceMeta(refined); 834 if (metaRes) { 835 return { 836 _raw: input, 837 sourceId: protectedApi.guid(), 838 sourceDefId: metaRes.sourceDefId, 839 meta: metaRes.meta, 840 publicId: refined.id || '', 841 isFetching: false, 842 latestFetchId: '', 843 fetchRange: null, 844 }; 845 } 846 } 847 return null; 848 }
849 function buildResourceSourceMeta(refined) { 850 let defs = getResourceSourceDefs(); 851 for (let i = defs.length - 1; i >= 0; i -= 1) { // later-added plugins take precedence 852 let def = defs[i]; 853 let meta = def.parseMeta(refined); 854 if (meta) { 855 return { meta, sourceDefId: i }; 856 } 857 } 858 return null; 859 } 860 function warnUnknownProps(props) { 861 for (let propName in props) { 862 protectedApi.warn(`Unknown resource-source property \`${propName}\`.`); 863 } 864 } 865 866 function reduceResourceSource(source, action, context) { 867 let { options, dateProfile } = context; 868 if (!source || !action) { 869 return createSource(options.initialResources || options.resources, dateProfile.activeRange, options.refetchResourcesOnNavigate, context); 870 } 871 switch (action.type) { 872 case 'RESET_RESOURCE_SOURCE': 873 return createSource(action.resourceSourceInput, dateProfile.activeRange, options.refetchResourcesOnNavigate, context); 874 case 'PREV': // TODO: how do we track all actions that affect dateProfile :( 875 case 'NEXT': 876 case 'CHANGE_DATE': 877 case 'CHANGE_VIEW_TYPE': 878 return handleRangeChange(source, dateProfile.activeRange, options.refetchResourcesOnNavigate, context); 879 case 'RECEIVE_RESOURCES': 880 case 'RECEIVE_RESOURCE_ERROR': 881 return receiveResponse(source, action.fetchId, action.fetchRange); 882 case 'REFETCH_RESOURCES': 883 return fetchSource(source, dateProfile.activeRange, context); 884 default: 885 return source; 886 } 887 } 888 function createSource(input, activeRange, refetchResourcesOnNavigate, context) { 889 if (input) { 890 let source = parseResourceSource(input); 891 source = fetchSource(source, refetchResourcesOnNavigate ? activeRange : null, context); 892 return source; 893 } 894 return null; 895 } 896 function handleRangeChange(source, activeRange, refetchResourcesOnNavigate, context) { 897 if (refetchResourcesOnNavigate && 898 !doesSourceIgnoreRange(source) && 899 (!source.fetchRange || !protectedApi.rangesEqual(source.fetchRange, activeRange))) { 900 return fetchSource(source, activeRange, context); 901 } 902 return source; 903 } 904 function doesSourceIgnoreRange(source) { 905 return Boolean(getResourceSourceDef(source.sourceDefId).ignoreRange); 906 } 907 function fetchSource(source, fetchRange, context) { 908 let sourceDef = getResourceSourceDef(source.sourceDefId); 909 let fetchId = protectedApi.guid(); 910 sourceDef.fetch({ 911 resourceSource: source, 912 range: fetchRange, 913 context, 914 }, (res) => { 915 context.dispatch({ 916 type: 'RECEIVE_RESOURCES', 917 fetchId, 918 fetchRange, 919 rawResources: res.rawResources, 920 }); 921 }, (error) => { 922 context.dispatch({ 923 type: 'RECEIVE_RESOURCE_ERROR', 924 fetchId, 925 fetchRange, 926 error, 927 }); 928 }); 929 return { 930 ...source, 931 isFetching: true, 932 latestFetchId: fetchId, 933 }; 934 } 935 function receiveResponse(source, fetchId, fetchRange) { 936 if (fetchId === source.latestFetchId) { 937 return { 938 ...source, 939 isFetching: false, 940 fetchRange, 941 }; 942 } 943 return source; 944 } 945 946 function reduceResourceStore(store, action, source, context) { 947 if (!store || !action) { 948 return {}; 949 } 950 switch (action.type) { 951 case 'RECEIVE_RESOURCES': 952 return receiveRawResources(store, action.rawResources, action.fetchId, source, context); 953 case 'ADD_RESOURCE': 954 return addResource(store, action.resourceHash); 955 case 'REMOVE_RESOURCE': 956 return removeResource(store, action.resourceId); 957 case 'SET_RESOURCE_PROP': 958 return setResourceProp(store, action.resourceId, action.propName, action.propValue); 959 case 'SET_RESOURCE_EXTENDED_PROP': 960 return setResourceExtendedProp(store, action.resourceId, action.propName, action.propValue); 961 default: 962 return store; 963 } 964 } 965 function receiveRawResources(existingStore, inputs, fetchId, source, context) { 966 if (source.latestFetchId === fetchId) { 967 let nextStore = {}; 968 for (let input of inputs) { 969 parseResource(input, '', nextStore, context); 970 } 971 return nextStore; 972 } 973 return existingStore; 974 } 975 function addResource(existingStore, additions) { 976 // TODO: warn about duplicate IDs 977 return { ...existingStore, ...additions }; 978 } 979 function removeResource(existingStore, resourceId) { 980 let newStore = { ...existingStore }; 981 delete newStore[resourceId]; 982 // promote children 983 for (let childResourceId in newStore) { // a child, *maybe* but probably not 984 if (newStore[childResourceId].parentId === resourceId) { 985 newStore[childResourceId] = { 986 ...newStore[childResourceId], 987 parentId: '', 988 }; 989 } 990 } 991 return newStore; 992 } 993 function setResourceProp(existingStore, resourceId, name, value) { 994 let existingResource = existingStore[resourceId]; 995 // TODO: sanitization 996 if (existingResource) { 997 return { 998 ...existingStore, 999 [resourceId]: { 1000 ...existingResource, 1001 [name]: value, 1002 }, 1003 }; 1004 } 1005 return existingStore; 1006 } 1007 function setResourceExtendedProp(existingStore, resourceId, name, value) { 1008 let existingResource = existingStore[resourceId]; 1009 if (existingResource) { 1010 return { 1011 ...existingStore, 1012 [resourceId]: { 1013 ...existingResource, 1014 extendedProps: { 1015 ...existingResource.extendedProps, 1016 [name]: value, 1017 }, 1018 }, 1019 }; 1020 } 1021 return existingStore; 1022 } 1023 1024 function reduceResourceEntityExpansions(expansions, action) { 1025 if (!expansions || !action) { 1026 return {}; 1027 } 1028 switch (action.type) { 1029 case 'SET_RESOURCE_ENTITY_EXPANDED': 1030 return { 1031 ...expansions, 1032 [action.id]: action.isExpanded, 1033 }; 1034 default: 1035 return expansions; 1036 } 1037 } 1038 1039 function reduceResources(state, action, context) { 1040 let resourceSource = reduceResourceSource(state && state.resourceSource, action, context); 1041 let resourceStore = reduceResourceStore(state && state.resourceStore, action, resourceSource, context); 1042 let resourceEntityExpansions = reduceResourceEntityExpansions(state && state.resourceEntityExpansions, action); 1043 return { 1044 resourceSource, 1045 resourceStore, 1046 resourceEntityExpansions, 1047 }; 1048 } 1049 1050 const EVENT_REFINERS = { 1051 resourceId: String, 1052 resourceIds: protectedApi.identity, 1053 resourceEditable: Boolean, 1054 }; 1055 function generateEventDefResourceMembers(refined) { 1056 return { 1057 resourceIds: ensureStringArray(refined.resourceIds) 1058 .concat(refined.resourceId ? [refined.resourceId] : []), 1059 resourceEditable: refined.resourceEditable, 1060 }; 1061 } 1062 function ensureStringArray(items) { 1063 return (items || []).map((item) => String(item)); 1064 } 1065 1066 function transformDateSelectionJoin(hit0, hit1) { 1067 let resourceId0 = hit0.dateSpan.resourceId;
1068 let resourceId1 = hit1.dateSpan.resourceId; 1069 if (resourceId0 && resourceId1) { 1070 return { resourceId: resourceId0 }; 1071 } 1072 return null; 1073 } 1074 1075 /* 1076 splits things BASED OFF OF which resources they are associated with. 1077 creates a '' entry which is when something has NO resource. 1078 */ 1079 class ResourceSplitter extends protectedApi.Splitter { 1080 getKeyInfo(props) { 1081 return { 1082 '': {}, // needed for non-resource 1083 ...props.resourceStore, // already has `ui` and `businessHours` keys! 1084 }; 1085 } 1086 getKeysForDateSpan(dateSpan) { 1087 return [dateSpan.resourceId || '']; 1088 } 1089 getKeysForEventDef(eventDef) { 1090 let resourceIds = eventDef.resourceIds; 1091 if (!resourceIds.length) { 1092 return ['']; 1093 } 1094 return resourceIds; 1095 } 1096 } 1097 1098 function isPropsValidWithResources(combinedProps, context) { 1099 let splitter = new ResourceSplitter(); 1100 let sets = splitter.splitProps({ 1101 ...combinedProps, 1102 resourceStore: context.getCurrentData().resourceStore, 1103 }); 1104 for (let resourceId in sets) { 1105 let props = sets[resourceId]; 1106 // merge in event data from the non-resource segment 1107 if (resourceId && sets['']) { // current segment is not the non-resource one, and there IS a non-resource one 1108 props = { 1109 ...props, 1110 eventStore: protectedApi.mergeEventStores(sets[''].eventStore, props.eventStore), 1111 eventUiBases: { ...sets[''].eventUiBases, ...props.eventUiBases }, 1112 }; 1113 } 1114 if (!protectedApi.isPropsValid(props, context, { resourceId }, filterConfig.bind(null, resourceId))) { 1115 return false; 1116 } 1117 } 1118 return true; 1119 } 1120 function filterConfig(resourceId, config) { 1121 return { 1122 ...config, 1123 constraints: filterConstraints(resourceId, config.constraints), 1124 }; 1125 } 1126 function filterConstraints(resourceId, constraints) { 1127 return constraints.map((constraint) => { 1128 let defs = constraint.defs; 1129 if (defs) { // we are dealing with an EventStore 1130 // if any of the events define constraints to resources that are NOT this resource, 1131 // then this resource is unconditionally prohibited, which is what a `false` value does. 1132 for (let defId in defs) { 1133 let resourceIds = defs[defId].resourceIds; 1134 if (resourceIds.length && resourceIds.indexOf(resourceId) === -1) { // TODO: use a hash?!!! (for other reasons too) 1135 return false; 1136 } 1137 } 1138 } 1139 return constraint; 1140 }); 1141 } 1142 1143 function transformExternalDef(dateSpan) { 1144 return dateSpan.resourceId ? 1145 { resourceId: dateSpan.resourceId } : 1146 {}; 1147 } 1148 1149 const optionChangeHandlers = { 1150 resources: handleResources, 1151 }; 1152 function handleResources(newSourceInput, context) { 1153 let oldSourceInput = context.getCurrentData().resourceSource._raw; 1154 if (oldSourceInput !== newSourceInput) { 1155 context.dispatch({ 1156 type: 'RESET_RESOURCE_SOURCE', 1157 resourceSourceInput: newSourceInput, 1158 }); 1159 } 1160 } 1161 1162 const DEFAULT_RESOURCE_ORDER = protectedApi.parseFieldSpecs('id,title'); 1163 function handleResourceStore(resourceStore, calendarData) { 1164 let { emitter } = calendarData; 1165 if (emitter.hasHandlers('resourcesSet')) { 1166 emitter.trigger('resourcesSet', buildResourceApis(resourceStore, calendarData)); 1167 } 1168 } 1169 1170 var resourcePlugin = { 1171 name: 'resource', 1172 premiumReleaseDate: '2026-07-24', 1173 deps: [premiumCommonPlugin], 1174 reducers: [reduceResources], 1175 isLoadingFuncs: [ 1176 (state) => state.resourceSource && state.resourceSource.isFetching, 1177 ], 1178 eventRefiners: EVENT_REFINERS, 1179 eventDefMemberAdders: [generateEventDefResourceMembers], 1180 isDraggableTransformers: [transformIsDraggable], 1181 eventDragMutationMassagers: [massageEventDragMutation], 1182 eventDefMutationAppliers: [applyEventDefMutation], 1183 dateSelectionTransformers: [transformDateSelectionJoin], 1184 datePointTransforms: [transformDatePoint], 1185 dateSpanTransforms: [transformDateSpan], 1186 viewPropsTransformers: [ResourceDataAdder, ResourceEventC
1186onfigAdder], 1187 isPropsValid: isPropsValidWithResources, 1188 externalDefTransforms: [transformExternalDef], 1189 eventDropTransformers: [transformEventDrop], 1190 optionChangeHandlers, 1191 propSetHandlers: { resourceStore: handleResourceStore }, 1192 }; 1193 1194 /* 1195 TODO: move away from pooledHeight. simply create child-parent relationships w/ groups+resource 1196 The groups that are row-groups VS col-groups can be determined by colGroupDepth 1197 */ 1198 function buildResourceHierarchy(resourceStore, orderSpecs, // why accepted when resourceNodeHash already ordered? 1199 groupSpecs = [], groupRowDepth = 0) { 1200 const resourceNodeHash = buildResourceNodeHash(resourceStore, orderSpecs); 1201 const resNodes = []; 1202 for (let resourceId in resourceNodeHash) { 1203 let resourceNode = resourceNodeHash[resourceId]; 1204 if (!resourceNode.entity.parentId) { 1205 insertResourceNode(resourceNode, orderSpecs, groupSpecs, groupRowDepth, resNodes); 1206 } 1207 } 1208 return resNodes; 1209 } 1210 /* 1211 Builds a hash-by-id that contains ALL resources, 1212 but .children[] array is wired up as well 1213 */
1214 function buildResourceNodeHash(resourceStore, orderSpecs) { 1215 let nodeHash = {}; 1216 for (let resourceId in resourceStore) { 1217 let resource = resourceStore[resourceId]; 1218 nodeHash[resourceId] = { 1219 entity: resource, 1220 resourceFields: buildResourceFields(resource), 1221 children: [], 1222 }; 1223 } 1224 for (let resourceId in resourceStore) { 1225 let resource = resourceStore[resourceId]; 1226 if (resource.parentId) { 1227 let parentNode = nodeHash[resource.parentId]; 1228 if (parentNode) { 1229 insertResourceNodeInSiblings(nodeHash[resourceId], orderSpecs, parentNode.children); 1230 } 1231 } 1232 } 1233 return nodeHash; 1234 } 1235 function insertResourceNode(resourceNode, orderSpecs, groupSpecs, groupRowDepth, resNodes, groupIdPath = []) { 1236 if (groupSpecs.length) { 1237 const groupSpec = groupSpecs[0]; 1238 const nextGroupIdPath = buildGroupIdPath(groupIdPath, groupSpec.field, resourceNode.resourceFields[groupSpec.field]); 1239 let groupNode = ensureGroupNodes(resourceNode, groupSpec, groupRowDepth <= 0, resNodes, nextGroupIdPath); 1240 insertResourceNode(resourceNode, orderSpecs, groupSpecs.slice(1), groupRowDepth - 1, groupNode.children, nextGroupIdPath); 1241 } 1242 else { 1243 insertResourceNodeInSiblings(resourceNode, orderSpecs, resNodes); 1244 } 1245 } 1246 function ensureGroupNodes(resourceNode, groupSpec, pooledHeight, resNodes, groupIdPath) { 1247 let groupValue = resourceNode.resourceFields[groupSpec.field]; 1248 let groupNode; 1249 let newGroupIndex; 1250 // find an existing group that matches, or determine the position for a new group 1251 if (groupSpec.order) { 1252 for (newGroupIndex = 0; newGroupIndex < resNodes.length; newGroupIndex += 1) { 1253 let node = resNodes[newGroupIndex]; 1254 let group = node.entity; 1255 let cmp = protectedApi.flexibleCompare(groupValue, group.value) * groupSpec.order; 1256 if (cmp === 0) { 1257 groupNode = node; 1258 break; 1259 } 1260 else if (cmp < 0) { 1261 break; 1262 } 1263 } 1264 } 1265 else { // the groups are unordered 1266 for (newGroupIndex = 0; newGroupIndex < resNodes.length; newGroupIndex += 1) { 1267 let node = resNodes[newGroupIndex]; 1268 let group = node.entity; 1269 if (group && groupValue === group.value) { 1270 groupNode = node; 1271 break; 1272 } 1273 } 1274 } 1275 if (!groupNode) { 1276 groupNode = { 1277 entity: { 1278 id: buildGroupId(groupIdPath), 1279 value: groupValue, 1280 spec: groupSpec, 1281 }, 1282 pooledHeight, 1283 children: [], 1284 }; 1285 resNodes.splice(newGroupIndex, 0, groupNode); 1286 } 1287 return groupNode; 1288 } 1289 function insertResourceNodeInSiblings(resourceNode, orderSpecs, siblings) { 1290 let i; 1291 for (i = 0; i < siblings.length; i += 1) { 1292 let cmp = protectedApi.compareByFieldSpecs(// TODO: pass in ResourceApi? 1293 siblings[i].resourceFields, resourceNode.resourceFields, orderSpecs); 1294 if (cmp > 0) { // went 1 past. insert at i 1295 break; 1296 } 1297 } 1298 siblings.splice(i, 0, resourceNode); 1299 }
1300 function buildResourceFields(resource) { 1301 let obj = { ...resource.extendedProps, ...resource.ui, ...resource }; 1302 delete obj.ui; 1303 delete obj.extendedProps; 1304 return obj; 1305 } 1306 // Hierarchy + Flattening 1307 // ------------------------------------------------------------------------------------------------- 1308 function flattenResources(resourceStore, orderSpecs) { 1309 const hierarchy = buildResourceHierarchy(resourceStore, orderSpecs); 1310 const resResources = []; 1311 flattenResourceHierarchy(hierarchy, resResources); 1312 return resResources; 1313 } 1314 function flattenResourceHierarchy(resourceNodes, resResources) { 1315 for (const node of resourceNodes) { 1316 resResources.push(node.entity); 1317 flattenResourceHierarchy(node.children, resResources); 1318 } 1319 } 1320 // Utils 1321 // ------------------------------------------------------------------------------------------------- 1322 function createEntityId(entity) { 1323 return isEntityGroup(entity) ? createGroupId(entity) : entity.id; 1324 } 1325 function createGroupId(group) { 1326 return group.id || buildGroupId(buildGroupIdPath([], group.spec.field, group.value)); 1327 } 1328 function isEntityGroup(entity) { 1329 return Boolean(entity.spec); 1330 } 1331 function isGroupsEqual(group0, group1) { 1332 return createGroupId(group0) === createGroupId(group1); 1333 } 1334 function buildGroupIdPath(parentPath, field, value) { 1335 return parentPath.concat([[field, value]]); 1336 } 1337 function buildGroupId(path) { 1338 return JSON.stringify(path); 1339 } 1340 1341 class ResourceIndex { 1342 constructor(resources) { 1343 let indicesById = {}; 1344 let ids = []; 1345 for (let i = 0; i < resources.length; i += 1) { 1346 let id = resources[i].id; 1347 ids.push(id); 1348 indicesById[id] = i; 1349 } 1350 this.ids = ids; 1351 this.indicesById = indicesById; 1352 this.length = resources.length; 1353 } 1354 } 1355 1356 /* 1357 TODO: move this so @fullcalendar/resource-daygrid 1358 */ 1359 class AbstractResourceDayTableModel { 1360 constructor(dayTableModel, resources, rowCount, colCount) { 1361 this.dayTableModel = dayTableModel; 1362 this.resources = resources; 1363 this.rowCount = rowCount; 1364 this.colCount = colCount; 1365 this.resourceIndex = new ResourceIndex(resources); 1366 this.cells = this.buildCells(); 1367 } 1368 } 1369 1370 /* 1371 TODO: move this so @fullcalendar/resource-daygrid 1372 */ 1373 class ResourcefulDayTableModel extends AbstractResourceDayTableModel { 1374 constructor(dayTableModel, resources, context) { 1375 super(dayTableModel, resources, dayTableModel.rowCount, // rowCount 1376 dayTableModel.colCount * resources.length); 1377 this.context = context; 1378 } 1379 buildCells() { 1380 let { rowCount, dayTableModel, resources } = this; 1381 let rows = []; 1382 let hasMajor = resources.length > 1 && dayTableModel.colCount > 1; 1383 for (let row = 0; row < rowCount; row += 1) { 1384 let rowCells = []; 1385 for (let dateCol = 0; dateCol < dayTableModel.colCount; dateCol += 1) { 1386 for (let resourceCol = 0; resourceCol < resources.length; resourceCol += 1) { 1387 let resource = resources[resourceCol]; 1388 let date = dayTableModel.cellRows[row][dateCol].date; 1389 let renderProps = { resource: new ResourceApi(this.context, resource) }; 1390 let attrs = { 'data-resource-id': resource.id }; 1391 let dateSpanProps = { resourceId: resource.id }; 1392 let realCol = this.computeCol(dateCol, resourceCol); 1393 rowCells[realCol] = { 1394 key: resource.id + ':' + date.toISOString(), // TODO: DRY up this logic with header-tier somehow 1395 date, 1396 isMajor: hasMajor && this.colIsMajor(realCol), 1397 renderProps, 1398 attrs, 1399 className: '', // kill this property? 1400 dateSpanProps, 1401 }; 1402 } 1403 } 1404 rows.push(rowCells); 1405 } 1406 return rows; 1407 } 1408 } 1409 1410 /* 1411 dates over resources 1412 */ 1413 class DayResourceTableModel extends ResourcefulDayTableModel { 1414 computeCol(dateI, resourceI) { 1415 return dateI * this.resources.length + resourceI; 1416 } 1417 colIsMajor(col) { 1418 const resourceCnt = this.resources.length; 1419 return resourceCnt > 1 && !(col % resourceCnt); 1420 } 1421 /* 1422 every single day is broken up 1423 */ 1424 computeColRanges(dateStartI, dateEndI, resourceI) { 1425 let segs = []; 1426 for (let i = dateStartI; i < dateEndI; i += 1) { 1427 let col = this.computeCol(i, resourceI); 1428 segs.push({ 1429 start: col, 1430 end: col + 1, 1431 isStart: i === dateStartI, 1432 isEnd: i === dateEndI - 1, 1433 }); 1434 } 1435 return segs; 1436 } 1437 } 1438 1439 /* 1440 resources over dates 1441 */ 1442 class ResourceDayTableModel extends ResourcefulDayTableModel { 1443 computeCol(dateI, resourceI) { 1444 return resourceI * this.dayTableModel.colCount + dateI; 1445 } 1446 colIsMajor(col) { 1447 const dayCnt = this.dayTableModel.colCount; 1448 return dayCnt > 1 && !(col % dayCnt); 1449 } 1450 /* 1451 all date ranges are intact 1452 */ 1453 computeColRanges(dateStartI, dateEndI, resourceI) { 1454 return [ 1455 { 1456 start: this.computeCol(dateStartI, resourceI), 1457 end: this.computeCol(dateEndI, resourceI), 1458 isStart: true, 1459 isEnd: true, 1460 }, 1461 ]; 1462 } 1463 } 1464 1465 /* 1466 TODO: move this so @fullcalendar/resource-daygrid 1467 */ 1468 class ResourcelessDayTableModel extends AbstractResourceDayTableModel { 1469 constructor(dayTableModel) { 1470 super(dayTableModel, [], // resources 1471 dayTableModel.rowCount, // rowCount 1472 dayTableModel.colCount); 1473 } 1474 computeCol(dateI, _resourceI) { 1475 return dateI; 1476 } 1477 computeColRanges(dateStartI, dateEndI, _resourceI) { 1478 return [ 1479 { 1480 start: dateStartI, 1481 end: dateEndI, 1482 isStart: true, 1483 isEnd: true, 1484 }, 1485 ]; 1486 } 1487 buildCells() { 1488 return this.dayTableModel.buildCells();
vendor: 6,171 bytes, lines 1489-1628
1489 } 1490 } 1491 1492 /* 1493 TODO: just use ResourceHash somehow? could then use the generic ResourceSplitter 1494 */ 1495 class VResourceSplitter extends protectedApi.Splitter { 1496 getKeyInfo(props) { 1497 let { resourceDayTableModel } = props; 1498 let hash = protectedApi.mapHash(resourceDayTableModel.resourceIndex.indicesById, (i) => resourceDayTableModel.resources[i]); // :( 1499 hash[''] = {}; 1500 return hash; 1501 } 1502 getKeysForDateSpan(dateSpan) { 1503 return [dateSpan.resourceId || '']; 1504 } 1505 getKeysForEventDef(eventDef) { 1506 let resourceIds = eventDef.resourceIds; 1507 if (!resourceIds.length) { 1508 return ['']; 1509 } 1510 return resourceIds; 1511 } 1512 } 1513 1514 const NO_SEGS = []; // for memoizing 1515 class VResourceJoiner { 1516 constructor() { 1517 this.joinDateSelection = protectedApi.memoize(this.joinSegs); 1518 this.joinBusinessHours = protectedApi.memoize(this.joinSegs); 1519 this.joinFgEvents = protectedApi.memoize(this.joinSegs); 1520 this.joinBgEvents = protectedApi.memoize(this.joinSegs); 1521 this.joinEventDrags = protectedApi.memoize(this.joinInteractions); 1522 this.joinEventResizes = protectedApi.memoize(this.joinInteractions); 1523 } 1524 /* 1525 propSets also has a '' key for things with no resource 1526 */ 1527 joinProps(propSets, resourceDayTable) { 1528 let dateSelectionSets = []; 1529 let businessHoursSets = []; 1530 let fgEventSets = []; 1531 let bgEventSets = []; 1532 let eventDrags = []; 1533 let eventResizes = []; 1534 let eventSelection = ''; 1535 let keys = resourceDayTable.resourceIndex.ids.concat(['']); // add in the all-resource key 1536 for (let key of keys) { 1537 let props = propSets[key]; 1538 dateSelectionSets.push(props.dateSelectionSegs); 1539 businessHoursSets.push(key ? props.businessHourSegs : NO_SEGS); // don't include redundant all-resource businesshours 1540 fgEventSets.push(key ? props.fgEventSegs : NO_SEGS); // don't include fg all-resource segs 1541 bgEventSets.push(props.bgEventSegs); 1542 eventDrags.push(props.eventDrag); 1543 eventResizes.push(props.eventResize); 1544 eventSelection = eventSelection || props.eventSelection; 1545 } 1546 return { 1547 dateSelectionSegs: this.joinDateSelection(resourceDayTable, ...dateSelectionSets), 1548 businessHourSegs: this.joinBusinessHours(resourceDayTable, ...businessHoursSets), 1549 fgEventSegs: this.joinFgEvents(resourceDayTable, ...fgEventSets), 1550 bgEventSegs: this.joinBgEvents(resourceDayTable, ...bgEventSets), 1551 eventDrag: this.joinEventDrags(resourceDayTable, ...eventDrags), 1552 eventResize: this.joinEventResizes(resourceDayTable, ...eventResizes), 1553 eventSelection, 1554 }; 1555 } 1556 joinSegs(resourceDayTable, ...segGroups) { 1557 let resourceCnt = resourceDayTable.resources.length; 1558 let transformedSegs = []; 1559 for (let i = 0; i < resourceCnt; i += 1) { 1560 for (let seg of segGroups[i]) { 1561 transformedSegs.push(...this.transformSeg(seg, resourceDayTable, i)); 1562 } 1563 for (let seg of segGroups[resourceCnt]) { // one beyond. the all-resource 1564 transformedSegs.push(...this.transformSeg(seg, resourceDayTable, i)); 1565 } 1566 } 1567 return transformedSegs; 1568 } 1569 /* 1570 for expanding non-resource segs to all resources. 1571 only for public use. 1572 no memoizing. 1573 */ 1574 expandSegs(resourceDayTable, segs) { 1575 let resourceCnt = resourceDayTable.resources.length; 1576 let transformedSegs = []; 1577 for (let i = 0; i < resourceCnt; i += 1) { 1578 for (let seg of segs) { 1579 transformedSegs.push(...this.transformSeg(seg, resourceDayTable, i)); 1580 } 1581 } 1582 return transformedSegs; 1583 } 1584 joinInteractions(resourceDayTable, ...interactions) { 1585 let resourceCnt = resourceDayTable.resources.length; 1586 let affectedInstances = {}; 1587 let transformedSegs = []; 1588 let anyInteractions = false; 1589 let isEvent = false; 1590 for (let i = 0; i < resourceCnt; i += 1) { 1591 let interaction = interactions[i]; 1592 if (interaction) { 1593 anyInteractions = true; 1594 for (let seg of interaction.segs) { 1595 transformedSegs.push(...this.transformSeg(seg, resourceDayTable, i)); 1596 } 1597 Object.assign(affectedInstances, interaction.affectedInstances); 1598 isEvent = isEvent || interaction.isEvent; 1599 } 1600 if (interactions[resourceCnt]) { // one beyond. the all-resource 1601 for (let seg of interactions[resourceCnt].segs) { 1602 transformedSegs.push(...this.transformSeg(seg, resourceDayTable, i)); 1603 } 1604 } 1605 } 1606 if (anyInteractions) { 1607 return { 1608 affectedInstances, 1609 segs: transformedSegs, 1610 isEvent, 1611 }; 1612 } 1613 return null; 1614 } 1615 } 1616 1617 class ResourceDayTableJoiner extends VResourceJoiner { 1618 transformSeg(seg, resourceDayTableModel, resourceI) { 1619 let colRanges = resourceDayTableModel.computeColRanges(seg.start, seg.end, resourceI); 1620 return colRanges.map((colRange) => ({ 1621 ...seg, 1622 ...colRange, 1623 isStart: seg.isStart && colRange.isStart, 1624 isEnd: seg.isEnd && colRange.isEnd, 1625 })); 1626 } 1627 } 1628
1629 function buildResourceRowConfigs(resources, datesAboveResources, dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, // TODO: rename to dateHeaderFormat? 1630 context) { 1631 if (!resources.length) { 1632 return [ 1633 protectedApi.buildDateRowConfig(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context) 1634 ]; 1635 } 1636 if (dates.length === 1) { 1637 return [ 1638 buildResourceRowConfig(resources, dates[0], dateProfile, todayRange, context) 1639 ]; 1640 } 1641 if (datesAboveResources) { 1642 const resourceDataConfigsPerDate = dates.map((date) => { 1643 return buildResourceDataConfigs(resources, date, dateProfile, todayRange, context, 1644 /* colSpan = */ 1, 1645 /* isMajorMod = */ resources.length); 1646 }); 1647 return [ 1648 // date row 1649 protectedApi.buildDateRowConfig(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context, 1650 /* colSpan = */ resources.length, 1651 /* isMajorMod = */ 1), 1652 // resource row 1653 { 1654 isDateRow: false, 1655 renderConfig: buildResourceRenderConfig(context), 1656 dataConfigs: [].concat(...resourceDataConfigsPerDate), // flatten 1657 } 1658 ]; 1659 } 1660 else { // resources above dates 1661 const dateDataConfigsPerResource = resources.map((resource) => { 1662 const resourceApi = new ResourceApi(context, resource); 1663 const resourceApiId = resourceApi.id; 1664 return protectedApi.buildDateDataConfigs(dates, datesRepDistinctDays, dateProfile, todayRange, dayHeaderFormat, context, 1, // rowSpan 1665 resourceApiId + ':', { 1666 resource: resourceApi, 1667 }, { 1668 'data-resource-id': resourceApiId, 1669 }, 1670 /* className = */ undefined, // TODO: remove 1671 /* isMajorMod = */ dates.length); 1672 }); 1673 return [ 1674 // resource row 1675 buildResourceRowConfig(resources, undefined, undefined, undefined, context, 1676 /* colSpan = */ dates.length, 1677 /* isMajorMod = */ 1), 1678 // date row 1679 { 1680 isDateRow: true, 1681 renderConfig: protectedApi.buildDateRenderConfig(dayHeaderFormat, datesRepDistinctDays, context), 1682 dataConfigs: [].concat(...dateDataConfigsPerResource), // flatten 1683 } 1684 ]; 1685 } 1686 } 1687 /* 1688 Single row, just resources (might be under dates, might not) 1689 */
1690 function buildResourceRowConfig(resources, dateMarker, dateProfile, todayRange, context, colSpan, isMajorMod) { 1691 return { 1692 isDateRow: false, 1693 renderConfig: buildResourceRenderConfig(context), 1694 dataConfigs: buildResourceDataConfigs(resources, dateMarker, dateProfile, todayRange, context, colSpan, isMajorMod), 1695 }; 1696 } 1697 function buildResourceRenderConfig(context) { 1698 const { options } = context; 1699 return { 1700 generatorName: 'resourceDayHeaderContent', 1701 customGenerator: options.resourceDayHeaderContent, 1702 innerClassNameGenerator: options.resourceDayHeaderInnerClass, 1703 classNameGenerator: options.resourceDayHeaderClass, 1704 didMount: options.resourceDayHeaderDidMount, 1705 willUnmount: options.resourceDayHeaderWillUnmount, 1706 align: options.resourceDayHeaderAlign, 1707 sticky: options._resourceDayHeaderSticky, 1708 }; 1709 } 1710 function buildResourceDataConfigs(resources, dateMarker, dateProfile, todayRange, context, colSpan = 1, isMajorMod) { 1711 const dateMeta = dateMarker 1712 ? protectedApi.getDateMeta(dateMarker, context.dateEnv, dateProfile, todayRange) 1713 : {}; 1714 return resources.map((resource, i) => { 1715 const resourceApi = new ResourceApi(context, resource); 1716 const resourceApiId = resourceApi.id; 1717 return { 1718 key: (dateMarker ? dateMarker.toUTCString() + ':' : '') + resource.id, 1719 dateMarker, 1720 renderProps: { 1721 isDisabled: false, 1722 ...dateMeta, 1723 resource: resourceApi, 1724 isMajor: isMajorMod != null && !(i % isMajorMod), 1725 isNarrow: false, // HACK. gets overridden later 1726 level: 0, // HACK. gets overridden later 1727 text: resource.title || resourceApiId || '', 1728 view: context.viewApi, 1729 }, 1730 attrs: { 1731 'data-resource-id': resourceApiId, 1732 'data-date': dateMarker ? protectedApi.formatDayString(dateMarker) : undefined, 1733 }, 1734 colSpan, 1735 className: '', // TODO: remove this prop 1736 }; 1737 }); 1738 } 1739 1740 class ResourceDayGridView extends protectedApi.DateComponent { 1741 constructor() { 1742 super(...arguments); 1743 // memo 1744 this.flattenResources = protectedApi.memoize(flattenResources); 1745 this.buildResourceDayTableModel = protectedApi.memoize(buildResourceDayTableModel); 1746 this.createDayHeaderFormatter = protectedApi.memoize(protectedApi.createDayHeaderFormatter); 1747 this.buildResourceRowConfigs = protectedApi.memoize(buildResourceRowConfigs); 1748 this.splitter = new VResourceSplitter(); 1749 this.slicers = {}; 1750 this.joiner = new ResourceDayTableJoiner(); 1751 this.isHitComboAllowed = (hit0, hit1) => { 1752 let allowAcrossResources = this.resourceDayTableModel.dayTableModel.colCount === 1; 1753 return allowAcrossResources || hit0.dateSpan.resourceId === hit1.dateSpan.resourceId; 1754 }; 1755 } 1756 render() { 1757 let { props, context } = this; 1758 let { options } = context; 1759 let resourceOrderSpecs = options.resourceOrder || DEFAULT_RESOURCE_ORDER; 1760 let resources = this.flattenResources(props.resourceStore, resourceOrderSpecs); 1761 let resourceDayTableModel = this.resourceDayTableModel = this.buildResourceDayTableModel(props.dateProfile, context.dateProfileGenerator, resources, options.datesAboveResources, context); 1762 let splitProps = this.splitter.splitProps({ 1763 resourceDayTableModel, 1764 businessHours: props.businessHours, 1765 dateSelection: props.dateSelection, 1766 eventStore: props.eventStore, 1767 eventUiBases: props.eventUiBases, 1768 eventSelection: props.eventSelection, 1769 eventDrag: props.eventDrag, 1770 eventResize: props.eventResize, 1771 }); 1772 this.slicers = protectedApi.mapHash(splitProps, (split, resourceId) => this.slicers[resourceId] || new protectedApi.DayTableSlicer()); 1773 let slicedProps = protectedApi.mapHash(this.slicers, (slicer, resourceId) => slicer.sliceProps(splitProps[resourceId], props.dateProfile, options.nextDayThreshold, context, resourceDayTableModel.dayTableModel)); 1774 let joinedSlicedProps = this.joiner.joinProps(slicedProps, resourceDayTableModel); 1775 let datesRepDistinctDays = resourceDayTableModel.dayTableModel.rowCount === 1; 1776 let dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, resourceDayTableModel.colCount); 1777 return (jsxRuntime.jsx(protectedApi.NowTimer, { unit: "day", children: (nowDate, todayRange) => {
1778 const headerTiers = this.buildResourceRowConfigs(resources, options.datesAboveResources, resourceDayTableModel.dayTableModel.headerDates, datesRepDistinctDays, props.dateProfile, todayRange, dayHeaderFormat, context); 1779 return (jsxRuntime.jsx(protectedApi.DayGridLayout, { labelId: props.labelId, labelStr: props.labelStr, dateProfile: props.dateProfile, todayRange: todayRange, cellRows: resourceDayTableModel.cells, forPrint: props.forPrint, isHitComboAllowed: this.isHitComboAllowed, className: props.className, 1780 // header content 1781 headerTiers: headerTiers, 1782 // body content 1783 fgEventSegs: joinedSlicedProps.fgEventSegs, bgEventSegs: joinedSlicedProps.bgEventSegs, businessHourSegs: joinedSlicedProps.businessHourSegs, dateSelectionSegs: joinedSlicedProps.dateSelectionSegs, eventDrag: joinedSlicedProps.eventDrag, eventResize: joinedSlicedProps.eventResize, eventSelection: joinedSlicedProps.eventSelection })); 1784 } })); 1785 } 1786 }
1787 function buildResourceDayTableModel(dateProfile, dateProfileGenerator, resources, datesAboveResources, context) { 1788 let dayTable = protectedApi.buildDayTableModel(dateProfile, dateProfileGenerator, context.dateEnv); 1789 if (!resources.length) { 1790 return new ResourcelessDayTableModel(dayTable); 1791 } 1792 return datesAboveResources ? 1793 new DayResourceTableModel(dayTable, resources, context) : 1794 new ResourceDayTableModel(dayTable, resources, context); 1795 } 1796 1797 var resourceDayGridPlugin = { 1798 name: 'resource-daygrid', 1799 premiumReleaseDate: '2026-07-24', 1800 deps: [ 1801 premiumCommonPlugin, 1802 resourcePlugin, 1803 dayGridPlugin__default["default"], 1804 ], 1805 initialView: 'resourceDayGridDay', 1806 views: { 1807 resourceDayGrid: { 1808 type: 'dayGrid', // will inherit this configuration 1809 component: ResourceDayGridView, 1810 needsResourceData: true, 1811 }, 1812 resourceDayGridDay: { 1813 type: 'resourceDayGrid', 1814 duration: { days: 1 }, 1815 }, 1816 resourceDayGridWeek: { 1817 type: 'resourceDayGrid', 1818 duration: { weeks: 1 }, 1819 }, 1820 resourceDayGridMonth: { 1821 type: 'resourceDayGrid', 1822 duration: { months: 1 }, 1823 fixedWeekCount: true, 1824 }, 1825 }, 1826 }; 1827 1828 /* 1829 TODO: detangle and use from @full-ui/headless-grid 1830 */ 1831 class ScrollerSyncer { 1832 constructor(isHorizontal = false) { 1833 this.isHorizontal = isHorizontal; 1834 this.emitter = new protectedApi.Emitter(); 1835 this.scrollers = []; 1836 this.destroyFuncs = []; 1837 this.isPaused = false; 1838 } 1839 handleChildren(scrollers) { 1840 if (!protectedApi.isArraysEqual(this.scrollers, scrollers)) { 1841 this.destroy(); 1842 for (const scroller of scrollers) { 1843 if (scroller) { // could be null 1844 this.destroyFuncs.push(this.bindScroller(scroller)); 1845 this.scrollers.push(scroller); 1846 } 1847 } 1848 } 1849 } 1850 destroy() { 1851 for (let destroyFunc of this.destroyFuncs) { 1852 destroyFunc(); 1853 } 1854 this.destroyFuncs = []; 1855 this.scrollers = []; 1856 } 1857 get x() { 1858 const { scrollers, masterScroller } = this; 1859 return (masterScroller || scrollers[0]).x; 1860 } 1861 get y() { 1862 const { scrollers, masterScroller } = this; 1863 return (masterScroller || scrollers[0]).y; 1864 } 1865 scrollTo(scrollArg) { 1866 this.isPaused = true; // Not sure how this works, given that scroll handlers fire asynchronously 1867 const { scrollers } = this; 1868 for (let scroller of scrollers) { 1869 scroller.scrollTo(scrollArg); 1870 } 1871 this.isPaused = false; 1872 } 1873 addScrollListener(handler) { 1874 this.emitter.on('scroll', handler); 1875 } 1876 removeScrollListener(handler) { 1877 this.emitter.off('scroll', handler); 1878 } 1879 addScrollStartListener(handler) { 1880 this.emitter.on('scrollStart', handler); 1881 } 1882 removeScrollStartListener(handler) { 1883 this.emitter.off('scrollStart', handler); 1884 } 1885 addScrollEndListener(handler) { 1886 this.emitter.on('scrollEnd', handler); 1887 } 1888 removeScrollEndListener(handler) { 1889 this.emitter.off('scrollEnd', handler); 1890 } 1891 bindScroller(scroller) { 1892 let { isHorizontal } = this; 1893 const onScroll = (isDevice) => { 1894 if (!this.isPaused) { 1895 if (!this.masterScroller) { 1896 this.emitter.trigger('scrollStart', isDevice); 1897 } 1898 if (!this.masterScroller || (this.masterScroller !== scroller && isDevice)) { 1899 this.assignMaster(scroller); 1900 } 1901 if (this.masterScroller === scroller) { // dealing with current 1902 for (let otherScroller of this.scrollers) { 1903 if (otherScroller !== scroller) { 1904 if (isHorizontal) { 1905 // TODO: user raw scrollLeft for better performance. No normalization necessary 1906 otherScroller.scrollTo({ x: scroller.x }); 1907 } 1908 else { 1909 otherScroller.scrollTo({ y: scroller.y }); 1910 } 1911 } 1912 } 1913 this.emitter.trigger('scroll', isDevice, isHorizontal ? scroller.x : scroller.y); 1914 } 1915 } 1916 }; 1917 const onScrollEnd = (isDevice) => { 1918 if (this.masterScroller === scroller) { 1919 this.masterScroller = null; 1920 const { x, y } = this; // new values 1921 let isMoved = false; 1922 if (this.isHorizontal) { 1923 if (x !== this.prevX) { 1924 this.prevX = x; 1925 isMoved = true; 1926 } 1927 } 1928 else { 1929 if (y !== this.prevY) { 1930 this.prevY = y; 1931 isMoved = true; 1932 } 1933 } 1934 if (isMoved) { 1935 this.emitter.trigger('scrollEnd', isDevice); 1936 } 1937 } 1938 }; 1939 scroller.listener.emitter.on('scroll', onScroll); 1940 scroller.listener.emitter.on('scrollEnd', onScrollEnd); 1941 return () => { 1942 scroller.listener.emitter.off('scroll', onScroll); 1943 scroller.listener.emitter.off('scrollEnd', onScrollEnd); 1944 }; 1945 } 1946 assignMaster(masterScroller) { 1947 this.masterScroller = masterScroller; 1948 for (let scroller of this.scrollers) { 1949 if (scroller !== masterScroller) { 1950 scroller.endScroll(); // to prevent residual scrolls from reclaiming master 1951 } 1952 } 1953 } 1954 } 1955 1956 function joinDateTimeFormatParts(parts) { 1957 let s = ''; 1958 for (const part of parts) { 1959 s += part.value; 1960 } 1961 return s; 1962 } 1963 1964 const MIN_AUTO_LABELS = 18; // more than `12` months but less that `24` hours 1965 const MAX_AUTO_SLOTS_PER_LABEL = 6; // allows 6 10-min slots in an hour 1966 const MAX_AUTO_CELLS = 200; // allows 4-days to have a :30 slot duration 1967 protectedApi.config.MAX_TIMELINE_SLOTS = 1000; 1968 // potential nice values for slot-duration and interval-duration 1969 const STOCK_SUB_DURATIONS = [ 1970 { years: 1 }, 1971 { months: 1 }, 1972 { days: 1 }, 1973 { hours: 1 }, 1974 { minutes: 30 }, 1975 { minutes: 15 }, 1976 { minutes: 10 }, 1977 { minutes: 5 }, 1978 { minutes: 1 }, 1979 { seconds: 30 }, 1980 { seconds: 15 }, 1981 { seconds: 10 }, 1982 { seconds: 5 }, 1983 { seconds: 1 }, 1984 { milliseconds: 500 }, 1985 { milliseconds: 100 }, 1986 { milliseconds: 10 }, 1987 { milliseconds: 1 }, 1988 ]; 1989 function buildTimelineDateProfile(dateProfile, dateEnv, allOptions, dateProfileGenerator) { 1990 let tDateProfile = { 1991 labelInterval: allOptions.slotHeaderInterval, 1992 slotDuration: allOptions.slotDuration, 1993 }; 1994 validateLabelAndSlot(tDateProfile, dateProfile, dateEnv); // validate after computed grid duration 1995 ensureLabelInterval(tDateProfile, dateProfile, dateEnv); 1996 ensureSlotDuration(tDateProfile, dateProfile, dateEnv); 1997 let input = allOptions.slotHeaderFormat; 1998 let rawFormats; 1999 let headerFormatUnits; 2000 if (Array.isArray(input)) { 2001 rawFormats = input; 2002 headerFormatUnits = input.map(inferHeaderFormatUnit); 2003 } 2004 else if (input != null) { 2005 rawFormats = [input]; 2006 headerFormatUnits = [inferHeaderFormatUnit(input)]; 2007 } 2008 else { 2009 const computed = computeHeaderFormats(tDateProfile, dateProfile, dateEnv, allOptions); 2010 rawFormats = computed.formats; 2011 headerFormatUnits = computed.units; 2012 } 2013 tDateProfile.headerFormats = rawFormats.map((rawFormat) => protectedApi.createFormatter(rawFormat)); 2014 tDateProfile.headerFormatUnits = headerFormatUnits; 2015 tDateProfile.isTimeScale = Boolean(tDateProfile.slotDuration.milliseconds); 2016 let largeUnit = null; 2017 if (!tDateProfile.isTimeScale) { 2018 const slotUnit = protectedApi.greatestDurationDenominator(tDateProfile.slotDuration).unit; 2019 if (/year|month|week/.test(slotUnit)) { 2020 largeUnit = slotUnit; 2021 } 2022 } 2023 tDateProfile.largeUnit = largeUnit; 2024 /* 2025 console.log('label interval =', timelineView.labelInterval.humanize()) 2026 console.log('slot duration =', timelineView.slotDuration.humanize()) 2027 console.log('header formats =', timelineView.headerFormats) 2028 console.log('isTimeScale', timelineView.isTimeScale) 2029 console.log('largeUnit', timelineView.largeUnit) 2030 */ 2031 let rawSnapDuration = allOptions.snapDuration; 2032 let snapDuration; 2033 let snapsPerSlot; 2034 if (rawSnapDuration) { 2035 snapDuration = protectedApi.createDuration(rawSnapDuration); 2036 snapsPerSlot = protectedApi.wholeDivideDurations(tDateProfile.slotDuration, snapDuration); 2037 // ^ TODO: warning if not whole? 2038 } 2039 if (snapsPerSlot == null) { 2040 snapDuration = tDateProfile.slotDuration; 2041 snapsPerSlot = 1; 2042 } 2043 tDateProfile.snapDuration = snapDuration; 2044 tDateProfile.snapsPerSlot = snapsPerSlot; 2045 // more... 2046 let timeWindowMs = protectedApi.asRoughMs(dateProfile.slotMaxTime) - protectedApi.asRoughMs(dateProfile.slotMinTime); 2047 // TODO: why not use normalizeRange!? 2048 let normalizedStart = normalizeDate(dateProfile.renderRange.start, tDateProfile, dateEnv); 2049 let normalizedEnd = normalizeDate(dateProfile.renderRange.end, tDateProfile, dateEnv); 2050 // apply slotMinTime/slotMaxTime 2051 // TODO: View should be responsible. 2052 if (tDateProfile.isTimeScale) { 2053 normalizedStart = dateEnv.add(normalizedStart, dateProfile.slotMinTime); 2054 normalizedEnd = dateEnv.add(protectedApi.addDays(normalizedEnd, -1), dateProfile.slotMaxTime); 2055 } 2056 tDateProfile.timeWindowMs = timeWindowMs; 2057 tDateProfile.normalizedRange = { start: normalizedStart, end: normalizedEnd }; 2058 let slotDates = []; 2059 let slotDatesMajor = []; 2060 let date = normalizedStart; 2061 let majorUnit = protectedApi.computeMajorUnit(dateProfile, dateEnv); 2062 while (date < normalizedEnd) { 2063 if (isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator)) { 2064 slotDates.push(date); 2065 slotDatesMajor.push(protectedApi.isMajorUnit(date, majorUnit, dateEnv)); 2066 } 2067 date = dateEnv.add(date, tDateProfile.slotDuration); 2068 } 2069 tDateProfile.slotDates = slotDates; 2070 tDateProfile.slotDatesMajor = slotDatesMajor; 2071 // more... 2072 let snapIndex = -1; 2073 let snapDiff = 0; // index of the diff :( 2074 const snapDiffToIndex = []; 2075 const snapIndexToDiff = []; 2076 date = normalizedStart; 2077 while (date < normalizedEnd) { 2078 if (isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator)) { 2079 snapIndex += 1;
2080 snapDiffToIndex.push(snapIndex); 2081 snapIndexToDiff.push(snapDiff); 2082 } 2083 else { 2084 snapDiffToIndex.push(snapIndex + 0.5); 2085 } 2086 date = dateEnv.add(date, tDateProfile.snapDuration); 2087 snapDiff += 1; 2088 } 2089 tDateProfile.snapDiffToIndex = snapDiffToIndex; 2090 tDateProfile.snapIndexToDiff = snapIndexToDiff; 2091 tDateProfile.snapCnt = snapIndex + 1; // is always one behind 2092 tDateProfile.slotCnt = tDateProfile.snapCnt / tDateProfile.snapsPerSlot; 2093 // more... 2094 tDateProfile.cellRows = buildCellRows(tDateProfile, dateEnv, majorUnit); 2095 tDateProfile.slotsPerLabel = protectedApi.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration); 2096 return tDateProfile; 2097 } 2098 /* 2099 snaps to appropriate unit 2100 */ 2101 function normalizeDate(date, tDateProfile, dateEnv) { 2102 let normalDate = date; 2103 if (!tDateProfile.isTimeScale) { 2104 normalDate = protectedApi.startOfDay(normalDate); 2105 if (tDateProfile.largeUnit) { 2106 normalDate = dateEnv.startOf(normalDate, tDateProfile.largeUnit); 2107 } 2108 } 2109 return normalDate; 2110 } 2111 /* 2112 snaps to appropriate unit 2113 */ 2114 function normalizeRange(range, tDateProfile, dateEnv) { 2115 if (!tDateProfile.isTimeScale) { 2116 range = protectedApi.computeVisibleDayRange(range); 2117 if (tDateProfile.largeUnit) { 2118 let dayRange = range; // preserve original result 2119 range = { 2120 start: dateEnv.startOf(range.start, tDateProfile.largeUnit), 2121 end: dateEnv.startOf(range.end, tDateProfile.largeUnit), 2122 }; 2123 // if date is partially through the interval, or is in the same interval as the start, 2124 // make the exclusive end be the *next* interval 2125 if (range.end.valueOf() !== dayRange.end.valueOf() || range.end <= range.start) { 2126 range = { 2127 start: range.start, 2128 end: dateEnv.add(range.end, tDateProfile.slotDuration), 2129 }; 2130 } 2131 } 2132 } 2133 return range; 2134 } 2135 function isValidDate(date, tDateProfile, dateProfile, dateProfileGenerator) { 2136 if (dateProfileGenerator.isHiddenDay(date)) { 2137 return false; 2138 } 2139 if (tDateProfile.isTimeScale) { 2140 // determine if the time is within slotMinTime/slotMaxTime, which may have wacky values 2141 let day = protectedApi.startOfDay(date); 2142 let timeMs = date.valueOf() - day.valueOf(); 2143 let ms = timeMs - protectedApi.asRoughMs(dateProfile.slotMinTime); // milliseconds since slotMinTime 2144 ms = ((ms % 86400000) + 86400000) % 86400000; // make negative values wrap to 24hr clock 2145 return ms < tDateProfile.timeWindowMs; // before the slotMaxTime? 2146 } 2147 return true; 2148 } 2149 function validateLabelAndSlot(tDateProfile, dateProfile, dateEnv) { 2150 const { currentRange } = dateProfile; 2151 // make sure labelInterval doesn't exceed the max number of cells 2152 if (tDateProfile.labelInterval) { 2153 const labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.labelInterval); 2154 if (labelCnt > protectedApi.config.MAX_TIMELINE_SLOTS) { 2155 protectedApi.warn('Invalid option `slotHeaderInterval`: too many timeline cells.'); 2156 tDateProfile.labelInterval = null; 2157 } 2158 } 2159 // make sure slotDuration doesn't exceed the maximum number of cells 2160 if (tDateProfile.slotDuration) { 2161 const slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.slotDuration); 2162 if (slotCnt > protectedApi.config.MAX_TIMELINE_SLOTS) { 2163 protectedApi.warn('Invalid option `slotDuration`: too many timeline cells.'); 2164 tDateProfile.slotDuration = null; 2165 } 2166 } 2167 // make sure labelInterval is a multiple of slotDuration 2168 if (tDateProfile.labelInterval && tDateProfile.slotDuration) {
2169 const slotsPerLabel = protectedApi.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration); 2170 if (slotsPerLabel === null || slotsPerLabel < 1) { 2171 protectedApi.warn('Invalid option `slotHeaderInterval`: must be a multiple of `slotDuration`.'); 2172 tDateProfile.slotDuration = null; 2173 } 2174 } 2175 } 2176 function ensureLabelInterval(tDateProfile, dateProfile, dateEnv) { 2177 const { currentRange } = dateProfile; 2178 let { labelInterval } = tDateProfile; 2179 if (!labelInterval) { 2180 // compute based off the slot duration 2181 // find the largest label interval with an acceptable slots-per-label 2182 let input; 2183 if (tDateProfile.slotDuration) { 2184 for (input of STOCK_SUB_DURATIONS) { 2185 const tryLabelInterval = protectedApi.createDuration(input); 2186 const slotsPerLabel = protectedApi.wholeDivideDurations(tryLabelInterval, tDateProfile.slotDuration);
2187 if (slotsPerLabel !== null && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) { 2188 labelInterval = tryLabelInterval; 2189 break; 2190 } 2191 } 2192 // use the slot duration as a last resort 2193 if (!labelInterval) { 2194 labelInterval = tDateProfile.slotDuration; 2195 } 2196 // compute based off the view's duration 2197 // find the largest label interval that yields the minimum number of labels 2198 } 2199 else { 2200 for (input of STOCK_SUB_DURATIONS) { 2201 labelInterval = protectedApi.createDuration(input); 2202 const labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, labelInterval); 2203 if (labelCnt >= MIN_AUTO_LABELS) { 2204 break; 2205 } 2206 } 2207 } 2208 tDateProfile.labelInterval = labelInterval; 2209 } 2210 return labelInterval; 2211 } 2212 function ensureSlotDuration(tDateProfile, dateProfile, dateEnv) { 2213 const { currentRange } = dateProfile; 2214 let { slotDuration } = tDateProfile; 2215 if (!slotDuration) { 2216 const labelInterval = ensureLabelInterval(tDateProfile, dateProfile, dateEnv); // will compute if necessary 2217 // compute based off the label interval 2218 // find the largest slot duration that is different from labelInterval, but still acceptable 2219 for (let input of STOCK_SUB_DURATIONS) { 2220 const trySlotDuration = protectedApi.createDuration(input);
2221 const slotsPerLabel = protectedApi.wholeDivideDurations(labelInterval, trySlotDuration); 2222 if (slotsPerLabel !== null && slotsPerLabel > 1 && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) { 2223 slotDuration = trySlotDuration; 2224 break; 2225 } 2226 } 2227 // only allow the value if it won't exceed the view's # of slots limit 2228 if (slotDuration) { 2229 const slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, slotDuration); 2230 if (slotCnt > MAX_AUTO_CELLS) { 2231 slotDuration = null; 2232 } 2233 } 2234 // use the label interval as a last resort 2235 if (!slotDuration) { 2236 slotDuration = labelInterval; 2237 } 2238 tDateProfile.slotDuration = slotDuration; 2239 } 2240 return slotDuration; 2241 } 2242 function computeHeaderFormats(tDateProfile, dateProfile, dateEnv, allOptions) { 2243 let format1; 2244 let format2; 2245 let unit1 = null; 2246 let unit2 = null; 2247 const { labelInterval } = tDateProfile; 2248 const { currentRange } = dateProfile; 2249 let unit = protectedApi.greatestDurationDenominator(labelInterval).unit; 2250 const weekNumbersVisible = allOptions.weekNumbers; 2251 let format0 = (format1 = (format2 = null)); 2252 let unit0 = null; 2253 // NOTE: weekNumber computation function wont work 2254 if ((unit === 'week') && !weekNumbersVisible) { 2255 unit = 'day'; 2256 } 2257 switch (unit) { 2258 case 'year': 2259 format0 = { year: 'numeric' }; // '2015' 2260 unit0 = 'year'; 2261 break; 2262 case 'month': 2263 if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) { 2264 format0 = { year: 'numeric' }; // '2015' 2265 unit0 = 'year'; 2266 } 2267 format1 = { month: 'short' }; // 'Jan' 2268 unit1 = 'month'; 2269 break; 2270 case 'week': 2271 if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) { 2272 format0 = { year: 'numeric' }; // '2015' 2273 unit0 = 'year'; 2274 } 2275 format1 = { week: 'narrow' }; // 'Wk4' 2276 unit1 = 'week'; 2277 break; 2278 case 'day': 2279 if (dateEnv.diffWholeYears(currentRange.start, currentRange.end) > 1) { 2280 format0 = { year: 'numeric', month: 'long' }; // 'January 2014' 2281 unit0 = 'month'; 2282 } 2283 else if (dateEnv.diffWholeMonths(currentRange.start, currentRange.end) > 1) { 2284 format0 = { month: 'long' }; // 'January' 2285 unit0 = 'month'; 2286 } 2287 if (weekNumbersVisible) { 2288 format1 = { week: 'short' }; // 'Wk 4' 2289 unit1 = 'week'; 2290 } 2291 format2 = { weekday: 'narrow', day: 'numeric' }; // 'Su 9' 2292 unit2 = 'day'; 2293 break; 2294 case 'hour': 2295 if (weekNumbersVisible) { 2296 format0 = { week: 'short' }; // 'Wk 4' 2297 unit0 = 'week'; 2298 } 2299 if (protectedApi.diffWholeDays(currentRange.start, currentRange.end) > 1) { 2300 format1 = { weekday: 'short', day: 'numeric', month: 'numeric', omitCommas: true }; // Sat 4/7 2301 unit1 = 'day'; 2302 } 2303 format2 = { 2304 hour: 'numeric', 2305 minute: '2-digit', 2306 omitZeroMinute: true, 2307 meridiem: 'short', 2308 }; 2309 unit2 = null; 2310 break; 2311 case 'minute': 2312 // sufficiently large number of different minute cells? 2313 if ((protectedApi.asRoughMinutes(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) { 2314 format0 = { 2315 hour: 'numeric', 2316 meridiem: 'short', 2317 }; 2318 format1 = (params) => (':' + protectedApi.padStart(params.date.minute, 2) // ':30' 2319 ); 2320 } 2321 else { 2322 format0 = { 2323 hour: 'numeric', 2324 minute: 'numeric', 2325 meridiem: 'short', 2326 }; 2327 } 2328 break; 2329 case 'second': 2330 // sufficiently large number of different second cells? 2331 if ((protectedApi.asRoughSeconds(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) { 2332 format0 = { hour: 'numeric', minute: '2-digit', meridiem: 'lowercase' }; // '8:30 PM' 2333 format1 = (params) => (':' + protectedApi.padStart(params.date.second, 2) // ':30' 2334 ); 2335 } 2336 else { 2337 format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM' 2338 } 2339 break; 2340 case 'millisecond': 2341 format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM' 2342 format1 = (params) => ('.' + protectedApi.padStart(params.millisecond, 3)); 2343 break; 2344 } 2345 return { 2346 formats: [].concat(format0 || [], format1 || [], format2 || []), 2347 units: [].concat(format0 ? [unit0] : [], format1 ? [unit1] : [], format2 ? [unit2] : []), 2348 }; 2349 } 2350 function inferHeaderFormatUnit(rawFormat) { 2351 if (typeof rawFormat === 'object' && rawFormat) { 2352 if (rawFormat.fractionalSecondDigits || 2353 rawFormat.second || 2354 rawFormat.minute || 2355 rawFormat.hour || 2356 rawFormat.timeZoneName) { 2357 return 'time'; 2358 } 2359 if (rawFormat.day || rawFormat.weekday) { 2360 return 'day'; 2361 } 2362 if (rawFormat.week) { 2363 return 'week'; 2364 } 2365 if (rawFormat.month) { 2366 return 'month'; 2367 } 2368 if (rawFormat.year) { 2369 return 'year'; 2370 } 2371 } 2372 return null; 2373 } 2374 function buildCellRows(tDateProfile, dateEnv, majorUnit) { 2375 let slotDates = tDateProfile.slotDates; 2376 let formats = tDateProfile.headerFormats; 2377 let cellRows = formats.map(() => []); // indexed by row,col 2378 let slotAsDays = protectedApi.asCleanDays(tDateProfile.slotDuration); 2379 let guessedSlotUnit = slotAsDays === 7 ? 'week' : 2380 slotAsDays === 1 ? 'day' : 2381 null; 2382 // specifically for navclicks 2383 let rowUnitsFromFormats = tDateProfile.headerFormatUnits; 2384 // builds cellRows and slotCells 2385 for (let i = 0; i < slotDates.length; i += 1) { 2386 let date = slotDates[i]; 2387 for (let row = 0; row < formats.length; row += 1) { 2388 let format = formats[row]; 2389 let rowCells = cellRows[row]; 2390 let leadingCell = rowCells[rowCells.length - 1]; 2391 let isLastRow = row === formats.length - 1; 2392 let isSuperRow = formats.length > 1 && !isLastRow; // more than one row and not the last 2393 let isMajor = protectedApi.isMajorUnit(date, majorUnit, dateEnv); 2394 let newCell = null; 2395 let rowUnit = rowUnitsFromFormats[row] || (isLastRow ? guessedSlotUnit : null); 2396 if (isSuperRow) { 2397 let text = joinDateTimeFormatParts(dateEnv.formatToParts(date, format)); 2398 if (!leadingCell || (leadingCell.text !== text)) { 2399 newCell = buildCellObject(date, isMajor, text, rowUnit); 2400 } 2401 else { 2402 leadingCell.colspan += 1;
2403 } 2404 } 2405 else if (!leadingCell || 2406 protectedApi.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval))) { 2407 let text = joinDateTimeFormatParts(dateEnv.formatToParts(date, format)); 2408 newCell = buildCellObject(date, isMajor, text, rowUnit); 2409 } 2410 else { 2411 leadingCell.colspan += 1; 2412 } 2413 if (newCell) { 2414 rowCells.push(newCell); 2415 } 2416 } 2417 } 2418 return cellRows; 2419 } 2420 function buildCellObject(date, isMajor, text, rowUnit) { 2421 return { date, isMajor, text, rowUnit, colspan: 1 }; // colspan mutated later 2422 } 2423 2424 class TimelineSlatCell extends protectedApi.BaseComponent { 2425 constructor() { 2426 super(...arguments); 2427 // memo 2428 this.getDateMeta = protectedApi.memoize(protectedApi.getDateMeta); 2429 } 2430 render() { 2431 let { props, context } = this; 2432 let { dateEnv, options } = context; 2433 let { date, tDateProfile, isMajor } = props; 2434 let dateMeta = this.getDateMeta(props.date, dateEnv, props.dateProfile, props.todayRange, props.nowDate); 2435 let isMinor = tDateProfile.isTimeScale && 2436 !protectedApi.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, props.date, tDateProfile.labelInterval)); 2437 let renderProps = { 2438 ...dateMeta, 2439 isMajor, 2440 isMinor, 2441 view: context.viewApi, 2442 }; 2443 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].alignStart, // shrinks width of InnerContent 2444 props.borderStart ? classNames__default["default"].borderOnlyS : classNames__default["default"].borderNone, classNames__default["default"].internalTimelineSlot), attrs: { 2445 'data-date': dateEnv.formatIso(date, { 2446 omitTimeZoneOffset: true, 2447 omitTime: !tDateProfile.isTimeScale, 2448 }), 2449 ...(dateMeta.isToday ? { 'aria-current': 'date' } : {}), 2450 }, style: { 2451 width: props.width, 2452 }, renderProps: renderProps, generatorName: undefined, classNameGenerator: options.slotLaneClass, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount })); 2453 } 2454 } 2455 2456 class TimelineSlats extends protectedApi.BaseComponent { 2457 render() { 2458 let { props } = this; 2459 let { tDateProfile, slotWidth, slatStartIndex, slatCount } = props; 2460 let { slotDates, slotDatesMajor } = tDateProfile; 2461 slatStartIndex = props.slatStartIndex || 0; 2462 if (slatStartIndex || slatCount !== undefined) { 2463 slotDates = slotDates.slice(slatStartIndex, slatStartIndex + slatCount); 2464 } 2465 return (jsxRuntime.jsx("div", { "aria-hidden": true, className: publicApi.joinClassNames(classNames__default["default"].flexRow, classNames__default["default"].fillY), style: { 2466 height: props.height, 2467 width: (props.slotWidth ?? 0) * slotDates.length, 2468 insetInlineStart: 0, 2469 }, children: slotDates.map((slotDate, i) => { 2470 let key = slotDate.toISOString(); 2471 return (jsxRuntime.jsx(TimelineSlatCell, { date: slotDate, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isMajor: slotDatesMajor[slatStartIndex + i], borderStart: Boolean(slatStartIndex + i), 2472 // dimensions 2473 width: slotWidth }, key)); 2474 }) })); 2475 } 2476 } 2477 2478 class TimelineHeaderCell extends protectedApi.BaseComponent { 2479 constructor() { 2480 super(...arguments); 2481 this.state = {}; 2482 // memo 2483 this.getDateMeta = protectedApi.memoize(protectedApi.getDateMeta); 2484 // ref 2485 this.innerWrapperElRef = compat.createRef(); 2486 } 2487 render() { 2488 let { props, state, context } = this; 2489 let { dateEnv, options } = context; 2490 let { cell, dateProfile, tDateProfile } = props; 2491 // the cell.rowUnit is f'd 2492 // giving 'month' for a 3-day view 2493 // workaround: to infer day, do NOT time 2494 let dateMeta = this.getDateMeta(cell.date, dateEnv, dateProfile, props.todayRange, props.nowDate); 2495 let hasNavLink = options.navLinks && !dateMeta.isDisabled && (cell.rowUnit && cell.rowUnit !== 'time'); 2496 let isTime = tDateProfile.isTimeScale && !props.rowLevel; // HACK: faulty way of determining this 2497 let renderProps = { 2498 ...dateMeta, 2499 level: props.rowLevel, 2500 isMajor: cell.isMajor, 2501 isMinor: false,
2502 isNarrow: false, 2503 isTime, 2504 hasNavLink, 2505 text: cell.text, 2506 isFirst: props.isFirst, 2507 view: context.viewApi, 2508 }; 2509 const { slotHeaderAlign } = options; 2510 const align = this.align = 2511 typeof slotHeaderAlign === 'function' 2512 ? slotHeaderAlign({ level: props.rowLevel, isTime }) 2513 : slotHeaderAlign; 2514 const isSticky = this.isSticky = 2515 props.rowLevel && options.slotHeaderSticky !== false; 2516 let edgeCoord; 2517 if (isSticky) { 2518 if (align === 'center') { 2519 if (state.innerWidth != null) { 2520 edgeCoord = `calc(50% - ${state.innerWidth / 2}px)`; 2521 } 2522 } 2523 else { 2524 edgeCoord = (typeof options.slotHeaderSticky === 'number' || 2525 typeof options.slotHeaderSticky === 'string') ? options.slotHeaderSticky : 0; 2526 } 2527 } 2528 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, props.isFirst ? classNames__default["default"].borderNone : classNames__default["default"].borderOnlyS, align === 'center' ? classNames__default["default"].alignCenter : 2529 align === 'end' ? classNames__default["default"].alignEnd : 2530 classNames__default["default"].alignStart, classNames__default["default"].internalTimelineSlot), attrs: { 2531 'data-date': dateEnv.formatIso(cell.date, { 2532 omitTime: !tDateProfile.isTimeScale, 2533 omitTimeZoneOffset: true, 2534 }), 2535 ...(dateMeta.isToday ? { 'aria-current': 'date' } : {}), 2536 }, style: { 2537 width: props.slotWidth != null 2538 ? props.slotWidth * cell.colspan 2539 : undefined, 2540 }, renderProps: renderProps, generatorName: "slotHeaderContent", customGenerator: options.slotHeaderContent, defaultGenerator: renderInnerContent, classNameGenerator: options.slotHeaderClass, didMount: options.slotHeaderDidMount, willUnmount: options.slotHeaderWillUnmount, children: (InnerContent) => (jsxRuntime.jsx("div", { ref: this.innerWrapperElRef, className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].noShrink, classNames__default["default"].whiteSpaceNoWrap, isSticky && classNames__default["default"].sticky), style: { 2541 left: edgeCoord, 2542 right: edgeCoord, 2543 }, children: jsxRuntime.jsx(InnerContent, { tag: 'div', attrs: hasNavLink 2544 // not tabbable because parent is aria-hidden 2545 ? protectedApi.buildNavLinkAttrs(context, cell.date, cell.rowUnit, undefined, /* isTabbable = */ false) 2546 : {} // don't bother with aria-hidden because parent already hidden 2547 , className: protectedApi.generateClassName(options.slotHeaderInnerClass, renderProps) }) })) })); 2548 } 2549 componentDidMount() { 2550 this._isUnmounting = false; 2551 const { props } = this; 2552 const innerWrapperEl = this.innerWrapperElRef.current; // TODO: make dynamic with useEffect 2553 this.disconnectSize = protectedApi.watchSize(innerWrapperEl, (width, height) => { 2554 if (this._isUnmounting) 2555 return; 2556 protectedApi.setRef(props.innerWidthRef, width); 2557 protectedApi.setRef(props.innerHeightRef, height); 2558 if (this.align === 'center' && this.isSticky) { 2559 this.setState({ innerWidth: width }); 2560 } 2561 }); 2562 } 2563 componentWillUnmount() { 2564 const { props } = this; 2565 this._isUnmounting = true; 2566 this.disconnectSize(); 2567 protectedApi.setRef(props.innerWidthRef, null); 2568 protectedApi.setRef(props.innerHeightRef, null); 2569 } 2570 } 2571 // Utils 2572 // ------------------------------------------------------------------------------------------------- 2573 function renderInnerContent(renderProps) { 2574 return renderProps.text; 2575 } 2576 2577 class TimelineHeaderRow extends protectedApi.BaseComponent { 2578 constructor() { 2579 super(...arguments); 2580 this.state = {}; 2581 // refs 2582 this.innerWidthRefMap = new protectedApi.RefMap(() => { 2583 protectedApi.afterSize(this.handleInnerWidths); 2584 }); 2585 this.innerHeightRefMap = new protectedApi.RefMap(() => { 2586 protectedApi.afterSize(this.handleInnerHeights); 2587 }); 2588 this.handleInnerWidths = () => { 2589 if (this._isUnmounting) 2590 return; 2591 const innerWidthMap = this.innerWidthRefMap.current; 2592 let max = null; 2593 for (const innerWidth of innerWidthMap.values()) { 2594 max = Math.max(max || 0, innerWidth);
2595 } 2596 // TODO: ensure not equal? 2597 protectedApi.setRef(this.props.innerWidthRef, max); 2598 }; 2599 this.handleInnerHeights = () => { 2600 if (this._isUnmounting) 2601 return; 2602 const innerHeightMap = this.innerHeightRefMap.current; 2603 let max = null; 2604 for (const innerHeight of innerHeightMap.values()) { 2605 max = Math.max(max || 0, innerHeight); 2606 } 2607 // TODO: ensure not equal? 2608 protectedApi.setRef(this.props.innerHeighRef, max); 2609 this.setState({ innerHeight: max }); 2610 }; 2611 } 2612 render() { 2613 const { props, innerWidthRefMap, innerHeightRefMap, state, context } = this; 2614 const { options } = context; 2615 let { cells, cellStartIndex, cellCount } = props; 2616 cellStartIndex = cellStartIndex || 0; 2617 if (cellStartIndex || cellCount !== undefined) { 2618 cells = cells.slice(cellStartIndex, cellStartIndex + cellCount); 2619 } 2620 return (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(props.className, options.slotHeaderRowClass, classNames__default["default"].flexRow, classNames__default["default"].grow, props.rowLevel // not the last row? 2621 ? classNames__default["default"].borderOnlyB 2622 : classNames__default["default"].borderNone), style: { 2623 insetInlineStart: props.insetInlineStart, 2624 width: props.width, 2625 // we assign height because we allow cells to have distorted heights for visual effect 2626 // but we still want to keep the overall extrenal mass 2627 height: state.innerHeight, 2628 }, children: cells.map((cell, cellI) => { 2629 // TODO: make this part of the cell obj? 2630 // TODO: rowUnit seems wrong sometimes. says 'month' when it should be day 2631 // TODO: rowUnit is relevant to whole row. put it on a row object, not the cells 2632 // TODO: use rowUnit to key the Row itself? 2633 const key = cell.rowUnit + ':' + cell.date.toISOString(); 2634 return (jsxRuntime.jsx(TimelineHeaderCell, { cell: cell, rowLevel: props.rowLevel, dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, todayRange: props.todayRange, nowDate: props.nowDate, isFirst: cellStartIndex + cellI === 0, 2635 // refs 2636 innerWidthRef: innerWidthRefMap.createRef(key), innerHeightRef: innerHeightRefMap.createRef(key), 2637 // dimensions 2638 slotWidth: props.slotWidth }, key)); 2639 }) })); 2640 } 2641 componentDidMount() { 2642 this._isUnmounting = false; 2643 } 2644 componentWillUnmount() { 2645 this._isUnmounting = true; 2646 protectedApi.setRef(this.props.innerWidthRef, null); 2647 protectedApi.setRef(this.props.innerHeighRef, null); 2648 } 2649 } 2650 2651 // Timeline-specific 2652 // ------------------------------------------------------------------------------------------------- 2653 /* 2654 TODO: DRY with computeSlatHeight? 2655 */ 2656 function computeSlotWidth(slatCnt, slatsPerLabel, slatMinWidth, labelInnerWidth, viewportWidth) { 2657 if (labelInnerWidth == null || viewportWidth == null) { 2658 return [undefined, undefined, false]; 2659 } 2660 if (slatMinWidth == null) { 2661 slatMinWidth = Math.ceil((labelInnerWidth + 1) / slatsPerLabel); 2662 } 2663 const slatTryWidth = viewportWidth / slatCnt; 2664 let slotLiquid; 2665 let slatWidth; 2666 if (slatTryWidth >= slatMinWidth) { 2667 slotLiquid = true; 2668 slatWidth = slatTryWidth; 2669 } 2670 else { 2671 slotLiquid = false; 2672 slatWidth = Math.max(slatMinWidth, slatTryWidth); 2673 } 2674 return [slatWidth * slatCnt, slatWidth, slotLiquid]; 2675 } 2676 function timeToCoord(// pixels 2677 time, dateEnv, dateProfile, tDateProfile, slowWidth) { 2678 let date = dateEnv.add(dateProfile.activeRange.start, time); 2679 if (!tDateProfile.isTimeScale) { 2680 date = protectedApi.startOfDay(date); 2681 } 2682 return dateToCoord(date, dateEnv, tDateProfile, slowWidth); 2683 } 2684 function dateToCoord(// pixels 2685 date, dateEnv, tDateProfile, slotWidth) { 2686 let snapCoverage = computeDateSnapCoverage$1(date, tDateProfile, dateEnv); 2687 let slotCoverage = snapCoverage / tDateProfile.snapsPerSlot; 2688 return slotCoverage * slotWidth; 2689 } 2690 /* 2691 returned value is between 0 and the number of snaps 2692 */ 2693 function computeDateSnapCoverage$1(date, tDateProfile, dateEnv) { 2694 let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration); 2695 if (snapDiff < 0) { 2696 return 0; 2697 } 2698 if (snapDiff >= tDateProfile.snapDiffToIndex.length) { 2699 return tDateProfile.snapCnt; 2700 } 2701 let snapDiffInt = Math.floor(snapDiff); 2702 let snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt]; 2703 if (protectedApi.isInt(snapCoverage)) { // not an in-between value 2704 snapCoverage += snapDiff - snapDiffInt; // add the remainder 2705 } 2706 else { 2707 // a fractional value, meaning the date is not visible 2708 // always round up in this case. works for start AND end dates in a range.
2709 snapCoverage = Math.ceil(snapCoverage); 2710 } 2711 return snapCoverage; 2712 } 2713 2714 class TimelineNowIndicatorLine extends protectedApi.BaseComponent { 2715 render() { 2716 const { props, context } = this; 2717 const clipStart = props.clipStart ?? 0; 2718 const xStyle = props.slotWidth == null 2719 ? {} 2720 : { 2721 insetInlineStart: dateToCoord(props.nowDate, context.dateEnv, props.tDateProfile, props.slotWidth) - clipStart 2722 }; 2723 return (jsxRuntime.jsxs("div", { className: classNames__default["default"].fill, style: { 2724 zIndex: 2, // inlined from $now-indicator-z 2725 pointerEvents: 'none', // TODO: className 2726 }, children: [jsxRuntime.jsx(protectedApi.NowIndicatorLineContainer, { className: publicApi.joinClassNames(classNames__default["default"].fillY, classNames__default["default"].noMarginY, classNames__default["default"].borderlessY), style: xStyle, date: props.nowDate }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, // better for negative margins 2727 classNames__default["default"].fillY), style: xStyle, children: jsxRuntime.jsx("div", { 2728 // stickiness on NowIndicatorDot misbehaves b/c of negative marginss 2729 className: classNames__default["default"].stickyT, children: jsxRuntime.jsx(protectedApi.NowIndicatorDot, {}) }) })] })); 2730 } 2731 } 2732 2733 /* 2734 TODO: DRY with other NowIndicator components 2735 */ 2736 class TimelineNowIndicatorArrow extends protectedApi.BaseComponent { 2737 render() { 2738 const { props, context } = this; 2739 const xStyle = props.slotWidth == null 2740 ? {} 2741 : { 2742 insetInlineStart: dateToCoord(props.nowDate, context.dateEnv, props.tDateProfile, props.slotWidth) 2743 }; 2744 return (jsxRuntime.jsx("div", { 2745 // crop any overflow that the arrow/line might cause 2746 // TODO: just do this on the entire canvas within the scroller 2747 className: publicApi.joinClassNames(classNames__default["default"].fill, classNames__default["default"].crop), style: { 2748 zIndex: 2, // inlined from $now-indicator-z 2749 pointerEvents: 'none', // TODO: className 2750 }, children: jsxRuntime.jsx(protectedApi.NowIndicatorHeaderContainer, { className: classNames__default["default"].abs, style: xStyle, date: props.nowDate }) })); 2751 } 2752 } 2753 2754 function getTimelineSlotEl(parentEl, index) { 2755 return parentEl.querySelectorAll(`.${classNames__default["default"].internalTimelineSlot}`)[index]; 2756 } 2757 2758 /* 2759 TODO: rename this file! 2760 */ 2761 // returned value is between 0 and the number of snaps 2762 function computeDateSnapCoverage(date, tDateProfile, dateEnv) { 2763 let snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration); 2764 if (snapDiff < 0) { 2765 return 0; 2766 } 2767 if (snapDiff >= tDateProfile.snapDiffToIndex.length) { 2768 return tDateProfile.snapCnt; 2769 } 2770 let snapDiffInt = Math.floor(snapDiff); 2771 let snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt]; 2772 if (protectedApi.isInt(snapCoverage)) { // not an in-between value 2773 snapCoverage += snapDiff - snapDiffInt; // add the remainder 2774 } 2775 else { 2776 // a fractional value, meaning the date is not visible 2777 // always round up in this case. works for start AND end dates in a range. 2778 snapCoverage = Math.ceil(snapCoverage); 2779 } 2780 return snapCoverage; 2781 } 2782 2783 class TimelineLaneSlicer extends protectedApi.Slicer { 2784 sliceRange(origRange, dateProfile, dateProfileGenerator, tDateProfile, dateEnv) { 2785 let normalRange = normalizeRange(origRange, tDateProfile, dateEnv); 2786 let segs = []; 2787 // protect against when the span is entirely in an invali
2787d date region 2788 if (computeDateSnapCoverage(normalRange.start, tDateProfile, dateEnv) 2789 < computeDateSnapCoverage(normalRange.end, tDateProfile, dateEnv)) { 2790 // intersect the footprint's range with the grid's range 2791 let slicedRange = protectedApi.intersectRanges(normalRange, tDateProfile.normalizedRange); 2792 if (slicedRange) { 2793 segs.push({ 2794 startDate: slicedRange.start, 2795 endDate: slicedRange.end, 2796 isStart: slicedRange.start.valueOf() === normalRange.start.valueOf() 2797 && isValidDate(slicedRange.start, tDateProfile, dateProfile, dateProfileGenerator), 2798 isEnd: slicedRange.end.valueOf() === normalRange.end.valueOf() 2799 && isValidDate(protectedApi.addMs(slicedRange.end, -1), tDateProfile, dateProfile, dateProfileGenerator), 2800 }); 2801 } 2802 } 2803 return segs; 2804 } 2805 } 2806 2807 const DEFAULT_TIME_FORMAT = protectedApi.createFormatter({ 2808 hour: 'numeric', 2809 minute: '2-digit', 2810 omitZeroMinute: true, 2811 meridiem: 'narrow', 2812 }); 2813 class TimelineEvent extends protectedApi.BaseComponent { 2814 render() { 2815 let { props } = this; 2816 return (jsxRuntime.jsx(protectedApi.StandardEvent, { ...props, display: 'row', defaultTimeFormat: DEFAULT_TIME_FORMAT, defaultDisplayEventTime: !props.isTimeScale })); 2817 } 2818 } 2819 2820 class TimelineLaneMoreLink extends protectedApi.BaseComponent { 2821 render() { 2822 let { props } = this; 2823 let { hiddenSegs, resourceId } = props; 2824 let dateSpanProps = resourceId ? { resourceId } : {}; 2825 return (jsxRuntime.jsx(protectedApi.MoreLinkContainer, { display: 'row', allDayDate: null, segs: hiddenSegs, hiddenSegs: hiddenSegs, dateProfile: props.dateProfile, todayRange: props.todayRange, dateSpanProps: dateSpanProps, isNarrow: false, isMicro: false, popoverContent: () => (jsxRuntime.jsx(jsxRuntime.Fragment, { children: hiddenSegs.map((seg) => { 2826 let { eventRange } = seg; 2827 let { instanceId } = eventRange.instance; 2828 let isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]); 2829 let isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]); 2830 let isInvisible = isDragging || isResizing; 2831 return (jsxRuntime.jsx("div", { style: { visibility: isInvisible ? 'hidden' : undefined }, children: jsxRuntime.jsx(TimelineEvent, { isTimeScale: props.isTimeScale, eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: false, isSelected: instanceId === props.eventSelection, ...protectedApi.getEventRangeMeta(eventRange, props.todayRange, props.nowDate) }) }, instanceId)); 2832 }) })) })); 2833 } 2834 } 2835 2836 function computeManySegHorizontals(segs, segMinWidth, dateEnv, tDateProfile, slotWidth, clipStart = 0, // uses it as a new origin! 2837 clipEnd = Infinity) { 2838 const res = {}; 2839 for (const seg of segs) { 2840 const horizontals = computeSegHorizontals(seg, segMinWidth, dateEnv, tDateProfile, slotWidth, clipStart, clipEnd); 2841 if (horizontals) { 2842 res[protectedApi.getEventKey(seg)] = horizontals; 2843 } 2844 } 2845 return res; 2846 } 2847 function computeSegHorizontals(seg, segMinWidth, dateEnv, tDateProfile, slotWidth, clipStart = 0, // uses it as a new origin! 2848 clipEnd = Infinity) { 2849 let startCoord = dateToCoord(seg.startDate, dateEnv, tDateProfile, slotWidth);
2850 let endCoord = dateToCoord(seg.endDate, dateEnv, tDateProfile, slotWidth); 2851 startCoord = Math.max(startCoord, clipStart); 2852 endCoord = Math.min(endCoord, clipEnd); 2853 if (startCoord < endCoord) { 2854 let size = endCoord - startCoord; 2855 if (segMinWidth) { 2856 size = Math.max(size, segMinWidth); 2857 } 2858 return { start: startCoord - clipStart, size }; 2859 } 2860 } 2861 function computeFgSegPlacements(// mostly horizontals 2862 segs, segHorizontals, // TODO: use Map 2863 segHeights, // keyed by instanceId 2864 hiddenGroupHeights, strictOrder, maxDepth) { 2865 const segRanges = []; 2866 // isn't it true that there will either be ALL hcoords or NONE? can optimize 2867 for (const seg of segs) { 2868 const hcoords = segHorizontals[protectedApi.getEventKey(seg)]; 2869 if (hcoords) { 2870 segRanges.push({ 2871 ...seg, 2872 start: hcoords.start, 2873 end: hcoords.start + hcoords.size, 2874 }); 2875 } 2876 } 2877 const hierarchy = new protectedApi.SegHierarchy(segRanges, (seg) => segHeights.get(protectedApi.getEventKey(seg)), strictOrder, undefined, // maxCoord 2878 maxDepth); 2879 const segTops = new Map(); 2880 hierarchy.traverseSegs((seg, segTop) => { 2881 segTops.set(protectedApi.getEventKey(seg), segTop); 2882 }); 2883 const { hiddenSegs } = hierarchy; 2884 let totalHeight = 0; 2885 for (const segRange of segRanges) { 2886 const segKey = protectedApi.getEventKey(segRange); 2887 const segHeight = segHeights.get(segKey); 2888 const segTop = segTops.get(segKey); 2889 if (segHeight != null) { 2890 if (segTop != null) { 2891 totalHeight = Math.max(totalHeight, segTop + segHeight); 2892 } 2893 } 2894 } 2895 const hiddenGroups = protectedApi.groupIntersectingSegs(hiddenSegs); 2896 const hiddenGroupTops = new Map(); 2897 // HACK for hiddenGroup findInsertion() call 2898 hierarchy.strictOrder = true; 2899 for (const hiddenGroup of hiddenGroups) { 2900 const { levelCoord: top } = hierarchy.findInsertion(hiddenGroup, 0); 2901 const hiddenGroupHeight = hiddenGroupHeights.get(hiddenGroup.key) || 0; 2902 hiddenGroupTops.set(hiddenGroup.key, top); 2903 totalHeight = Math.max(totalHeight, top + hiddenGroupHeight); 2904 } 2905 return [ 2906 segTops, 2907 hiddenGroups, 2908 hiddenGroupTops, 2909 totalHeight, 2910 ]; 2911 } 2912 2913 /* 2914 TODO: make DRY with other Event Harnesses 2915 */ 2916 class TimelineEventHarness extends compat.Component { 2917 constructor() { 2918 super(...arguments); 2919 // ref 2920 this.rootElRef = compat.createRef(); 2921 } 2922 render() { 2923 const { props } = this; 2924 return (jsxRuntime.jsx("div", { className: classNames__default["default"].abs, style: props.style, ref: this.rootElRef, children: props.children })); 2925 } 2926 componentDidMount() { 2927 this._isUnmounting = false; 2928 const rootEl = this.rootElRef.current; // TODO: make dynamic with useEffect 2929 this.disconnectHeight = protectedApi.watchHeight(rootEl, (height) => { 2930 if (this._isUnmounting) 2931 return; 2932 protectedApi.setRef(this.props.heightRef, height); 2933 }); 2934 } 2935 componentWillUnmount() { 2936 this._isUnmounting = true; 2937 this.disconnectHeight(); 2938 protectedApi.setRef(this.props.heightRef, null); 2939 } 2940 } 2941 2942 class TimelineFg extends protectedApi.BaseComponent { 2943 constructor() { 2944 super(...arguments); 2945 this.state = {}; 2946 // memo 2947 this.sortEventSegs = protectedApi.memoize(protectedApi.sortEventSegs); 2948 // refs 2949 this.segHeightRefMap = new protectedApi.RefMap(() => { 2950 protectedApi.afterSize(this.handleSegHeights); 2951 }); 2952 this.moreLinkHeightRefMap = new protectedApi.RefMap(() => { 2953 protectedApi.afterSize(this.handleMoreLinkHeights); 2954 }); 2955 this.handleMoreLinkHeights = () => { 2956 if (this._isUnmounting) 2957 return; 2958 this.setState({ moreLinkHeightRev: this.moreLinkHeightRefMap.rev }); // will trigger rerender 2959 }; 2960 this.handleSegHeights = () => { 2961 if (this._isUnmounting) 2962 return; 2963 this.setState({ segHeightRev: this.segHeightRefMap.rev }); // will trigger rerender 2964 }; 2965 } 2966 /* 2967 TODO: lots of memoization needed here! 2968 */ 2969 render() { 2970 let { props, context, segHeightRefMap, moreLinkHeightRefMap } = this; 2971 let { options } = context; 2972 let { tDateProfile } = props; 2973 let mirrorSegs = (props.eventDrag ? props.eventDrag.segs : null) || 2974 (props.eventResize ? props.eventResize.segs : null) || 2975 []; 2976 let fgSegs = this.sortEventSegs(props.fgEventSegs, options.eventOrder); 2977 let fgSegHorizontals = computeManySegHorizontals(fgSegs, options.eventMinWidth, context.dateEnv, tDateProfile, props.slotWidth, props.clipStart, props.clipEnd); 2978 let [fgSegTops, hiddenGroups, hiddenGroupTops, totalHeight] = computeFgSegPlacements(fgSegs, fgSegHorizontals, segHeightRefMap.current, moreLinkHeightRefMap.current, options.eventOrderStrict, options.eventMaxStack); 2979 this.totalHeight = totalHeight; 2980 return (jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(classNames__default["default"].rel, classNames__default["default"].noShrink), style: { 2981 height: totalHeight, 2982 }, children: [this.renderFgSegs(fgSegs, fgSegHorizontals, fgSegTops, hiddenGroups, hiddenGroupTops, 2983 /* isMirror = */ false), this.renderFgSegs(mirrorSegs, props.slotWidth // TODO: memoize 2984 ? computeManySegHorizontals(mirrorSegs, options.eventMinWidth, context.dateEnv, tDateProfile, props.slotWidth, props.clipStart, props.clipEnd) 2985 : {}, fgSegTops, 2986 /* hiddenGroups = */ [], 2987 /* hiddenGroupTops = */ new Map(), 2988 /* isMirror = */ true)] })); 2989 } 2990 renderFgSegs(segs, segHorizontals, segTops, hiddenGroups, hiddenGroupTops, isMirror) { 2991 const { props, segHeightRefMap, moreLinkHeightRefMap } = this; 2992 return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [segs.map((seg) => { 2993 const { eventRange } = seg; 2994 const { instanceId } = eventRange.instance; 2995 const segTop = segTops.get(instanceId); 2996 const segHorizontalMaybe = segHorizontals[instanceId]; 2997 const segHorizontal = segHorizontalMaybe || {}; 2998 const isDragging = Boolean(props.eventDrag && props.eventDrag.affectedInstances[instanceId]); 2999 const isResizing = Boolean(props.eventResize && props.eventResize.affectedInstances[instanceId]); 3000 const isInvisible = !isMirror && (isDragging || isResizing || !segHorizontalMaybe || segTop == null); 3001 const isSelected = instanceId === props.eventSelection; 3002 return (jsxRuntime.jsx(TimelineEventHarness, { style: { 3003 visibility: isInvisible ? 'hidden' : undefined, 3004 zIndex: isSelected ? 1000 : 1, // scope z
3004-indexes within; HACK: relies on hardcoded z-index offset; fragile if stacking context changes 3005 top: segTop || 0, 3006 insetInlineStart: segHorizontal.start, 3007 width: segHorizontal.size, 3008 }, heightRef: isMirror ? undefined : segHeightRefMap.createRef(instanceId), children: jsxRuntime.jsx(TimelineEvent, { isTimeScale: props.tDateProfile.isTimeScale, eventRange: eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isDragging: isDragging, isResizing: isResizing, isMirror: isMirror, isSelected: isSelected, ...protectedApi.getEventRangeMeta(eventRange, props.todayRange, props.nowDate) }) }, instanceId)); 3009 }), hiddenGroups.map((hiddenGroup) => (jsxRuntime.jsx(TimelineEventHarness, { style: { 3010 top: hiddenGroupTops.get(hiddenGroup.key) || 0, 3011 insetInlineStart: hiddenGroup.start, 3012 width: hiddenGroup.end - hiddenGroup.start, 3013 }, heightRef: moreLinkHeightRefMap.createRef(hiddenGroup.key), children: jsxRuntime.jsx(TimelineLaneMoreLink, { hiddenSegs: hiddenGroup.segs, dateProfile: props.dateProfile, nowDate: props.nowDate, todayRange: props.todayRange, isTimeScale: props.tDateProfile.isTimeScale, eventDrag: props.eventDrag, eventResize: props.eventResize, eventSelection: props.eventSelection, resourceId: props.resourceId }) }, hiddenGroup.key)))] })); 3014 } 3015 componentDidMount() { 3016 this._isUnmounting = false; 3017 this.fireHeight(); // could be ready to fire if zero events and zero height 3018 } 3019 componentDidUpdate() { 3020 this.fireHeight(); 3021 } 3022 componentWillUnmount() { 3023 this._isUnmounting = true; 3024 this.firedTotalHeight = undefined; 3025 protectedApi.setRef(this.props.heightRef, null); 3026 } 3027 fireHeight() { 3028 if (this.totalHeight !== this.firedTotalHeight) { 3029 this.firedTotalHeight = this.totalHeight; 3030 protectedApi.setRef(this.props.heightRef, this.totalHeight); 3031 } 3032 } 3033 } 3034 3035 class TimelineBg extends protectedApi.BaseComponent { 3036 render() { 3037 let { props } = this; 3038 let highlightSeg = [].concat(props.eventResizeSegs || [], props.dateSelectionSegs || []); 3039 return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [this.renderSegs(props.businessHourSegs || [], 'non-business'), this.renderSegs(props.bgEventSegs || [], 'bg-event'), this.renderSegs(highlightSeg, 'highlight')] })); 3040 } 3041 renderSegs(segs, fillType) { 3042 const { props, context } = this; 3043 const { dateEnv, options } = context; 3044 const { tDateProfile, todayRange, nowDate, slotWidth } = props; 3045 const clipStart = props.clipStart ?? 0; 3046 const clipEnd = props.clipEnd ?? Infinity; 3047 return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: segs.map((seg) => { 3048 if (slotWidth != null) { 3049 let { start, size } = computeSegHorizontals(seg, undefined, dateEnv, tDateProfile, slotWidth);
3050 let end = start + size; 3051 start = Math.max(start, clipStart); 3052 end = Math.min(end, clipEnd); 3053 if (start < end) { 3054 return (jsxRuntime.jsx("div", { className: classNames__default["default"].fillY, style: { 3055 insetInlineStart: start - clipStart, 3056 width: end - start, 3057 }, children: fillType === 'bg-event' ? (jsxRuntime.jsx(protectedApi.BgEvent, { eventRange: seg.eventRange, isStart: seg.isStart, isEnd: seg.isEnd, isVertical: false, ...protectedApi.getEventRangeMeta(seg.eventRange, todayRange, nowDate) })) : (protectedApi.renderFill(fillType, options)) }, protectedApi.buildEventRangeKey(seg.eventRange))); 3058 } 3059 } 3060 }) })); 3061 } 3062 } 3063 3064 class TimelineView extends protectedApi.DateComponent { 3065 constructor() { 3066 super(...arguments); 3067 this.state = {}; 3068 // memoized 3069 this.buildTimelineDateProfile = protectedApi.memoize(buildTimelineDateProfile); 3070 this.computeSlotWidth = protectedApi.memoize(computeSlotWidth);
3071 // refs 3072 this.headerScrollerRef = compat.createRef(); 3073 this.bodyScrollerRef = compat.createRef(); 3074 this.footerScrollerRef = compat.createRef(); 3075 this.headerRowInnerWidthMap = new protectedApi.RefMap(() => { 3076 protectedApi.afterSize(this.handleSlotInnerWidths); 3077 }); 3078 this.slicer = new TimelineLaneSlicer(); 3079 // Sizing 3080 // ----------------------------------------------------------------------------------------------- 3081 this.handleSlotInnerWidths = () => { 3082 if (this._isUnmounting) 3083 return; 3084 const headerSlotInnerWidth = this.headerRowInnerWidthMap.current.get(this.tDateProfile.cellRows.length - 1); 3085 if (headerSlotInnerWidth != null && headerSlotInnerWidth !== this.state.slotInnerWidth) { 3086 this.setState({ slotInnerWidth: headerSlotInnerWidth }); 3087 } 3088 }; 3089 this.handleTotalWidth = (totalWidth) => { 3090 if (this._isUnmounting) 3091 return; 3092 this.setState({ 3093 totalWidth, 3094 }); 3095 }; 3096 this.handleClientWidth = (clientWidth) => { 3097 if (this._isUnmounting) 3098 return; 3099 this.setState({ 3100 clientWidth, 3101 }); 3102 }; 3103 this.handleTimeScrollRequest = (scrollTime) => { 3104 this.scrollTime = scrollTime; 3105 this.scrollX = undefined; 3106 this.applyTimeScroll(); 3107 }; 3108 this.handleTimeScrollStart = (isDevice) => { 3109 if (isDevice) { 3110 this.scrollX = undefined; 3111 this.scrollTime = undefined; 3112 } 3113 }; 3114 this.handleTimeScrollEnd = (isDevice) => { 3115 if (isDevice) { 3116 this.scrollX = this.syncedScroller.x; 3117 this.scrollTime = undefined; 3118 } 3119 }; 3120 // Hit System 3121 // ----------------------------------------------------------------------------------------------- 3122 this.handeBodyEl = (el) => { 3123 this.bodyEl = el; 3124 if (el) { 3125 this.context.registerInteractiveComponent(this, { el }); 3126 } 3127 else { 3128 this.context.unregisterInteractiveComponent(this); 3129 } 3130 }; 3131 } 3132 render() { 3133 const { props, state, context } = this; 3134 const { options } = context; 3135 const { borderlessX, borderlessTop, borderlessBottom } = protectedApi.computeViewBorderless(options); 3136 const { totalWidth, clientWidth } = state; 3137 const endScrollbarWidth = (totalWidth != null && clientWidth != null) 3138 ? totalWidth - clientWidth 3139 : undefined; 3140 /* date */ 3141 const tDateProfile = this.tDateProfile = this.buildTimelineDateProfile(props.dateProfile, context.dateEnv, options, context.dateProfileGenerator); 3142 const { cellRows } = tDateProfile; 3143 let { unit: timerUnit, value: timerUnitValue } = protectedApi.greatestDurationDenominator(tDateProfile.slotDuration); 3144 /* table settings */ 3145 const verticalScrolling = !props.forPrint && !protectedApi.getIsHeightAuto(options); 3146 const tableHeaderSticky = !props.forPrint && protectedApi.getTableHeaderSticky(options); 3147 const footerScrollbarSticky = !props.forPrint && protectedApi.getFooterScrollbarSticky(options); 3148 /* table positions */ 3149 const [canvasWidth, slotWidth] = this.computeSlotWidth(tDateProfile.slotCnt, tDateProfile.slotsPerLabel, options.slotMinWidth, state.slotInnerWidth, // is ACTUALLY the label width. rename? 3150 clientWidth); 3151 /* sliced */ 3152 let slicedProps = this.slicer.sliceProps(props, props.dateProfile, tDateProfile.isTimeScale ? null : options.nextDayThreshold, context, // wish we didn't have to pass in the rest of the args... 3153 props.dateProfile, context.dateProfileGenerator, tDateProfile, context.dateEnv); 3154 return (jsxRuntime.jsx(protectedApi.NowTimer, { unit: timerUnit, unitValue: timerUnitValue, children: (nowDate, todayRange) => { 3155 const enableNowIndicator = // TODO: DRY 3156 !props.forPrint && 3157 options.nowIndicator && 3158 slotWidth != null && 3159 protectedApi.rangeContainsMarker(props.dateProfile.currentRange, nowDate); 3160 return (jsxRuntime.jsxs(protectedApi.ViewContainer, { viewSpec: context.viewSpec, className: publicApi.joinClassNames( 3161 // HACK for Safari print-mode, where noScrollbars won't take effect for 3162 // the below Scrollers if they have liquid flex height 3163 !props.forPrint && classNames__default["default"].flexCol, props.className, protectedApi.generateClassName(options.tableClass, { 3164 borderlessX, 3165 borderlessTop, 3166 borderlessBottom, 3167 multiMonthColumns: 0, 3168 }), classNames__default["default"].isolate), children: [jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableHeaderClass, { 3169 isSticky: tableHeaderSticky, 3170 borderlessX, 3171 borderlessTop, 3172 borderlessBottom, 3173 multiMonthColumns: 0, 3174 }), classNames__default["default"].flexCol, tableHeaderSticky && classNames__default["default"].tableHeaderSticky), style: { 3175 zIndex: 1, 3176 }, children: [jsxRuntime.jsxs(protectedApi.Scroller, { horizontal: true, hideScrollbars: true, className: classNames__default["default"].flexRow, ref: this.headerScrollerRef, children: [jsxRuntime.jsxs("div", { 3177 // TODO: DRY 3178 className: publicApi.joinClassNames(classNames__default["default"].rel, // origin for now-indicator 3179 canvasWidth == null && classNames__default["default"].liquid
3179), style: { width: canvasWidth }, children: [cellRows.map((cells, rowIndex) => { 3180 const rowLevel = cellRows.length - rowIndex - 1; 3181 return (jsxRuntime.jsx(TimelineHeaderRow, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, rowLevel: rowLevel, cells: cells, slotWidth: slotWidth, innerWidthRef: this.headerRowInnerWidthMap.createRef(rowIndex) }, rowIndex)); 3182 }), enableNowIndicator && (jsxRuntime.jsx(TimelineNowIndicatorArrow, { tDateProfile: tDateProfile, nowDate: nowDate, slotWidth: slotWidth }))] }), Boolean(endScrollbarWidth) && (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.fillerClass, { inTableHeader: true }), classNames__default["default"].borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), jsxRuntime.jsx("div", { className: protectedApi.generateClassName(options.slotHeaderDividerClass, { 3183 inTableHeader: true, 3184 options: { dayMinWidth: options.dayMinWidth }, 3185 }) })] }), jsxRuntime.jsx(protectedApi.Scroller, { vertical: verticalScrolling, horizontal: true, hideScrollbars: footerScrollbarSticky || 3186 props.forPrint // prevents blank space in print-view on Safari 3187 , className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableBodyClass, { 3188 borderlessX, 3189 borderlessTop, 3190 borderlessBottom, 3191 multiMonthColumns: 0, 3192 }), classNames__default["default"].flexCol, verticalScrolling && classNames__default["default"].liquid), style: { 3193 zIndex: 0, 3194 }, ref: this.bodyScrollerRef, clientWidthRef: this.handleClientWidth, children: jsxRuntime.jsxs("div", { "aria-label": options.eventsHint, className: publicApi.joinClassNames(classNames__default["default"].rel, // for canvas origin? 3195 classNames__default["default"].grow), style: { width: canvasWidth }, ref: this.handeBodyEl, children: [jsxRuntime.jsx(TimelineSlats, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, 3196 // dimensions 3197 slotWidth: slotWidth }), jsxRuntime.jsx(TimelineBg, { tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, 3198 // content 3199 bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: props.forPrint ? null : slicedProps.businessHourSegs, dateSelectionSegs: props.forPrint ? null : slicedProps.dateSelectionSegs, eventResizeSegs: props.forPrint ? null : (slicedProps.eventResize ? slicedProps.eventResize.segs : null), 3200 // dimensions 3201 slotWidth: slotWidth }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.timelineTopClass) }), jsxRuntime.jsx(TimelineFg, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, 3202 // content 3203 fgEventSegs: slicedProps.fgEventSegs, eventDrag: props.forPrint ? null : slicedProps.eventDrag, eventResize: props.forPrint ? null : slicedProps.eventResize, eventSelection: slicedProps.eventSelection, 3204 // dimensions 3205 slotWidth: slotWidth }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.timelineBottomClass) }), enableNowIndicator && (jsxRuntime.jsx(TimelineNowIndicatorLine, { tDateProfile: tDateProfile, nowDate: n
3205owDate, slotWidth: slotWidth }))] }) }), Boolean(footerScrollbarSticky) && (jsxRuntime.jsx(protectedApi.FooterScrollbar, { isSticky: true, canvasWidth: canvasWidth, scrollerRef: this.footerScrollerRef })), jsxRuntime.jsx(protectedApi.Ruler, { widthRef: this.handleTotalWidth })] })); 3206 } })); 3207 } 3208 // Lifecycle 3209 // ----------------------------------------------------------------------------------------------- 3210 componentDidMount() { 3211 this._isUnmounting = false; 3212 this.syncedScroller = new ScrollerSyncer(true); // horizontal=true 3213 this.updateSyncedScroller(); 3214 this.resetScroll(); 3215 this.context.emitter.on('_timeScrollRequest', this.handleTimeScrollRequest); 3216 this.syncedScroller.addScrollStartListener(this.handleTimeScrollStart); 3217 this.syncedScroller.addScrollEndListener(this.handleTimeScrollEnd); 3218 } 3219 componentDidUpdate(prevProps, prevState) { 3220 const { props } = this; 3221 const { options } = this.context; 3222 this.updateSyncedScroller(); 3223 const dateProfileChange = prevProps.dateProfile !== props.dateProfile; 3224 const slotWidthChange = prevState.clientWidth !== this.state.clientWidth || 3225 prevState.slotInnerWidth !== this.state.slotInnerWidth; 3226 if (dateProfileChange || slotWidthChange) { 3227 if (dateProfileChange && options.scrollTimeReset) { 3228 this.resetScroll(); 3229 } 3230 else { 3231 this.applyTimeScroll(); 3232 } 3233 } 3234 } 3235 componentWillUnmount() { 3236 this._isUnmounting = true; 3237 this.syncedScroller.destroy(); 3238 this.context.emitter.off('_timeScrollRequest', this.handleTimeScrollRequest); 3239 this.syncedScroller.removeScrollStartListener(this.handleTimeScrollStart); 3240 this.syncedScroller.removeScrollEndListener(this.handleTimeScrollEnd); 3241 } 3242 // Scrolling 3243 // ----------------------------------------------------------------------------------------------- 3244 updateSyncedScroller() { 3245 this.syncedScroller.handleChildren([ 3246 this.headerScrollerRef.current, 3247 this.bodyScrollerRef.current, 3248 this.footerScrollerRef.current 3249 ]); 3250 } 3251 resetScroll() { 3252 this.handleTimeScrollRequest(this.context.options.scrollTime); 3253 } 3254 applyTimeScroll() { 3255 const x = this.computeTimeScroll(); 3256 if (x != null) { 3257 this.syncedScroller.scrollTo({ x }); 3258 } 3259 } 3260 computeTimeScroll() { 3261 const { props, context, tDateProfile, scrollTime, scrollX } = this; 3262 const slotWidth = this.getSlotWidth(); 3263 let x = scrollX; 3264 if (x == null && scrollTime != null && slotWidth != null) { 3265 x = timeToCoord(scrollTime, context.dateEnv, props.dateProfile, tDateProfile, slotWidth);
3266 if (x) { 3267 x += 1; // overcome border. TODO: DRY this up 3268 } 3269 } 3270 return x; 3271 } 3272 getSlotWidth() { 3273 const { tDateProfile, state, context } = this; 3274 if (tDateProfile) { 3275 return this.computeSlotWidth(tDateProfile.slotCnt, tDateProfile.slotsPerLabel, context.options.slotMinWidth, state.slotInnerWidth, state.clientWidth)[1]; 3276 } 3277 } 3278 queryHit(isRtl, positionLeft, positionTop, elWidth, elHeight) { 3279 const { props, context, tDateProfile } = this; 3280 const { dateEnv } = context; 3281 const slotWidth = this.getSlotWidth(); 3282 if (slotWidth) { 3283 const x = isRtl ? elWidth - positionLeft : positionLeft; 3284 const slatIndex = Math.floor(x / slotWidth); 3285 const slatX = slatIndex * slotWidth; 3286 const partial = (x - slatX) / slotWidth; // floating point number between 0 and 1 3287 const localSnapIndex = Math.floor(partial * tDateProfile.snapsPerSlot); // the snap # relative to start of slat 3288 let startDate = dateEnv.add(tDateProfile.slotDates[slatIndex], protectedApi.multiplyDuration(tDateProfile.snapDuration, localSnapIndex)); 3289 let endDate = dateEnv.add(startDate, tDateProfile.snapDuration); 3290 // TODO: generalize this coord stuff to TimeGrid? 3291 let snapWidth = slotWidth / tDateProfile.snapsPerSlot; 3292 let startCoord = slatIndex * slotWidth + (snapWidth * localSnapIndex); 3293 let endCoord = startCoord + snapWidth; 3294 let left, right; 3295 if (isRtl) { 3296 left = elWidth - endCoord; 3297 right = elWidth - startCoord; 3298 } 3299 else { 3300 left = startCoord; 3301 right = endCoord; 3302 } 3303 return { 3304 dateProfile: props.dateProfile, 3305 dateSpan: { 3306 range: { start: startDate, end: endDate }, 3307 allDay: !tDateProfile.isTimeScale, 3308 }, 3309 rect: { 3310 left, 3311 right, 3312 top: 0, 3313 bottom: elHeight, 3314 }, 3315 getDayEl: () => getTimelineSlotEl(this.bodyEl, slatIndex), 3316 layer: 0, 3317 }; 3318 } 3319 return null; 3320 } 3321 } 3322 3323 var timelinePlugin = { 3324 name: 'timeline', 3325 premiumReleaseDate: '2026-07-24', 3326 deps: [premiumCommonPlugin], 3327 initialView: 'timelineDay', 3328 views: { 3329 timeline: { 3330 component: TimelineView, 3331 usesMinMaxTime: true, 3332 eventResizableFromStart: true, // how is this consumed for TimelineView tho? 3333 }, 3334 timelineDay: { 3335 type: 'timeline', 3336 duration: { days: 1 }, 3337 }, 3338 timelineWeek: { 3339 type: 'timeline', 3340 duration: { weeks: 1 }, 3341 }, 3342 timelineMonth: { 3343 type: 'timeline', 3344 duration: { months: 1 }, 3345 }, 3346 timelineYear: { 3347 type: 'timeline', 3348 duration: { years: 1 }, 3349 }, 3350 }, 3351 }; 3352 3353 class ResourceDayTimeColsJoiner extends VResourceJoiner { 3354 transformSeg(seg, resourceDayTable, resourceI) { 3355 return [ 3356 { 3357 ...seg, 3358 col: resourceDayTable.computeCol(seg.col, resourceI), 3359 }, 3360 ]; 3361 } 3362 } 3363 3364 class ResourceTimeGridView extends protectedApi.DateComponent { 3365 constructor() { 3366 super(...arguments); 3367 this.state = {}; 3368 // memo 3369 this.flattenResources = protectedApi.memoize(flattenResources);
3370 this.buildResourceTimeColsModel = protectedApi.memoize(buildResourceTimeColsModel); 3371 this.buildResourceRowConfigs = protectedApi.memoize(buildResourceRowConfigs); 3372 this.createDayHeaderFormatter = protectedApi.memoize(protectedApi.createDayHeaderFormatter); 3373 // internal 3374 this.allDaySplitter = new protectedApi.AllDaySplitter(); 3375 // for all-day-resource props 3376 this.allDayResourceSplitter = new VResourceSplitter(); 3377 this.allDayResourceSlicers = {}; 3378 this.allDayResourceJoiner = new ResourceDayTableJoiner(); 3379 // for timed resource props 3380 this.buildDayRanges = protectedApi.memoize(protectedApi.buildDayRanges); 3381 this.timedResourceSplitter = new VResourceSplitter(); 3382 this.timedResourceSlicers = {}; 3383 this.timedResourceJoiner = new ResourceDayTimeColsJoiner(); 3384 // timed-only column splitting 3385 this.splitFgEventSegs = protectedApi.memoize(protectedApi.organizeSegsByCol); 3386 this.splitBgEventSegs = protectedApi.memoize(protectedApi.organizeSegsByCol); 3387 this.splitBusinessHourSegs = protectedApi.memoize(protectedApi.organizeSegsByCol); 3388 this.splitNowIndicatorSegs = protectedApi.memoize(protectedApi.organizeSegsByCol); 3389 this.splitDateSelectionSegs = protectedApi.memoize(protectedApi.organizeSegsByCol); 3390 this.splitEventDrag = protectedApi.memoize(protectedApi.splitInteractionByCol); 3391 this.splitEventResize = protectedApi.memoize(protectedApi.splitInteractionByCol); 3392 this.isHitComboAllowed = (hit0, hit1) => { 3393 let allowAcrossResources = this.dayRanges.length === 1; 3394 return allowAcrossResources || hit0.dateSpan.resourceId === hit1.dateSpan.resourceId; 3395 }; 3396 } 3397 render() { 3398 let { props, context } = this; 3399 let { options, dateEnv } = context; 3400 let { dateProfile } = props; 3401 let resourceOrderSpecs = options.resourceOrder || DEFAULT_RESOURCE_ORDER; 3402 let resources = this.flattenResources(props.resourceStore, resourceOrderSpecs); 3403 let resourceDayTableModel = this.buildResourceTimeColsModel(
3403dateProfile, context.dateProfileGenerator, resources, options.datesAboveResources, context); 3404 // split seg by all-day/timed 3405 let splitProps = this.allDaySplitter.splitProps(props); 3406 // split the all-day segs by resource 3407 let allDayResourceSplitProps = this.allDayResourceSplitter.splitProps({ 3408 businessHours: splitProps.allDay.businessHours, 3409 dateSelection: splitProps.allDay.dateSelection, 3410 eventStore: splitProps.allDay.eventStore, 3411 eventUiBases: splitProps.allDay.eventUiBases, 3412 eventSelection: splitProps.allDay.eventSelection, 3413 eventDrag: splitProps.allDay.eventDrag, 3414 eventResize: splitProps.allDay.eventResize, 3415 resourceDayTableModel, 3416 }); 3417 this.allDayResourceSlicers = protectedApi.mapHash(allDayResourceSplitProps, (split, resourceId) => this.allDayResourceSlicers[resourceId] || new protectedApi.DayTableSlicer()); 3418 let allDayResourceSlicedProps = protectedApi.mapHash(this.allDayResourceSlicers, (slicer, resourceId) => slicer.sliceProps(allDayResourceSplitProps[resourceId], dateProfile, options.nextDayThreshold, context, resourceDayTableModel.dayTableModel)); 3419 let allDayResourceJoinedProps = this.allDayResourceJoiner.joinProps(allDayResourceSlicedProps, resourceDayTableModel); 3420 // split the timed segs by resource 3421 let dayRanges = this.dayRanges = this.buildDayRanges(resourceDayTableModel.dayTableModel, dateProfile, dateEnv); 3422 let timedResourceSplitProps = this.timedResourceSplitter.splitProps({ 3423 businessHours: splitProps.timed.businessHours, 3424 dateSelection: splitProps.timed.dateSelection, 3425 eventStore: splitProps.timed.eventStore, 3426 eventUiBases: splitProps.timed.eventUiBases, 3427 eventSelection: splitProps.timed.eventSelection, // result not used 3428 eventDrag: splitProps.timed.eventDrag, 3429 eventResize: splitProps.timed.eventResize, 3430 resourceDayTableModel, 3431 }); 3432 this.timedResourceSlicers = protectedApi.mapHash(timedResourceSplitProps, (split, resourceId) => this.timedResourceSlicers[resourceId] || new protectedApi.DayTimeColsSlicer()); 3433 let timedResourceSlicedProps = protectedApi.mapHash(this.timedResourceSlicers, (slicer, resourceId) => slicer.sliceProps(timedResourceSplitProps[resourceId], dateProfile, null, context, dayRanges)); 3434 let timedResourceJoinedProps = this.timedResourceJoiner.joinProps(timedResourceSlicedProps, resourceDayTableModel); 3435 let datesRepDistinctDays = resourceDayTableModel.dayTableModel.rowCount === 1; 3436 let dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, resourceDayTableModel.colCount); 3437 return (jsxRuntime.jsx(protectedApi.NowTimer, { unit: options.nowIndicator ? 'minute' : 'day' /* hacky */, children: (nowDate, todayRange) => { 3438 // timed-only column splitting 3439 let colCount = resourceDayTableModel.colCount; 3440 let fgEventSegsByCol = this.splitFgEventSegs(timedResourceJoinedProps.fgEventSegs, colCount); 3441 let bgEventSegsByCol = this.splitBgEventSegs(timedResourceJoinedProps.bgEventSegs, colCount); 3442 let businessHourSegsByCol = this.splitBusinessHourSegs(timedResourceJoinedProps.businessHourSegs, colCount); 3443 let nowIndicatorSegsByCol = this.splitNowIndicatorSegs((() => { 3444 // was buildNowIndicatorSegs 3445 let nonResourceSegs = !props.forPrint && options.nowIndicator 3446 ? this.timedResourceSlicers[''].sliceNowDate(nowDate, this.props.dateProfile, this.context.options.nextDayThreshold, this.context, this.dayRanges) 3447 : []; // TODO: breaks memoization? 3448 const expandedSegs = this.timedResourceJoiner.expandSegs(resourceDayTableModel, nonResourceSegs); 3449 const dayCnt = resourceDayTableModel.dayTableModel.colCount; 3450 const resourceCnt = resourceDayTableModel.resources.length; 3451 return expandedSegs.map((seg) => ({ 3452 ...seg, 3453 showDot: !options.datesAboveResources && dayCnt > 1 3454 ? true 3455 : seg.col % resourceCnt === 0, 3456 })); 3457 })(), colCount); 3458 let dateSelectionSegsByCol = this.splitDateSelectionSegs(timedResourceJoinedProps.dateSelectionSegs, colCount); 3459 let eventDragByCol = this.splitEventDrag(timedResourceJoinedProps.eventDrag, colCount); 3460 let eventResizeByCol = this.splitEventResize(timedResourceJoinedProps.eventResize, colCount);
3461 const headerTiers = this.buildResourceRowConfigs(resources, options.datesAboveResources, resourceDayTableModel.dayTableModel.headerDates, datesRepDistinctDays, props.dateProfile, todayRange, dayHeaderFormat, context); 3462 return (jsxRuntime.jsx(protectedApi.TimeGridLayout, { labelId: props.labelId, labelStr: props.labelStr, dateProfile: dateProfile, nowDate: nowDate, todayRange: todayRange, cells: resourceDayTableModel.cells[0], forPrint: props.forPrint, isHitComboAllowed: this.isHitComboAllowed, className: props.className, 3463 // header content 3464 headerTiers: headerTiers, 3465 // all-day content 3466 fgEventSegs: allDayResourceJoinedProps.fgEventSegs, bgEventSegs: allDayResourceJoinedProps.bgEventSegs, businessHourSegs: allDayResourceJoinedProps.businessHourSegs, dateSelectionSegs: allDayResourceJoinedProps.dateSelectionSegs, eventDrag: allDayResourceJoinedProps.eventDrag, eventResize: allDayResourceJoinedProps.eventResize, 3467 // timed content 3468 fgEventSegsByCol: fgEventSegsByCol, bgEventSegsByCol: bgEventSegsByCol, businessHourSegsByCol: businessHourSegsByCol, nowIndicatorSegsByCol: nowIndicatorSegsByCol, dateSelectionSegsByCol: dateSelectionSegsByCol, eventDragByCol: eventDragByCol, eventResizeByCol: eventResizeByCol, 3469 // universal content 3470 eventSelection: allDayResourceJoinedProps.eventSelection || splitProps.timed.eventSelection })); 3471 } })); 3472 } 3473 } 3474 /* 3475 TODO: kill this and DayResourceTableModel/ResourceDayTableModel 3476 */
3477 function buildResourceTimeColsModel(dateProfile, dateProfileGenerator, resources, datesAboveResources, context) { 3478 let dayTable = protectedApi.buildTimeColsModel(dateProfile, dateProfileGenerator, context.dateEnv); 3479 if (!resources.length) { 3480 return new ResourcelessDayTableModel(dayTable); 3481 } 3482 return datesAboveResources ? 3483 new DayResourceTableModel(dayTable, resources, context) : 3484 new ResourceDayTableModel(dayTable, resources, context); 3485 } 3486 3487 var resourceTimeGridPlugin = { 3488 name: 'resource-timegrid', 3489 premiumReleaseDate: '2026-07-24', 3490 deps: [ 3491 premiumCommonPlugin, 3492 timeGridPlugin__default["default"], 3493 resourcePlugin, 3494 resourceDayGridPlugin, 3495 ], 3496 initialView: 'resourceTimeGridDay', 3497 views: { 3498 resourceTimeGrid: { 3499 type: 'timeGrid', // will inherit this configuration 3500 component: ResourceTimeGridView, 3501 needsResourceData: true, 3502 }, 3503 resourceTimeGridDay: { 3504 type: 'resourceTimeGrid', 3505 duration: { days: 1 }, 3506 }, 3507 resourceTimeGridWeek: { 3508 type: 'resourceTimeGrid', 3509 duration: { weeks: 1 }, 3510 }, 3511 }, 3512 }; 3513 3514 function fracToCssDim(frac) { 3515 return frac * 100 + '%'; 3516 } 3517 3518 function parseDimConfig(input, minDim = 0) { 3519 if (input != null) { 3520 if (typeof input === 'string') { 3521 let m = input.match(/^(.*)(%|px)$/); 3522 if (m) { 3523 const num = parseFloat(m[1]); 3524 if (!isNaN(num)) { 3525 if (m[2] === '%') { 3526 return { pixels: 0, frac: num / 100, min: minDim }; 3527 } 3528 else { 3529 return { pixels: num, frac: 0, min: minDim }; 3530 } 3531 } 3532 } 3533 } 3534 else if (typeof input === 'number' && !isNaN(input)) { 3535 return { pixels: input, frac: 0, min: minDim }; 3536 } 3537 } 3538 } 3539 function parseSiblingDimConfig(input, grow, // TODO: use (and sanitize) 3540 minDim) { 3541 const partialDimConfig = parseDimConfig(input, minDim); 3542 return partialDimConfig 3543 ? { ...partialDimConfig, grow: grow || 0 } 3544 : { pixels: 0, frac: 0, grow: grow || 1, min: minDim }; 3545 } 3546 /* 3547 Ensure at least one column can grow 3548 Mutates in-place 3549 */ 3550 function ensureDimConfigsGrow(dimConfigs) { 3551 for (const dimConfig of dimConfigs) { 3552 if (dimConfig.grow) { 3553 return; 3554 } 3555 } 3556 // make all expand equally 3557 for (const dimConfig of dimConfigs) { 3558 dimConfig.grow = 1; 3559 } 3560 } 3561 function pixelizeDimConfigs(dimConfigs, clientDim) { 3562 const pixelDims = []; 3563 let preGrowTotal = 0; 3564 let growDenom = 0; 3565 for (const dimConfig of dimConfigs) { 3566 const constrainedPixels = Math.max(dimConfig.pixels + (dimConfig.frac * clientDim), dimConfig.min); 3567 pixelDims.push(constrainedPixels); 3568 preGrowTotal += constrainedPixels; 3569 growDenom += dimConfig.grow; 3570 } 3571 if (preGrowTotal < clientDim) { 3572 const remainder = clientDim - preGrowTotal; 3573 for (let i = 0; i < dimConfigs.length; i++) { 3574 pixelDims[i] += remainder * (dimConfigs[i].grow / growDenom); 3575 } 3576 } 3577 return [pixelDims, preGrowTotal]; 3578 } 3579 function flexifyDimConfigs(dimConfigs, pixelDims) { 3580 const flexDims = []; 3581 const flexGrows = []; 3582 let pixelTotal = 0; 3583 let fracTotal = 0; 3584 for (let i = 0; i < dimConfigs.length; i++) { 3585 const dimConfig = dimConfigs[i]; 3586 const constrainedPixels = Math.max(dimConfig.pixels, dimConfig.min); 3587 flexDims.push(constrainedPixels); 3588 flexGrows.push(pixelDims[i] - constrainedPixels); // a pixel value, but used as a proportion 3589 pixelTotal += dimConfig.pixels + (dimConfig.grow ? dimConfig.min : 0); 3590 fracTotal += dimConfig.frac; // not possible to enforce min for percentage here 3591 } 3592 const minCanvasDim = serializeDimConfig({ 3593 pixels: pixelTotal, 3594 frac: fracTotal, 3595 }); 3596 return [flexDims, flexGrows, minCanvasDim]; 3597 } 3598 function serializeDimConfig({ pixels, frac }) { 3599 if (!frac) { 3600 return pixels; 3601 } 3602 if (!pixels) { 3603 return fracToCssDim(frac); 3604 } 3605 return `calc(${fracToCssDim(frac)} + ${pixels}px)`; 3606 } 3607 function resizeSiblingDimConfig(dimConfigs, pixelDims, clientDim, resizeIndex, resizeDim) { 3608 const newDimConfigs = []; 3609 for (let i = 0; i < resizeIndex; i++) { 3610 newDimConfigs.push(resizeDimConfig(dimConfigs[i], pixelDims[i], clientDim)); 3611 } 3612 newDimConfigs.push(resizeDimConfig(dimConfigs[resizeIndex], resizeDim, clientDim)); 3613 const len = dimConfigs.length; 3614 let anyGrow = false; 3615 for (let i = resizeIndex + 1; i < len; i++) { 3616 const dimConfig = dimConfigs[i]; 3617 newDimConfigs.push(dimConfig); 3618 if (dimConfig.grow) { 3619 anyGrow = true; 3620 } 3621 } 3622 if (!anyGrow) { 3623 for (let i = resizeIndex + 1; i < len; i++) { 3624 newDimConfigs[i] = Object.assign({}, newDimConfigs[i], { grow: 1 }); 3625 } 3626 } 3627 return newDimConfigs; 3628 } 3629 function resizeDimConfig(dimConfig, newPixels, clientDim) { 3630 const { min } = dimConfig; 3631 newPixels = Math.max(min, newPixels); 3632 if (dimConfig.pixels) { 3633 return { pixels: newPixels, frac: 0, grow: 0, min }; 3634 } 3635 return { pixels: 0, frac: newPixels / clientDim, grow: 0, min }; 3636 } 3637 3638 const ROW_BORDER_WIDTH = 1; 3639 // Resource/Group Verticals 3640 // ------------------------------------------------------------------------------------------------- 3641 function computeHeights(siblingNodes, getEntityKey, getEntityHeight, minHeight) { 3642 const heightMap = new Map(); 3643 let [totalHeight, expandableCount] = computeTightHeights(siblingNodes, getEntityKey, getEntityHeight, heightMap); 3644 if (minHeight != null && minHeight > totalHeight) { 3645 expandHeights(siblingNodes, getEntityKey, heightMap, (minHeight - totalHeight) / expandableCount); 3646 totalHeight = minHeight; 3647 } 3648 return [heightMap, totalHeight]; 3649 } 3650 function computeTightHeights(siblingNodes, getEntityKey, getEntityHeight, heightMap) { 3651 let totalHeight = 0; 3652 let expandableCount = 0; 3653 for (const siblingNode of siblingNodes) { 3654 const entityKey = getEntityKey(siblingNode.entity); 3655 let ownHeight = getEntityHeight(entityKey) || 0; 3656 const [childrenHeight, childrenExpandableCount] = computeTightHeights(siblingNode.children, getEntityKey, getEntityHeight, heightMap); 3657 if (siblingNode.pooledHeight) { // 'own' is side-by-side with children
3658 if (ownHeight > childrenHeight) { 3659 // children are smaller. grow them 3660 expandHeights(siblingNode.children, getEntityKey, heightMap, (ownHeight - childrenHeight) / childrenExpandableCount); 3661 } 3662 else { 3663 // own-element is smaller. grow to height of children 3664 ownHeight = childrenHeight; 3665 } 3666 totalHeight += ownHeight; 3667 } 3668 else { // 'own' is above children 3669 totalHeight += ownHeight + ROW_BORDER_WIDTH + childrenHeight; 3670 } 3671 heightMap.set(entityKey, ownHeight); 3672 // expandable? 3673 // vertically stacked, and not a group 3674 if (siblingNode.pooledHeight === undefined) { 3675 expandableCount++; 3676 } 3677 expandableCount += childrenExpandableCount; 3678 } 3679 return [ 3680 totalHeight + ROW_BORDER_WIDTH * (siblingNodes.length - 1), 3681 expandableCount, 3682 ]; 3683 } 3684 function expandHeights(siblingNodes, getEntityKey, heightMap, expansion) { 3685 for (const siblingNode of siblingNodes) { 3686 // expandable? 3687 // vertically stacked, and not a group 3688 if (siblingNode.pooledHeight === undefined) { 3689 const entityKey = getEntityKey(siblingNode.entity); 3690 heightMap.set(entityKey, heightMap.get(entityKey) + expansion); 3691 } 3692 expandHeights(siblingNode.children, getEntityKey, heightMap, expansion); 3693 } 3694 } 3695 function computeTopsFromHeights(siblingNodes, getEntityKey, heightMap) { 3696 const topMap = new Map(); 3697 computeTopsStartingAt(siblingNodes, getEntityKey, heightMap, topMap, 0); 3698 return topMap; 3699 } 3700 function computeTopsStartingAt(siblingNodes, getEntityKey, heightMap, topMap, top) { 3701 for (let i = 0; i < siblingNodes.length; i++) { 3702 const siblingNode = siblingNodes[i]; 3703 const entityKey = getEntityKey(siblingNode.entity); 3704 topMap.set(entityKey, top); 3705 if (!siblingNode.pooledHeight) { 3706 top += heightMap.get(entityKey) + ROW_BORDER_WIDTH; 3707 } 3708 top = computeTopsStartingAt(siblingNode.children, getEntityKey, heightMap, topMap, top); 3709 } 3710 return top; 3711 } 3712 function findEntityByCoord(siblingNodes, entityTops, // keyed by createEntityId 3713 entityHeights, // keyed by createEntityId 3714 coord, // assumed >= 0 3715 createEntityId) { 3716 for (const siblingNode of siblingNodes) { 3717 const entityKey = createEntityId(siblingNode.entity); 3718 const siblingOwnTop = entityTops.get(entityKey); 3719 const siblingOwnHeight = entityHeights.get(entityKey); 3720 // intersection of the sibling's own element? 3721 if (siblingOwnTop + siblingOwnHeight > coord) { 3722 if (siblingNode.pooledHeight) { 3723 // need more specific result 3724 return findEntityByCoord(siblingNode.children, entityTops, entityHeights, coord, createEntityId); 3725 } 3726 else { 3727 return [siblingNode.entity, siblingOwnTop, siblingOwnHeight]; 3728 } 3729 } 3730 else if (!siblingNode.pooledHeight) { 3731 // search siblings below 3732 const childrenRes = findEntityByCoord(siblingNode.children, entityTops, entityHeights, coord, createEntityId); 3733 if (childrenRes) { 3734 return childrenRes; 3735 } 3736 } 3737 } 3738 } 3739 3740 /* 3741 A "layout" wraps the resource/group entity with presentation information (indent, isExpanded, etc) 3742 Creates a layout hierarchy (`layouts`) and flattened projections for each row type and all rows. 3743 Filters away not-expanded nodes. 3744 3745 TODO: even when a row is collapsed, consistent global rowIndex 3746 */
3747 function buildResourceLayouts(hierarchy, hasNesting, expansions, expansionDefault) { 3748 const flatResourceRowLayouts = []; 3749 const flatGroupRowLayouts = []; 3750 const flatGroupColLayouts = []; 3751 const flatRowLayouts = []; 3752 function processNodes(nodes, startingIndex, startingVisibleIndex, depth, rowDepth, indent, isVisible) { 3753 const layouts = []; 3754 let totalCnt = 0; // visible or not 3755 let visibleCnt = 0; 3756 // TODO: more DRY within 3757 for (const node of nodes) { 3758 // ResourceRow 3759 if (node.resourceFields) { 3760 const isExpanded = expansions[node.entity.id] ?? expansionDefault; 3761 const rowIndex = startingIndex + totalCnt++; 3762 const visibleIndex = startingVisibleIndex + visibleCnt++; 3763 const resourceLayout = { 3764 rowIndex, 3765 visibleIndex, 3766 entity: node.entity, 3767 resourceFields: node.resourceFields, 3768 isExpanded, 3769 hasChildren: Boolean(node.children.length), 3770 depth, 3771 rowDepth, 3772 indent, 3773 children: [], 3774 }; 3775 if (isVisible) { 3776 flatRowLayouts.push(resourceLayout); 3777 flatResourceRowLayouts.push(resourceLayout); 3778 layouts.push(resourceLayout); 3779 } 3780 const [localChildren, subtreeTotalCnt, subtreeVisibleCnt] = processNodes(node.children, startingIndex + totalCnt, startingVisibleIndex + visibleCnt, depth + 1, rowDepth + 1, indent + 1, isVisible && isExpanded); 3781 if (isVisible && isExpanded) { 3782 resourceLayout.children = localChildren; 3783 visibleCnt += subtreeVisibleCnt; 3784 } 3785 totalCnt += subtreeTotalCnt; 3786 // GroupCell 3787 } 3788 else if (node.pooledHeight) { 3789 const rowIndex = startingIndex + totalCnt; // DON'T advance 3790 const visibleIndex = startingVisibleIndex + visibleCnt; // DON'T advance 3791 const groupCellLayout = { 3792 rowIndex, 3793 visibleIndex, 3794 rowSpan: 0, // populated very soon... 3795 entity: node.entity, 3796 pooledHeight: true, 3797 children: [], // populates very soon... 3798 }; 3799 if (isVisible) { 3800 (flatGroupColLayouts[depth] || (flatGroupColLayouts[depth] = [])) 3801 .push(groupCellLayout); 3802 layouts.push(groupCellLayout); 3803 } 3804 const [localChildren, subtreeTotalCnt, subtreeVisibleCnt] = processNodes(node.children, startingIndex + totalCnt, startingVisibleIndex + visibleCnt, depth + 1, rowDepth, // do NOT advance 3805 indent, isVisible); 3806 if (isVisible) { 3807 groupCellLayout.children = localChildren; 3808 groupCellLayout.rowSpan = subtreeTotalCnt; 3809 visibleCnt += subtreeVisibleCnt; 3810 } 3811 totalCnt += subtreeTotalCnt; 3812 // GroupRow 3813 } 3814 else { 3815 const isExpanded = expansions[createGroupId(node.entity)] ?? expansionDefault; 3816 const rowIndex = startingIndex + totalCnt++; 3817 const visibleIndex = startingVisibleIndex + visibleCnt++; 3818 const groupRowLayout = { 3819 rowIndex, 3820 visibleIndex, 3821 entity: node.entity, 3822 pooledHeight: false, 3823 isExpanded, 3824 hasChildren: Boolean(node.children.length), 3825 depth, 3826 rowDepth, 3827 indent, 3828 children: [], // populates very soon... 3829 }; 3830 if (isVisible) { 3831 flatRowLayouts.push(groupRowLayout); 3832 flatGroupRowLayouts.push(groupRowLayout); 3833 layouts.push(groupRowLayout); 3834 } 3835 const [localChildren, subtreeTotalCnt, subtreeVisibleCnt]
3835= processNodes(node.children, startingIndex + totalCnt, startingVisibleIndex + visibleCnt, depth + 1, rowDepth + 1, indent + 1, isVisible && isExpanded); 3836 if (isVisible && isExpanded) { 3837 groupRowLayout.children = localChildren; 3838 visibleCnt += subtreeVisibleCnt; 3839 } 3840 totalCnt += subtreeTotalCnt; 3841 } 3842 } 3843 return [layouts, totalCnt, visibleCnt]; 3844 } 3845 const [layouts, totalCnt] = processNodes(hierarchy, 0, 0, 0, 0, hasNesting ? 1 : 0, true); 3846 return { 3847 layouts, 3848 flatRowLayouts, 3849 flatResourceRowLayouts, 3850 flatGroupRowLayouts, 3851 flatGroupColLayouts, 3852 totalCnt, 3853 }; 3854 } 3855 3856 function buildAriaCellId(prefix, rowIndex, colIndex) { 3857 return `${prefix}-${rowIndex}-${colIndex}`; 3858 } 3859 // Derives the cell's DOM id (referenced by proxy-row aria-owns) and aria-colindex from the same 3860 // (prefix, rowIndex, colIndex) source, so the id and column attributes can never disagree. 3861 function buildAriaCellAttrs(props) { 3862 return { 3863 id: (props.cellIdPrefix && props.cellRowIndex != null && props.cellColIndex != null) 3864 ? buildAriaCellId(props.cellIdPrefix, props.cellRowIndex, props.cellColIndex) 3865 : undefined, 3866 'aria-colindex': props.cellColIndex != null ? 1 + props.cellColIndex : undefined, 3867 }; 3868 } 3869 // Renders visually hidden proxy rows that own cells split across the spreadsheet and timeline panes. 3870 // The grid declares aria-colcount and every owned cell declares aria-colindex. Inference from the 3871 // owned-cell order alone would misnumber rows covered by a vertically spanning group-column cell: 3872 // only the first covered row owns the spanning cell, so later covered rows' first owned cell would 3873 // be inferred as column 1 unless the AT applies aria-rowspan across proxy rows (unreliable). 3874 function AriaProxyRows(props) { 3875 const { cellIdPrefix, bodyRows, resourceColCnt, superHeaderRowSpan, hasNesting, } = props; 3876 const totalHeaderRowSpan = 1 + superHeaderRowSpan; 3877 const resourceHeaderCellIds = Array.from({ length: resourceColCnt }, (_value, colIndex) => { 3878 return buildAriaCellId(cellIdPrefix, 1 + superHeaderRowSpan, colIndex); 3879 }); 3880 const timelineHeaderCellId = buildAriaCellId(cellIdPrefix, 1, resourceColCnt); 3881 return (jsxRuntime.jsxs("div", { role: 'presentation', style: { position: 'absolute', width: 0, height: 0, overflow: 'hidden' }, children: [jsxRuntime.jsx("div", { role: 'row', "aria-rowindex": 1, "aria-owns": superHeaderRowSpan 3882 ? `${buildAriaCellId(cellIdPrefix, 1, 0)} ${timelineHeaderCellId}` 3883 : [...resourceHeaderCellIds, timelineHeaderCellId].join(' ') }), Boolean(superHeaderRowSpan) && (jsxRuntime.jsx("div", { role: 'row', "aria-rowindex": 2, "aria-owns": resourceHeaderCellIds.join(' ') })), bodyRows.map((ariaRow) => { 3884 const rowLayout = ariaRow.layout; 3885 const rowIndex = 1 + totalHeaderRowSpan + rowLayout.rowIndex; 3886 return (jsxRuntime.jsx("div", { role: 'row', "aria-rowindex": rowIndex, "aria-level": hasNesting ? 1 + rowLayout.rowDepth : undefined, "aria-expanded": isEntityGroup(rowLayout.entity) || rowLayout.hasChildren 3887 ? rowLayout.isExpanded 3888 : undefined, "aria-owns": Array.from(ariaRow.colIndices) 3889 .sort((colIndex0, colIndex1) => colIndex0 - colIndex1) 3890 .map((colIndex) => buildAriaCellId(cellIdPrefix, rowIndex, colIndex)) 3891 .join(' ') }, rowIndex)); 3892 })] })); 3893 } 3894 // Builds proxy body-row ownership from the independently virtualized cell sources. 3895 // Only mounted cells are included, and cells from different panes accumulate on the same logical row. 3896 function buildAriaBodyRows(flatRowLayouts, groupColPositions, rowPositions, groupColCnt, resourceColCnt) { 3897 const ariaBodyRowMap = new Map(); 3898 // Merge a mounted cell into its logical owner row while avoiding duplicate column references. 3899 const addCell = (rowLayout, colIndex) => { 3900 let ariaRow = ariaBodyRowMap.get(rowLayout.visibleIndex); 3901 if (!ariaRow) { 3902 ariaRow = { 3903 layout: rowLayout, 3904 colIndices: new Set(), 3905 }; 3906 ariaBodyRowMap.set(rowLayout.visibleIndex, ariaRow); 3907 } 3908 ariaRow.colIndices.add(colIndex); 3909 }; 3910 // A vertically spanning group-column cell belongs to the first logical row it covers, 3911 // mirroring HTML rowspan, where the spanned cell is a child of the first covered <tr>. 3912 // Because the cell and the rows are virtualized independently, the owner row might be outside 3913 // rowPositions while the spanning cell itself intersects the viewport (the cell covers later, 3914 // mounted rows). We still emit a proxy row for that unmounted owner — it will own only the 3915 // group cell, lacking the resource's naming rowheader and lane — because the alternative is
3916 // worse: a mounted cell owned by no row surfaces as a stray direct child of the grid, 3917 // breaking the table structure for assistive technology. 3918 for (let colIndex = 0; colIndex < groupColPositions.length; colIndex += 1) { 3919 for (const groupCellPosition of groupColPositions[colIndex]) { 3920 addCell(flatRowLayouts[groupCellPosition.item.visibleIndex], colIndex); 3921 } 3922 } 3923 // Add cells rendered once per mounted logical row. Group rows have one spreadsheet cell spanning 3924 // the resource columns; resource rows have one cell per non-group spreadsheet column. 3925 for (const rowPosition of rowPositions) { 3926 const rowLayout = rowPosition.item; 3927 if (isEntityGroup(rowLayout.entity)) { 3928 addCell(rowLayout, 0); 3929 } 3930 else { 3931 for (let colIndex = groupColCnt; colIndex < resourceColCnt; colIndex += 1) { 3932 addCell(rowLayout, colIndex); 3933 } 3934 } 3935 // The timeline lane is the final column for both group and resource rows. 3936 addCell(rowLayout, resourceColCnt); 3937 } 3938 // Collection begins with column-group cells, so map insertion order is not necessarily visual order. 3939 // Proxy-row DOM order must match the visual vertical order. 3940 return Array.from(ariaBodyRowMap.values()).sort((row0, row1) => { 3941 return row0.layout.visibleIndex - row1.layout.visibleIndex; 3942 }); 3943 } 3944 3945 /* 3946 parallels the ResourceGroupHeaderSubrow 3947 */ 3948 class GroupLane extends protectedApi.BaseComponent { 3949 constructor() { 3950 super(...arguments); 3951 // ref 3952 this.innerElRef = compat.createRef(); 3953 } 3954 render() { 3955 let { props, context } = this; 3956 let { group } = props; 3957 let groupSpec = group.spec; // type HACK 3958 let renderProps = { 3959 fieldValue: group.value, 3960 view: context.viewApi, 3961 }; 3962 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", attrs: { 3963 ...buildAriaCellAttrs(props), 3964 role: 'gridcell', 3965 }, className: publicApi.joinClassNames(classNames__default["default"].liquid, // expand to whole row 3966 classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, classNames__default["default"].contentBox, props.borderBottom ? classNames__default["default"].borderOnlyB : classNames__default["default"].borderNone), style: { 3967 height: props.height, 3968 }, renderProps: renderProps, generatorName: "resourceGroupLaneContent", customGenerator: groupSpec.laneContent, classNameGenerator: groupSpec.laneClass, didMount: groupSpec.laneDidMount, willUnmount: groupSpec.laneWillUnmount, children: (InnerContainer) => (jsxRuntime.jsx(InnerContainer, { tag: "div", elRef: this.innerElRef, className: publicApi.joinClassNames(protectedApi.generateClassName(groupSpec.laneInnerClass, renderProps), classNames__default["default"].noShrink, classNames__default["default"].noMargin) })) })); 3969 } 3970 componentDidMount() { 3971 this._isUnmounting = false; 3972 const innerEl = this.innerElRef.current; 3973 this.disconnectInnerHeight = protectedApi.watchHeight(innerEl, (height) => { 3974 if (this._isUnmounting) 3975 return; 3976 protectedApi.setRef(this.props.innerHeightRef, height); 3977 }); 3978 } 3979 componentWillUnmount() { 3980 this._isUnmounting = true; 3981 this.disconnectInnerHeight(); 3982 protectedApi.setRef(this.props.innerHeightRef, null); 3983 } 3984 } 3985 3986 class ResourceLane extends protectedApi.BaseComponent { 3987 constructor() { 3988 super(...arguments); 3989 // memo 3990 this.refineRenderProps = protectedApi.memoizeObjArg(refineRenderProps$1); 3991 this.slicer = new TimelineLaneSlicer(); 3992 this.handleEventsHeight = (eventsHeight) => { 3993 if (this._isUnmounting) 3994 return; 3995 this.eventsHeight = eventsHeight; 3996 protectedApi.afterSize(this.handleHeight); 3997 }; 3998 this.handleTopEl = (topEl) => { 3999 if (this.disconnectTopHeight) { 4000 this.disconnectTopHeight(); 4001 this.disconnectTopHeight = undefined; 4002 } 4003 if (topEl) { 4004 this.disconnectTopHeight = protectedApi.watchHeight(topEl, (topHeight) => { 4005 if (this._isUnmounting) 4006 return; 4007 this.topHeight = topHeight; 4008 protectedApi.afterSize(this.handleHeight); 4009 }); 4010 } 4011 }; 4012 this.handleBottomEl = (footerEl) => { 4013 if (this.disconnectBottomHeight) { 4014 this.disconnectBottomHeight(); 4015 this.disconnectBottomHeight = undefined; 4016 } 4017 if (footerEl) { 4018 this.disconnectBottomHeight = protectedApi.watchHeight(footerEl, (bottomHeight) => { 4019 if (this._isUnmounting) 4020 return; 4021 this.bottomHeight = bottomHeight; 4022 protectedApi.afterSize(this.handleHeight); 4023 }); 4024 } 4025 }; 4026 this.handleHeight = () => { 4027 if (this._isUnmounting) 4028 return; 4029 const { topHeight, bottomHeight, eventsHeight } = this; 4030 if (topHeight != null && bottomHeight != null && eventsHeight != null) { 4031 protectedApi.setRef(this.props.heightRef, topHeight + bottomHeight + eventsHeight); 4032 } 4033 }; 4034 } 4035 render() { 4036 let { props, context } = this; 4037 let { resource }
4037 = props; 4038 let { options } = context; 4039 let renderProps = this.refineRenderProps({ 4040 resource, 4041 context, 4042 eventOverlap: Boolean(options.eventOverlap), 4043 }); 4044 /* sliced */ 4045 let slicedProps = this.slicer.sliceProps(props, props.dateProfile, props.tDateProfile.isTimeScale ? null : options.nextDayThreshold, context, // wish we didn't have to pass in the rest of the args... 4046 props.dateProfile, context.dateProfileGenerator, props.tDateProfile, context.dateEnv); 4047 return (jsxRuntime.jsx("div", { role: props.role, className: publicApi.joinClassNames(props.className, // probably contains fillX 4048 classNames__default["default"].flexRow), style: { 4049 top: props.top, 4050 width: props.width, 4051 insetInlineStart: props.insetInlineStart ?? props.clipStart, 4052 }, children: jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", attrs: { 4053 ...buildAriaCellAttrs(props), 4054 role: 'gridcell', 4055 'data-resource-id': resource.id, 4056 }, className: publicApi.joinClassNames(classNames__default["default"].liquid, classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, classNames__default["default"].contentBox, props.borderBottom ? classNames__default["default"].borderOnlyB : classNames__default["default"].borderNone, classNames__default["default"].rel), style: { 4057 height: props.height, 4058 }, renderProps: renderProps, generatorName: undefined, classNameGenerator: options.resourceLaneClass, didMount: options.resourceLaneDidMount, willUnmount: options.resourceLaneWillUnmount, children: () => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(TimelineBg, { tDateProfile: props.tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, 4059 // content 4060 bgEventSegs: slicedProps.bgEventSegs, businessHourSegs: slicedProps.businessHourSegs, dateSelectionSegs: slicedProps.dateSelectionSegs, eventResizeSegs: slicedProps.eventResize ? slicedProps.eventResize.segs : null, 4061 // dimensions 4062 slotWidth: props.slotWidth, 4063 // virtualization 4064 clipStart: props.clipStart, clipEnd: props.clipEnd }), jsxRuntime.jsx(protectedApi.ContentContainer, { tag: 'div', elRef: this.handleTopEl, className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noShrink), renderProps: renderProps, generatorName: 'resourceLaneTopContent', customGenerator: options.resourceLaneTopContent, classNameGenerator: options.resourceLaneTopClass }), jsxRuntime.jsx(TimelineFg, { dateProfile: props.dateProfile, tDateProfile: props.tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, 4065 // content 4066 fgEventSegs: slicedProps.fgEventSegs, eventDrag: slicedProps.eventDrag, eventResize: slicedProps.eventResize, eventSelection: slicedProps.eventSelection, resourceId: resource.id, 4067 // dimensions 4068 slotWidth: props.slotWidth, 4069 // virtualization 4070 clipStart: props.clipStart, clipEnd: props.clipEnd, 4071 // ref 4072 heightRef: this.handleEventsHeight }), jsxRuntime.jsx(protectedApi.ContentContainer, { tag: 'div', elRef: this.handleBottomEl, className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noShrink), renderProps: renderProps, generatorName: 'resourceLaneBottomContent', customGenerator: options.resourceLaneBottomContent, classNameGenerator: options.resourceLaneBottomClass })] })) }) })); 4073 } 4074 componentDidMount() { 4075 this._isUnmounting = false; 4076 } 4077 componentWillUnmount() { 4078 this._isUnmounting = true; 4079 } 4080 } 4081 function refineRenderProps$1(input) { 4082 return { 4083 resource: new ResourceApi(input.context, input.resource), 4084 options: { eventOverlap: input.eventOverlap }, 4085 }; 4086 } 4087 4088 const MIN_RESOURCE_AREA_WIDTH = 30; // definitely bigger than scrollbars 4089 class ResizableTwoCol extends protectedApi.BaseComponent { 4090 constructor() { 4091 super(...arguments); 4092 this.state = {}; 4093 // memo 4094 this.parseWidthConfig = protectedApi.memoize(parseDimConfig); 4095 // ref 4096 this.rootEl = null; 4097 this.startElRef = compat.createRef(); 4098 this.resizerElRef = compat.createRef(); 4099 this.handleRootEl = (el) => { 4100 this.rootEl = el; 4101 if (this.props.elRef) { 4102 protectedApi.setRef(this.props.elRef, el); 4103 } 4104 }; 4105 } 4106 render() { 4107 const { props, state, context } = this; 4108 const { options } = context; 4109 const initialWidthConfig = this.parseWidthConfig(props.initialStartWidth, MIN_RESOURCE_AREA_WIDTH); 4110 const widthConfig = this.widthConfig = state.widthOverride || initialWidthConfig; 4111 return (jsxRuntime.jsxs("div", { ref: this.handleRootEl, className: publicApi.joinClassNames(props.className, classNames__default["default"].flexRow), children: [jsxRuntime.jsx("div", { className: props.startClassName, style: { 4112 width: serializeDimConfig(widthConfig), 4113 }, ref: this.startElRef, children: props.startContent }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.resourceColumnDividerClass, classNames__default["default"].rel), 4114 // TODO: make a className somehow?... 4115 // TODO: what if not resizable? 4116 style: { cursor: 'col-resize' }, ref: this.resizerElRef, children: jsxRuntime.jsx("div", { className: classNames__default["default"].hitXSkinny, style: { zIndex: 1 } }) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(props.endClassName, classNames__default["default"].liquid
4116), children: props.endContent })] })); 4117 } 4118 componentDidMount() { 4119 const ElementDraggingImpl = this.context.pluginHooks.elementDraggingImpl; 4120 if (ElementDraggingImpl) { 4121 const resizerEl = this.resizerElRef.current; 4122 const dragging = this.resizerDragging = new ElementDraggingImpl(resizerEl); 4123 dragging.emitter.on('dragstart', () => { 4124 const isRtl = protectedApi.computeElIsRtl(resizerEl); 4125 const viewWidth = this.rootEl.getBoundingClientRect().width; 4126 const origWidth = this.startElRef.current.getBoundingClientRect().width; 4127 const origWidthConfig = this.widthConfig; 4128 let newWidthConfig; 4129 dragging.emitter.on('dragmove', (pev) => { 4130 const newWidth = Math.min(origWidth + pev.deltaX * (isRtl ? -1 : 1), viewWidth); 4131 newWidthConfig = resizeDimConfig(origWidthConfig, newWidth, viewWidth); 4132 this.setState({ 4133 widthOverride: newWidthConfig, 4134 }); 4135 }); 4136 dragging.emitter.on('dragend', () => { 4137 if (newWidthConfig) { 4138 protectedApi.setRef(this.props.resizedWidthRef, serializeDimConfig(newWidthConfig)); 4139 } 4140 }); 4141 dragging.setAutoScrollEnabled(false); // because gets weird with auto-scrolling time area 4142 }); 4143 } 4144 } 4145 componentWillUnmount() { 4146 if (this.resizerDragging) { 4147 this.resizerDragging.destroy(); 4148 } 4149 } 4150 } 4151 4152 /* 4153 Group cell that spans vertically, consuming multiple rowspan 4154 */ 4155 class ResourceGroupSubrow extends protectedApi.BaseComponent { 4156 constructor() { 4157 super(...arguments); 4158 // ref 4159 this.innerElRef = compat.createRef(); 4160 } 4161 render() { 4162 let { props, context } = this; 4163 let { options } = context; 4164 let { colSpec } = props; 4165 let renderProps = { 4166 fieldValue: props.fieldValue, 4167 view: context.viewApi, 4168 }; 4169 // a grouped cell. no data that is specific to this specific resource 4170 // `colSpec` is for the group. a GroupSpec :( 4171 return (jsxRuntime.jsx("div", { role: props.role, className: publicApi.joinClassNames(options.resourceRowClass, props.className, classNames__default["default"].flexRow, props.borderBottom ? classNames__default["default"].borderOnlyB : classNames__default["default"].borderNone), style: { 4172 top: props.top, 4173 height: props.height, 4174 minWidth: 0, 4175 width: props.width, 4176 flexGrow: props.grow, 4177 }, children: jsxRuntime.jsx(protectedApi.ContentContainer // the "cell" 4178 , { tag: "div", attrs: { 4179 ...buildAriaCellAttrs(props), 4180 role: 'rowheader', 4181 'aria-rowspan': props.rowSpan, 4182 }, className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, classNames__default["default"].alignStart, classNames__default["default"].liquid, // liquid-height 4183 props.borderStart ? classNames__default["default"].borderOnlyS : classNames__default["default"].borderNone), renderProps: renderProps, generatorName: "resourceCellContent", customGenerator: colSpec.cellContent, defaultGenerator: renderGroupInner, classNameGenerator: colSpec.cellClass, didMount: colSpec.cellDidMount, willUnmount: colSpec.cellWillUnmount, children: (InnerContent) => (jsxRuntime.jsx("div", { ref: this.innerElRef, className: publicApi.joinClassNames(classNames__default["default"].noShrink, classNames__default["default"].whiteSpaceNoWrap, classNames__default["default"].flexRow, // mimics what ResourceCell does 4184 classNames__default["default"].stickyT), children: jsxRuntime.jsx(InnerContent // the "inner" 4185 , { tag: "div", className: protectedApi.generateClassName(colSpec.cellInnerClass, renderProps) }) })) }) })); 4186 } 4187 componentDidMount() { 4188 this._isUnmounting = false; 4189 const innerEl = this.innerElRef.current; 4190 this.disconnectInnerHeight = protectedApi.watchHeight(innerEl, (height) => { 4191 if (this._isUnmounting) 4192 return; 4193 protectedApi.setRef(this.props.innerHeightRef, height); 4194 }); 4195 } 4196 componentWillUnmount() { 4197 this._isUnmounting = true; 4198 this.disconnectInnerHeight(); 4199 protectedApi.setRef(this.props.innerHeightRef, null); 4200 } 4201 } 4202 function renderGroupInner(renderProps) { 4203 return renderProps.fieldValue || jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0" }); 4204 } 4205 4206 class ResourceIndent extends protectedApi.BaseComponent { 4207 render() { 4208 const { props, context } = this; 4209 return (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(context.options.resourceIndentClass, classNames__default["default"].noShrink, classNames__default["default"].flexCol, classNames__default["default"].alignEnd), style: { 4210 ...props.style, 4211 width: props.indentWidth != null 4212 ? props.indentWidth * props.level 4213 : 0 4214 }, children: props.children })); 4215 } 4216 } 4217 4218 class ResourceExpander extends protectedApi.BaseComponent { 4219 render() { 4220 const { props } = this; 4221 const { options } = this.context; 4222 const classNameGenerator = options.resourceExpanderClass; 4223 const contentGenerator = options.resourceExpanderContent; 4224 const renderProps = { 4225 isExpanded: props.isExpanded, 4226 }; 4227 return (jsxRuntime.jsx("span", { "aria-hidden": true, className: publicApi.joinClassNames(protectedApi.generateClassName(classNameGenerator, renderProps), classNames__default["default"].flexRow, classNames__default["default"].cursorPointer, classNames__default["default"].selectNone, // prevent text-selecting cell underneath 4228 classNames__default["default"].noMargin, props.className), onClick: props.onExpanderClick, ref: props.elRef, children: contentGenerator && (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: 'span', style: { display: 'contents' }, attrs: { 'aria-hidden': true }, renderProps: renderProps, generatorName: undefined, customGenerator: contentGenerator })) })); 4229 } 4230 } 4231 4232 /* 4233 Group cell that spans horizontally, consuming multiple colspans 4234 */ 4235 class ResourceGroupHeaderSubrow extends protectedApi.BaseComponent { 4236 constructor() { 4237 super(...arguments); 4238 // ref 4239 this.innerElRef = compat.createRef(); 4240 this.onExpanderClick = () => { 4241 let { props } = this; 4242 this.context.dispatch({ 4243 type: 'SET_RESOURCE_ENTITY_EXPANDED', 4244 id: createGroupId(props.group), 4245 isExpanded: !props.isExpanded, 4246 }); 4247 }; 4248 } 4249 render() { 4250 let { props, context } = this; 4251 let renderProps = { 4252 fieldValue: props.group.value, 4253 view: context.viewApi, 4254 }; 4255 let spec = props.group.spec; // type HACK 4256 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", attrs: { 4257 ...buildAriaCellAttrs(props), 4258 role: 'rowheader', 4259 'aria-colspan': props.colSpan, 4260 }, className: publicApi.joinClassNames(classNames__default["default"].liquid, // expand to whole row 4261 classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, classNames__default["default"].alignStart, // h-align 4262 classNames__default["default"].crop, classNames__default["default"].contentBox, props.borderBottom ? classNames__default["default"].borderOnlyB : classNames__default["default"].borderNone), style: { 4263 height: props.height, 4264 }, renderProps: renderProps, generatorName: "resourceGroupHeaderContent", customGenerator: spec.labelContent, defaultGenerator: renderCellInner, classNameGenerator: spec.labelClass, didMount: spec.labelDidMount, willUnmount: spec.labelWillUnmount, children: (InnerContent) => (jsxRuntime.jsxs("div", { ref: this.innerElRef, className: publicApi.joinClassNames(classNames__default["default"].noShrink, classNames__default["default"].whiteSpaceNoWrap, classNames__default["default"].flexRow), style: { 4265 isolation: 'isolate', // TODO: className 4266 }, children: [jsxRuntime.jsx(ResourceIndent, { level: 1, indentWidth: props.indentWidth, style: { zIndex: 2 }, children: jsxRuntime.jsx(ResourceExpander, { isExpanded: props.isExpanded, onExpanderClick: this.onExpanderClick }) }), jsxRuntime.jsx(InnerContent, { tag: 'div', className: protectedApi.generateClassName(spec.labelInnerClass, renderProps), style: { zIndex: 1 } })] })) })); 4267 } 4268 componentDidMount() { 4269 this._isUnmounting = false; 4270 const innerEl = this.innerElRef.current; 4271 this.disconnectInnerHeight = protectedApi.watchHeight(innerEl, (height) => { 4272 if (this._isUnmounting) 4273 return; 4274 protectedApi.setRef(this.props.innerHeightRef, height); 4275 }); 4276 } 4277 componentWillUnmount() { 4278 this._isUnmounting = true; 4279 this.disconnectInnerHeight(); 4280 protectedApi.setRef(this.props.innerHeightRef, null); 4281 } 4282 } 4283 ResourceGroupHeaderSubrow.addPropsEquality({ 4284 group: isGroupsEqual, 4285 });
4286 function renderCellInner(renderProps) { 4287 return renderProps.fieldValue || jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0" }); 4288 } 4289 4290 class ResourceCell extends protectedApi.BaseComponent { 4291 constructor() { 4292 super(...arguments); 4293 this.innerElRef = compat.createRef(); 4294 this.refineRenderProps = protectedApi.memoizeObjArg(refineRenderProps); 4295 this.onExpanderClick = (ev) => { 4296 let { props } = this; 4297 if (props.hasChildren) { 4298 this.context.dispatch({ 4299 type: 'SET_RESOURCE_ENTITY_EXPANDED', 4300 id: props.resource.id, 4301 isExpanded: !props.isExpanded, 4302 }); 4303 } 4304 }; 4305 } 4306 render() { 4307 let { props, context } = this; 4308 let { colSpec } = props; 4309 let renderProps = this.refineRenderProps({ 4310 resource: props.resource, 4311 field: props.field, 4312 fieldValue: props.fieldValue, 4313 context, 4314 }); 4315 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", attrs: { 4316 ...buildAriaCellAttrs(props), 4317 role: colSpec.isMain ? 'rowheader' : 'gridcell', 4318 'data-resource-id': props.resource.id, 4319 }, className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, classNames__default["default"].alignStart, props.borderStart ? classNames__default["default"].borderOnlyS : classNames__default["default"].borderNone, classNames__default["default"].crop), style: { 4320 minWidth: 0, 4321 width: props.width, 4322 flexGrow: props.grow, 4323 }, renderProps: renderProps, generatorName: 'resourceCellContent', customGenerator: colSpec.cellContent, defaultGenerator: renderResourceInner, classNameGenerator: colSpec.cellClass, didMount: colSpec.cellDidMount, willUnmount: colSpec.cellWillUnmount, children: (InnerContent) => (jsxRuntime.jsxs("div", { ref: this.innerElRef, className: publicApi.joinClassNames(classNames__default["default"].noShrink, classNames__default["default"].whiteSpaceNoWrap, classNames__default["default"].flexRow), style: { 4324 isolation: 'isolate', // TODO: className 4325 }, children: [colSpec.isMain && Boolean(props.indent) && (jsxRuntime.jsx(ResourceIndent, { level: props.indent, indentWidth: props.indentWidth, style: { zIndex: 2 }, children: props.hasChildren && (jsxRuntime.jsx(ResourceExpander, { isExpanded: props.isExpanded, onExpanderClick: this.onExpanderClick })) })), jsxRuntime.jsx(InnerContent, { tag: 'div', className: protectedApi.generateClassName(colSpec.cellInnerClass, renderProps), style: { zIndex: 1 } })] })) })); 4326 } 4327 componentDidMount() { 4328 this._isUnmounting = false; 4329 this.disconnectHeight = protectedApi.watchHeight(this.innerElRef.current, (height) => { 4330 if (this._isUnmounting) 4331 return; 4332 protectedApi.setRef(this.props.innerHeightRef, height); 4333 }); 4334 } 4335 componentWillUnmount() { 4336 this._isUnmounting = true; 4337 this.disconnectHeight(); 4338 protectedApi.setRef(this.props.innerHeightRef, null); 4339 } 4340 } 4341 function renderResourceInner(renderProps) { 4342 return renderProps.fieldValue || jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0" }); 4343 } 4344 function refineRenderProps(input) { 4345 return { 4346 resource: new ResourceApi(input.context, input.resource), 4347 field: input.field, 4348 fieldValue: input.fieldValue, 4349 view: input.context.viewApi, 4350 }; 4351 } 4352 4353 class ResourceSubrow extends protectedApi.BaseComponent { 4354 constructor() { 4355 super(...arguments); 4356 // refs 4357 this.innerHeightRefMap = new protectedApi.RefMap(() => { 4358 protectedApi.afterSize(this.handleInnerHeights); 4359 }); 4360 this.currentInnerHeight = null; 4361 this.handleInnerHeights = () => { 4362 if (this._isUnmounting) 4363 return; 4364 const innerHeightMap = this.innerHeightRefMap.current; 4365 let max = null; 4366 for (const innerHeight of innerHeightMap.values()) { 4367 if (max == null || innerHeight > max) {
4368 max = innerHeight; 4369 } 4370 } 4371 if (this.currentInnerHeight !== max) { 4372 this.currentInnerHeight = max; 4373 protectedApi.setRef(this.props.innerHeightRef, max); 4374 } 4375 }; 4376 } 4377 render() { 4378 const { props, innerHeightRefMap } = this; 4379 const { resource, resourceFields, colSpecs } = props; 4380 const { options } = this.context; 4381 const colWidths = props.colWidths || []; 4382 const colGrows = props.colGrows || []; 4383 let totalColWidth; 4384 let totalColGrow; 4385 if (props.totalX) { 4386 if (props.colWidths) { 4387 totalColWidth = totalColDims(props.colWidths, props.colStartIndex, colSpecs.length); 4388 } 4389 if (props.colGrows) { 4390 totalColGrow = totalColDims(props.colGrows, props.colStartIndex, colSpecs.length); 4391 } 4392 } 4393 return (jsxRuntime.jsx("div", { role: props.role, className: publicApi.joinClassNames(options.resourceRowClass, props.className, // what for??? 4394 classNames__default["default"].flexRow, props.borderBottom ? classNames__default["default"].borderOnlyB : classNames__default["default"].borderNone), style: { 4395 top: props.top, 4396 height: props.height, 4397 width: totalColWidth, 4398 flexGrow: totalColGrow, 4399 }, children: mapRange(props.colStartIndex, colSpecs.length, (i) => { 4400 const colSpec = colSpecs[i]; 4401 const fieldValue = colSpec.field ? resourceFields[colSpec.field] : 4402 (resource.title || getPublicId(resource.id)); 4403 return (jsxRuntime.jsx(ResourceCell, { cellIdPrefix: props.cellIdPrefix, cellRowIndex: props.cellRowIndex, cellColIndex: props.cellIdPrefix ? i : undefined, colSpec: colSpec, resource: resource, field: colSpec.field || 'title', fieldValue: fieldValue, indent: props.indent, hasChildren: props.hasChildren, isExpanded: props.isExpanded, innerHeightRef: innerHeightRefMap.createRef(i), width: colWidths[i], indentWidth: props.indentWidth, grow: colGrows[i], borderStart: props.borderStart || Boolean(i) }, i)); 4404 }) })); 4405 } 4406 componentDidMount() { 4407 this._isUnmounting = false; 4408 } 4409 componentWillUnmount() { 4410 this._isUnmounting = true; 4411 this.currentInnerHeight = null; 4412 } 4413 } 4414 ResourceSubrow.addPropsEquality({ 4415 colWidths: protectedApi.isArraysEqual, 4416 }); 4417 // Utils 4418 // ------------------------------------------------------------------------------------------------- 4419 function mapRange(start, end, func) { 4420 const items = []; 4421 for (let i = start; i < end; i++) { 4422 items.push(func(i)); 4423 } 4424 return items; 4425 } 4426 function totalColDims(colDims, startIndex, endIndex) { 4427 let total = 0; 4428 for (let i = startIndex; i < endIndex; i++) { 4429 total += colDims[i]; 4430 } 4431 return total; 4432 } 4433 4434 /* 4435 Takes up no vertical space. Fills itself within outer container. 4436 Caller is responsible for this. 4437 */ 4438 class BodySection extends protectedApi.BaseComponent { 4439 render() { 4440 const { props } = this; 4441 const { rowInnerHeightRefMap, headerRowSpan } = props; 4442 const groupColCnt = props.groupCellCnts.length; 4443 const colWidths = props.colWidths || []; 4444 return (jsxRuntime.jsxs("div", { className: classNames__default["default"].flexRow, style: { 4445 minWidth: props.canvasWidth, 4446 minHeight: props.canvasHeight, 4447 }, children: [props.groupColPositions.map((groupCellPositions, colIndex) => (jsxRuntime.jsx("div", { role: 'presentation', className: classNames__default["default"].rel /* origin for abs-positioned rows */, style: { 4448 height: 0, 4449 minWidth: 0, 4450 width: colWidths[colIndex], 4451 }, children: groupCellPositions.map((groupCellPosition, groupIndex) => { 4452 const groupCellLayout = groupCellPosition.item; 4453 const group = groupCellLayout.entity; 4454 const groupKey = groupCellPosition.key; 4455 const isNotLast = groupIndex < groupCellPositions.length - 1; 4456 const rowHeight = groupCellPosition.size; 4457 return (jsxRuntime.jsx(ResourceGroupSubrow, { colSpec: group.spec, rowSpan: groupCellLayout.rowSpan, fieldValue: group.value, className: classNames__default["default"].fillX, borderStart: Boolean(colIndex), borderBottom: isNotLast, role: 'presentation', cellIdPrefix: props.cellIdPrefix, cellRowIndex: 1 + headerRowSpan + groupCellLayout.rowIndex, cellColIndex: colIndex, innerHeightRef: rowInnerHeightRefMap.createRef(groupKey), top: groupCellPosition.start, height: (row
4457Height != null && isNotLast) 4458 ? rowHeight + ROW_BORDER_WIDTH // considering bottom border, which is added to cell 4459 : rowHeight }, groupKey)); 4460 }) }, colIndex))), jsxRuntime.jsxs("div", { role: 'presentation', className: classNames__default["default"].rel, style: { 4461 height: 0, 4462 minWidth: 0, 4463 width: sumArray(colWidths.slice(groupColCnt)), 4464 }, children: [props.groupRowPositions.map((rowPosition) => { 4465 const groupRowLayout = rowPosition.item; 4466 const group = groupRowLayout.entity; 4467 const groupKey = rowPosition.key; 4468 // Compare against every expansion-filtered row, not only the virtualized subset. 4469 const isNotLast = groupRowLayout.visibleIndex < props.displayedRowCnt - 1; 4470 return (jsxRuntime.jsx("div", { role: 'presentation', className: publicApi.joinClassNames(classNames__default["default"].fillX, classNames__default["default"].flexRow), style: { 4471 top: rowPosition.start, 4472 }, children: jsxRuntime.jsx(ResourceGroupHeaderSubrow, { cellIdPrefix: props.cellIdPrefix, cellRowIndex: 1 + headerRowSpan + groupRowLayout.rowIndex, cellColIndex: 0, group: group, isExpanded: groupRowLayout.isExpanded, innerHeightRef: rowInnerHeightRefMap.createRef(groupKey), colSpan: props.colSpecs.length, borderBottom: isNotLast, height: rowPosition.size, indentWidth: props.indentWidth, className: classNames__default["default"].fillX }) }, groupKey)); 4473 }), props.resourceRowPositions.map((rowPosition) => { 4474 const resourceLayout = rowPosition.item; 4475 const resource = resourceLayout.entity; 4476 const rowHeight = rowPosition.size; 4477 // Compare against every expansion-filtered row, not only the virtualized subset. 4478 const isNotLast = resourceLayout.visibleIndex < props.displayedRowCnt - 1; 4479 return (jsxRuntime.jsx(ResourceSubrow, { resource: resource, resourceFields: resourceLayout.resourceFields, indent: resourceLayout.indent, hasChildren: resourceLayout.hasChildren, isExpanded: resourceLayout.isExpanded, colStartIndex: groupColCnt, colSpecs: props.colSpecs, colWidths: colWidths, innerHeightRef: rowInnerHeightRefMap.createRef(resource.id), className: classNames__default["default"].fillX, borderStart: Boolean(groupColCnt), borderBottom: isNotLast, role: 'presentation', cellIdPrefix: props.cellIdPrefix, cellRowIndex: 1 + headerRowSpan + resourceLayout.rowIndex, top: rowPosition.start, height: (rowHeight != null && isNotLast) 4480 ? rowHeight + ROW_BORDER_WIDTH // considering bottom border, which is added to cell 4481 : rowHeight, indentWidth: props.indentWidth }, resource.id /* TODO: use rowPosition.key? */)); 4482 })] })] })); 4483 } 4484 } 4485 // Utils 4486 // ------------------------------------------------------------------------------------------------- 4487 function sumArray(a) { 4488 let sum = 0; 4489 for (const num of a) { 4490 sum += num; 4491 } 4492 return sum; 4493 } 4494 4495 class HeaderCell extends protectedApi.BaseComponent { 4496 constructor() { 4497 super(...arguments); 4498 // ref 4499 this.innerElRef = compat.createRef(); 4500 } 4501 render() { 4502 let { props, context } = this; 4503 let { colSpec } = props; 4504 let renderProps = { view: context.viewApi }; 4505 // need empty inner div for abs positioning for resizer 4506 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", attrs: { 4507 ...buildAriaCellAttrs(props), 4508 role: 'columnheader', 4509 }, className: publicApi.joinClassNames(classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, props.borderStart ? classNames__default["default"].borderOnlyS : classNames__default["default"].borderNone,
4509classNames__default["default"].rel), style: { 4510 minWidth: 0, 4511 width: props.width, 4512 flexGrow: props.grow, 4513 }, renderProps: renderProps, generatorName: "resourceColumnHeaderContent", customGenerator: colSpec.headerContent, defaultGenerator: colSpec.headerDefault, classNameGenerator: colSpec.headerClass, didMount: colSpec.headerDidMount, willUnmount: colSpec.headerWillUnmount, children: (InnerContent) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].crop, // must crop instead of outer 4514 classNames__default["default"].flexCol, classNames__default["default"].alignStart), children: jsxRuntime.jsxs("div", { ref: this.innerElRef, className: publicApi.joinClassNames(classNames__default["default"].noShrink, classNames__default["default"].whiteSpaceNoWrap, classNames__default["default"].flexRow), children: [this.props.indent && (jsxRuntime.jsx(ResourceIndent, { level: 1, indentWidth: props.indentWidth })), jsxRuntime.jsx(InnerContent, { tag: 'div', className: protectedApi.generateClassName(colSpec.headerInnerClass, renderProps) })] }) }), props.resizer && (jsxRuntime.jsx("div", { ref: props.resizerElRef, className: publicApi.joinClassNames(protectedApi.generateClassName(colSpec.headerResizerClass, renderProps), classNames__default["default"].z1, classNames__default["default"].cursorColResizer) }))] })) })); 4515 } 4516 componentDidMount() { 4517 this._isUnmounting = false; 4518 const innerEl = this.innerElRef.current; 4519 this.disconnectInnerHeight = protectedApi.watchHeight(innerEl, (height) => { 4520 if (this._isUnmounting) 4521 return; 4522 protectedApi.setRef(this.props.innerHeightRef, height); 4523 }); 4524 } 4525 componentWillUnmount() { 4526 this._isUnmounting = true; 4527 this.disconnectInnerHeight(); 4528 protectedApi.setRef(this.props.innerHeightRef, null); 4529 } 4530 } 4531 4532 class HeaderRow extends protectedApi.BaseComponent { 4533 constructor() { 4534 super(...arguments); 4535 // refs 4536 this.resizerElRefMap = new protectedApi.RefMap((resizerEl, index) => { 4537 let { colDraggings } = this; 4538 if (!resizerEl) { 4539 let dragging = colDraggings[index]; 4540 if (dragging) { 4541 dragging.destroy(); 4542 delete colDraggings[index]; 4543 } 4544 } 4545 else { 4546 let dragging = this.initColResizing(resizerEl, index); 4547 if (dragging) { 4548 colDraggings[index] = dragging; 4549 } 4550 } 4551 }); 4552 this.innerHeightRefMap = new protectedApi.RefMap(() => { 4553 protectedApi.afterSize(this.handleInnerHeights); 4554 }); 4555 this.colDraggings = {}; 4556 this.handleInnerHeights = () => { 4557 if (this._isUnmounting) 4558 return; 4559 const { innerHeightRefMap } = this; 4560 let max = 0; 4561 for (const innerHeight of innerHeightRefMap.current.values()) { 4562 max = Math.max(max, innerHeight); 4563 } 4564 if (this.currentInnerHeight !== max) { 4565 this.currentInnerHeight = max; 4566 protectedApi.setRef(this.props.innerHeightRef, max); 4567 } 4568 }; 4569 } 4570 render() { 4571 const { props, innerHeightRefMap, resizerElRefMap } = this; 4572 const { colSpecs } = props; 4573 const { options } = this.context; 4574 const colWidths = props.colWidths || []; 4575 const colGrows = props.colGrows || []; 4576 return (jsxRuntime.jsx("div", { role: props.role || 'row', "aria-rowindex": props.role !== 'presentation' && props.rowIndex != null ? 1 + props.rowIndex : undefined, className: publicApi.joinClassNames(options.resourceHeaderRowClass, classNames__default["default"].flexRow, classNames__default["default"].grow, classNames__default["default"].borderNone), children: colSpecs.map((colSpec, colIndex) => { 4577 return (jsxRuntime.jsx(HeaderCell, { cellIdPrefix: props.cellIdPrefix, cellRowIndex: props.rowIndex != null ? 1 + props.rowIndex : undefined, cellColIndex: props.cellIdPrefix ? colIndex : undefined, width: colWidths[colIndex], grow: colGrows[colIndex], colSpec: colSpec, resizer: colIndex < colSpecs.length - 1, indent: colSpec.isMain && props.indent, resizerElRef: resizerElRefMap.createRef(colIndex), innerHeightRef: innerHe
4577ightRefMap.createRef(colIndex), borderStart: Boolean(colIndex), indentWidth: props.indentWidth }, colIndex)); 4578 }) })); 4579 } 4580 componentDidMount() { 4581 this._isUnmounting = false; 4582 } 4583 componentWillUnmount() { 4584 this._isUnmounting = true; 4585 this.currentInnerHeight = undefined; 4586 protectedApi.setRef(this.props.innerHeightRef, null); 4587 } 4588 // Dragging 4589 // ----------------------------------------------------------------------------------------------- 4590 initColResizing(resizerEl, colIndex) { 4591 let { pluginHooks } = this.context; 4592 let ElementDraggingImpl = pluginHooks.elementDraggingImpl; 4593 if (ElementDraggingImpl) { 4594 let dragging = new ElementDraggingImpl(resizerEl); 4595 dragging.emitter.on('dragstart', () => { 4596 const isRtl = protectedApi.computeElIsRtl(resizerEl); 4597 const startWidth = this.props.colWidths[colIndex]; 4598 dragging.emitter.on('dragmove', (pev) => { 4599 if (this.props.onColResize) { 4600 this.props.onColResize(colIndex, startWidth + pev.deltaX * (isRtl ? -1 : 1)); 4601 } 4602 }); 4603 // because gets weird with auto-scrolling time area 4604 dragging.setAutoScrollEnabled(false); 4605 }); 4606 return dragging; 4607 } 4608 return null; 4609 } 4610 } 4611 4612 /* 4613 TODO: make more DRY with HeaderCell. Almost exactly the same except doesn't need resizer 4614 */ 4615 class SuperHeaderCell extends protectedApi.BaseComponent { 4616 constructor() { 4617 super(...arguments); 4618 // refs 4619 this.innerElRef = compat.createRef(); 4620 } 4621 render() { 4622 let { renderHooks } = this.props; 4623 let renderProps = { view: this.context.viewApi }; 4624 return (jsxRuntime.jsx(protectedApi.ContentContainer, { tag: "div", attrs: { 4625 ...buildAriaCellAttrs(this.props), 4626 role: 'columnheader', 4627 'aria-colspan': this.props.colSpan, 4628 }, className: publicApi.joinClassNames(classNames__default["default"].liquid, classNames__default["default"].noMargin, classNames__default["default"].noPadding, classNames__default["default"].flexCol, classNames__default["default"].alignStart, classNames__default["default"].borderNone, classNames__default["default"].crop), renderProps: renderProps, generatorName: "resourceColumnHeaderContent", customGenerator: renderHooks.headerContent, defaultGenerator: renderHooks.headerDefault, classNameGenerator: renderHooks.headerClass, didMount: renderHooks.headerDidMount, willUnmount: renderHooks.headerWillUnmount, children: (InnerContent) => (jsxRuntime.jsxs("div", { ref: this.innerElRef, className: publicApi.joinClassNames(classNames__default["default"].noShrink, classNames__default["default"].whiteSpaceNoWrap, classNames__default["default"].flexRow), children: [this.props.indent && (jsxRuntime.jsx(ResourceIndent, { level: 1, indentWidth: this.props.indentWidth })), jsxRuntime.jsx(InnerContent, { tag: 'div', className: protectedApi.generateClassName(renderHooks.headerInnerClass, renderProps) })] })) })); 4629 } 4630 componentDidMount() { 4631 this._isUnmounting = false; 4632 const innerEl = this.innerElRef.current; 4633 this.disconnectInnerHeight = protectedApi.watchHeight(innerEl, (height) => { 4634 if (this._isUnmounting) 4635 return; 4636 protectedApi.setRef(this.props.innerHeightRef, height); 4637 }); 4638 } 4639 componentWillUnmount() { 4640 this._isUnmounting = true; 4641 this.disconnectInnerHeight(); 4642 protectedApi.setRef(this.props.innerHeightRef, null); 4643 } 4644 } 4645 4646 class Virtualizer { 4647 constructor(getItemKey, getItemStart, getItemSize, requestRerender, overscan = 1) { 4648 this.getItemKey = getItemKey; 4649 this.getItemStart = getItemStart; 4650 this.getItemSize = getItemSize; 4651 this.requestRerender = requestRerender; 4652 this.overscan = overscan; 4653 // left exposed so they can be prepopulated :( 4654 this.viewportSize = 0; 4655 this.scroll = 0; 4656 this.items = []; 4657 } 4658 computePositions(items, disabled, forcedScroll) { 4659 if (disabled) { 4660 this.items = []; 4661 this.itemPositions = undefined; // affects _handleViewportChange 4662 return this.computeAllPositions(items); 4663 } 4664 this.items = items; 4665 if (forcedScroll !== undefined) { 4666 this.scroll = forcedScroll; 4667 } 4668 if (!this.viewportSize) { 4669 return []; 4670 } 4671 return (this.itemPositions = this.computePositionsInRange(items)); 4672 } 4673 handleViewportSize(size) { 4674 if (size !== this.viewportSize) { 4675 this.viewportSize = size; 4676 this._handleViewportChange(); 4677 } 4678 } 4679 handleScroll(scroll) { 4680 if (scroll !== this.scroll) { 4681 this.scroll = scroll; 4682 this._handleViewportChange(); 4683 } 4684 } 4685 _handleViewportChange() { 4686 if (this.itemPositions) { 4687 const testItemPositions = this.computePositionsInRange(this.items); 4688 if (testItemPositions.length !== this.itemPositions.length || (testItemPositions.length && 4689 testItemPositions[0].index !== this.itemPositions[0].index)) { 4690 this.requestRerender(); 4691 } 4692 } 4693 } 4694 computePositionsInRange(items) { 4695 const { scroll, viewportSize, getItemKey, getItemStart, getItemSize, overscan } = this; 4696 const count = items.length; 4697 let itemPositions = []; 4698 let index = 0; 4699 let runningStart = 0; 4700 while (index < count) { 4701 const item = items[index];
4702 const key = getItemKey(item); 4703 const start = getItemStart ? getItemStart(key, index, item) : runningStart; 4704 const size = getItemSize(key, index, item); 4705 if (start + size > scroll) { 4706 break; 4707 } 4708 index++; 4709 runningStart += size; 4710 } 4711 // back up for underscan 4712 const startIndex = Math.max(index - overscan, 0); 4713 while (index > startIndex) { 4714 const item = items[index - 1]; 4715 const key = getItemKey(item); 4716 const size = getItemSize(key, index - 1, item); 4717 index--; 4718 runningStart -= size; 4719 } 4720 while (index < count) { 4721 const item = items[index]; 4722 const key = getItemKey(item); 4723 const start = getItemStart ? getItemStart(key, index, item) : runningStart; 4724 const size = getItemSize(key, index, item); 4725 if (start >= scroll + viewportSize) { 4726 break; 4727 } 4728 itemPositions.push({ 4729 item, 4730 key, 4731 index, 4732 start, 4733 size, 4734 }); 4735 index++; 4736 runningStart += size; 4737 } 4738 const endIndex = Math.min(index + overscan, count); 4739 // overshoot for overscan 4740 while (index < endIndex) { 4741 const item = items[index]; 4742 const key = getItemKey(item); 4743 const start = getItemStart ? getItemStart(key, index, item) : runningStart; 4744 const size = getItemSize(key, index, item); 4745 itemPositions.push({ 4746 item, 4747 key, 4748 index, 4749 start, 4750 size, 4751 }); 4752 index++; 4753 runningStart += size; 4754 } 4755 return itemPositions; 4756 } 4757 computeAllPositions(items) { 4758 const { getItemKey, getItemStart, getItemSize } = this; 4759 let runningStart = 0; 4760 return items.map((item, index) => { 4761 const key = getItemKey(item); 4762 const start = getItemStart ? getItemStart(key, index, item) : runningStart; 4763 const size = getItemSize(key, index, item); 4764 runningStart += size; 4765 return { 4766 item, 4767 key, 4768 index, 4769 start, 4770 size, 4771 }; 4772 }); 4773 } 4774 } 4775 function computeShift(itemPositions) { 4776 const count = itemPositions.length; 4777 if (count) { 4778 const start = itemPositions[0].start; 4779 const end = itemPositions[count - 1].start + itemPositions[count - 1].size; 4780 return [start, end, itemPositions[0].index]; 4781 } 4782 } 4783 4784 const defaultOwnCellHeight = 40; 4785 const defaultSlotWidth = 50; 4786 class ResourceTimelineLayoutNormal extends protectedApi.DateComponent { 4787 constructor() { 4788 super(...arguments); 4789 this.state = {}; 4790 // memoized 4791 this.buildResourceLayouts = protectedApi.memoize(buildResourceLayouts); 4792 // TODO: make this nice 4793 // This is a means to recompute row positioning when *HeightMaps change 4794 this.queuedHeightChange = false; 4795 this.handleHeightChange = () => { 4796 if (this._isUnmounting) 4797 return; 4798 this.queuedHeightChange = true; 4799 protectedApi.afterSize(this.boundForceUpdate); 4800 }; 4801 this.boundForceUpdate = () => { 4802 if (this._isUnmounting) 4803 return; 4804 this.forceUpdate(); 4805 }; 4806 this.spreadsheetHeaderScrollerRef = compat.createRef(); 4807 this.spreadsheetBodyScrollerRef = compat.createRef(); 4808 this.spreadsheetFooterScrollerRef = compat.createRef(); 4809 this.timeHeaderScrollerRef = compat.createRef(); 4810 this.timeBodyScrollerRef = compat.createRef(); 4811 this.timeFooterScrollerRef = compat.createRef(); 4812 this.headerRowInnerWidthMap = new protectedApi.RefMap(() => { 4813 protectedApi.afterSize(this.handleSlotInnerWidths); 4814 }); 4815 this.timelineHeaderRowInnerHeightMap = new protectedApi.RefMap(this.handleHeightChange); 4816 this.dataGridHeaderRowInnerHeightMap = new protectedApi.RefMap(this.handleHeightChange); 4817 // keyed by createEntityId 4818 // NOTE: ignoring deletes can cause memory issues if resources constantly change and have new keys 4819 // refactor in future. FWIW, TanStack Virtual doesn't garbage collect unnused dimensions either
4820 this.dataGridEntityInnerHeightMap = new protectedApi.RefMap(this.handleHeightChange, /* ignoreDeletes = */ true); 4821 this.timeEntityInnerHeightMap = new protectedApi.RefMap(this.handleHeightChange, /* ignoreDeletes = */ true); 4822 // updated in-place 4823 // .time takes precedence 4824 // to operate on pixel value (.x), clear time 4825 this.scroll = {}; 4826 // virtualizers 4827 this.getEntityTop = (key) => this.bodyTops.get(key); 4828 this.getEntityHeight = (key) => this.bodyHeights.get(key); 4829 this.rowVirtualizer = new Virtualizer((rowLayout) => createEntityId(rowLayout.entity), this.getEntityTop, this.getEntityHeight, this.boundForceUpdate); 4830 this.groupColVirtualizers = []; 4831 this.slotVirtualizer = new Virtualizer((dateMarker) => dateMarker.toISOString(), (_key, index) => index * (this.props.slotWidth ?? defaultSlotWidth), () => this.props.slotWidth ?? defaultSlotWidth, this.boundForceUpdate, 4832 /* overscan = */ 3); 4833 this.timeHeaderVirtualizers = []; 4834 // Sizing 4835 // ----------------------------------------------------------------------------------------------- 4836 this.handleSpreadsheetClientWidth = (spreadsheetClientWidth) => { 4837 if (this._isUnmounting) 4838 return; 4839 protectedApi.setRef(this.props.spreadsheetClientWidthRef, spreadsheetClientWidth); 4840 }; 4841 this.handleSpreadsheetBottomScrollbarWidth = (spreadsheetBottomScrollbarWidth) => { 4842 this.setState({ 4843 spreadsheetBottomScrollbarWidth, 4844 }); 4845 }; 4846 this.handleTimeTotalWidth = (timeTotalWidth) => { 4847 if (this._isUnmounting) 4848 return; 4849 this.setState({ 4850 timeTotalWidth, 4851 }); 4852 }; 4853 this.handleTimeClientWidth = (timeClientWidth) => { 4854 if (this._isUnmounting) 4855 return; 4856 protectedApi.setRef(this.props.timeClientWidthRef, timeClientWidth); 4857 this.setState({ 4858 timeClientWidth, 4859 }); 4860 this.slotVirtualizer.handleViewportSize(timeClientWidth); 4861 for (const timeHeaderVirtualizer of this.timeHeaderVirtualizers) { 4862 timeHeaderVirtualizer.handleViewportSize(timeClientWidth); 4863 // TODO: what about difference in scrollbar width? 4864 } 4865 }; 4866 this.handleTimeClientHeight = (timeClientHeight) => { 4867 if (this._isUnmounting) 4868 return; 4869 this.setState({ 4870 timeClientHeight, 4871 }); 4872 if (!protectedApi.getIsHeightAuto(this.context.options)) { 4873 this.setVirtualizerViewportSize(timeClientHeight); 4874 } 4875 }; 4876 this.handleTimeBottomScrollbarWidth = (timeBottomScrollbarWidth) => { 4877 if (this._isUnmounting) 4878 return; 4879 this.setState({ 4880 timeBottomScrollbarWidth, 4881 }); 4882 }; 4883 this.handleSlotInnerWidths = () => { 4884 if (this._isUnmounting) 4885 return; 4886 const headerSlotInnerWidth = this.headerRowInnerWidthMap.current.get(this.props.tDateProfile.cellRows.length - 1); 4887 if (headerSlotInnerWidth != null && headerSlotInnerWidth !== this.slotInnerWidth) { 4888 this.slotInnerWidth = headerSlotInnerWidth; 4889 protectedApi.setRef(this.props.slotInnerWidthRef, headerSlotInnerWidth); 4890 } 4891 }; 4892 this.updateVirtualizerScroll = protectedApi.debounce(() => { 4893 const scroll = this.currentEntityScroll; 4894 this.rowVirtualizer.handleScroll(scroll); 4895 for (const groupColVirtualizer of this.groupColVirtualizers) { 4896 groupColVirtualizer.handleScroll(scroll); 4897 } 4898 }, 10)[0];
4899 // Window Scrolling 4900 // ----------------------------------------------------------------------------------------------- 4901 this.isTrackingWindowScroll = false; 4902 this.bodyOffset = 0; 4903 this.lastOffsetQuery = 0; 4904 this.handleWindowScroll = () => { 4905 const scrollY = window.scrollY; 4906 const now = Date.now(); 4907 if (now - this.lastOffsetQuery > 200) { 4908 this.lastOffsetQuery = now; 4909 this.bodyOffset = this.bodyEl.getBoundingClientRect().top + scrollY; 4910 } 4911 this.setVirtualizerScroll(scrollY - this.bodyOffset); 4912 }; 4913 this.handleTimeScrollRequest = (timeScroll) => { 4914 this.scroll.time = timeScroll; 4915 this.applyTimeScroll(); 4916 }; 4917 // HACKY 4918 // can't use isDevice because we want programmatic .scrollLeft assignments to work with scroll-virtualization too. 4919 // the main reason we use a isDevice gate is to prevent the initial-scroll assignment from triggering handleTimeScroll, 4920 // but the debounce luckily helps us in this case 4921 this.handleTimeScroll = (_isDevice, scroll) => { 4922 this.scroll.x = scroll; 4923 this._handleTimeScroll(); 4924 }; 4925 this._handleTimeScroll = protectedApi.debounce(() => { 4926 this.scroll.time = undefined; // prevents computeTimeScroll from computing based on time 4927 const scrollX = this.scroll.x; 4928 this.slotVirtualizer.handleScroll(scrollX); 4929 for (const timeHeaderVirtualizer of this.timeHeaderVirtualizers) { 4930 timeHeaderVirtualizer.handleScroll(scrollX); 4931 } 4932 }, 10)[0]; 4933 this.handleTimeScrollEnd = () => { 4934 protectedApi.setRef(this.props.scrollRef, this.scroll); 4935 }; 4936 // ENTITY (RESOURCE) Scrolling 4937 // ----------------------------------------------------------------------------------------------- 4938 this.handleResourceScrollRequest = (resourceId) => { 4939 this.scroll.entityId = resourceId; 4940 this.scroll.fromBottom = undefined; 4941 this.applyEntityScroll(); 4942 }; 4943 this.handleEntityScroll = (_isDevice, scroll) => { 4944 if (!protectedApi.getIsHeightAuto(this.context.options)) { 4945 this.setVirtualizerScroll(scroll); 4946 } 4947 }; 4948 this.handleEntityScrollStart = () => { 4949 this.scroll.entityId = undefined; 4950 this.scroll.fromBottom = undefined; 4951 }; 4952 /* 4953 Captures current values 4954 */ 4955 this.handleEntityScrollEnd = () => { 4956 if (!this.isTrackingWindowScroll) { 4957 const { bodyLayouts, bodyTops, bodyHeights, scroll } = this; 4958 const y = this.bodyScroller.y; 4959 const coordRes = findEntityByCoord(bodyLayouts, bodyTops, bodyHeights, y, createEntityId); 4960 if (coordRes) { 4961 const [entity, elTop, elHeight] = coordRes; 4962 scroll.entityId = createEntityId(entity); 4963 scroll.fromBottom = y 4964 ? elTop + elHeight - y 4965 : undefined; // if already at top, keep at top 4966 } 4967 } 4968 }; 4969 // Hit System 4970 // ----------------------------------------------------------------------------------------------- 4971 this.handleBodyEl = (el) => { 4972 this.bodyEl = el; 4973 if (el) { 4974 this.context.registerInteractiveComponent(this, { el }); 4975 } 4976 else { 4977 this.context.unregisterInteractiveComponent(this); 4978 } 4979 }; 4980 } 4981 render() { 4982 let { props, state, context } = this; 4983 let { dateProfile, hasNesting } = props; 4984 let { options, viewSpec } = context; 4985 const { borderlessX, borderlessTop, borderlessBottom } = protectedApi.computeViewBorderless(options); 4986 let { spreadsheetBottomScrollbarWidth, timeTotalWidth, timeClientWidth, timeClientHeight, timeBottomScrollbarWidth } = state; 4987 /* date */ 4988 const { tDateProfile } = props; 4989 let { cellRows } = tDateProfile; 4990 /* table settings */ 4991 let allowVerticalScrolling = !protectedApi.getIsHeightAuto(options);
4992 let tableHeaderSticky = protectedApi.getTableHeaderSticky(options); 4993 let footerScrollbarSticky = protectedApi.getFooterScrollbarSticky(options); 4994 const { colSpecs, groupColCnt, superHeaderRendering } = props; 4995 /* table hierarchy */ 4996 const { resourceHierarchy } = props; 4997 const superHeaderRowSpan = superHeaderRendering ? 1 : 0; 4998 const totalHeaderRowSpan = superHeaderRowSpan + 1; 4999 /* table display */ 5000 let { layouts: bodyLayouts, flatRowLayouts, flatGroupColLayouts, totalCnt, } = this.buildResourceLayouts(resourceHierarchy, hasNesting, props.resourceEntityExpansions, options.resourcesInitiallyExpanded); 5001 this.bodyLayouts = bodyLayouts; 5002 // update groupColVirtualizers 5003 let groupColVirtualizers = this.groupColVirtualizers; 5004 let oldLen = groupColVirtualizers.length; 5005 let newLen = flatGroupColLayouts.length; 5006 if (newLen > oldLen) { 5007 for (let i = oldLen; i < newLen; i++) { 5008 groupColVirtualizers[i] = new Virtualizer((groupCellLayout) => createGroupId(groupCellLayout.entity), this.getEntityTop, this.getEntityHeight, this.boundForceUpdate); 5009 } 5010 } 5011 else if (newLen < oldLen) { 5012 groupColVirtualizers = groupColVirtualizers.slice(0, newLen); 5013 } 5014 this.groupColVirtualizers = groupColVirtualizers; 5015 // update timeHeaderVirtualizers 5016 let timeHeaderVirtualizers = this.timeHeaderVirtualizers; 5017 oldLen = timeHeaderVirtualizers.length; 5018 newLen = cellRows.length; 5019 if (newLen > oldLen) { 5020 for (let i = oldLen; i < newLen; i++) { 5021 timeHeaderVirtualizers[i] = new Virtualizer((cell) => cell.rowUnit + ':' + cell.date.toISOString(), // TODO: DRY with TimelineHeaderRow 5022 undefined, // getItemStart (let Virtualizer compute it) 5023 (key, index, cell) => cell.colspan * (this.props.slotWidth ?? defaultSlotWidth), this.boundForceUpdate); 5024 // HACK to prepopulate 5025 timeHeaderVirtualizers[i].viewportSize = timeHeaderVirtualizers[0].viewportSize; 5026 timeHeaderVirtualizers[i].scroll = timeHeaderVirtualizers[0].scroll; 5027 } 5028 } 5029 this.timeHeaderVirtualizers = timeHeaderVirtualizers; 5030 // Expansion-filtered rows, including those outside the virtualized viewport. 5031 // `totalCnt` also includes collapsed descendants; `rowPositions` below is the mounted subset. 5032 const displayedRowCnt = flatRowLayouts.length; 5033 /* table positions */ 5034 // does not include bottom border between header and body 5035 const headerHeight = computeHeaderHeight(this.dataGridHeaderRowInnerHeightMap.current, this.timelineHeaderRowInnerHeightMap.current, Boolean(superHeaderRendering), tDateProfile.cellRows.length); 5036 let endScrollbarWidth = (timeTotalWidth != null && timeClientWidth != null) 5037 ? timeTotalWidth - timeClientWidth 5038 : undefined; 5039 // totalBodyHeight is the height WITHIN the scrollers 5040 let [bodyHeights, totalBodyHeight] = computeHeights(// TODO: memoize? 5041 bodyLayouts, createEntityId, (entityKey) => { 5042 const entitySpreadsheetHeight = this.dataGridEntityInnerHeightMap.current.get(entityKey) ?? defaultOwnCellHeight; 5043 // map doesn't contain group-column-cell heights, thus ||0 5044 // TODO: better use defaultOwnCellHeight 5045 const entityTimeHeight = this.timeEntityInnerHeightMap.current.get(entityKey) || 0; 5046 return Math.max(entitySpreadsheetHeight, entityTimeHeight); 5047 }, 5048 /* minHeight = */ (allowVerticalScrolling && options.expandRows) 5049 ? timeClientHeight 5050 : undefined); 5051 this.bodyHeights = bodyHeights; 5052 let bodyTops = computeTopsFromHeights(bodyLayouts, createEntityId, bodyHeights); 5053 this.bodyTops = bodyTops; 5054 /* virtualize */ 5055 const virtualizationDisabled = !options.virtualization; 5056 // ensure in-range items are computed with most recent scroll, even tho maybe not applied yet 5057 const forcedEntityScroll = this.computeEntityScroll(); 5058 const rowPositions = this.rowVirtualizer.computePositions(flatRowLayouts, virtualizationDisabled, forcedEntityScroll); 5059 // Keep render loops component-type-specific for future row recycling. 5060 const groupRowPositions = []; 5061 const resourceRowPositions = []; 5062 for (const rowPosition of rowPositions) { 5063 if ('resourceFields' in rowPosition.item) { 5064 resourceRowPositions.push(rowPosition); 5065 } 5066 else { 5067 groupRowPositions.push(rowPosition); 5068 } 5069 } 5070 const groupColPositions = this.groupColVirtualizers.map((groupColVirtualizer, i) => { 5071 return groupColVirtualizer.computePositions(flatGroupColLayouts[i], virtualizationDisabled, forcedEntityScroll); 5072 }); 5073 // Scope deterministic row/column cell IDs to this calendar instance. 5074 const cellIdPrefix = `${context.baseId}rt`; 5075 const ariaBodyRows = buildAriaBodyRows(flatRowLayouts, groupColPositions, rowPositions, groupColCnt, colSpecs.length); 5076 // Only paint vertical fills/lines that are in view 5077 // Big performance impact for very tall virtualized lists 5078 // NOTE: could be zero rows and groups! 5079 const rowPositionShift = computeShift(rowPositions); 5080 const yFillTop = rowPositionShift?.[0] ?? 0; 5081 const yFillBottom = rowPositionShift?.[1] ?? 0; 5082 let yFillHeight; 5083 if (allowVerticalScrolling) { 5084 yFillHeight = timeClientHeight != null 5085 ? Math.max(yFillBottom, timeClientHeight) - yFillTop 5086 : undefined; 5087 } 5088 else { 5089 yFillHeight = yFillBottom - yFillTop; 5090 } 5091 const forcedTimeScroll = this.computeTimeScroll(); 5092 const slotDatePositions = this.slotVirtualizer.computePositions(tDateProfile.slotDates, virtualizationDisabled, forcedTimeScroll); 5093 const slotDateShift = computeShift(slotDatePositions); 5094 /* */ 5095 const { timeCanvasWidth } = props; 5096 const slotWidth = props.slotWidth ?? defaultSlotWidth; 5097 const { spreadsheetColWidths, spreadsheetCanvasWidth } = props; 5098 /* event display */ 5099 const { splitProps, bgSlicedProps } = props; 5100 /* business hour display */ 5101 const { hasResourceBusinessHours, fallbackBusinessHours } = props; 5102 const enableNowIndicator = // TODO: DRY 5103 options.nowIndicator && 5104 protectedApi.rangeContainsMarker(props.dateProfile.currentRange, props.nowDate); 5105 /* filler */ 5106 const spreadsheetBottomFiller = Math.max(0, (timeBottomScrollbarWidth || 0) - 5107 (spreadsheetBottomScrollbarWidth || 0)); 5108 const timelineBottomFiller = Math.max(0, (spreadsheetBottomScrollbarWidth || 0) - 5109 (timeBottomScrollbarWidth || 0)); 5110 const maxVScroll = this.maxVScroll = 5111 (allowVerticalScrolling && timeClientHeight != null) 5112 ? Math.max(0, totalBodyHeight - timeClientHeight) 5113 : 0; 5114 const allowBottomFiller = !options.expandRows && // if rows expand, then we never need filler 5115 !maxVScroll; // no scrolling possible 5116 const spreadsheetNeedsBottomFiller = allowBottomFiller || Boolean(spreadsheetBottomFiller); 5117 const timelineNeedsBottomFiller = allowBottomFiller || Boolean(timelineBottomFiller); 5118 return (jsxRuntime.jsxs(protectedApi.ViewContainer, { viewSpec: viewSpec, attrs: { 5119 role: hasNesting ? 'treegrid' : 'grid', // TODO: DRY 5120 'aria-rowcount': totalHeaderRowSpan + totalCnt, 5121 'aria-colcount': colSpecs.length + 1, 5122 'aria-label': props.labelStr, 5123 'aria-labelledby': props.labelId, 5124 }, className: publicApi.joinClassNames(props.className, protectedApi.generateClassName(options.tableClass, { 5125 borderlessX, 5126 borderlessTop, 5127 borderlessBottom, 5128 multiMonthColumns: 0, 5129 }), classNames__default["default"].flexCol), children: [jsxRuntime.jsx(AriaProxyRows, { cellIdPrefix: cellIdPrefix, bodyRows: ariaBodyRows, resourceColCnt: colSpecs.length, superHeaderRowSpan: superHeaderRowSpan, hasNesting: hasNesting }), jsxRuntime.jsx(ResizableTwoCol, { initialStartWidth: props.initialSpreadsheetWidth, resizedWidthRef: props.spreadsheetResizedWidthRef, className: allowVerticalScrolling ? classNames__default["default"].liquid : '', 5130 /* spreadsheet
5131 --------------------------------------------------------------------------------- */ 5132 startClassName: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].isolate), startContent: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { role: 'presentation', className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableHeaderClass, { 5133 isSticky: tableHeaderSticky, 5134 borderlessX, 5135 borderlessTop, 5136 borderlessBottom, 5137 multiMonthColumns: 0, 5138 }), classNames__default["default"].flexCol, tableHeaderSticky && classNames__default["default"].tableHeaderSticky), style: { 5139 zIndex: 1, 5140 }, children: [jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].contentBox), style: { 5141 height: headerHeight, 5142 }, children: [Boolean(superHeaderRendering) && (jsxRuntime.jsx("div", { role: 'presentation', className: publicApi.joinClassNames(options.resourceHeaderRowClass, classNames__default["default"].flexRow, classNames__default["default"].grow, classNames__default["default"].borderOnlyB), children: jsxRuntime.jsx(SuperHeaderCell, { cellIdPrefix: cellIdPrefix, cellRowIndex: 1, cellColIndex: 0, renderHooks: superHeaderRendering, indent: hasNesting && !groupColCnt /* group-cols are leftmost, making expander alignment irrelevant */, innerHeightRef: this.dataGridHeaderRowInnerHeightMap.createRef(true), colSpan: colSpecs.length, indentWidth: props.indentWidth }) })), jsxRuntime.jsx(protectedApi.Scroller, { horizontal: true, hideScrollbars: true, className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].grow), ref: this.spreadsheetHeaderScrollerRef, children: jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].grow), style: { minWidth: spreadsheetCanvasWidth }, children: jsxRuntime.jsx(HeaderRow, { role: 'presentation', colSpecs: colSpecs, colWidths: spreadsheetColWidths, indent: hasNesting, indentWidth: props.indentWidth, rowIndex: superHeaderRowSpan, cellIdPrefix: cellIdPrefix, 5143 // refs 5144 innerHeightRef: this.dataGridHeaderRowInnerHeightMap.createRef(false), onColResize: props.onColResize }) }) })] }), jsxRuntime.jsx("div", { className: protectedApi.generateClassName(options.slotHeaderDividerClass, { 5145 inTableHeader: true, 5146 options: { dayMinWidth: options.dayMinWidth }, 5147 }) })] }), jsxRuntime.jsxs(protectedApi.Scroller, { vertical: allowVerticalScrolling, horizontal: true, hideScrollbars: true, className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableBodyClass, { 5148 borderlessX, 5149 borderlessTop, 5150 borderlessBottom, 5151 multiMonthColumns: 0, 5152 }), classNames__default["default"].flexCol, classNames__default["default"].rel, // for Ruler.fillStart 5153 allowVerticalScrolling && classNames__default["default"].liquid), style: { 5154 zIndex: 0, 5155 }, ref: this.spreadsheetBodyScrollerRef, clientWidthRef: this.handleSpreadsheetClientWidth, children: [jsxRuntime.jsx(BodySection, { cellIdPrefix: cellIdPrefix, groupRowPositions: groupRowPositions, resourceRowPositions: resourceRowPositions, groupColPositions: groupColPositions, displayedRowCnt: displayedRowCnt, groupCellCnts: flatGroupColLayouts.map((flatGroupCellLayouts) => flatGroupCellLayouts.length), colWidths: spreadsheetColWidths, c
5155olSpecs: colSpecs, rowInnerHeightRefMap: this.dataGridEntityInnerHeightMap, headerRowSpan: totalHeaderRowSpan, indentWidth: props.indentWidth, canvasWidth: spreadsheetCanvasWidth, canvasHeight: totalBodyHeight }), spreadsheetNeedsBottomFiller && (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.fillerClass, { inTableHeader: false }), classNames__default["default"].borderOnlyT), style: { minHeight: spreadsheetBottomFiller } }))] }), jsxRuntime.jsx(protectedApi.FooterScrollbar, { isSticky: footerScrollbarSticky, canvasWidth: spreadsheetCanvasWidth, scrollerRef: this.spreadsheetFooterScrollerRef, scrollbarWidthRef: this.handleSpreadsheetBottomScrollbarWidth })] }), 5156 /* time-area (TODO: try to make this DRY-er with TimelineView???) 5157 --------------------------------------------------------------------------------- */ 5158 endClassName: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].isolate), endContent: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableHeaderClass, { 5159 isSticky: tableHeaderSticky, 5160 borderlessX, 5161 borderlessTop, 5162 borderlessBottom, 5163 multiMonthColumns: 0, 5164 }), classNames__default["default"].flexCol, tableHeaderSticky && classNames__default["default"].tableHeaderSticky), style: { 5165 zIndex: 1, 5166 }, children: [jsxRuntime.jsxs(protectedApi.Scroller, { ref: this.timeHeaderScrollerRef, horizontal: true, hideScrollbars: true, className: publicApi.joinClassNames(classNames__default["default"].flexRow, classNames__default["default"].contentBox), style: { 5167 height: headerHeight, 5168 }, children: [jsxRuntime.jsx("div", { role: 'presentation', children: jsxRuntime.jsx("div", { ...buildAriaCellAttrs({ 5169 cellIdPrefix, 5170 cellRowIndex: 1, 5171 cellColIndex: colSpecs.length, 5172 }), role: 'columnheader', "aria-rowspan": totalHeaderRowSpan, "aria-label": options.eventsHint }) }), jsxRuntime.jsxs("div", { "aria-hidden": true, className: publicApi.joinClassNames(// TODO: DRY 5173 classNames__default["default"].rel, // origin for now-indicator 5174 classNames__default["default"].flexCol, timeCanvasWidth == null && classNames__default["default"].liquid, 5175 // slotLiquid:true implies that slots are expanding bigger than their min, 5176 // and that there are NOT any horizontal scrollbars. if so, then no worries 5177 // about sticky-event-titles. if so, we're able to apply cropping to prevent 5178 // event hover-resizers and whatnot from bleeding out of the canvas and causing 5179 // horizontal scrollbars when there normally wouldn't be any 5180 props.slotLiquid && classNames__default["default"].crop), style: { minWidth: timeCanvasWidth }, children: [cellRows.map((cells, rowIndex) => { 5181 const rowLevel = cellRows.length - rowIndex - 1; 5182 const headerCellPositions = timeHeaderVirtualizers[rowIndex].computePositions(cells, virtualizationDisabled, forcedTimeScroll); 5183 const shift = computeShift(headerCellPositions); 5184 if (shift) { 5185 timeHeaderVirtualizers[rowIndex].computePositions(cells, virtualizationDisabled, forcedTimeScroll); 5186 } 5187 return (jsxRuntime.jsx(TimelineHeaderRow, { className: classNames__default["default"].rel, dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, rowLevel: rowLevel, cells: cells, slotWidth:
5187slotWidth, innerWidthRef: this.headerRowInnerWidthMap.createRef(rowIndex), innerHeighRef: this.timelineHeaderRowInnerHeightMap.createRef(rowIndex), insetInlineStart: shift && shift[0], width: shift ? (shift[1] - shift[0]) : undefined, cellStartIndex: shift && shift[2], cellCount: headerCellPositions.length }, rowIndex)); 5188 }), enableNowIndicator && ( 5189 // TODO: make this positioned WITHIN padding 5190 jsxRuntime.jsx(TimelineNowIndicatorArrow, { tDateProfile: tDateProfile, nowDate: props.nowDate, slotWidth: slotWidth }))] }), Boolean(endScrollbarWidth) && (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.fillerClass, { inTableHeader: true }), classNames__default["default"].borderOnlyS), style: { minWidth: endScrollbarWidth } }))] }), jsxRuntime.jsx("div", { className: protectedApi.generateClassName(options.slotHeaderDividerClass, { 5191 inTableHeader: false, 5192 options: { dayMinWidth: options.dayMinWidth }, 5193 }) })] }), jsxRuntime.jsx(protectedApi.Scroller, { vertical: allowVerticalScrolling && 5194 // infer whether scrolling would happen. prevents clientHeight thrashing due to horizontal scrollbars 5195 (totalBodyHeight != null && timeClientHeight != null && totalBodyHeight > timeClientHeight), horizontal: true, hideScrollbars: footerScrollbarSticky /* FYI, this view is never print */, className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableBodyClass, { 5196 borderlessX, 5197 borderlessTop, 5198 borderlessBottom, 5199 multiMonthColumns: 0, 5200 }), classNames__default["default"].flexCol, classNames__default["default"].rel, // for Ruler.fillStart 5201 allowVerticalScrolling && classNames__default["default"].liquid), style: { 5202 zIndex: 0, 5203 }, ref: this.timeBodyScrollerRef, clientWidthRef: this.handleTimeClientWidth, clientHeightRef: allowVerticalScrolling 5204 ? this.handleTimeClientHeight 5205 : undefined // for when height:auto, which will have dynamic height and needlessly fire ResizeObserver 5206 , bottomScrollbarWidthRef: allowVerticalScrolling 5207 ? this.handleTimeBottomScrollbarWidth 5208 : undefined // for when height:auto, which will have dynamic height and needlessly fire ResizeObserver 5209 , children: jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(classNames__default["default"].rel, // origin for canvas? 5210 classNames__default["default"].grow, classNames__default["default"].flexCol, 5211 // slotLiquid:true implies that slots are expanding bigger than their min, 5212 // and that there are NOT any horizontal scrollbars. if so, then no worries 5213 // about sticky-event-titles. if so, we're able to apply cropping to prevent 5214 // event hover-resizers and whatnot from bleeding out of the canvas and causing 5215 // horizontal scrollbars when there normally wouldn't be any 5216 props.slotLiquid && classNames__default["default"].crop), style: { 5217 minWidth: timeCanvasWidth, 5218 minHeight: totalBodyHeight, 5219 }, ref: this.handleBodyEl, children: [jsxRuntime.jsxs("div", { 5220 // roving-origin for anything that fills y-axis, 5221 // like slots or non-resource bg events 5222 // is ZERO-width, but has defined top/bottom 5223 className: classNames__default["default"].abs, style: { 5224 top: yFillTop, 5225 height: yFillHeight, 5226 bottom: yFillHeight == null ? 0 : undefined, 5227 insetInlineStart: slotDateShift?.[0], 5228 }, children: [jsxRuntime.jsx(TimelineSlats, { dateProfile: dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, slatStartIndex: slotDateShift?.[2], slatCount: slotDatePositions.length, slotWidth:
5228slotWidth }), jsxRuntime.jsx(TimelineBg, { tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, 5229 // content 5230 bgEventSegs: bgSlicedProps.bgEventSegs, businessHourSegs: hasResourceBusinessHours ? null : bgSlicedProps.businessHourSegs, dateSelectionSegs: bgSlicedProps.dateSelectionSegs, eventResizeSegs: (bgSlicedProps.eventResize ? bgSlicedProps.eventResize.segs : null), 5231 // dimensions 5232 slotWidth: slotWidth, 5233 // virtualization 5234 clipStart: slotDateShift?.[0], clipEnd: slotDateShift?.[1] }), enableNowIndicator && (jsxRuntime.jsx(TimelineNowIndicatorLine, { tDateProfile: tDateProfile, nowDate: props.nowDate, slotWidth: slotWidth, clipStart: slotDateShift?.[0] }))] }), jsxRuntime.jsxs("div", { 5235 // roving-origin for all types of resource(-group)-rows 5236 // is ZERO-height, always fixed to top, but has definet x-coordinate and width 5237 role: 'presentation', className: classNames__default["default"].abs, style: { 5238 top: 0, 5239 insetInlineStart: slotDateShift?.[0], 5240 width: slotDateShift ? (slotDateShift[1] - slotDateShift[0]) : undefined, 5241 }, children: [groupRowPositions.map((rowPosition) => { 5242 const rowLayout = rowPosition.item; 5243 const group = rowLayout.entity; 5244 const groupKey = rowPosition.key; 5245 return (jsxRuntime.jsx("div", { role: 'presentation', className: publicApi.joinClassNames(classNames__default["default"].fillX, classNames__default["default"].flexRow), style: { 5246 top: rowPosition.start, 5247 }, children: jsxRuntime.jsx(GroupLane, { cellIdPrefix: cellIdPrefix, cellRowIndex: 1 + totalHeaderRowSpan + rowLayout.rowIndex, cellColIndex: colSpecs.length, group: group, borderBottom: rowLayout.visibleIndex < displayedRowCnt - 1, innerHeightRef: this.timeEntityInnerHeightMap.createRef(groupKey), height: rowPosition.size }) }, groupKey)); 5248 }), resourceRowPositions.map((rowPosition) => { 5249 const resourceLayout = rowPosition.item; 5250 const resource = resourceLayout.entity; 5251 return (compat.createElement(ResourceLane, { ...splitProps[resource.id], key: resource.id /* TODO: use rowPosition.key? */, role: 'presentation', cellIdPrefix: cellIdPrefix, cellRowIndex: 1 + totalHeaderRowSpan + resourceLayout.rowIndex, cellColIndex: colSpecs.length, className: classNames__default["default"].fillX, resource: resource, dateProfile: dateProfile, tDateProfile: tDateProfile, nowDate: props.nowDate, todayRange: props.todayRange, businessHours: resource.businessHours || fallbackBusinessHours, borderBottom: resourceLayout.visibleIndex < displayedRowCnt - 1, 5252 // ref 5253 heightRef: this.timeEntityInnerHeightMap.createRef(resource.id), 5254 // dimensions 5255 slotWidth: slotWidth, 5256 // position 5257 top: rowPosition.start, height: rowPosition.size, 5258 // virtualization 5259 clipStart: slotDateShift?.[0], clipEnd: slotDateShift?.[1] })); 5260 })] }), timelineNeedsBottomFiller && (jsxRuntime.jsx("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.fillerClass, { inTableHeader: false }), classNames__default["default"].borderOnlyT, classNames__default["default"].fillX), style: { 5261 top: yFillBottom, 5262 minHeight: timelineBottomFiller 5263 } }))] }) }), Boolean(footerScrollbarSticky) && (jsxRuntime.jsx(protectedApi.FooterScrollbar, { isSticky: true, canvasWidth: timeCanvasWidth, scrollbarWidthRef: this.handleTimeBottomScrollbarWidth, scrollerRef: this.timeFooterScrollerRef })), jsxRuntime.jsx(protectedApi.Ruler, { widthRef: this.handleTimeTotalWidth })] }) })] })); 5264 } 5265 // Lifecycle 5266 // ----------------------------------------------------------------------------------------------- 5267 componentDidMount() { 5268 const { props, context } = this; 5269 this._isUnmounting = false; 5270 this.timeScroller = new ScrollerSyncer(true); // horizontal=true 5271 this.bodyScroller = new ScrollerSyncer(); // horizontal=false
5272 this.spreadsheetScroller = new ScrollerSyncer(true); // horizontal=true 5273 this.updateScrollersSyncers(); 5274 this.updateWindowScrolling(); 5275 if (props.initialScroll) { 5276 this.scroll = { ...props.initialScroll }; // copy 5277 this.applyEntityScroll(); 5278 this.applyTimeScroll(); 5279 } 5280 else { 5281 this.resetTimeScroll(); 5282 } 5283 this.timeScroller.addScrollListener(this.handleTimeScroll); 5284 this.timeScroller.addScrollEndListener(this.handleTimeScrollEnd); 5285 this.bodyScroller.addScrollStartListener(this.handleEntityScrollStart); 5286 this.bodyScroller.addScrollListener(this.handleEntityScroll); 5287 this.bodyScroller.addScrollEndListener(this.handleEntityScrollEnd); 5288 context.emitter.on('_timeScrollRequest', this.handleTimeScrollRequest); 5289 context.emitter.on('_resourceScrollRequest', this.handleResourceScrollRequest); 5290 } 5291 componentDidUpdate(prevProps, prevState) { 5292 const { props, state } = this; 5293 const { options } = this.context; 5294 this.updateScrollersSyncers(); 5295 this.updateWindowScrolling(); 5296 const dateProfileChange = prevProps.dateProfile !== props.dateProfile; 5297 const slotWidthChange = prevProps.slotWidth !== props.slotWidth; 5298 const timeCanvasWidthChange = prevProps.timeCanvasWidth !== props.timeCanvasWidth; 5299 const timeClientWidthChange = prevState.timeClientWidth !== state.timeClientWidth; 5300 if (dateProfileChange || slotWidthChange || timeCanvasWidthChange || timeClientWidthChange) { 5301 if (dateProfileChange && options.scrollTimeReset) { 5302 this.resetTimeScroll(); 5303 } 5304 else { 5305 this.applyTimeScroll(); 5306 } 5307 } 5308 /* 5309 Unfortunately this will execute after auto-scroll finished but before scrollEnd can record 5310 the updated scroll positioning, causing a scroll-jump to the last recorded entityScroll. 5311 */ 5312 if (this.queuedHeightChange) { 5313 this.queuedHeightChange = false; 5314 this.applyEntityScroll(); 5315 } 5316 } 5317 componentWillUnmount() { 5318 this._isUnmounting = true; 5319 this.slotInnerWidth = undefined; 5320 this.timeScroller.destroy(); 5321 this.bodyScroller.destroy(); 5322 this.spreadsheetScroller.destroy(); 5323 this.destroyWindowScrolling(); 5324 this.bodyScroller.removeScrollEndListener(this.handleEntityScrollEnd); 5325 this.bodyScroller.removeScrollListener(this.handleEntityScroll); 5326 this.context.emitter.off('_timeScrollRequest', this.handleTimeScrollRequest); 5327 this.context.emitter.off('_resourceScrollRequest', this.handleResourceScrollRequest); 5328 } 5329 // Virtualizer Utils 5330 // ----------------------------------------------------------------------------------------------- 5331 setVirtualizerViewportSize(height) { 5332 this.rowVirtualizer.handleViewportSize(height); 5333 for (const groupColVirtualizer of this.groupColVirtualizers) { 5334 groupColVirtualizer.handleViewportSize(height); 5335 } 5336 } 5337 setVirtualizerScroll(scroll) { 5338 this.currentEntityScroll = scroll; 5339 this.updateVirtualizerScroll(); 5340 } 5341 updateWindowScrolling() { 5342 if (protectedApi.getIsHeightAuto(this.context.options)) { 5343 if (!this.isTrackingWindowScroll) { 5344 this.isTrackingWindowScroll = true; 5345 this.setVirtualizerViewportSize(window.innerHeight); 5346 ([this.handleWindowResize, this.cancelWindowResize] = protectedApi.debounce(() => { 5347 this.setVirtualizerViewportSize(window.innerHeight); 5348 }, 200)); 5349 window.addEventListener('resize', this.handleWindowResize); 5350 window.addEventListener('scroll', this.handleWindowScroll, { passive: true });
5351 } 5352 } 5353 else { 5354 this.destroyWindowScrolling(); 5355 } 5356 } 5357 destroyWindowScrolling() { 5358 if (this.isTrackingWindowScroll) { 5359 this.isTrackingWindowScroll = false; 5360 this.cancelWindowResize(); 5361 window.removeEventListener('resize', this.handleWindowResize); 5362 window.removeEventListener('scroll', this.handleWindowScroll, { passive: true }); 5363 } 5364 } 5365 // Scrolling 5366 // ----------------------------------------------------------------------------------------------- 5367 updateScrollersSyncers() { 5368 this.timeScroller.handleChildren([ 5369 this.timeHeaderScrollerRef.current, 5370 this.timeBodyScrollerRef.current, 5371 this.timeFooterScrollerRef.current, 5372 ]); 5373 this.bodyScroller.handleChildren([ 5374 this.spreadsheetBodyScrollerRef.current, 5375 this.timeBodyScrollerRef.current, 5376 ]); 5377 this.spreadsheetScroller.handleChildren([ 5378 this.spreadsheetHeaderScrollerRef.current, 5379 this.spreadsheetBodyScrollerRef.current, 5380 this.spreadsheetFooterScrollerRef.current, 5381 ]); 5382 } 5383 // TIME Scrolling 5384 // ----------------------------------------------------------------------------------------------- 5385 resetTimeScroll() { 5386 this.handleTimeScrollRequest(this.context.options.scrollTime); 5387 } 5388 applyTimeScroll() { 5389 const x = this.computeTimeScroll(); 5390 if (x !== undefined) { 5391 this.timeScroller.scrollTo({ x }); 5392 if (this.scroll.x !== x) { 5393 this.scroll.x = x; 5394 protectedApi.setRef(this.props.scrollRef, this.scroll); 5395 } 5396 } 5397 } 5398 // used by render() for virtualization too 5399 computeTimeScroll() { 5400 const { props, context, scroll } = this; 5401 const { tDateProfile } = props; 5402 const slotWidth = props.slotWidth ?? defaultSlotWidth; 5403 let { x, time } = scroll; 5404 if (time) { 5405 x = timeToCoord(time, context.dateEnv, props.dateProfile, tDateProfile, slotWidth);
5406 if (x) { 5407 x += 1; // overcome border. TODO: DRY this up 5408 } 5409 } 5410 return x; 5411 } 5412 applyEntityScroll() { 5413 const scrollTop = this.computeEntityScroll(); 5414 if (scrollTop !== undefined) { 5415 this.bodyScroller.scrollTo({ y: scrollTop }); 5416 } 5417 } 5418 computeEntityScroll() { 5419 const { bodyTops, bodyHeights, scroll } = this; 5420 const { entityId, fromBottom } = scroll; 5421 if (entityId) { 5422 const top = bodyTops.get(entityId); 5423 const height = bodyHeights.get(entityId); 5424 if (top != null) { 5425 const bottom = top + height; 5426 const requestedScrollTop = fromBottom != null ? 5427 bottom - fromBottom : // pixels from bottom edge 5428 top + // just use top edge 5429 (top ? 1 : 0); // overcome top border 5430 return Math.min(this.maxVScroll, requestedScrollTop); 5431 } 5432 } 5433 } 5434 prepareHits() { 5435 /* 5436 HACK for queuedHeightChange usage in componentDidUpdate 5437 This executes when a drag/resize starts and clears the last recorded entity scroll 5438 */ 5439 this.scroll.entityId = undefined; 5440 this.scroll.fromBottom = undefined; 5441 } 5442 queryHit(isRtl, positionLeft, positionTop) { 5443 let { props, context, bodyLayouts, bodyTops, bodyHeights } = this; 5444 const { dateProfile, tDateProfile, timeCanvasWidth } = props; 5445 const slotWidth = props.slotWidth ?? defaultSlotWidth; 5446 const { dateEnv } = context; 5447 let coordRes = findEntityByCoord(bodyLayouts, bodyTops, bodyHeights, positionTop, createEntityId); 5448 if (coordRes) { 5449 let [entityAtTop, top, height] = coordRes; 5450 if (!isEntityGroup(entityAtTop)) { 5451 let resource = entityAtTop; 5452 let bottom = top + height; 5453 /* 5454 TODO: DRY-up ith TimelineView!!! 5455 */ 5456 const x = isRtl ? timeCanvasWidth - positionLeft : positionLeft; 5457 const slatIndex = Math.floor(x / slotWidth);
5458 const slatLeft = slatIndex * slotWidth; 5459 const partial = (x - slatLeft) / slotWidth; // floating point number between 0 and 1 5460 const localSnapIndex = Math.floor(partial * tDateProfile.snapsPerSlot); // the snap # relative to start of slat 5461 let start = dateEnv.add(tDateProfile.slotDates[slatIndex], protectedApi.multiplyDuration(tDateProfile.snapDuration, localSnapIndex)); 5462 let end = dateEnv.add(start, tDateProfile.snapDuration); 5463 // TODO: generalize this coord stuff to TimeGrid? 5464 let snapWidth = slotWidth / tDateProfile.snapsPerSlot; 5465 let startCoord = slatIndex * slotWidth + (snapWidth * localSnapIndex); 5466 let endCoord = startCoord + snapWidth; 5467 let left, right; 5468 if (isRtl) { 5469 left = timeCanvasWidth - endCoord; 5470 right = timeCanvasWidth - startCoord; 5471 } 5472 else { 5473 left = startCoord; 5474 right = endCoord; 5475 } 5476 return { 5477 dateProfile, 5478 dateSpan: { 5479 range: { start, end }, 5480 allDay: !tDateProfile.isTimeScale, 5481 resourceId: resource.id, 5482 }, 5483 rect: { 5484 left, 5485 right, 5486 top, 5487 bottom, 5488 }, 5489 getDayEl: () => getTimelineSlotEl(this.bodyEl, slatIndex), 5490 layer: 0, 5491 }; 5492 } 5493 } 5494 return null; 5495 } 5496 } 5497 function computeHeaderHeight(dataGridHeaderRowInnerHeightMap, timelineHeaderRowInnerHeightMap, hasDateGridSuperHeader, timelineHeaderRowCnt) { 5498 const dataGridKeys = (hasDateGridSuperHeader ? [true] : []).concat(false); 5499 let dataGridH = 0; 5500 // TODO: use summing util? 5501 for (const key of dataGridKeys) { 5502 const rowHeight = dataGridHeaderRowInnerHeightMap.get(key); 5503 if (rowHeight == null) { 5504 return; 5505 } 5506 dataGridH += rowHeight; 5507 } 5508 let timelineH = 0; 5509 // TODO: use summing util? 5510 for (let row = 0; row < timelineHeaderRowCnt; row++) { 5511 const rowHeight = timelineHeaderRowInnerHeightMap.get(row); 5512 if (rowHeight == null) { 5513 return; 5514 } 5515 timelineH += rowHeight; 5516 } 5517 // add in-between borders too 5518 return Math.max(dataGridH + (hasDateGridSuperHeader ? ROW_BORDER_WIDTH : 0), timelineH + ROW_BORDER_WIDTH * (timelineHeaderRowCnt - 1)); 5519 } 5520 5521 /* 5522 TODO: test multiple levels of group-col nesting 5523 */ 5524 function buildPrintLayouts(hierarchy, hasNesting, expansions, expansionDefault) { 5525 const layouts = []; 5526 function processNodes(nodes, indent, colGroups) { 5527 let totalRowCnt = 0; 5528 for (const node of nodes) { 5529 if (node.resourceFields) { // resource 5530 const isExpanded = expansions[node.entity.id] ?? expansionDefault; 5531 const hasChildren = Boolean(node.children.length); 5532 layouts.push({ 5533 entity: node.entity, 5534 resourceFields: node.resourceFields, 5535 colGroups, 5536 isExpanded, 5537 hasChildren, 5538 indent, 5539 }); 5540 totalRowCnt += 1; 5541 totalRowCnt += processNodes(node.children, indent + 1, colGroups); 5542 } 5543 else if (node.pooledHeight) { // column-group 5544 totalRowCnt += processNodes(node.children, indent, colGroups.concat(node.entity)); 5545 } 5546 else { // row-group (FYI, can't be within a column-group) 5547 const isExpanded = expansions[createGroupId(node.entity)] ?? expansionDefault; 5548 const hasChildren = Boolean(node.children.length); 5549 layouts.push({ 5550 entity: node.entity, 5551 isExpanded, 5552 hasChildren, 5553 indent, 5554 }); 5555 totalRowCnt += 1; 5556 totalRowCnt += processNodes(node.children, indent + 1, colGroups); 5557 } 5558 } 5559 return totalRowCnt; 5560 } 5561 processNodes(hierarchy, hasNesting ? 1 : 0, []); 5562 return layouts; 5563 } 5564 5565 /* 5566 Just for print 5567 */ 5568 class ResourceGroupSubrows extends protectedApi.BaseComponent { 5569 render() { 5570 const { props, context } = this; 5571 const { options } = context; 5572 const { colGroups, colGroupStats } = props; 5573 const colWidths = props.colWidths || []; 5574 const colGrows = props.colGrows || []; 5575 return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: colGroups.map((colGroup, i) => { 5576 const stats = colGroupStats[i]; 5577 return (stats.render ? (jsxRuntime.jsx(ResourceGroupSubrow, { colSpec: colGroup.spec, fieldValue: colGroup.value, width: colWidths[i], grow: colGrows[i], borderStart: Boolean(i), borderBottom: stats.borderBottom })) : (jsxRuntime.jsx("div", { 5578 // TODO: make className DRY somehow? 5579 className: publicApi.joinClassNames(options.resourceRowClass, stats.borderBottom ? classNames__default["default"].borderOnlyB : classNames__default["default"].borderNone), style: { 5580 minWidth: 0, 5581 width: colWidths[i], 5582 flexGrow: colGrows[i], 5583 } }))); 5584 }) })); 5585 } 5586 } 5587 5588 const BG_HEIGHT = 100000; 5589 /* 5590 ARIA considerations: 5591 try to put correct role="" on things, just for readability, but it's pointless to pursue more 5592 complex things like indexes, level, complex-header relationships (via aria-labelledby) 5593 */ 5594 class ResourceTimelineLayoutPrint extends protectedApi.BaseComponent { 5595 constructor() { 5596 super(...arguments); 5597 // memoized 5598 this.buildPrintLayouts = protectedApi.memoize(buildPrintLayouts); 5599 this.compileColWidths = protectedApi.memoize(compileColWidths); 5600 } 5601 render() { 5602 let { props, context } = this; 5603 let { dateProfile } = props; 5604 let { options, viewSpec } = context; 5605 const { borderlessX, borderlessTop, borderlessBottom } = protectedApi.computeViewBorderless(options); 5606 const { tDateProfile, todayRange, nowDate, hasNesting } = props; 5607 const { slotWidth, timeCanvasWidth } = props; 5608 const { splitProps, bgSlicedProps, timeAreaOffset } = props; 5609 const { superHeaderRendering, groupColCnt, colSpecs } = props;
5610 const [colWidths, colGrows, spreadsheetCanvasWidth] = this.compileColWidths(props.spreadsheetColWidthConfigs, props.spreadsheetColWidths); 5611 const { cellRows } = tDateProfile; 5612 const { resourceHierarchy } = props; 5613 let printLayouts = this.buildPrintLayouts(resourceHierarchy, hasNesting, props.resourceEntityExpansions, options.resourcesInitiallyExpanded); 5614 if (printLayouts.length > options.printMaxRows) { 5615 printLayouts = printLayouts.slice(0, options.printMaxRows); 5616 protectedApi.warn(`Rows truncated for print. Only included the first ${options.printMaxRows} rows.`); 5617 } 5618 return (jsxRuntime.jsxs(protectedApi.ViewContainer, { viewSpec: viewSpec, attrs: { 5619 role: hasNesting ? 'treegrid' : 'grid', // TODO: DRY 5620 'aria-label': props.labelStr, 5621 'aria-labelledby': props.labelId, 5622 }, className: publicApi.joinClassNames(props.className, protectedApi.generateClassName(options.tableClass, { 5623 borderlessX, 5624 borderlessTop, 5625 borderlessBottom, 5626 multiMonthColumns: 0, 5627 }), classNames__default["default"].printRoot), children: [jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableHeaderClass, { 5628 isSticky: false, 5629 borderlessX, 5630 borderlessTop, 5631 borderlessBottom, 5632 multiMonthColumns: 0, 5633 }), classNames__default["default"].printHeader), children: [jsxRuntime.jsxs("div", { className: classNames__default["default"].flexRow, children: [jsxRuntime.jsxs("div", { role: 'rowgroup', className: classNames__default["default"].flexCol, style: { width: props.spreadsheetWidth }, children: [Boolean(superHeaderRendering) && (jsxRuntime.jsx("div", { role: 'row', className: publicApi.joinClassNames(options.resourceHeaderRowClass, classNames__default["default"].flexRow, classNames__default["default"].grow, classNames__default["default"].borderOnlyB), children: jsxRuntime.jsx(SuperHeaderCell, { renderHooks: superHeaderRendering, indent: hasNesting && !groupColCnt /* group-cols are leftmost, making expander alignment irrelevant */, colSpan: colSpecs.length, indentWidth: props.indentWidth }) })), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].grow, classNames__default["default"].crop), children: jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].grow, // height-grow, for matching height with timeline axis 5634 classNames__default["default"].flexCol), style: { minWidth: spreadsheetCanvasWidth }, children: jsxRuntime.jsx(HeaderRow, { colSpecs: colSpecs, colWidths: colWidths, colGrows: colGrows, indent: hasNesting, indentWidth: props.indentWidth }) }) })] }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.resourceColumnDividerClass) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].liquid, classNames__default["default"].flexRow, classNames__default["default"].crop), children: jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].rel), style: { 5635 width: timeCanvasWidth, 5636 insetInlineStart: -timeAreaOffset, 5637 }, children: cellRows.map((cells, rowIndex) => { 5638 const rowLevel = cellRows.length - rowIndex - 1; 5639 return (jsxRuntime.jsx(TimelineHeaderRow, { dateProfile: props.dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, rowLevel: rowLevel, cells: cells, slotWidth:
5639slotWidth }, rowLevel)); 5640 }) }) })] }), jsxRuntime.jsx("div", { className: protectedApi.generateClassName(options.slotHeaderDividerClass, { 5641 inTableHeader: true, 5642 options: { dayMinWidth: options.dayMinWidth }, 5643 }) })] }), jsxRuntime.jsxs("div", { role: 'rowgroup', className: publicApi.joinClassNames(protectedApi.generateClassName(options.tableBodyClass, { 5644 borderlessX, 5645 borderlessTop, 5646 borderlessBottom, 5647 multiMonthColumns: 0, 5648 }), 5649 // leave display:block for print! 5650 classNames__default["default"].rel, classNames__default["default"].crop), children: [jsxRuntime.jsxs("div", { "aria-hidden": true, className: publicApi.joinClassNames(classNames__default["default"].fillY, classNames__default["default"].flexRow), style: { 5651 // HACK for print where header-height prevents absolutely-positioned events 5652 // from falling short in height for subsequent pages 5653 height: BG_HEIGHT, 5654 // TODO: add the divider width too!!! 5655 insetInlineStart: props.spreadsheetWidth, 5656 insetInlineEnd: 0, 5657 }, children: [jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.resourceColumnDividerClass, classNames__default["default"].invisible) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].liquidX, classNames__default["default"].crop, classNames__default["default"].rel), children: jsxRuntime.jsxs("div", { className: classNames__default["default"].fillY, style: { 5658 width: timeCanvasWidth, 5659 height: BG_HEIGHT, // HACK 5660 insetInlineStart: -timeAreaOffset, 5661 }, children: [jsxRuntime.jsx(TimelineSlats, { dateProfile: dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, height: BG_HEIGHT, 5662 // dimensions 5663 slotWidth: slotWidth }), jsxRuntime.jsx(TimelineBg, { tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, 5664 // content 5665 bgEventSegs: bgSlicedProps.bgEventSegs, businessHourSegs: null, dateSelectionSegs: null, eventResizeSegs: null, 5666 // dimensions 5667 slotWidth: slotWidth })] }) })] }), printLayouts.map((printLayout, rowIndex0) => { 5668 const isNotLast = rowIndex0 < printLayouts.length - 1; 5669 if (printLayout.colGroups) { 5670 const resource = printLayout.entity; 5671 const colGroupStats = createColGroupStats(printLayout, printLayouts[rowIndex0 - 1], printLayouts[rowIndex0 + 1]); 5672 return (jsxRuntime.jsxs("div", { role: 'row', "aria-level": hasNesting ? printLayout.indent : undefined, "aria-expanded": printLayout.hasChildren 5673 ? printLayout.isExpanded 5674 : undefined, className: publicApi.joinClassNames(classNames__default["default"].flexRow, classNames__default["default"].breakInsideAvoid), children: [jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, // mimics what non-print scrollpane does 5675 classNames__default["default"].crop), style: { width: props.spreadsheetWidth }, children: jsxRuntime.jsxs("div", { className: publicApi.joinClassNames(classNames__default["default"].grow, // he
5675ight-grow, for matching tall timeline heights 5676 classNames__default["default"].flexRow), style: { minWidth: spreadsheetCanvasWidth }, children: [jsxRuntime.jsx(ResourceGroupSubrows // usually does NOT have bottom-line 5677 , { colGroups: printLayout.colGroups, colGroupStats: colGroupStats, colWidths: colWidths, colGrows: colGrows }), jsxRuntime.jsx(ResourceSubrow // almost always has bottom-line 5678 , { resource: resource, resourceFields: printLayout.resourceFields, indent: printLayout.indent, hasChildren: printLayout.hasChildren, isExpanded: printLayout.isExpanded, colStartIndex: groupColCnt, colSpecs: colSpecs, colWidths: colWidths, colGrows: colGrows, borderStart: Boolean(groupColCnt), borderBottom: isNotLast, indentWidth: props.indentWidth, totalX // set width and flexgrow on this subrow 5679 : true })] }) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.resourceColumnDividerClass) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].flexCol, classNames__default["default"].crop, classNames__default["default"].liquid), children: jsxRuntime.jsx(ResourceLane, { ...splitProps[resource.id], className: publicApi.joinClassNames(classNames__default["default"].rel, classNames__default["default"].grow), resource: resource, dateProfile: dateProfile, tDateProfile: tDateProfile, nowDate: nowDate, todayRange: todayRange, businessHours: null, dateSelection: null, eventDrag: null, eventResize: null, width: timeCanvasWidth, slotWidth: slotWidth, insetInlineStart: -timeAreaOffset, borderBottom: isNotLast }) })] }, resource.id)); 5680 } 5681 else { 5682 const group = printLayout.entity; 5683 const groupKey = createGroupId(group); 5684 return (jsxRuntime.jsxs("div", { role: 'row', "aria-expanded": printLayout.isExpanded, className: publicApi.joinClassNames(classNames__default["default"].flexRow, classNames__default["default"].breakInsideAvoid), children: [jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].crop, classNames__default["default"].flexRow), style: { width: props.spreadsheetWidth }, children: jsxRuntime.jsx(ResourceGroupHeaderSubrow, { group: group, isExpanded: printLayout.isExpanded, colSpan: props.colSpecs.length, borderBottom: isNotLast, indentWidth: props.indentWidth }) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(options.resourceColumnDividerClass) }), jsxRuntime.jsx("div", { className: publicApi.joinClassNames(classNames__default["default"].crop, classNames__default["default"].flexRow, classNames__default["default"].liquid), children: jsxRuntime.jsx(GroupLane, { group: group, borderBottom: isNotLast }) })] }, groupKey)); 5685 } 5686 })] })] })); 5687 } 5688 } 5689 function createColGroupStats(currentLayout, prevLayout, nextLayout) { 5690 return currentLayout.colGroups.map((colGroup, colGroupI) => ({ 5691 render: !prevLayout || 5692 !prevLayout.colGroups || 5693 prevLayout.colGroups[colGroupI] !== colGroup, 5694 borderBottom: nextLayout && (!nextLayout.colGroups || 5695 nextLayout.colGroups[colGroupI] !== colGroup), 5696 })); 5697 } 5698 function compileColWidths(colWidthConfigs, colWidths) { 5699 if (colWidths != null) { 5700 return flexifyDimConfigs(colWidthConfigs, colWidths); 5701 } 5702 return [undefined, undefined, undefined]; 5703 } 5704 5705 const SPREADSHEET_COL_MIN_WIDTH = 50; 5706 function processColOptions(options) { 5707 let allColSpecs = options.resourceColumns || []; 5708 let superHeaderRendering = null; 5709 if (!allColSpecs.length) { 5710 allColSpecs.push({ 5711 headerDefault: () => 'Resources', // TODO: view.defaults 5712 }); 5713 } 5714 else if (options.resourceColumnHeaderContent) { // weird way to determine if content 5715 superHeaderRendering = { 5716 headerClass: options.resourceColumnHeaderClass, 5717 headerInnerClass: options.resourceColumnHeaderInnerClass, 5718 headerContent: options.resourceColumnHeaderContent, 5719 headerDidMount: options.resourceColumnHeaderDidMount, 5720 headerWillUnmount: options.resourceColumnHeaderWillUnmount, 5721 }; 5722 } 5723 let resourceColSpecs = []; 5724 let groupColSpecs = []; // part of the colSpecs, but filtered out in order to put first 5725 let groupSpecs = []; 5726 let groupRowDepth = 0; 5727 let foundMainColSpec = false; 5728 for (let colSpec of allColSpecs) { 5729 (colSpec.group ? groupColSpecs : resourceColSpecs).push({ 5730 ...colSpec, 5731 isMain: ((!colSpec.group && !foundMainColSpec) 5732 ? (foundMainColSpec = true) 5733 : false), 5734 headerClass: protectedApi.joinFuncishClassNames(options.resourceColumnHeaderClass, colSpec.headerClass, 'resourceColumnHeaderClass'), 5735 headerInnerClass: protectedApi.joinFuncishClassNames(options.resourceColumnHeaderInnerClass, colSpec.headerInnerClass, 'resourceColumnHeaderInnerClass'), 5736 headerResizerClass: protectedApi.joinFuncishClassNames(options.resourceColumnResizerClass, colSpec.headerResizerClass, 'resourceColumnResizerClass'), 5737 headerContent: protectedApi.mergeContentInjectors(options.resourceColumnHeaderContent, colSpec.headerContent), 5738 headerDidMount: protectedApi.mergeLifecycleCallbacks(options.resourceColumnHeaderDidMount, colSpec.headerDidMount), 5739 headerWillUnmount: protectedApi.mergeLifecycleCallbacks(options.resourceColumnHeaderWillUnmount, colSpec.headerWillUnmount), 5740 cellClass: protectedApi.joinFuncishClassNames(options.resourceCellClass, colSpec.cellClass, 'resourceCellClass'), 5741 cellInnerClass: protectedApi.joinFuncishClassNames(options.resourceCellInnerClass, colSpec.cellInnerClass, 'resourceCellInnerClass'), 5742 cellContent: protectedApi.mergeContentInjectors(options.resourceCellContent, colSpec.cellContent), 5743 cellDidMount: protectedApi.mergeLifecycleCallbacks(options.resourceCellDidMount, colSpec.cellDidMount), 5744 cellWillUnmount: protectedApi.mergeLifecycleCallbacks(options.resourceCellWillUnmount, colSpec.cellWillUnmount), 5745 }); 5746 } 5747 if (groupColSpecs.length) { 5748 groupSpecs = groupColSpecs; 5749 } 5750 else { 5751 groupRowDepth = 1; 5752 let hGroupField = options.resourceGroupField; 5753 if (hGroupField) { 5754 groupSpecs.push({ 5755 field: hGroupField, 5756 labelClass: options.resourceGroupHeaderClass, 5757 labelInnerClass: options.resourceGroupHeaderInnerClass, 5758 labelContent: options.resourceGroupHeaderContent, 5759 labelDidMount: options.resourceGroupHeaderDidMount, 5760 labelWillUnmount: options.resourceGroupHeaderWillUnmount, 5761 laneClass: options.resourceGroupLaneClass, 5762 laneInnerClass: options.resourceGroupLaneInnerClass, 5763 laneContent: options.resourceGroupLaneContent, 5764 laneDidMount: options.resourceGroupLaneDidMount, 5765 laneWillUnmount: options.resourceGroupLaneWillUnmount, 5766 }); 5767 } 5768 } 5769 let allOrderSpecs = options.resourceOrder || DEFAULT_RESOURCE_ORDER; 5770 let plainOrderSpecs = []; 5771 for (let orderSpec of allOrderSpecs) { 5772 let isGroup = false; 5773 for (let groupSpec of groupSpecs) { 5774 if (groupSpec.field === orderSpec.field) { 5775 groupSpec.order = orderSpec.order; // -1, 0, 1 5776 isGroup = true; 5777 break; 5778 } 5779 } 5780 if (!isGroup) { 5781 plainOrderSpecs.push(orderSpec); 5782 } 5783 } 5784 const colSpecs = groupColSpecs.concat(resourceColSpecs); 5785 const colWidthConfigs = colSpecs.map((colSpec) => (parseSiblingDimConfig(colSpec.width, 5786 /* grow = */ undefined, SPREADSHEET_COL_MIN_WIDTH))); 5787 ensureDimConfigsGrow(colWidthConfigs); 5788 return { 5789 groupSpecs, 5790 groupRowDepth, 5791 orderSpecs: plainOrderSpecs, 5792 colSpecs, 5793 groupColCnt: groupColSpecs.length, 5794 colWidthConfigs, 5795 superHeaderRendering, 5796 }; 5797 } 5798 5799 class ResourceTimelineView extends protectedApi.DateComponent { 5800 constructor() { 5801 super(...arguments); 5802 this.state = {}; 5803 // memoized 5804 this.buildTimelineDateProfile = protectedApi.memoize(buildTimelineDateProfile); 5805 this.processColOptions = protectedApi.memoize(processColOptions);
5806 this.buildResourceHierarchy = protectedApi.memoize(buildResourceHierarchy); 5807 this.computeSlotWidth = protectedApi.memoize(computeSlotWidth); 5808 this.computeHasNesting = protectedApi.memoize(computeHasNesting); 5809 this.computeHasResourceBusinessHours = protectedApi.memoize(computeHasResourceBusinessHours); 5810 // ref 5811 this.scrollRef = compat.createRef(); 5812 this.spreadsheetResizedWidthRef = compat.createRef(); // the CSS dimension. could be percent 5813 this.resourceSplitter = new ResourceSplitter(); 5814 this.bgSlicer = new TimelineLaneSlicer(); 5815 this.handleColResize = (colIndex, newWidth) => { 5816 if (this._isUnmounting) 5817 return; 5818 const colWidthOverrides = resizeSiblingDimConfig(this.spreadsheetColWidthConfigs, this.spreadsheetColWidths, this.state.spreadsheetClientWidth, colIndex, newWidth); 5819 this.setState({ colWidthOverrides }); 5820 }; 5821 this.handleSpreadsheetClientWidth = (spreadsheetClientWidth) => { 5822 if (this._isUnmounting) 5823 return; 5824 if (spreadsheetClientWidth != null) { 5825 this.setState({ spreadsheetClientWidth }); 5826 } 5827 }; 5828 this.handleTimeClientWidth = (timeClientWidth) => { 5829 if (this._isUnmounting) 5830 return; 5831 if (timeClientWidth != null) { 5832 this.setState({ timeClientWidth }); 5833 } 5834 }; 5835 this.handleSlotInnerWidth = (slotInnerWidth) => { 5836 if (this._isUnmounting) 5837 return; 5838 if (slotInnerWidth != null) { 5839 this.setState({ slotInnerWidth }); 5840 } 5841 }; 5842 this.handleExpanderEl = (expanderEl) => { 5843 if (this.disconnectExpanderWidth) { 5844 this.disconnectExpanderWidth(); 5845 this.disconnectExpanderWidth = undefined; 5846 } 5847 if (expanderEl) { 5848 this.disconnectExpanderWidth = protectedApi.watchWidth(expanderEl, (width) => { 5849 if (this._isUnmounting) 5850 return; 5851 this.setState({ expanderWidth: width }); 5852 }); 5853 } 5854 }; 5855 } 5856 render() { 5857 let { props, state, context } = this; 5858 let { dateProfile } = props; 5859 let { options } = context; 5860 /* date */ 5861 let tDateProfile = this.buildTimelineDateProfile(dateProfile, context.dateEnv, options, context.dateProfileGenerator); 5862 let { unit: timerUnit, value: timeUnitValue } = protectedApi.greatestDurationDenominator(tDateProfile.slotDuration); 5863 /* table settings */ 5864 let { groupSpecs, groupRowDepth, orderSpecs, colSpecs, groupColCnt, colWidthConfigs: initialColWidthConfigs, superHeaderRendering, } = this.processColOptions(context.options); 5865 /* spreadsheet col widths */ 5866 let spreadsheetColWidthConfigs = state.colWidthOverrides || initialColWidthConfigs; 5867 let [spreadsheetColWidths, spreadsheetCanvasWidth] = state.spreadsheetClientWidth != null 5868 ? pixelizeDimConfigs(spreadsheetColWidthConfigs, state.spreadsheetClientWidth) 5869 : [undefined, undefined]; 5870 this.spreadsheetColWidthConfigs = spreadsheetColWidthConfigs; 5871 this.spreadsheetColWidths = spreadsheetColWidths; 5872 /* table hierarchy */ 5873 let resourceHierarchy = this.buildResourceHierarchy(props.resourceStore, orderSpecs, groupSpecs, groupRowDepth); 5874 let hasNesting = this.computeHasNesting(resourceHierarchy); 5875 /* table positions */ 5876 let [timeCanvasWidth, slotWidth, slotLiquid] = this.computeSlotWidth(tDateProfile.slotCnt, tDateProfile.slotsPerLabel, options.slotMinWidth, state.slotInnerWidth, // is ACTUALLY the last-level label width. rename? 5877 state.timeClientWidth);
5878 /* event display */ 5879 let splitProps = this.resourceSplitter.splitProps(props); 5880 let bgLaneProps = splitProps['']; 5881 let bgSlicedProps = this.bgSlicer.sliceProps(bgLaneProps, dateProfile, tDateProfile.isTimeScale ? null : options.nextDayThreshold, context, // wish we didn't need to pass in the rest of these args... 5882 dateProfile, context.dateProfileGenerator, tDateProfile, context.dateEnv); 5883 /* business hour display */ 5884 let hasResourceBusinessHours = this.computeHasResourceBusinessHours(resourceHierarchy); 5885 let fallbackBusinessHours = hasResourceBusinessHours ? props.businessHours : null; 5886 return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(protectedApi.NowTimer, { unit: timerUnit, unitValue: timeUnitValue, children: (nowDate, todayRange) => { 5887 const baseProps = { 5888 className: props.className, 5889 labelId: props.labelId, 5890 labelStr: props.labelStr, 5891 tDateProfile, 5892 dateProfile, 5893 resourceHierarchy, 5894 resourceEntityExpansions: props.resourceEntityExpansions, 5895 hasNesting, 5896 nowDate, 5897 todayRange, 5898 colSpecs, 5899 groupColCnt, 5900 superHeaderRendering, 5901 splitProps, 5902 bgSlicedProps, 5903 eventSelection: props.eventSelection, 5904 hasResourceBusinessHours, 5905 fallbackBusinessHours, 5906 slotWidth, 5907 timeCanvasWidth, 5908 spreadsheetColWidths, 5909 }; 5910 return props.forPrint ? (jsxRuntime.jsx(ResourceTimelineLayoutPrint, { ...baseProps, spreadsheetWidth: this.spreadsheetResizedWidthRef.current ?? 5911 options.resourceColumnsWidth, spreadsheetColWidthConfigs: spreadsheetColWidthConfigs, timeAreaOffset: this.scrollRef.current.x /* for simulating horizontal scroll */, indentWidth: state.expanderWidth })) : (jsxRuntime.jsx(ResourceTimelineLayoutNormal, { ...baseProps, slotLiquid: slotLiquid, timeClientWidthRef: this.handleTimeClientWidth, slotInnerWidthRef: this.handleSlotInnerWidth, initialSpreadsheetWidth: this.spreadsheetResizedWidthRef.current ?? 5912 options.resourceColumnsWidth, spreadsheetCanvasWidth: spreadsheetCanvasWidth, initialScroll: this.scrollRef.current /* for reviving after print-view */, 5913 // refs 5914 spreadsheetResizedWidthRef: this.spreadsheetResizedWidthRef, spreadsheetClientWidthRef: this.handleSpreadsheetClientWidth, scrollRef: this.scrollRef, 5915 // handlers 5916 onColResize: this.handleColResize, indentWidth: state.expanderWidth })); 5917 } }), jsxRuntime.jsx(ResourceExpander // for probing size 5918 , { isExpanded: true, elRef: this.handleExpanderEl, className: classNames__default["default"].offscreen })] })); 5919 } 5920 componentDidMount() { 5921 this._isUnmounting = false; 5922 } 5923 componentWillUnmount() { 5924 this._isUnmounting = true; 5925 } 5926 } 5927 ResourceTimelineView.addStateEquality({ 5928 colWidthOverrides: protectedApi.isArraysEqual, 5929 }); 5930 function computeHasResourceBusinessHours(resourceHierarchy) { 5931 for (const node of resourceHierarchy) { 5932 if ((node.resourceFields && node.entity.businessHours) || 5933 computeHasResourceBusinessHours(node.children)) { 5934 return true; 5935 } 5936 } 5937 return false; 5938 } 5939 function computeHasNesting(resourceHierarchy) { 5940 for (const node of resourceHierarchy) { 5941 if ((node.children.length && !node.pooledHeight) || // has children, but NOT a col-spanning group 5942 computeHasNesting(node.children)) { 5943 return true; 5944 } 5945 } 5946 return false; 5947 } 5948 5949 var resourceTimelinePlugin = { 5950 name: 'resource-timeline', 5951 premiumReleaseDate: '2026-07-24', 5952 deps: [ 5953 premiumCommonPlugin, 5954 resourcePlugin, 5955 timelinePlugin, 5956 ], 5957 initialView: 'resourceTimelineDay', 5958 views: { 5959 resourceTimeline: { 5960 type: 'timeline', // inherit configuration 5961 component: ResourceTimelineView, 5962 needsResourceData: true, 5963 resourceColumnsWidth: '30%', 5964 resourcesInitiallyExpanded: true, 5965 eventResizableFromStart: true, // TODO: not DRY with this same setting in the main timeline config 5966 }, 5967 resourceTimelineDay: { 5968 type: 'resourceTimeline', 5969 duration: { days: 1 }, 5970 }, 5971 resourceTimelineWeek: { 5972 type: 'resourceTimeline', 5973 duration: { weeks: 1 }, 5974 }, 5975 resourceTimelineMonth: { 5976 type: 'resourceTimeline', 5977 duration: { months: 1 }, 5978 }, 5979 resourceTimelineYear: { 5980 type: 'resourceTimeline', 5981 duration: { years: 1 }, 5982 }, 5983 }, 5984 }; 5985 5986 var scrollGridPlugin = { 5987 name: 'scrollgrid', 5988 premiumReleaseDate: '2026-07-24', 5989 deps: [premiumCommonPlugin], 5990 scrollerSyncerClass: ScrollerSyncer 5991 }; 5992
5993 let contexts = []; 5994 function contextInit(context) { 5995 if (!contexts.length) { 5996 attachGlobalHandlers(); 5997 } 5998 contexts.push(context); 5999 context.calendarApi.on('_unmount', () => { 6000 protectedApi.removeExact(contexts, context); 6001 if (!contexts.length) { 6002 removeGlobalHandlers(); 6003 } 6004 }); 6005 } 6006 function attachGlobalHandlers() { 6007 window.addEventListener('beforeprint', handleBeforePrint); 6008 window.addEventListener('afterprint', handleAfterPrint); 6009 /* 6010 // for testing 6011 let forPrint = false 6012 document.addEventListener('keypress', (ev) => { 6013 if (ev.key === 'p') { 6014 forPrint = !forPrint 6015 if (forPrint) { 6016 handleBeforePrint() 6017 } else { 6018 handleAfterPrint() 6019 } 6020 } 6021 }) 6022 */ 6023 } 6024 function removeGlobalHandlers() { 6025 window.removeEventListener('beforeprint', handleBeforePrint); 6026 window.removeEventListener('afterprint', handleAfterPrint); 6027 } 6028 function handleBeforePrint() { 6029 for (let context of contexts) { 6030 context.emitter.trigger('_beforeprint'); 6031 } 6032 } 6033 function handleAfterPrint() { 6034 for (let context of contexts) { 6035 context.emitter.trigger('_afterprint'); 6036 } 6037 } 6038 6039 var adaptivePlugin = { 6040 name: 'adaptive', 6041 premiumReleaseDate: '2026-07-24', 6042 deps: [premiumCommonPlugin], 6043 contextInit, 6044 }; 6045 6046 publicApi.globalPlugins.push(resourceDayGridPlugin, resourceTimeGridPlugin, resourceTimelinePlugin, scrollGridPlugin, timelinePlugin, adaptivePlugin); 6047 6048})(FullCalendar.ProtectedApi, FullCalendar.DayGrid, FullCalendar.PreactJSXRuntime, FullCalendar, FullCalendar.ProtectedStyles, FullCalendar.Preact, FullCalendar.TimeGrid);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.