goblin-magic 1.7.0 → 1.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-magic",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "goblin-magic",
5
5
  "author": "Epsitec SA",
6
6
  "contributors": [
@@ -17,7 +17,7 @@ class CalendarMenu extends Widget {
17
17
  };
18
18
 
19
19
  render() {
20
- const {allowEmpty, value, onChange, children} = this.props;
20
+ const {allowEmpty, selectWeek, value, onChange, children} = this.props;
21
21
  return (
22
22
  <Menu>
23
23
  {children}
@@ -26,6 +26,7 @@ class CalendarMenu extends Widget {
26
26
  {(menu) => (
27
27
  <CalendarMenuContent
28
28
  allowEmpty={allowEmpty}
29
+ selectWeek={selectWeek}
29
30
  value={value || DateConverters.getNowCanonical()}
30
31
  onChange={(date) => this.handleCalendarChange(date, menu)}
31
32
  onCancel={menu.close}
@@ -52,6 +52,7 @@ class CalendarMenuContent extends Widget {
52
52
  >
53
53
  <SmallCalendar
54
54
  ref={this.calendarRef}
55
+ selectWeek={this.props.selectWeek}
55
56
  date={this.state.date}
56
57
  onDateChange={this.handleDateChange}
57
58
  onDayClick={this.handleDayClick}
@@ -0,0 +1,28 @@
1
+ export default function styles() {
2
+ const items = {
3
+ padding: '3px 3px 2px 3px',
4
+ };
5
+
6
+ const item = {
7
+ display: 'flex',
8
+ flexDirection: 'row',
9
+ };
10
+
11
+ const hr = {
12
+ width: '100%',
13
+ margin: 0,
14
+ };
15
+
16
+ const buttons = {
17
+ display: 'flex',
18
+ flexDirection: 'row',
19
+ padding: '1px',
20
+ };
21
+
22
+ return {
23
+ items,
24
+ item,
25
+ hr,
26
+ buttons,
27
+ };
28
+ }
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import * as styles from './styles.js';
4
+ import MagicButton from '../magic-button/widget.js';
5
+ import Icon from '@mdi/react';
6
+ import {
7
+ mdiCheckboxBlankOutline,
8
+ mdiCheckboxIntermediateVariant,
9
+ mdiCheckboxMarked,
10
+ } from '@mdi/js';
11
+ import MagicCheckbox from '../magic-checkbox/widget.js';
12
+ import Menu from '../menu/widget.js';
13
+ import withC from 'goblin-laboratory/widgets/connect-helpers/with-c.js';
14
+
15
+ class CheckboxMenuItemsNC extends Widget {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.styles = styles;
19
+ }
20
+
21
+ toggle = (value) => {
22
+ const {checkedValues} = this.props;
23
+ if (!checkedValues) {
24
+ this.props.onChange(this.values.filter((v) => v !== value));
25
+ } else if (checkedValues.includes(value)) {
26
+ this.props.onChange([...checkedValues].filter((v) => v !== value));
27
+ } else {
28
+ this.props.onChange([...checkedValues, value]);
29
+ }
30
+ };
31
+
32
+ set = (value, event) => {
33
+ const {checkedValues} = this.props;
34
+ if (event.ctrlKey) {
35
+ this.toggle(value);
36
+ } else if (checkedValues?.length === 1 && checkedValues.includes(value)) {
37
+ this.all();
38
+ } else {
39
+ this.props.onChange([value]);
40
+ }
41
+ event.preventDefault();
42
+ };
43
+
44
+ all = () => {
45
+ this.props.onChange(null);
46
+ };
47
+
48
+ empty = () => {
49
+ this.props.onChange([]);
50
+ };
51
+
52
+ invert = () => {
53
+ const {checkedValues} = this.props;
54
+ if (checkedValues) {
55
+ if (checkedValues.length === 0) {
56
+ this.all();
57
+ } else {
58
+ this.props.onChange(
59
+ this.values.filter((value) => !checkedValues.includes(value))
60
+ );
61
+ }
62
+ } else {
63
+ this.empty();
64
+ }
65
+ };
66
+
67
+ renderItem(value, text) {
68
+ const {checkedValues} = this.props;
69
+ return (
70
+ <div key={value} className={this.styles.classNames.item}>
71
+ <MagicCheckbox
72
+ value={!checkedValues || checkedValues.includes(value)}
73
+ onChange={() => this.toggle(value)}
74
+ // disabled
75
+ />
76
+ <Menu.Item onPointerUp={(event) => this.set(value, event)}>
77
+ {typeof text === 'string' ? <span>{text}</span> : text}
78
+ </Menu.Item>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ render() {
84
+ const {values, checkedValues, renderValue, children} = this.props;
85
+ this.values = values ? [...values] : [];
86
+ const renderedChildren = React.Children.map(children, (child) => {
87
+ if (React.isValidElement(child) && child.type === 'option') {
88
+ const {value, children} = child.props;
89
+ this.values.push(value);
90
+ return this.renderItem(value, children);
91
+ }
92
+ return child;
93
+ });
94
+ return (
95
+ <>
96
+ <div className={this.styles.classNames.items}>
97
+ {values?.map((value) => this.renderItem(value, renderValue(value)))}
98
+ {renderedChildren}
99
+ </div>
100
+ <Menu.Hr className={this.styles.classNames.hr} />
101
+ <div className={this.styles.classNames.buttons}>
102
+ <MagicButton simple onPointerUp={this.all}>
103
+ <Icon path={mdiCheckboxMarked} size="1.2em" />
104
+ Tout
105
+ </MagicButton>
106
+ <MagicButton simple onPointerUp={this.empty}>
107
+ <Icon path={mdiCheckboxBlankOutline} size="1.2em" />
108
+ Aucun
109
+ </MagicButton>
110
+ <MagicButton simple onPointerUp={this.invert}>
111
+ <Icon path={mdiCheckboxIntermediateVariant} size="1.2em" />
112
+ Inverser
113
+ </MagicButton>
114
+ </div>
115
+ </>
116
+ );
117
+ }
118
+ }
119
+
120
+ const CheckboxMenuItems = withC(CheckboxMenuItemsNC);
121
+
122
+ export default CheckboxMenuItems;
@@ -68,7 +68,6 @@ class MagicDatetimeFieldNC extends Widget {
68
68
  requiredDate,
69
69
  requiredTime,
70
70
  isEndTime,
71
- dispatch,
72
71
  ...props
73
72
  } = this.props;
74
73
  const {date, time} = value ? parseZonedDateTime(value) : {};
@@ -46,7 +46,9 @@ let MagicNavigationView = class extends Widget {
46
46
  return (
47
47
  <div className={this.styles.classNames.view} data-visible={visible}>
48
48
  <WithModel model={serviceId ? `backend.${serviceId}` : ''}>
49
- <ViewContext.Provider value={view.set('id', viewId)}>
49
+ <ViewContext.Provider
50
+ value={view.set('id', viewId).set('visible', visible)}
51
+ >
50
52
  <ErrorHandler big>
51
53
  <Component id={serviceId} viewId={viewId} {...widgetProps} />
52
54
  </ErrorHandler>
@@ -81,10 +81,8 @@ class MagicTableRow extends Widget {
81
81
  }
82
82
 
83
83
  componentDidMount() {
84
- this.observer = new MutationObserver((mutations, observer) => {
85
- for (const mutation of mutations) {
86
- this.props.onUpdate(mutation.target.data);
87
- }
84
+ this.observer = new MutationObserver(() => {
85
+ this.props.onUpdate();
88
86
  });
89
87
  this.observer.observe(this.row.current, {
90
88
  subtree: true,
@@ -291,7 +289,6 @@ class MagicTableContainer extends Widget {
291
289
  sortOrder,
292
290
  sortCustom,
293
291
  showMenu,
294
- dispatch,
295
292
  ...props
296
293
  } = this.props;
297
294
 
@@ -0,0 +1,57 @@
1
+ /******************************************************************************/
2
+
3
+ export default function styles() {
4
+ const waveAnim = {
5
+ '0%': {
6
+ transform: 'scaleY(0.3)',
7
+ },
8
+ '15%': {
9
+ transform: 'scaleY(1.7)',
10
+ },
11
+ '30%': {
12
+ transform: 'scaleY(0.6)',
13
+ },
14
+ '55%': {
15
+ transform: 'scaleY(2)',
16
+ },
17
+ '75%': {
18
+ transform: 'scaleY(0.8)',
19
+ },
20
+ '100%': {
21
+ transform: 'scaleY(0.3)',
22
+ },
23
+ };
24
+
25
+ const wave = {
26
+ 'display': 'inline-flex',
27
+ 'flexDirection': 'row',
28
+ 'alignItems': 'center',
29
+ 'justifyContent': 'center',
30
+ 'gap': '2px',
31
+
32
+ '& span': {
33
+ width: '2px',
34
+ height: 'calc(6px + 16px * var(--level))',
35
+ borderRadius: '50px',
36
+ backgroundColor:
37
+ 'color-mix(in srgb, var(--button-accent-color), transparent 70%)',
38
+ borderColor:
39
+ 'color-mix(in srgb, var(--button-accent-color), transparent 10%)',
40
+ display: 'inline-block',
41
+
42
+ animationName: waveAnim,
43
+ animationDuration: 'var(--speed, 0.6s)',
44
+ animationTimingFunction: 'cubic-bezier(0.37, 0, 0.63, 1)',
45
+ animationIterationCount: 'infinite',
46
+ animationDirection: 'alternate',
47
+ animationDelay: 'calc(var(--i) * -0.08s)',
48
+ transformOrigin: 'bottom',
49
+ },
50
+ };
51
+
52
+ return {
53
+ wave,
54
+ };
55
+ }
56
+
57
+ /******************************************************************************/
@@ -0,0 +1,36 @@
1
+ import * as styles from './styles.js';
2
+ import React from 'react';
3
+ import Widget from 'goblin-laboratory/widgets/widget';
4
+
5
+ class MagicWave extends Widget {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.styles = styles;
9
+ }
10
+
11
+ render() {
12
+ const bars = this.props.bars || 5;
13
+ return (
14
+ <div className={this.styles.classNames.wave}>
15
+ {[...Array(bars)].map((_, i) => {
16
+ const center = (bars - 1) / 2;
17
+ const distance = Math.abs(i - center);
18
+ const level = 0.1 + (1 - distance / center) * 0.1;
19
+ const speed = `${(0.45 + Math.random() * 0.35).toFixed(2)}s`;
20
+ return (
21
+ <span
22
+ key={i}
23
+ style={{
24
+ '--i': i,
25
+ '--level': level.toFixed(2),
26
+ '--speed': speed,
27
+ }}
28
+ />
29
+ );
30
+ })}
31
+ </div>
32
+ );
33
+ }
34
+ }
35
+
36
+ export default MagicWave;
@@ -150,7 +150,7 @@ export default function styles() {
150
150
  };
151
151
 
152
152
  const menuHr = {
153
- width: 'calc(100% - 20px)',
153
+ width: 'calc(100% - 12px)',
154
154
  opacity: '0.5',
155
155
  margin: '3px 0',
156
156
  alignSelf: 'center',
@@ -193,10 +193,10 @@ class MenuButton extends Widget {
193
193
  }
194
194
 
195
195
  componentWillUnmount() {
196
- window.removeEventListener('click', this.stopPropagation, {
197
- capture: true,
198
- once: true,
199
- });
196
+ // window.removeEventListener('click', this.stopPropagation, {
197
+ // capture: true,
198
+ // once: true,
199
+ // });
200
200
  }
201
201
 
202
202
  stopPropagation(event) {
@@ -220,10 +220,10 @@ class MenuButton extends Widget {
220
220
  handlePointerDown = (event, menu) => {
221
221
  event.currentTarget?.focus();
222
222
  // Prevent click event on parent elements
223
- window.addEventListener('click', this.stopPropagation, {
224
- capture: true,
225
- once: true,
226
- });
223
+ // window.addEventListener('click', this.stopPropagation, {
224
+ // capture: true,
225
+ // once: true,
226
+ // });
227
227
  menu.open(event);
228
228
  };
229
229
 
@@ -33,6 +33,11 @@ export default function styles() {
33
33
 
34
34
  const weekRow = {
35
35
  ...row,
36
+
37
+ '&[data-select-week=true]:hover': {
38
+ backgroundColor:
39
+ 'color-mix(in srgb, var(--accent-color), transparent 75%)',
40
+ },
36
41
  };
37
42
 
38
43
  const weekNumber = {
@@ -68,15 +68,17 @@ class SmallCalendarGrid extends Widget {
68
68
  }
69
69
 
70
70
  renderWeeks(currentDay) {
71
- const {startDate} = this.props;
72
- const weekStarts = CalendarHelpers.generateWeekStarts(
73
- CalendarHelpers.getMonthStart(startDate),
74
- 6
75
- );
71
+ const {startDate, selectWeek} = this.props;
72
+ const monthStart = CalendarHelpers.getMonthStart(startDate);
73
+ const weekStarts = CalendarHelpers.generateWeekStarts(monthStart, 6);
76
74
  return weekStarts.map((weekStart) => {
77
75
  const days = CalendarHelpers.generateDays(weekStart);
78
76
  return (
79
- <div key={weekStart} className={this.styles.classNames.weekRow}>
77
+ <div
78
+ key={monthStart + '_' + weekStart}
79
+ className={this.styles.classNames.weekRow}
80
+ data-select-week={selectWeek}
81
+ >
80
82
  <div className={this.styles.classNames.weekNumber}>
81
83
  {CalendarHelpers.getWeekNumber(toJsDate(weekStart))}
82
84
  </div>
@@ -0,0 +1,63 @@
1
+ export default function styles() {
2
+ const yearMonth = {};
3
+
4
+ const top = {
5
+ 'display': 'flex',
6
+ 'flexDirection': 'row',
7
+ 'gap': '5px',
8
+ 'marginBottom': '5px',
9
+
10
+ '& > *': {
11
+ flexBasis: 0,
12
+ flexGrow: 1,
13
+ display: 'flex',
14
+ flexDirection: 'row',
15
+ justifyContent: 'space-between',
16
+ },
17
+ };
18
+
19
+ const today = {
20
+ color: 'var(--text-color)',
21
+ };
22
+
23
+ const group = {
24
+ display: 'flex',
25
+ flexDirection: 'row',
26
+ alignItems: 'center',
27
+ zIndex: 0,
28
+ };
29
+
30
+ const arrow = {
31
+ 'zIndex': -1,
32
+ 'padding': '6px 0px 4px 0px',
33
+ 'margin': '0 -3px',
34
+ ':not(:hover)': {
35
+ opacity: 0.5,
36
+ },
37
+ };
38
+
39
+ const datePart = {
40
+ 'fontSize': '18px',
41
+ 'padding': '4px 4px',
42
+ '&[class][class]' /* increase specificity */: {
43
+ opacity: 1,
44
+ },
45
+ };
46
+
47
+ const years = {
48
+ columnCount: 2,
49
+ columnRule:
50
+ '1px solid color-mix(in srgb, var(--text-color), transparent 75%)',
51
+ columnGap: '10px',
52
+ };
53
+
54
+ return {
55
+ yearMonth,
56
+ top,
57
+ today,
58
+ group,
59
+ arrow,
60
+ datePart,
61
+ years,
62
+ };
63
+ }
@@ -0,0 +1,163 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import * as styles from './styles.js';
4
+ import MagicButton from '../magic-button/widget.js';
5
+ import {mdiCalendarToday, mdiChevronLeft, mdiChevronRight} from '@mdi/js';
6
+ import Icon from '@mdi/react';
7
+ import CalendarHelpers from '../calendar-helpers.js';
8
+ import MaxTextWidth from '../max-text-width/widget.js';
9
+ import YearMonthsGrid from '../year-month-grid/widget.js';
10
+ import {yearMonth as YearMonthConverters} from 'xcraft-core-converters';
11
+
12
+ class YearMonth extends Widget {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.styles = styles;
16
+ }
17
+
18
+ setToday = (event) => {
19
+ const yearMonth = YearMonthConverters.getNowCanonical();
20
+ this.props.onMonthChange(yearMonth);
21
+ };
22
+
23
+ validateToday = (event) => {
24
+ const yearMonth = YearMonthConverters.getNowCanonical();
25
+ this.props.onMonthClick(yearMonth, event);
26
+ };
27
+
28
+ addMonth = (event, count) => {
29
+ let newDate = YearMonthConverters.addMonths(this.props.value, count);
30
+ this.props.onMonthChange(newDate);
31
+ };
32
+
33
+ previousMonth = (event) => {
34
+ this.addMonth(event, -1);
35
+ };
36
+
37
+ nextMonth = (event) => {
38
+ this.addMonth(event, 1);
39
+ };
40
+
41
+ previousYear = (event) => {
42
+ let newDate = YearMonthConverters.addYears(this.props.value, -1);
43
+ this.props.onMonthChange(newDate);
44
+ };
45
+
46
+ nextYear = (event) => {
47
+ let newDate = YearMonthConverters.addYears(this.props.value, 1);
48
+ this.props.onMonthChange(newDate);
49
+ };
50
+
51
+ handleKeyDown = (event) => {
52
+ this.props.onKeyDown?.(event);
53
+ if (event.key === 'Enter') {
54
+ if (event.target === event.currentTarget) {
55
+ this.props.onMonthClick(this.props.value, event);
56
+ event.stopPropagation();
57
+ }
58
+ } else if (event.key === 'ArrowUp') {
59
+ this.addMonth(event, -3);
60
+ event.stopPropagation();
61
+ } else if (event.key === 'ArrowDown') {
62
+ this.addMonth(event, 3);
63
+ event.stopPropagation();
64
+ } else if (event.key === 'ArrowLeft') {
65
+ this.addMonth(event, -1);
66
+ event.stopPropagation();
67
+ } else if (event.key === 'ArrowRight') {
68
+ this.addMonth(event, 1);
69
+ event.stopPropagation();
70
+ }
71
+ };
72
+
73
+ render() {
74
+ const {value, onMonthClick} = this.props;
75
+ const [year, month] = YearMonthConverters.parse(value);
76
+ const monthNames = CalendarHelpers.getMonthNames('fr-CH', 'long', {
77
+ upperCase: true,
78
+ });
79
+ const monthName = monthNames[month - 1];
80
+ return (
81
+ <div
82
+ className={this.styles.classNames.yearMonth}
83
+ onKeyDown={this.handleKeyDown}
84
+ >
85
+ <div className={this.styles.classNames.top}>
86
+ <div className={this.styles.classNames.topLeft}>
87
+ <MagicButton
88
+ simple
89
+ onClick={this.setToday}
90
+ onDoubleClick={this.validateToday}
91
+ title="Aujourd'hui"
92
+ className={this.styles.classNames.today}
93
+ >
94
+ <Icon path={mdiCalendarToday} size={0.8} />
95
+ </MagicButton>
96
+ <div className={this.styles.classNames.group}>
97
+ <MagicButton
98
+ simple
99
+ onClick={this.previousMonth}
100
+ className={this.styles.classNames.arrow}
101
+ >
102
+ <Icon path={mdiChevronLeft} size={1.1} />
103
+ </MagicButton>
104
+ <MagicButton
105
+ simple
106
+ disabled
107
+ className={this.styles.classNames.datePart}
108
+ >
109
+ <MaxTextWidth texts={monthNames}>{monthName}</MaxTextWidth>
110
+ </MagicButton>
111
+ <MagicButton
112
+ simple
113
+ onClick={this.nextMonth}
114
+ className={this.styles.classNames.arrow}
115
+ >
116
+ <Icon path={mdiChevronRight} size={1.1} />
117
+ </MagicButton>
118
+ </div>
119
+ </div>
120
+ <div className={this.styles.classNames.topRight}>
121
+ <div className={this.styles.classNames.group}>
122
+ <MagicButton
123
+ simple
124
+ onClick={this.previousYear}
125
+ className={this.styles.classNames.arrow}
126
+ >
127
+ <Icon path={mdiChevronLeft} size={1.1} />
128
+ </MagicButton>
129
+ <MagicButton
130
+ simple
131
+ disabled
132
+ className={this.styles.classNames.datePart}
133
+ >
134
+ {year}
135
+ </MagicButton>
136
+ <MagicButton
137
+ simple
138
+ onClick={this.nextYear}
139
+ className={this.styles.classNames.arrow}
140
+ >
141
+ <Icon path={mdiChevronRight} size={1.1} />
142
+ </MagicButton>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ <div className={this.styles.classNames.years}>
147
+ <YearMonthsGrid
148
+ year={year}
149
+ value={value}
150
+ onMonthClick={onMonthClick}
151
+ />
152
+ <YearMonthsGrid
153
+ year={year + 1}
154
+ value={value}
155
+ onMonthClick={onMonthClick}
156
+ />
157
+ </div>
158
+ </div>
159
+ );
160
+ }
161
+ }
162
+
163
+ export default YearMonth;
@@ -0,0 +1,57 @@
1
+ export default function styles() {
2
+ const yearMonthGrid = {
3
+ display: 'flex',
4
+ flexDirection: 'column',
5
+ userSelect: 'none',
6
+ padding: '5px',
7
+ };
8
+
9
+ const yearHeader = {
10
+ color: 'color-mix(in srgb, var(--text-color), transparent 50%)',
11
+ paddingLeft: '3px',
12
+ };
13
+
14
+ const monthsGrid = {
15
+ display: 'grid',
16
+ gridTemplateColumns: '1fr 1fr 1fr',
17
+ };
18
+
19
+ const month = {
20
+ 'display': 'flex',
21
+ 'alignItems': 'center',
22
+ 'border': '1px solid transparent',
23
+ 'minHeight': '33px',
24
+ 'padding': '0 3px',
25
+
26
+ '&[data-today=true]': {
27
+ borderColor:
28
+ 'color-mix(in srgb, var(--button-accent-color), transparent 80%)',
29
+ },
30
+
31
+ '&[data-selected=true]': {
32
+ borderRadius: '5px',
33
+ backgroundColor:
34
+ 'color-mix(in srgb, var(--accent-color), transparent 70%)',
35
+ borderColor:
36
+ 'color-mix(in srgb, var(--button-accent-color), transparent 40%)',
37
+ },
38
+
39
+ '&[data-enable-selection=true]': {
40
+ 'cursor': 'pointer',
41
+ '&:hover': {
42
+ borderRadius: '5px',
43
+ backgroundColor:
44
+ 'color-mix(in srgb, var(--button-accent-color), transparent 65%)',
45
+ borderColor:
46
+ 'color-mix(in srgb, var(--button-accent-color), transparent 10%)',
47
+ },
48
+ },
49
+ };
50
+
51
+ return {
52
+ yearMonthGrid,
53
+ yearHeader,
54
+ monthsGrid,
55
+ month,
56
+ };
57
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import * as styles from './styles.js';
4
+ import CurrentDay from '../time-interval/current-day.js';
5
+ import CalendarHelpers from '../calendar-helpers.js';
6
+ import {yearMonth as YearMonthConverters} from 'xcraft-core-converters';
7
+
8
+ class YearMonthsGrid extends Widget {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.styles = styles;
12
+ }
13
+
14
+ /** @type {React.PointerEventHandler} */
15
+ handleMonthClick = (event) => {
16
+ const yearMonth = event.currentTarget.getAttribute('data-month');
17
+ this.props.onMonthClick?.(yearMonth, event);
18
+ };
19
+
20
+ renderMonth(name, index, currentMonth) {
21
+ const {year, value, onMonthClick} = this.props;
22
+ const yearMonth = YearMonthConverters.from(year, index + 1);
23
+ return (
24
+ <div
25
+ key={index}
26
+ className={this.styles.classNames.month}
27
+ data-selected={yearMonth === value}
28
+ data-today={yearMonth === currentMonth}
29
+ data-enable-selection={Boolean(onMonthClick)}
30
+ onClick={this.handleMonthClick}
31
+ data-month={yearMonth}
32
+ >
33
+ {name}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ renderMonths(currentDay) {
39
+ const monthNames = CalendarHelpers.getMonthNames('fr-CH', 'long', {
40
+ upperCase: true,
41
+ });
42
+ const currentMonth = currentDay.split('-', 2).join('-');
43
+ return monthNames.map((name, index) =>
44
+ this.renderMonth(name, index, currentMonth)
45
+ );
46
+ }
47
+
48
+ render() {
49
+ const {year} = this.props;
50
+ return (
51
+ <div role="grid" className={this.styles.classNames.yearMonthGrid}>
52
+ <div className={this.styles.classNames.yearHeader}>{year}</div>
53
+ <div className={this.styles.classNames.monthsGrid}>
54
+ <CurrentDay>
55
+ {(currentDay) => this.renderMonths(currentDay)}
56
+ </CurrentDay>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+ }
62
+
63
+ export default YearMonthsGrid;
@@ -0,0 +1,22 @@
1
+ export default function styles() {
2
+ const yearMonthMenuContent = {
3
+ padding: '5px',
4
+ outline: 'none',
5
+ };
6
+
7
+ const clearButton = {
8
+ '&[class]' /* increase specificity */: {
9
+ minWidth: 'unset',
10
+ },
11
+ };
12
+
13
+ const buttons = {
14
+ marginTop: '5px',
15
+ };
16
+
17
+ return {
18
+ yearMonthMenuContent,
19
+ clearButton,
20
+ buttons,
21
+ };
22
+ }
@@ -0,0 +1,117 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import * as styles from './styles.js';
4
+ import Menu from '../menu/widget.js';
5
+ import MagicButton from '../magic-button/widget.js';
6
+ import Icon from '@mdi/react';
7
+ import {mdiBackspaceOutline, mdiCancel, mdiCheck} from '@mdi/js';
8
+ import DialogButtons from '../dialog-buttons/widget.js';
9
+ import {yearMonth as YearMonthConverters} from 'xcraft-core-converters';
10
+ import YearMonth from '../year-month/widget.js';
11
+
12
+ class YearMonthMenuContent extends Widget {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.styles = styles;
16
+ this.state = {
17
+ yearMonth: this.props.value,
18
+ };
19
+ this.calendarRef = React.createRef();
20
+ }
21
+
22
+ /** @type {React.KeyboardEventHandler} */
23
+ handleKeyDown = (event) => {
24
+ this.calendarRef.current?.handleKeyDown(event);
25
+ };
26
+
27
+ handleMonthChange = (yearMonth) => {
28
+ this.setState({yearMonth});
29
+ };
30
+
31
+ handleMonthClick = (value) => {
32
+ this.props.onChange(value);
33
+ };
34
+
35
+ clear = () => {
36
+ this.props.onChange(null);
37
+ };
38
+
39
+ cancel = () => {
40
+ this.props.onCancel();
41
+ };
42
+
43
+ validate = () => {
44
+ this.props.onChange(this.state.yearMonth);
45
+ };
46
+
47
+ render() {
48
+ return (
49
+ <div
50
+ className={this.styles.classNames.yearMonthMenuContent}
51
+ tabIndex={0}
52
+ onKeyDown={this.handleKeyDown}
53
+ >
54
+ <YearMonth
55
+ ref={this.calendarRef}
56
+ value={this.state.yearMonth}
57
+ onMonthChange={this.handleMonthChange}
58
+ onMonthClick={this.handleMonthClick}
59
+ />
60
+
61
+ <DialogButtons className={this.styles.classNames.buttons}>
62
+ {this.props.allowEmpty && (
63
+ <MagicButton
64
+ onClick={this.clear}
65
+ className={this.styles.classNames.clearButton}
66
+ >
67
+ <Icon path={mdiBackspaceOutline} size={0.8} /> Effacer
68
+ </MagicButton>
69
+ )}
70
+ <DialogButtons.Spacing />
71
+ <MagicButton onClick={this.cancel}>
72
+ <Icon path={mdiCancel} size={0.8} /> Annuler
73
+ </MagicButton>
74
+ <MagicButton onClick={this.validate} className="main">
75
+ <Icon path={mdiCheck} size={0.8} />{' '}
76
+ {YearMonthConverters.toLocaleString(this.state.yearMonth, 'fr-CH')}
77
+ </MagicButton>
78
+ </DialogButtons>
79
+ </div>
80
+ );
81
+ }
82
+ }
83
+
84
+ class YearMonthMenu extends Widget {
85
+ constructor() {
86
+ super(...arguments);
87
+ this.styles = styles;
88
+ }
89
+
90
+ handleChange = (value, menu) => {
91
+ menu.close();
92
+ this.props.onChange(value);
93
+ };
94
+
95
+ render() {
96
+ const {allowEmpty, value, onChange, children} = this.props;
97
+ return (
98
+ <Menu>
99
+ {children}
100
+ <Menu.Content position="bottom center" addTabIndex={false}>
101
+ <Menu.Context.Consumer>
102
+ {(menu) => (
103
+ <YearMonthMenuContent
104
+ allowEmpty={allowEmpty}
105
+ value={value || YearMonthConverters.getNowCanonical()}
106
+ onChange={(value) => this.handleChange(value, menu)}
107
+ onCancel={menu.close}
108
+ />
109
+ )}
110
+ </Menu.Context.Consumer>
111
+ </Menu.Content>
112
+ </Menu>
113
+ );
114
+ }
115
+ }
116
+
117
+ export default YearMonthMenu;