@bryntum/scheduler-react-thin 7.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/lib/BryntumEventColorField.d.ts +232 -0
- package/lib/BryntumEventColorField.js +169 -0
- package/lib/BryntumEventColorField.js.map +1 -0
- package/lib/BryntumProjectCombo.d.ts +268 -0
- package/lib/BryntumProjectCombo.js +203 -0
- package/lib/BryntumProjectCombo.js.map +1 -0
- package/lib/BryntumResourceCombo.d.ts +268 -0
- package/lib/BryntumResourceCombo.js +203 -0
- package/lib/BryntumResourceCombo.js.map +1 -0
- package/lib/BryntumResourceFilter.d.ts +215 -0
- package/lib/BryntumResourceFilter.js +154 -0
- package/lib/BryntumResourceFilter.js.map +1 -0
- package/lib/BryntumScheduler.d.ts +2039 -0
- package/lib/BryntumScheduler.js +642 -0
- package/lib/BryntumScheduler.js.map +1 -0
- package/lib/BryntumSchedulerBase.d.ts +2038 -0
- package/lib/BryntumSchedulerBase.js +641 -0
- package/lib/BryntumSchedulerBase.js.map +1 -0
- package/lib/BryntumSchedulerDatePicker.d.ts +314 -0
- package/lib/BryntumSchedulerDatePicker.js +216 -0
- package/lib/BryntumSchedulerDatePicker.js.map +1 -0
- package/lib/BryntumSchedulerProjectModel.d.ts +91 -0
- package/lib/BryntumSchedulerProjectModel.js +98 -0
- package/lib/BryntumSchedulerProjectModel.js.map +1 -0
- package/lib/BryntumTimelineHistogram.d.ts +1185 -0
- package/lib/BryntumTimelineHistogram.js +448 -0
- package/lib/BryntumTimelineHistogram.js.map +1 -0
- package/lib/BryntumUndoRedo.d.ts +190 -0
- package/lib/BryntumUndoRedo.js +152 -0
- package/lib/BryntumUndoRedo.js.map +1 -0
- package/lib/BryntumViewPresetCombo.d.ts +216 -0
- package/lib/BryntumViewPresetCombo.js +158 -0
- package/lib/BryntumViewPresetCombo.js.map +1 -0
- package/lib/WrapperHelper.d.ts +26 -0
- package/lib/WrapperHelper.js +569 -0
- package/lib/WrapperHelper.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -0
- package/license.pdf +0 -0
- package/licenses.md +310 -0
- package/package.json +25 -0
- package/src/BryntumEventColorField.tsx +996 -0
- package/src/BryntumProjectCombo.tsx +1233 -0
- package/src/BryntumResourceCombo.tsx +1236 -0
- package/src/BryntumResourceFilter.tsx +931 -0
- package/src/BryntumScheduler.tsx +5184 -0
- package/src/BryntumSchedulerBase.tsx +5182 -0
- package/src/BryntumSchedulerDatePicker.tsx +1365 -0
- package/src/BryntumSchedulerProjectModel.tsx +424 -0
- package/src/BryntumTimelineHistogram.tsx +3427 -0
- package/src/BryntumUndoRedo.tsx +886 -0
- package/src/BryntumViewPresetCombo.tsx +915 -0
- package/src/WrapperHelper.tsx +1125 -0
- package/src/index.ts +15 -0
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React widget helper
|
|
3
|
+
*/
|
|
4
|
+
import React, { ReactElement } from 'react';
|
|
5
|
+
import ReactDOM, { flushSync } from 'react-dom';
|
|
6
|
+
import { DomHelper, StringHelper, Widget } from '@bryntum/core-thin';
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
|
|
10
|
+
interface Window {
|
|
11
|
+
bryntum: {
|
|
12
|
+
isTestEnv?: boolean
|
|
13
|
+
react?: {
|
|
14
|
+
isReactElement?: (element: any) => boolean
|
|
15
|
+
handleReactElement?: (widget: Widget, element: any) => void
|
|
16
|
+
handleReactHeaderElement?: (column: { grid: any; id: string }, headerElement: HTMLElement, html: any) => void
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Development warning. Showed when environment is set to 'development'
|
|
24
|
+
* @param {String} clsName react component instance
|
|
25
|
+
* @param {String} msg console message
|
|
26
|
+
*/
|
|
27
|
+
function devWarning(clsName: string, msg: string): void {
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
if (window.bryntum?.isTestEnv || process.env.NODE_ENV === 'development') {
|
|
30
|
+
console.warn(
|
|
31
|
+
`Bryntum${clsName}Component development warning!\n${msg}\n` +
|
|
32
|
+
'Please check React integration guide: https://bryntum.com/products/scheduler/docs/guide/Scheduler/integration/react/guide'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function devWarningContainer(clsName: string, containerParam: string): void {
|
|
38
|
+
devWarning(
|
|
39
|
+
clsName,
|
|
40
|
+
`Using "${containerParam}" parameter for configuration is not recommended.\n` +
|
|
41
|
+
"Widget is placed automatically inside it's container element.\n" +
|
|
42
|
+
`Solution: remove "${containerParam}" parameter from configuration.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function devWarningConfigProp(clsName: string, prop: string): void {
|
|
47
|
+
devWarning(
|
|
48
|
+
clsName,
|
|
49
|
+
`Using "${prop}" parameter for configuration is not recommended.\n` +
|
|
50
|
+
`Solution: Use separate parameter for each "${prop}" value to enable reactive updates of the API instance`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function devWarningProjectProp(clsName: string): void {
|
|
55
|
+
devWarning(
|
|
56
|
+
clsName,
|
|
57
|
+
'Using the "project" prop with inner store configurations is not recommended.\n' +
|
|
58
|
+
`Solution: Use an instance of the 'ProjectModel' class or the '<Bryntum${clsName}ProjectModel/>' component`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns `true` if the provided element is an instance of React Element.
|
|
64
|
+
* All React elements require an additional $$typeof: Symbol.for('react.element') field declared on the object for security reasons.
|
|
65
|
+
* The object, which React.createElement() return has $$typeof property equals to Symbol.for('react.element')
|
|
66
|
+
*
|
|
67
|
+
* Sources:
|
|
68
|
+
* https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html
|
|
69
|
+
* https://github.com/facebook/react/pull/4832
|
|
70
|
+
*
|
|
71
|
+
* @param {*} element
|
|
72
|
+
* @returns {Boolean}
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
function isReactElement(element: any): boolean {
|
|
76
|
+
return Boolean(element?.$$typeof === Symbol.for('react.element') || element?.$$typeof === Symbol.for('react.transitional.element'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates bryntum component config from react component
|
|
81
|
+
* @param {Object} reactInstance react component instance
|
|
82
|
+
* @returns {Object} config object
|
|
83
|
+
*/
|
|
84
|
+
function createConfig(reactInstance: any): object {
|
|
85
|
+
const
|
|
86
|
+
{ element, props, constructor } = reactInstance,
|
|
87
|
+
{ instanceClass, instanceName, isView } = constructor,
|
|
88
|
+
filter = (arr: any[]) => arr.filter(prop => props[prop] !== undefined),
|
|
89
|
+
configNames = filter(constructor.configNames || []),
|
|
90
|
+
propertyConfigNames = filter(constructor.propertyConfigNames || []),
|
|
91
|
+
propertyNames = filter(constructor.propertyNames || []),
|
|
92
|
+
featureNames = filter(constructor.featureNames || []),
|
|
93
|
+
bryntumConfig = {
|
|
94
|
+
adopt : undefined,
|
|
95
|
+
appendTo : undefined,
|
|
96
|
+
href : undefined,
|
|
97
|
+
reactComponent : reactInstance,
|
|
98
|
+
listeners : {},
|
|
99
|
+
features : {},
|
|
100
|
+
hasFrameworkRenderer : isView ? hasFrameworkRenderer : undefined,
|
|
101
|
+
processCellContent : isView ? processCellContent : undefined,
|
|
102
|
+
processCellEditor : isView ? processCellEditor : undefined,
|
|
103
|
+
processEventContent : isView ? processEventContent : undefined,
|
|
104
|
+
processTaskItemContent : isView ? processTaskItemContent : undefined,
|
|
105
|
+
processResourceHeader : isView ? processResourceHeader : undefined
|
|
106
|
+
} as any;
|
|
107
|
+
|
|
108
|
+
// Data store configs support reactive behavior
|
|
109
|
+
const isDataStoreConfig = (prop: any) => {
|
|
110
|
+
if (reactInstance.dataStores) {
|
|
111
|
+
const dataStoreNames = Object.values(reactInstance.dataStores);
|
|
112
|
+
return dataStoreNames.includes(prop) || dataStoreNames.includes(`${prop}Data`);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Assign configs. Skip properties
|
|
117
|
+
configNames
|
|
118
|
+
.concat(propertyConfigNames)
|
|
119
|
+
.concat(featureNames)
|
|
120
|
+
.forEach(prop => {
|
|
121
|
+
applyPropValue(bryntumConfig, prop, props[prop]);
|
|
122
|
+
if (['features', 'config'].includes(prop) && !isDataStoreConfig(prop)) {
|
|
123
|
+
devWarningConfigProp(instanceClass.$name, prop);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Warn when using project prop with inner stores configs
|
|
127
|
+
if (prop === 'project' && reactInstance.projectStores) {
|
|
128
|
+
// @ts-ignore
|
|
129
|
+
if (Object.values(reactInstance.dataStores).some(store => props[prop][store])) {
|
|
130
|
+
devWarningProjectProp(instanceClass.$name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Prepare watch arrays
|
|
136
|
+
reactInstance.configNames = configNames;
|
|
137
|
+
reactInstance.propertyNames = configNames
|
|
138
|
+
.concat(propertyNames)
|
|
139
|
+
.concat(propertyConfigNames)
|
|
140
|
+
.concat(featureNames);
|
|
141
|
+
|
|
142
|
+
// Handle inline data for stores
|
|
143
|
+
if (reactInstance.dataStores) {
|
|
144
|
+
Object.values<string>(reactInstance.dataStores).forEach((dataName: string) => {
|
|
145
|
+
if (props[dataName]) {
|
|
146
|
+
bryntumConfig[dataName] = props[dataName];
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Cleanup unused instance arrays
|
|
152
|
+
if (reactInstance.propertyConfigNames) {
|
|
153
|
+
delete reactInstance.propertyConfigNames;
|
|
154
|
+
}
|
|
155
|
+
if (reactInstance.featureNames) {
|
|
156
|
+
delete reactInstance.featureNames;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If component has no container specified in config then use adopt to Wrapper's element
|
|
160
|
+
const containerParam = ['adopt', 'appendTo', 'insertAfter', 'insertBefore'].find(
|
|
161
|
+
prop => bryntumConfig[prop]
|
|
162
|
+
);
|
|
163
|
+
if (!containerParam) {
|
|
164
|
+
if (instanceName === 'Button') {
|
|
165
|
+
// Button should always be <a> or <button> inside owner element
|
|
166
|
+
bryntumConfig.appendTo = element;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
bryntumConfig.adopt = element;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
devWarningContainer(instanceClass.$name, containerParam);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return bryntumConfig;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Applies property value to Bryntum config or instance.
|
|
181
|
+
* @param {Object} configOrInstance target object
|
|
182
|
+
* @param {String} prop property name
|
|
183
|
+
* @param {Object} value value
|
|
184
|
+
* @param {Boolean} isConfig config setting mode
|
|
185
|
+
*/
|
|
186
|
+
function applyPropValue(configOrInstance: any, prop: string, value: any, isConfig = true): void {
|
|
187
|
+
// Assigning React wrapper component instance
|
|
188
|
+
if (value?.current?.instance) {
|
|
189
|
+
value = value.current.instance;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (prop === 'features' && typeof value === 'object') {
|
|
193
|
+
Object.keys(value).forEach(key =>
|
|
194
|
+
applyPropValue(configOrInstance, `${key}Feature`, value[key], isConfig)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
else if (prop === 'config' && typeof value === 'object') {
|
|
198
|
+
Object.keys(value).forEach(key =>
|
|
199
|
+
applyPropValue(configOrInstance, key, value[key], isConfig)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
else if (prop === 'columns' && !isConfig) {
|
|
203
|
+
configOrInstance.columns = value;
|
|
204
|
+
}
|
|
205
|
+
else if (prop.endsWith('Feature')) {
|
|
206
|
+
const
|
|
207
|
+
{ features } = configOrInstance,
|
|
208
|
+
featureName = prop.replace('Feature', '');
|
|
209
|
+
if (isConfig) {
|
|
210
|
+
features[featureName] = value;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const feature = features[featureName];
|
|
214
|
+
if (feature) {
|
|
215
|
+
feature.setConfig(value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
configOrInstance[prop] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Creates bryntum Widget from react component
|
|
226
|
+
* @param {*} component react component instance
|
|
227
|
+
* @returns {*} widget object
|
|
228
|
+
*/
|
|
229
|
+
function createWidget(component: any): any {
|
|
230
|
+
const { instanceClass, isView } = component.constructor,
|
|
231
|
+
config = createConfig(component) as any,
|
|
232
|
+
instance = instanceClass.$name === 'Widget' ? Widget.create(config) : new instanceClass(config);
|
|
233
|
+
|
|
234
|
+
// Backwards compatibility for gridInstance, schedulerInstance etc.
|
|
235
|
+
if (isView) {
|
|
236
|
+
component[StringHelper.uncapitalize(instanceClass.$name) + 'Instance'] = instance;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isView) {
|
|
240
|
+
// Backwards compatibility for gridInstance, schedulerInstance etc.
|
|
241
|
+
component[StringHelper.uncapitalize(instanceClass.$name) + 'Instance'] = instance;
|
|
242
|
+
|
|
243
|
+
const subscribeStores = (storeInstance: { [x: string]: any }, stores: Record<string, unknown>) => {
|
|
244
|
+
if (stores) {
|
|
245
|
+
Object.keys(stores).forEach(storeName => {
|
|
246
|
+
const store = storeInstance[storeName];
|
|
247
|
+
if (store) {
|
|
248
|
+
// Default syncDataOnLoad to true if store is not configured with a readUrl (AjaxStore)
|
|
249
|
+
// and it doesn't belong to a project that has a loadUrl configured
|
|
250
|
+
if (store.syncDataOnLoad == null && !store.readUrl && !store.lazyLoad && (!store.crudManager || !store.crudManager.loadUrl)) {
|
|
251
|
+
store.syncDataOnLoad = true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
store.on('beforeRemove', (context: { records: any; removingAll: any }) => beforeRemoveRecords(component, context));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
subscribeStores(component.projectStores ? instance.project : instance, component.dataStores);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// To be able to detect data changes later
|
|
264
|
+
if (config['data']) {
|
|
265
|
+
instance.lastDataset = config['data'].slice();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return instance;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Calculates the portalId from passed ids
|
|
273
|
+
* @param {String|Number} id
|
|
274
|
+
* @param {String|Number} columnId
|
|
275
|
+
* @returns {String} portalId as `portal-${id}-${columnId}`
|
|
276
|
+
*/
|
|
277
|
+
function getPortalId(id: string | number, columnId: string | number): string {
|
|
278
|
+
return `portal-${id}-${columnId}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Delete portal and its container
|
|
283
|
+
* @param {*} component React Component, the wrapper itself
|
|
284
|
+
* @param {String} portalId As returned from getPortalId function
|
|
285
|
+
*/
|
|
286
|
+
function deletePortal(component: any, portalId: string): void {
|
|
287
|
+
const portal = component.state.portals.get(portalId);
|
|
288
|
+
if (portal) {
|
|
289
|
+
const portalContainer = portal.containerInfo;
|
|
290
|
+
|
|
291
|
+
// remove portal from Map
|
|
292
|
+
component.state.portals.delete(portalId);
|
|
293
|
+
|
|
294
|
+
// cleanup portal container
|
|
295
|
+
portalContainer.parentElement?.removeChild(portalContainer);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Release (now only delete) React portal hosted in this cell
|
|
301
|
+
* @param {*} component React Component, the wrapper itself
|
|
302
|
+
* @param {HTMLElement} cellElement The grid cell to be freed of the React portal
|
|
303
|
+
*/
|
|
304
|
+
function releaseReactCell(component: any, cellElement: any): void {
|
|
305
|
+
const { id, columnId, hasPortal } = cellElement._domData;
|
|
306
|
+
|
|
307
|
+
if (hasPortal) {
|
|
308
|
+
const portalId = getPortalId(id, columnId);
|
|
309
|
+
deletePortal(component, portalId);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Calls releaseReactCell that implements the cleanup
|
|
315
|
+
* @param {*} component React Component, the wrapper itself
|
|
316
|
+
* @param {Object} context
|
|
317
|
+
* @param {Core.data.Model[]} context.records Array of records that are going to be removed
|
|
318
|
+
*/
|
|
319
|
+
function beforeRemoveRecords(component: any, { records, removingAll }: { records: any[]; removingAll: boolean }): void {
|
|
320
|
+
const { instance } = component;
|
|
321
|
+
|
|
322
|
+
if (removingAll) {
|
|
323
|
+
[...component.state.portals.keys()].forEach(portalId => deletePortal(component, portalId));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
records.forEach(record => {
|
|
327
|
+
// grid.getRowById is not defined in Calendar
|
|
328
|
+
const row = instance.getRowById ? instance.getRowById(record.id) : undefined;
|
|
329
|
+
if (row) {
|
|
330
|
+
row.cells.forEach((cell: any) => {
|
|
331
|
+
releaseReactCell(component, cell);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Increments generation - necessary to trigger React updates
|
|
340
|
+
*/
|
|
341
|
+
function updateGeneration(component: any, thisTick = false, callback = () => {}): void {
|
|
342
|
+
const updateState = () => {
|
|
343
|
+
component.setState((currentState: { generation: number; portals: Map<string, ReactElement> }) => {
|
|
344
|
+
return {
|
|
345
|
+
...currentState,
|
|
346
|
+
generation : currentState.generation + 1
|
|
347
|
+
};
|
|
348
|
+
}, callback);
|
|
349
|
+
};
|
|
350
|
+
if (thisTick) {
|
|
351
|
+
// Update state in this tick
|
|
352
|
+
updateState();
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// React updates on next frame
|
|
356
|
+
requestAnimationFrame(updateState);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Component about to be updated, from changing a prop using state.
|
|
362
|
+
* React to it depending on what changed and prevent react from re-rendering our component.
|
|
363
|
+
* @param {*} component react component instance
|
|
364
|
+
* @param nextProps
|
|
365
|
+
* @param nextState
|
|
366
|
+
* @returns {Boolean}
|
|
367
|
+
*/
|
|
368
|
+
function shouldComponentUpdate(component: any, nextProps: Readonly<any>, nextState: Readonly<any>): boolean {
|
|
369
|
+
const { props, instance, propertyNames } = component;
|
|
370
|
+
|
|
371
|
+
propertyNames.forEach((prop: string) => {
|
|
372
|
+
if (props[prop] !== nextProps[prop]) {
|
|
373
|
+
// Check if property is not a config
|
|
374
|
+
applyPropValue(instance, prop, nextProps[prop], false);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Reflect JSX cell changes
|
|
379
|
+
return nextState?.generation !== component.state?.generation;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
*
|
|
384
|
+
* @param { Object } context
|
|
385
|
+
* @param { * } context.cellContent Content to be rendered in cell (set by renderer)
|
|
386
|
+
* @returns { Boolean } `true` if there is a React Renderer in this cell, `false` otherwise
|
|
387
|
+
*/
|
|
388
|
+
function hasFrameworkRenderer({ cellContent }: { cellContent: any }): boolean {
|
|
389
|
+
// @ts-ignore
|
|
390
|
+
return DomHelper.isReactElement(cellContent);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Hook called by instance when rendering cells within
|
|
395
|
+
* Row::renderCell(), creates portals for JSX supplied by renderers
|
|
396
|
+
* @param {Object} context
|
|
397
|
+
* @param {Object} context.rendererData Data passed from renderCell
|
|
398
|
+
* @param {Object} context.cellElementData Data passed from renderCell
|
|
399
|
+
*/
|
|
400
|
+
function processCellContent(this: { reactComponent: any; isExporting: boolean; $isCollapsing: boolean; }, { rendererData, cellElementData, rendererHtml }: {
|
|
401
|
+
rendererData: any
|
|
402
|
+
cellElementData: any
|
|
403
|
+
rendererHtml: any
|
|
404
|
+
}): void {
|
|
405
|
+
// Collect variables
|
|
406
|
+
const
|
|
407
|
+
component = this.reactComponent,
|
|
408
|
+
{ state, portalsCache, portalContainerClass } = component,
|
|
409
|
+
{ cellElement, column, record } = rendererData,
|
|
410
|
+
portalId = getPortalId(record.id, column.id),
|
|
411
|
+
renderElement = cellElement.querySelector(column.editTargetSelector) || cellElement;
|
|
412
|
+
|
|
413
|
+
// Do nothing if we have no place to render to
|
|
414
|
+
if (!renderElement) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
rendererHtml &&
|
|
419
|
+
// @ts-ignore
|
|
420
|
+
DomHelper.isReactElement(rendererHtml) &&
|
|
421
|
+
!record.meta.specialRow
|
|
422
|
+
) {
|
|
423
|
+
// Move React portal container out of the way if necessary
|
|
424
|
+
if (
|
|
425
|
+
renderElement.portalContainer &&
|
|
426
|
+
renderElement.portalContainer.dataset.portalId === portalId
|
|
427
|
+
) {
|
|
428
|
+
portalsCache.appendChild(renderElement.portalContainer);
|
|
429
|
+
renderElement.portalContainer = null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Try to get portal from the portals Map
|
|
433
|
+
let portal = state.portals.get(portalId),
|
|
434
|
+
forceFlushSync = false;
|
|
435
|
+
|
|
436
|
+
// Handle measuring
|
|
437
|
+
if (rendererData.isMeasuring) {
|
|
438
|
+
if (portal) {
|
|
439
|
+
// Remember the original parent of portal and the cell element width
|
|
440
|
+
const
|
|
441
|
+
portalContainer = portal.containerInfo,
|
|
442
|
+
parent = portalContainer.parentNode;
|
|
443
|
+
cellElement.style.width = 'auto'; // element is re-used, need to reset width
|
|
444
|
+
const cellElementWidth = cellElement.offsetWidth;
|
|
445
|
+
|
|
446
|
+
// Append portal to the provided cell and get width
|
|
447
|
+
cellElement.appendChild(portalContainer);
|
|
448
|
+
const width = portalContainer.offsetWidth;
|
|
449
|
+
|
|
450
|
+
// Move the portal back to its original container
|
|
451
|
+
parent.appendChild(portalContainer);
|
|
452
|
+
|
|
453
|
+
// Set width of the cell. It will be processed by Column code.
|
|
454
|
+
cellElement.style.width = `${width + cellElementWidth}px`;
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check if record changed, delete portal and its container if yes
|
|
460
|
+
if (portal && (portal.generation !== record.generation || this.isExporting || this.$isCollapsing)) {
|
|
461
|
+
deletePortal(component, portalId);
|
|
462
|
+
portal = null;
|
|
463
|
+
forceFlushSync = true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Cleanup renderElement - necessary for grouping feature
|
|
467
|
+
const childPortalContainer = renderElement.querySelector(`.${portalContainerClass}`);
|
|
468
|
+
if (childPortalContainer && childPortalContainer.dataset.portalId !== portalId) {
|
|
469
|
+
portalsCache.appendChild(childPortalContainer);
|
|
470
|
+
}
|
|
471
|
+
if (renderElement.textContent && renderElement === cellElement) {
|
|
472
|
+
renderElement.textContent = ''; // group title can be still here
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (portal) {
|
|
476
|
+
// Move portal container back to the cell if we have one
|
|
477
|
+
renderElement.appendChild(portal.containerInfo);
|
|
478
|
+
renderElement.portalContainer = portal.containerInfo;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// Create new portal container
|
|
482
|
+
const portalContainer = DomHelper.append(renderElement, {
|
|
483
|
+
tag : 'div',
|
|
484
|
+
className : portalContainerClass,
|
|
485
|
+
dataset : { portalId } // for reference in tests
|
|
486
|
+
});
|
|
487
|
+
renderElement.portalContainer = portalContainer;
|
|
488
|
+
|
|
489
|
+
// Create a new portal in the portal container
|
|
490
|
+
portal = ReactDOM.createPortal(rendererHtml, portalContainer, portalId);
|
|
491
|
+
|
|
492
|
+
// Add the new portal to Map
|
|
493
|
+
state.portals.set(portalId, portal);
|
|
494
|
+
|
|
495
|
+
// Trigger React redraw
|
|
496
|
+
// Update cell synchronously when exporting or when forced to
|
|
497
|
+
if (forceFlushSync) {
|
|
498
|
+
flushSync(() => updateGeneration(component, true));
|
|
499
|
+
forceFlushSync = false;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
updateGeneration(component, this.isExporting);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Save data for use elsewhere
|
|
507
|
+
cellElementData.hasPortal = true;
|
|
508
|
+
portal.generation = record.generation;
|
|
509
|
+
}
|
|
510
|
+
else if (!rendererHtml && cellElementData.hasPortal) {
|
|
511
|
+
cellElement.portalContainer.remove();
|
|
512
|
+
cellElementData.hasPortal = false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Hook called by engine when requesting a cell editor
|
|
518
|
+
*/
|
|
519
|
+
function processCellEditor({ editor, field }: { editor: any; field: string }): boolean | object | undefined {
|
|
520
|
+
|
|
521
|
+
// @ts-ignore
|
|
522
|
+
const component = this.reactComponent;
|
|
523
|
+
|
|
524
|
+
// String etc. handled by feature, only care about fns returning React components here
|
|
525
|
+
if (!component || typeof editor !== 'function') {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Wrap React editor in an empty widget, to match expectations from CellEdit/Editor and make alignment
|
|
530
|
+
// etc. work out of the box
|
|
531
|
+
const wrapperWidget = new Widget({
|
|
532
|
+
// For editor to be hooked up to field correctly,
|
|
533
|
+
// @ts-ignore
|
|
534
|
+
name : field,
|
|
535
|
+
// Prevent editor from swallowing mouse events
|
|
536
|
+
allowMouseEvents : true,
|
|
537
|
+
// Used by container, pass on to overridden setter
|
|
538
|
+
assignValue(values: any, key: any, value: any) {
|
|
539
|
+
// @ts-ignore
|
|
540
|
+
this.setValue(value);
|
|
541
|
+
}
|
|
542
|
+
}) as any;
|
|
543
|
+
|
|
544
|
+
// Ref for accessing the React editor later
|
|
545
|
+
const widgetRef = React.createRef();
|
|
546
|
+
wrapperWidget['reactRef'] = widgetRef;
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
// @ts-ignore
|
|
551
|
+
const editorComponent = editor(widgetRef, this);
|
|
552
|
+
|
|
553
|
+
// @ts-ignore
|
|
554
|
+
if (!DomHelper.isReactElement(editorComponent)) {
|
|
555
|
+
throw new Error('Expect a React element');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let editorValidityChecked = false;
|
|
559
|
+
|
|
560
|
+
wrapperWidget.setValue = async(value: any) => {
|
|
561
|
+
const widget: any = widgetRef.current;
|
|
562
|
+
|
|
563
|
+
// It may happen that set is called before the widget is created
|
|
564
|
+
if (widget) {
|
|
565
|
+
if (!editorValidityChecked) {
|
|
566
|
+
const
|
|
567
|
+
cellMethods = ['setValue', 'getValue', 'isValid', 'focus'],
|
|
568
|
+
misses = cellMethods.filter(fn => !(fn in widget));
|
|
569
|
+
|
|
570
|
+
if (misses.length > 0) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`Missing method(s) ${misses.join(', ')} in ${
|
|
573
|
+
widget.constructor.name
|
|
574
|
+
}. Cell editors must ${cellMethods.join(', ')}`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
editorValidityChecked = true;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const context = wrapperWidget.owner['cellEditorContext'];
|
|
581
|
+
await widget.setValue(value, context);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
else {
|
|
585
|
+
wrapperWidget.firstValue = value;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Add getter/setter for value on the wrapper, relaying to getValue()/setValue() on the React editor
|
|
590
|
+
Object.defineProperty(wrapperWidget, 'value', {
|
|
591
|
+
enumerable : true,
|
|
592
|
+
|
|
593
|
+
configurable : true,
|
|
594
|
+
|
|
595
|
+
get() {
|
|
596
|
+
const widget: any = widgetRef.current;
|
|
597
|
+
return widget?.getValue();
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Add getter for isValid to the wrapper, mapping to isValid() on the React editor
|
|
602
|
+
Object.defineProperty(wrapperWidget, 'isValid', {
|
|
603
|
+
enumerable : true,
|
|
604
|
+
|
|
605
|
+
configurable : true,
|
|
606
|
+
|
|
607
|
+
get() {
|
|
608
|
+
const widget: any = widgetRef.current;
|
|
609
|
+
return widget?.isValid();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Override widgets focus handling, relaying it to focus() on the React editor
|
|
614
|
+
wrapperWidget.focus = () => {
|
|
615
|
+
const widget: any = widgetRef.current;
|
|
616
|
+
if (!widget) {
|
|
617
|
+
wrapperWidget.focusPending = true;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
widget.focus?.();
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// Create a portal, making the React editor belong to the React tree although displayed in a Widget
|
|
625
|
+
const portal = ReactDOM.createPortal(editorComponent, wrapperWidget.element);
|
|
626
|
+
|
|
627
|
+
wrapperWidget['reactPortal'] = portal;
|
|
628
|
+
|
|
629
|
+
const { state } = component;
|
|
630
|
+
// Store portal in state to let React keep track of it (inserted into the Bryntum component)
|
|
631
|
+
state.portals.set(`portal-${field}`, portal);
|
|
632
|
+
updateGeneration(component, true,
|
|
633
|
+
|
|
634
|
+
() => {
|
|
635
|
+
if (wrapperWidget.firstValue !== undefined) {
|
|
636
|
+
wrapperWidget.setValue(wrapperWidget.firstValue);
|
|
637
|
+
delete wrapperWidget.firstValue;
|
|
638
|
+
}
|
|
639
|
+
if (wrapperWidget.focusPending) {
|
|
640
|
+
wrapperWidget.focus();
|
|
641
|
+
delete wrapperWidget.focusPending;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
return { editor : wrapperWidget };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Creates portal in the widget contentElement and triggers its rendering.
|
|
650
|
+
// Called from Widget.updateHtml
|
|
651
|
+
function processWidgetContent({ reactElement, widget, reactComponent, contentElement }:
|
|
652
|
+
{ reactElement: any; widget: any; reactComponent: any; contentElement: any }): React.ReactPortal | undefined {
|
|
653
|
+
const
|
|
654
|
+
portals = reactComponent?.state?.portals,
|
|
655
|
+
portalId = widget?.id;
|
|
656
|
+
|
|
657
|
+
if (isReactElement(reactElement)) {
|
|
658
|
+
// Ensure the contentElement is cleared of any previously rendered plain HTML before rendering a React portal.
|
|
659
|
+
// This prevents mixing old HTML content with new React-rendered content, ensuring only the JSX is displayed.
|
|
660
|
+
if (!portals.has(portalId) && widget.html) {
|
|
661
|
+
widget.contentElement.innerHTML = '';
|
|
662
|
+
}
|
|
663
|
+
const portal = ReactDOM.createPortal(reactElement, contentElement || widget.contentElement);
|
|
664
|
+
|
|
665
|
+
portals.set(portalId, portal);
|
|
666
|
+
|
|
667
|
+
// In Tooltip.js, we hide the tooltip if there’s no HTML content. However, when using JSX for eventResize
|
|
668
|
+
// and eventDrag, the portal content isn’t in the DOM yet when that “empty” check runs, causing the tooltip
|
|
669
|
+
// to be hidden prematurely. To prevent this, we now update the tooltip content synchronously.
|
|
670
|
+
// Note: flushSync is only called once when the tooltip instance is first created.
|
|
671
|
+
// Fixes https://github.com/bryntum/support/issues/10462
|
|
672
|
+
if (widget.isTooltip && !widget.html) {
|
|
673
|
+
flushSync(() => updateGeneration(reactComponent, true));
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
updateGeneration(reactComponent, true);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return portal;
|
|
680
|
+
}
|
|
681
|
+
else if (portals?.has(portalId)) {
|
|
682
|
+
// If we replace the content with plain HTML (not via React), React is unaware and its virtual DOM may become out of sync
|
|
683
|
+
// with the real DOM. Later, when rendering a new portal into the same node, React may fail to properly re-attach or re-render,
|
|
684
|
+
// especially if the previous portal wasn't unmounted or the DOM node was mutated outside React's control.
|
|
685
|
+
// Therefore, before setting plain HTML, we delete the portal and update the DOM synchronously.
|
|
686
|
+
// See: https://github.com/bryntum/support/issues/9822
|
|
687
|
+
portals.delete(portalId);
|
|
688
|
+
flushSync(() => updateGeneration(reactComponent, true));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function processTaskItemContent({
|
|
693
|
+
jsx,
|
|
694
|
+
targetElement,
|
|
695
|
+
reactComponent,
|
|
696
|
+
domConfig
|
|
697
|
+
}: {
|
|
698
|
+
jsx: ReactElement
|
|
699
|
+
targetElement: HTMLElement
|
|
700
|
+
reactComponent: React.Component
|
|
701
|
+
domConfig: { [key: string]: any }
|
|
702
|
+
}): void {
|
|
703
|
+
if (!reactComponent || !jsx) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const
|
|
707
|
+
{ state } = reactComponent,
|
|
708
|
+
// @ts-expect-error
|
|
709
|
+
{ portals } = state,
|
|
710
|
+
cardElement = targetElement.closest('.b-task-board-card') as any,
|
|
711
|
+
portalId = `task-item-${domConfig.reference}-${cardElement?.elementData.taskId}`,
|
|
712
|
+
portal = ReactDOM.createPortal(jsx, targetElement, portalId);
|
|
713
|
+
|
|
714
|
+
portals.set(portalId, portal);
|
|
715
|
+
|
|
716
|
+
updateGeneration(reactComponent);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* This method is called from processEventContent callback to
|
|
721
|
+
* handle JSX of Calendar events
|
|
722
|
+
*/
|
|
723
|
+
function processCalendarEventContent(this: any, {
|
|
724
|
+
jsx, // React element to render in portal
|
|
725
|
+
action, // Rendering action
|
|
726
|
+
domConfig, // domConfig passed by DomSync callback
|
|
727
|
+
targetElement, // DOM element to create portal in
|
|
728
|
+
reactComponent // the React wrapper component
|
|
729
|
+
}: {
|
|
730
|
+
jsx: ReactElement
|
|
731
|
+
action: string
|
|
732
|
+
domConfig: any
|
|
733
|
+
targetElement: HTMLElement
|
|
734
|
+
reactComponent: React.Component & { syncContent: (fn: () => unknown) => void }
|
|
735
|
+
|
|
736
|
+
}): boolean {
|
|
737
|
+
const
|
|
738
|
+
me = this,
|
|
739
|
+
returnValue = false,
|
|
740
|
+
{ state } = reactComponent,
|
|
741
|
+
|
|
742
|
+
// @ts-expect-error
|
|
743
|
+
{ portals } = state,
|
|
744
|
+
|
|
745
|
+
wrap = targetElement.closest('.b-cal-event-wrap') as HTMLElement,
|
|
746
|
+
dataHolder = targetElement.closest('[data-date]') as HTMLElement,
|
|
747
|
+
refHolder = targetElement.closest('[data-ref]') as HTMLElement,
|
|
748
|
+
mode = typeof this.mode === 'string' ? this.mode : this.mode?.type?.slice(0, 4),
|
|
749
|
+
lastPortalId = wrap?.dataset.lastPortalId,
|
|
750
|
+
portalKey = domConfig.dataset?.date || (dataHolder)?.dataset.date,
|
|
751
|
+
containerRef = refHolder?.dataset?.ref;
|
|
752
|
+
|
|
753
|
+
reactComponent.syncContent = reactComponent.syncContent || function(fn: () => unknown) {
|
|
754
|
+
flushSync(fn);
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
let portalId = `portal-${mode}-${wrap?.dataset.eventId}`;
|
|
758
|
+
|
|
759
|
+
me.portalKey = portalKey === undefined ? me.portalKey : portalKey;
|
|
760
|
+
me.containerRef = containerRef === undefined ? me.containerRef : containerRef;
|
|
761
|
+
|
|
762
|
+
if (action === 'none' || action === 'reuseElement') {
|
|
763
|
+
return returnValue;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (action === 'removeElement' && lastPortalId) {
|
|
767
|
+
portals.delete(lastPortalId);
|
|
768
|
+
updateGeneration(reactComponent);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (jsx && wrap) {
|
|
772
|
+
portalId = `${portalId}-${me.portalKey}-${me.containerRef}`;
|
|
773
|
+
|
|
774
|
+
const parent = wrap.querySelector('.b-cal-event-desc') as HTMLElement;
|
|
775
|
+
parent!.innerHTML = '';
|
|
776
|
+
|
|
777
|
+
const
|
|
778
|
+
jsxContainer = DomHelper.createElement({
|
|
779
|
+
className : 'b-jsx-container',
|
|
780
|
+
parent,
|
|
781
|
+
retainElement : true
|
|
782
|
+
}) as HTMLElement,
|
|
783
|
+
portal = ReactDOM.createPortal(jsx, jsxContainer);
|
|
784
|
+
|
|
785
|
+
portals.set(portalId, portal);
|
|
786
|
+
|
|
787
|
+
reactComponent.syncContent(() => {
|
|
788
|
+
updateGeneration(reactComponent, true);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
wrap.dataset.lastPortalId = portalId;
|
|
792
|
+
}
|
|
793
|
+
return returnValue;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Generate a unique portal ID for an event.
|
|
798
|
+
* @param {Object} assignmentRecord The assignment record
|
|
799
|
+
* @param {Object} [eventRecord] The event record
|
|
800
|
+
* @param {string} [segment] Segment identifier for split events
|
|
801
|
+
* @param {boolean} [isExporting] To check if exporting is in progress
|
|
802
|
+
* @returns {string} The generated portal ID
|
|
803
|
+
*/
|
|
804
|
+
function generateEventPortalId(assignmentRecord: any, eventRecord?: any, segment?: string, isExporting?: boolean): string {
|
|
805
|
+
const isRecurring = eventRecord?.isRecurring || eventRecord?.isOccurrence;
|
|
806
|
+
return `assignment-${assignmentRecord?.id}${isRecurring ? '-' + (eventRecord.recurrence?.id || '') : ''}${segment ? '-' + segment : ''}${isExporting ? '-export' : ''}`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Delete portals for all nested event children for a given parent event. When a parent event is released,
|
|
811
|
+
* DomSync only fires callback for it and not for children. The nested children are removed as part of
|
|
812
|
+
* removing the parent, so their callbacks don't fire individually. We must explicitly clean up their portals here.
|
|
813
|
+
* @param {*} component React Component, the wrapper itself
|
|
814
|
+
* @param {Object} parentEventRecord The parent event record
|
|
815
|
+
* @param {Object} resourceRecord The resource record (needed for assignment lookup)
|
|
816
|
+
* @param {boolean} isExporting Whether we're in export mode
|
|
817
|
+
*/
|
|
818
|
+
function deleteNestedEventPortals(component: any, parentEventRecord: any, resourceRecord: any, isExporting: boolean): void {
|
|
819
|
+
const children = parentEventRecord?.children;
|
|
820
|
+
|
|
821
|
+
if (!children?.length) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const { portals } = component.state;
|
|
826
|
+
|
|
827
|
+
for (const childEvent of children) {
|
|
828
|
+
// Find the assignment for this child event and resource
|
|
829
|
+
const assignmentRecord = childEvent.assignments?.find((a: any) => a.resourceId === resourceRecord?.id);
|
|
830
|
+
|
|
831
|
+
if (assignmentRecord) {
|
|
832
|
+
const portalId = generateEventPortalId(assignmentRecord, null, '', isExporting);
|
|
833
|
+
if (portals.has(portalId)) {
|
|
834
|
+
deletePortal(component, portalId);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Recursively handle nested children because nesting depth can be more than 1
|
|
839
|
+
if (childEvent.isParent && childEvent.children?.length) {
|
|
840
|
+
deleteNestedEventPortals(component, childEvent, resourceRecord, isExporting);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function processEventContent(this: any, {
|
|
846
|
+
jsx, // React element to render in portal
|
|
847
|
+
action, // Rendering action
|
|
848
|
+
targetElement, // DOM element to create portal in
|
|
849
|
+
isRelease, // true if releasing element
|
|
850
|
+
reactComponent, // the React wrapper component
|
|
851
|
+
scrolling,
|
|
852
|
+
domConfig
|
|
853
|
+
}: {
|
|
854
|
+
jsx: ReactElement
|
|
855
|
+
action: string
|
|
856
|
+
targetElement: HTMLElement
|
|
857
|
+
isRelease: boolean
|
|
858
|
+
reactComponent: React.Component & { syncContent: (fn: () => unknown) => void }
|
|
859
|
+
scrolling: boolean
|
|
860
|
+
domConfig: any
|
|
861
|
+
}): boolean {
|
|
862
|
+
|
|
863
|
+
if (this.type === 'calendar' && this.mode !== 'timeline' && this.mode?.type !== 'scheduler') {
|
|
864
|
+
|
|
865
|
+
return processCalendarEventContent.call(this, arguments[0]);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const { eventResize, eventDrag, eventEdit, nestedEvents } = this.features;
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
// @ts-ignore
|
|
873
|
+
if (!reactComponent || action === 'none' || (eventResize?.isResizing && !eventResize.dragging.completed && !scrolling)) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Vertical nests renderData, while horizontal does not
|
|
878
|
+
const domConfigData = this.isVertical ? domConfig?.elementData?.renderData : domConfig?.elementData;
|
|
879
|
+
|
|
880
|
+
let wrap: any = targetElement,
|
|
881
|
+
parent: any = null,
|
|
882
|
+
// Non-empty string only for split event segments
|
|
883
|
+
segment = '',
|
|
884
|
+
// True signals the caller to finish further processing of this event
|
|
885
|
+
returnValue = false;
|
|
886
|
+
|
|
887
|
+
// When passed jsx, we are event content
|
|
888
|
+
if (jsx) {
|
|
889
|
+
// Milestone has a dedicated label element used for padding
|
|
890
|
+
// We are looking here for `.b-sch-event-wrap`. This method is faster than `.closest(...)`
|
|
891
|
+
if (domConfig?.dataset?.isMilestone) {
|
|
892
|
+
wrap = targetElement.parentElement!.parentElement!.parentElement!;
|
|
893
|
+
parent = targetElement.parentElement!;
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
wrap = targetElement.parentElement!.parentElement!;
|
|
897
|
+
parent = targetElement;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// If we are processing a split event (an event with segments) we don't have the proper wrap so find it
|
|
901
|
+
if (!wrap.elementData) {
|
|
902
|
+
wrap = wrap.closest('.b-sch-event-wrap');
|
|
903
|
+
|
|
904
|
+
// Set the segment id for portalId calculation
|
|
905
|
+
segment = parent.parentElement.dataset?.segment || '';
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// When not passed jsx, we only care about continuing if we are the wrap, and not a ResourceTimeRange
|
|
910
|
+
else if (!domConfigData?.isWrap || domConfigData.eventRecord.isResourceTimeRange) {
|
|
911
|
+
return returnValue;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const
|
|
915
|
+
// Vertical nests renderData, while horizontal does not
|
|
916
|
+
wrapData = this.isVertical ? wrap.elementData.renderData : wrap.elementData,
|
|
917
|
+
// Use domConfigData when available to avoid stale data from reused elements. When elements are reused across
|
|
918
|
+
// recurring/non-recurring events, wrap.elementData may contain data from a different event type, causing
|
|
919
|
+
// incorrect isRecurring detection and wrong portalId calculation. This caused non-recurring events to
|
|
920
|
+
// render without JSX when scrolling back (when both recurring and non-recurring events are present).
|
|
921
|
+
// Fixes: https://github.com/bryntum/support/issues/10595
|
|
922
|
+
currentData = domConfigData ?? wrapData,
|
|
923
|
+
{
|
|
924
|
+
assignmentRecord,
|
|
925
|
+
eventRecord,
|
|
926
|
+
resourceRecord
|
|
927
|
+
} = currentData,
|
|
928
|
+
// Store portals in state to let React keep track of them
|
|
929
|
+
{ state } = reactComponent,
|
|
930
|
+
// @ts-ignore
|
|
931
|
+
{ portals } = state,
|
|
932
|
+
// Recurring events are handled a bit differently so get the flag
|
|
933
|
+
isRecurring = eventRecord.isRecurring || eventRecord.isOccurrence,
|
|
934
|
+
isExporting = this.isExporting,
|
|
935
|
+
// Portals are used for cells etc too, use a unique id for the map
|
|
936
|
+
portalId = generateEventPortalId(assignmentRecord, eventRecord, segment, isExporting);
|
|
937
|
+
|
|
938
|
+
// When a parent event is reused, its nested children DOM elements were removed (due to releaseThreshold: 0),
|
|
939
|
+
// but DomSync won't re-sync them because lastDomConfig appears unchanged. Clear it to force re-sync.
|
|
940
|
+
if (nestedEvents?.enabled && action === 'reuseOwnElement' && eventRecord.isParent) {
|
|
941
|
+
const eventElement = targetElement.firstElementChild;
|
|
942
|
+
if (eventElement) {
|
|
943
|
+
// @ts-ignore - lastDomConfig is a DomSync internal property
|
|
944
|
+
eventElement.lastDomConfig = null;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Make this function available for testing. It should only be called on event drop.
|
|
949
|
+
reactComponent.syncContent = reactComponent.syncContent || function(fn: () => unknown) {
|
|
950
|
+
flushSync(fn);
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
// Don't delete portals of recurring events
|
|
954
|
+
if (isRelease && !isRecurring) {
|
|
955
|
+
let generationUpdateFlag = false;
|
|
956
|
+
|
|
957
|
+
if (portals.has(portalId)) {
|
|
958
|
+
deletePortal(reactComponent, portalId);
|
|
959
|
+
generationUpdateFlag = true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// If nestedEvents feature is enabled and this is a event with nested children, need
|
|
963
|
+
// to clean up their portals. Because DomSync only fires callback for the parent when it's released,
|
|
964
|
+
// nested children are removed as part of removing the parent, so their callbacks don't fire.
|
|
965
|
+
if (nestedEvents?.enabled && eventRecord.children?.length) {
|
|
966
|
+
deleteNestedEventPortals(reactComponent, eventRecord, resourceRecord, isExporting);
|
|
967
|
+
generationUpdateFlag = true;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (generationUpdateFlag) {
|
|
971
|
+
updateGeneration(reactComponent, true);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
// We also need to handle reusing own element containing a React element.
|
|
976
|
+
// The portal for it has been released and needs to be restored
|
|
977
|
+
jsx = jsx || (action === 'reuseOwnElement' && wrap.lastJSX);
|
|
978
|
+
|
|
979
|
+
if (jsx) {
|
|
980
|
+
parent = parent || wrap.querySelector('.b-sch-event-content');
|
|
981
|
+
|
|
982
|
+
const
|
|
983
|
+
portal = portals.get(portalId),
|
|
984
|
+
updateContent = () => {
|
|
985
|
+
// Clean-up the content element
|
|
986
|
+
parent.innerHTML = '';
|
|
987
|
+
const
|
|
988
|
+
jsxContainer = DomHelper.createElement({
|
|
989
|
+
className : 'b-jsx-container b-event-text-wrap',
|
|
990
|
+
// As stated above, we are at the wrapper, but we should render the JSX inside the content element
|
|
991
|
+
parent,
|
|
992
|
+
|
|
993
|
+
retainElement : true
|
|
994
|
+
}) as Element,
|
|
995
|
+
portal = ReactDOM.createPortal(jsx, jsxContainer);
|
|
996
|
+
|
|
997
|
+
// Store eventGeneration on the portal to easily determine later if it needs to be updated
|
|
998
|
+
// @ts-expect-error
|
|
999
|
+
portal.eventGeneration = wrap.elementData.eventGeneration;
|
|
1000
|
+
|
|
1001
|
+
// Store the React element so that we can reuse it when reusing wrap element
|
|
1002
|
+
wrap.lastJSX = jsx;
|
|
1003
|
+
// Store portal in map in state, so that React can keep track of it
|
|
1004
|
+
portals.set(portalId, portal);
|
|
1005
|
+
|
|
1006
|
+
// When exporting/printing, use "flushSync" to force React to render the portal content synchronously. During
|
|
1007
|
+
// export, the scheduler creates temporary DOM elements attached to document.body, renders events into them
|
|
1008
|
+
// via DomSync, and then captures the HTML via outerHTML. Without "flushSync", React's default async rendering
|
|
1009
|
+
// means the portal content wouldn't be in the DOM when outerHTML is captured, resulting in empty event content
|
|
1010
|
+
// in the printed output. "flushSync" forces React to process the state update and render the portal content
|
|
1011
|
+
// immediately, ensuring the JSX is converted to DOM elements before the HTML capture occurs.
|
|
1012
|
+
// Fixes https://github.com/bryntum/support/issues/12135
|
|
1013
|
+
if (isExporting) {
|
|
1014
|
+
flushSync(() => updateGeneration(reactComponent, true));
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
updateGeneration(reactComponent, true);
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
isEditing = eventEdit?.isEditing,
|
|
1022
|
+
isDragging = eventDrag?.isDragging;
|
|
1023
|
+
|
|
1024
|
+
// Recurring events need re-creating always:
|
|
1025
|
+
// - https://github.com/bryntum/support/issues/10595
|
|
1026
|
+
// Re-create portal only if the underlying eventRecord changed its generation or if exporting
|
|
1027
|
+
if (!portal || isRecurring || isExporting || portal.eventGeneration !== wrap.elementData.eventGeneration) {
|
|
1028
|
+
if ((scrolling || !isDragging) && !isEditing) {
|
|
1029
|
+
updateContent();
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
|
|
1033
|
+
reactComponent.syncContent(updateContent);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
returnValue = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return returnValue;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Called from DomSync callback to handle JSX
|
|
1044
|
+
* returned from ResourceHeader headerRenderer
|
|
1045
|
+
*/
|
|
1046
|
+
function processResourceHeader(
|
|
1047
|
+
{ jsx, targetElement }:
|
|
1048
|
+
{ jsx: ReactElement; targetElement: HTMLElement }): void {
|
|
1049
|
+
|
|
1050
|
+
if (!jsx) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const
|
|
1055
|
+
// @ts-ignore
|
|
1056
|
+
{ reactComponent } = this,
|
|
1057
|
+
{ state } = reactComponent,
|
|
1058
|
+
{ portals } = state,
|
|
1059
|
+
portalId = `resource-header-${targetElement.dataset.resourceId}`;
|
|
1060
|
+
|
|
1061
|
+
// Delete portal if we already have one
|
|
1062
|
+
if (portals.has(portalId)) {
|
|
1063
|
+
portals.delete(portalId);
|
|
1064
|
+
|
|
1065
|
+
// Update the generation (force React re-rendering) in this tick
|
|
1066
|
+
updateGeneration(reactComponent, true);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
portals.set(portalId, ReactDOM.createPortal(jsx, targetElement));
|
|
1070
|
+
|
|
1071
|
+
// Trigger the React portals re-rendering in the next animation frame
|
|
1072
|
+
updateGeneration(reactComponent);
|
|
1073
|
+
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Handles the content provided by a React component for the widget.
|
|
1078
|
+
* @param {Widget} widget Owner widget
|
|
1079
|
+
* @param {*} element React element
|
|
1080
|
+
*/
|
|
1081
|
+
function handleReactElement(widget: Widget, element: any): void {
|
|
1082
|
+
const
|
|
1083
|
+
parent = widget.closest((c: any) => Boolean(c.reactComponent)),
|
|
1084
|
+
// Attempt to find a React component
|
|
1085
|
+
|
|
1086
|
+
reactComponent = (parent as any)?.reactComponent ||
|
|
1087
|
+
(Widget.query((c: any) => Boolean(c.reactComponent?.state)) as any)?.reactComponent;
|
|
1088
|
+
|
|
1089
|
+
reactComponent?.processWidgetContent({
|
|
1090
|
+
reactElement : element,
|
|
1091
|
+
widget,
|
|
1092
|
+
reactComponent
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Handles the React header element by processing JSX content within the widget.
|
|
1098
|
+
*
|
|
1099
|
+
* @param {Object} column Object containing grid and id properties
|
|
1100
|
+
* @param {HTMLElement} headerElement The header element to be processed
|
|
1101
|
+
* @param {Any} html JSX content to be processed
|
|
1102
|
+
*/
|
|
1103
|
+
function handleReactHeaderElement(column: { grid: any; id: string }, headerElement: HTMLElement, html: any): void {
|
|
1104
|
+
const
|
|
1105
|
+
{ reactComponent } = column.grid,
|
|
1106
|
+
contentElement = headerElement.querySelector('.b-grid-header-text-content');
|
|
1107
|
+
|
|
1108
|
+
// Process JSX as if it was in the widget.
|
|
1109
|
+
reactComponent?.processWidgetContent({
|
|
1110
|
+
reactElement : html,
|
|
1111
|
+
widget : { id : column.id },
|
|
1112
|
+
reactComponent,
|
|
1113
|
+
contentElement
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export { createWidget, shouldComponentUpdate, processWidgetContent };
|
|
1118
|
+
|
|
1119
|
+
// Expose wrapper methods on window.bryntum
|
|
1120
|
+
window.bryntum = window.bryntum || {};
|
|
1121
|
+
window.bryntum.react = {
|
|
1122
|
+
isReactElement,
|
|
1123
|
+
handleReactHeaderElement,
|
|
1124
|
+
handleReactElement
|
|
1125
|
+
};
|