@dvrd/dvr-controls 1.0.40 → 1.0.41

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/index.ts CHANGED
@@ -56,6 +56,7 @@ import DvrdSwitch from './src/js/switch/dvrdSwitch';
56
56
  import DvrdHeaderController from './src/js/header/v2/dvrdHeaderController';
57
57
  import FileUpload from './src/js/fileUpload/fileUpload';
58
58
  import DvrdRadioController from './src/js/radio/dvrdRadioController';
59
+ import MobileNavigation from "./src/js/navigation/mobileNavigation";
59
60
 
60
61
  export {
61
62
  // Components
@@ -104,6 +105,7 @@ export {
104
105
  DvrdHeaderController as DvrdHeader,
105
106
  FileUpload,
106
107
  DvrdRadioController as DvrdRadio,
108
+ MobileNavigation,
107
109
 
108
110
  // Interfaces / Enums
109
111
  DialogActionShape,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvrd/dvr-controls",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "description": "Custom web controls",
5
5
  "main": "index.ts",
6
6
  "files": [
@@ -13,13 +13,21 @@
13
13
  "author": "Dave van Rijn",
14
14
  "license": "ISC",
15
15
  "browserslist": {
16
- "0": ">0.2%",
17
- "1": "not dead",
18
- "2": "ie >= 11"
16
+ "production": [
17
+ ">0.2%",
18
+ "not dead",
19
+ "ie >= 11"
20
+ ],
21
+ "development": [
22
+ ">0.2%",
23
+ "not dead",
24
+ "ie >= 11"
25
+ ]
19
26
  },
20
27
  "peerDependencies": {
21
28
  "react": "18.2.0",
22
- "react-dom": "18.2.0"
29
+ "react-dom": "18.2.0",
30
+ "react-router-dom": "6.15.0"
23
31
  },
24
32
  "devDependencies": {
25
33
  "@types/dompurify": "2.4.0",
@@ -45,7 +53,6 @@
45
53
  "moment": "2.29.4",
46
54
  "react-color": "2.19.3",
47
55
  "react-rnd": "10.4.1",
48
- "react-router-dom": "6.15.0",
49
56
  "uuid": "9.0.0"
50
57
  }
51
58
  }
@@ -5,12 +5,13 @@ import './style/dvrdDatePicker.scss';
5
5
 
6
6
  import classNames from 'classnames';
7
7
  import {Moment} from 'moment';
8
- import React, {PureComponent, useState} from 'react';
8
+ import React, {PureComponent, useMemo, useState} from 'react';
9
9
  import {ChangeFunction, ErrorType} from '../util/interfaces';
10
10
  import AwesomeIcon from '../icon/awesomeIcon';
11
11
  import {toMoment} from '../util/momentUtil';
12
12
  import WithBackground from '../popup/withBackground';
13
13
  import DvrdButton from "../button/dvrdButton";
14
+ import {generateComponentId} from "../util/componentUtil";
14
15
 
15
16
  interface Props {
16
17
  onChange: ChangeFunction<Moment>;
@@ -23,13 +24,26 @@ interface Props {
23
24
  dateFormat?: string;
24
25
  disabled?: boolean;
25
26
  alwaysShowArrows?: boolean;
27
+ useMobileNative?: boolean;
28
+ id?: string;
26
29
  }
27
30
 
28
31
  export default function DvrdDatePicker(props: Props) {
29
32
  const {
30
- onChange, className, closeOnChange, error, label, value, dateFormat, placeholder, disabled, alwaysShowArrows
31
- } = props,
32
- [pickerOpen, setPickerOpen] = useState(false);
33
+ onChange, className, closeOnChange, error, label, value, placeholder, disabled, alwaysShowArrows,
34
+ useMobileNative
35
+ } = props;
36
+ const [pickerOpen, setPickerOpen] = useState(false);
37
+ const dateFormat = useMemo(() => {
38
+ return props.dateFormat ?? 'YYYY-MM-DD';
39
+ }, [props.dateFormat]);
40
+ const id = useMemo(() => {
41
+ return generateComponentId(props.id);
42
+ }, [props.id]);
43
+
44
+ function onChangeNative(evt: React.ChangeEvent<HTMLInputElement>) {
45
+ onChange(toMoment(evt.target.value), evt);
46
+ }
33
47
 
34
48
  function onClickContainer() {
35
49
  if (!disabled)
@@ -45,14 +59,24 @@ export default function DvrdDatePicker(props: Props) {
45
59
  setPickerOpen(false)
46
60
  }
47
61
 
48
- const format = dateFormat ?? 'YYYY-MM-DD';
62
+ function renderMobilePicker() {
63
+ if (!useMobileNative) return null;
64
+ return (
65
+ <div className='native-picker-container'>
66
+ <input type='date' onChange={onChangeNative} value={value?.format('YYYY-MM-DD') ?? ''}
67
+ placeholder={placeholder} className='native-date-field' id={`${id}-native`}/>
68
+ <AwesomeIcon name='calendar-alt' className='calendar-icon' htmlFor={`${id}-native`}/>
69
+ </div>
70
+ )
71
+ }
49
72
 
50
73
  return (
51
74
  <div className={classNames('dvrd-date-picker', className, error && 'error', disabled && 'disabled')}>
52
75
  <label className='field-label'>{label}</label>
53
- <div className='value-container' onClick={onClickContainer}>
54
- <label className={classNames('value', !value && 'placeholder')}>{value?.format(format) ?? placeholder ??
55
- ''}</label>
76
+ {renderMobilePicker()}
77
+ <div className={classNames('value-container', useMobileNative && 'no-mob')} onClick={onClickContainer}>
78
+ <label className={classNames('value', !value && 'placeholder')}>{value?.format(dateFormat) ??
79
+ placeholder ?? ''}</label>
56
80
  <AwesomeIcon name='calendar-alt' className='calendar-icon'/>
57
81
  </div>
58
82
  <Picker onClose={onClosePicker} onChange={onChangePicker} open={pickerOpen} value={value}
@@ -250,7 +274,7 @@ class Picker extends PureComponent<PickerProps> {
250
274
  if (open && !prevOpen) {
251
275
  document.addEventListener('keydown', this.keyListener);
252
276
  this.addMountClass();
253
- window.setTimeout(this.removeMountClass, 500);
277
+ window.setTimeout(this.removeMountClass, 1000);
254
278
  } else if (!open && prevOpen) document.removeEventListener('keydown', this.keyListener);
255
279
  };
256
280
 
@@ -41,6 +41,12 @@
41
41
  top: 50%;
42
42
  transform: translateY(-50%);
43
43
  }
44
+
45
+ &.no-mob {
46
+ @include break-xs {
47
+ display: none;
48
+ }
49
+ }
44
50
  }
45
51
 
46
52
  .picker {
@@ -2,12 +2,13 @@
2
2
  * Copyright (c) 2021. Dave van Rijn Development
3
3
  */
4
4
 
5
- import React from 'react';
5
+ import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';
6
6
  import classNames from 'classnames';
7
7
 
8
8
  import {ControlContext} from "../util/controlContext";
9
9
  import {convertColor} from "../util/colorUtil";
10
10
  import {calculateTextWidth, getComputedProperty, remToPx} from "../util/controlUtil";
11
+ import { defer } from 'lodash';
11
12
 
12
13
  export enum LabelType {
13
14
  LABEL, SPAN, PAR,
@@ -17,137 +18,112 @@ export enum BreakType {
17
18
  CHAR, WORD
18
19
  }
19
20
 
21
+ type LabelProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement> &
22
+ React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement> &
23
+ React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>;
24
+
20
25
  interface Props {
21
- labelType: LabelType,
22
- className: string,
23
- text: string,
24
- color?: string,
25
- maxLines: number,
26
- width?: string | number,
27
- labelProps: object,
28
- breakMode: BreakType,
26
+ text: string;
27
+ labelType?: LabelType;
28
+ className?: string;
29
+ color?: string;
30
+ maxLines?: number;
31
+ width?: string | number;
32
+ labelProps?: LabelProps;
33
+ breakMode?: BreakType;
34
+ }
35
+
36
+ function getCurrentFontSize(element: HTMLLabelElement & HTMLSpanElement & HTMLParagraphElement): number {
37
+ let fontSize: number | string = getComputedProperty('font-size', element);
38
+ fontSize = parseInt(fontSize.slice(0, fontSize.length - 2), 10);
39
+ return fontSize;
29
40
  }
30
41
 
31
- interface State {
32
- text: string,
42
+ /**
43
+ * Break a line by removing characters from the end until a whitespace or dash is found. Removed characters are
44
+ * returned as remainingChars.
45
+ * @param validLine Text to break
46
+ * @return Object containing the remainder of the the line and the removed characters.
47
+ */
48
+ function breakLine(validLine: string): { validLine: string, remainingChars: string } {
49
+ let remainingChars = '';
50
+ if (/[ -]$/.test(validLine)) {
51
+ validLine = validLine.slice(0, -1);
52
+ remainingChars = ' ';
53
+ }
54
+
55
+ while (!/[ -]$/.test(validLine) && validLine.length) {
56
+ remainingChars = validLine.slice(-1) + remainingChars;
57
+ validLine = validLine.slice(0, -1);
58
+ }
59
+ return {validLine, remainingChars};
33
60
  }
34
61
 
35
62
  /**
36
63
  * Label component which breaks its contents automatically. If maxLines is specified, the label will only break the
37
64
  * text until the number of lines is reached. Remaining text will be shown in the title.
38
65
  */
39
- export default class Label extends React.Component<Props, State> {
40
- declare context: React.ContextType<typeof ControlContext>;
41
- static contextType = ControlContext;
42
-
43
- static defaultProps = {
44
- labelType: LabelType.LABEL,
45
- className: '',
46
- labelProps: {},
47
- breakMode: BreakType.CHAR,
48
- maxLines: 0,
49
- };
50
-
51
- label: HTMLElement | null = null;
52
-
53
- state = {
54
- text: '',
55
- };
56
-
57
- getColor = (): string => {
58
- const {contrastColor} = this.context, {color} = this.props, finalColor: string = color ? color : contrastColor;
59
- return convertColor(finalColor);
60
- };
61
-
62
- getWidth = (): string => {
63
- const {width} = this.props;
64
- let finalWidth: string = 'initial';
65
- if (width !== undefined && width !== null) {
66
- if (typeof width === 'string') finalWidth = width;
67
- else finalWidth = width + 'px';
66
+ export default function Label(props: Props) {
67
+ const context = useContext(ControlContext);
68
+ const [text, setText] = useState(props.text);
69
+ const labelRef = useRef<HTMLLabelElement & HTMLParagraphElement & HTMLSpanElement>(null);
70
+ const labelType = props.labelType ?? LabelType.LABEL;
71
+ const breakMode = props.breakMode ?? BreakType.CHAR;
72
+ const maxLines = props.maxLines ?? 0;
73
+ const color = useMemo(() => {
74
+ return convertColor(props.color ?? context.contrastColor);
75
+ }, [props.color, context.contrastColor]);
76
+ const width = useMemo(() => {
77
+ let width = 'initial';
78
+ if (props.width) {
79
+ if (typeof props.width === 'string') width = props.width;
80
+ else width = `${width}px`;
68
81
  }
69
- return finalWidth;
70
- };
71
-
72
- getLabelProps = () => {
73
- const {className, labelProps, text} = this.props;
82
+ return width;
83
+ }, [props.width]);
84
+ const labelProps: LabelProps = useMemo(() => {
74
85
  return {
75
- className: classNames('dvr-label', className),
86
+ className: classNames('dvr-label', props.className),
76
87
  style: {
77
- color: this.getColor(),
78
- width: this.getWidth(),
88
+ color,
89
+ width,
79
90
  display: 'inline-block',
80
91
  },
81
92
  title: text,
82
- ref: (ref: HTMLElement | null) => {
83
- this.label = ref
84
- },
85
- ...labelProps,
93
+ ref: labelRef,
94
+ ...(props.labelProps ?? {}),
86
95
  };
87
- };
88
-
89
- getFontSize = (): number | undefined => {
90
- if (this.label) {
91
- let fontSize: number | string = getComputedProperty('font-size', this.label);
92
- fontSize = parseInt(fontSize.slice(0, fontSize.length - 2), 10);
93
- return fontSize;
94
- }
95
- return undefined;
96
- };
96
+ }, [props.className, color, width, props.labelProps]);
97
97
 
98
98
  /**
99
99
  * Truncate given text to given maxWidth. Truncating is done by removing characters from the end until the text + ...
100
- * width is less then the given maxWidth. Trailing white spaces are removed and ... are appended.
101
- * @param text Text to truncate
102
- * @param maxWidth Max width in px
100
+ * width is less than the given maxWidth. Trailing white spaces are removed and ... are appended.
103
101
  */
104
- reduceText = (text: string, maxWidth: number): string => {
102
+ function truncateText(text: string, maxWidth: number): string {
105
103
  let fontSize, fontFamily;
106
- if (this.label) {
107
- fontSize = this.getFontSize();
108
- fontFamily = getComputedProperty('font-family', this.label);
104
+ if (labelRef.current) {
105
+ fontSize = getCurrentFontSize(labelRef.current);
106
+ fontFamily = getComputedProperty('font-family', labelRef.current);
109
107
  }
110
108
  while (calculateTextWidth(text + '...', fontSize, fontFamily) >= maxWidth)
111
109
  text = text.slice(0, -1);
112
- return text.trimRight() + '...';
113
- };
114
-
115
- /**
116
- * Break a line by removing characters from the end until a whitespace or dash is found. Removed characters are
117
- * returned as remainingChars.
118
- * @param validLine Text to break
119
- * @return Object containing the remainder of the the line and the removed characters.
120
- */
121
- breakLine = (validLine: string): { validLine: string, remainingChars: string } => {
122
- let remainingChars = '';
123
- if (/[ -]$/.test(validLine)) {
124
- validLine = validLine.slice(0, -1);
125
- remainingChars = ' ';
126
- }
127
-
128
- while (!/[ -]$/.test(validLine) && validLine.length) {
129
- remainingChars = validLine.slice(-1) + remainingChars;
130
- validLine = validLine.slice(0, -1);
131
- }
132
- return {validLine, remainingChars};
133
- };
110
+ return text.trimEnd() + '...';
111
+ }
134
112
 
135
113
  /**
136
114
  * Process the given text. This function divides the text into lines, by breaking the text according to the break
137
115
  * mode given in the props. The width of the text is calculating by using a non-mounted canvas. The font family and
138
116
  * font size are calculated from the computed properties of the rendered label.
139
117
  */
140
- renderText = () => {
141
- const {text, maxLines, breakMode} = this.props, chars = text.split(''), width = this.getWidth();
142
- if (width === 'initial') return text;
118
+ function renderText() {
119
+ const text = props.text;
120
+ if (width === 'initial' || !labelRef.current) return text;
143
121
 
144
- const pxWidth: number = width.endsWith('px') ? Number(width.slice(0, -2)) : remToPx(Number(width.slice(0, -3))),
145
- lines: string[] = new Array<string>();
146
- let fontSize, fontFamily;
147
- if (this.label) {
148
- fontSize = this.getFontSize();
149
- fontFamily = getComputedProperty('font-family', this.label);
150
- }
122
+ const chars = text.split('');
123
+ const pxWidth: number = width.endsWith('px') ? Number(width.slice(0, -2)) : remToPx(Number(width.slice(0, -3)));
124
+ const lines: Array<string> = [];
125
+ const fontSize = getCurrentFontSize(labelRef.current);
126
+ const fontFamily = getComputedProperty('font-family', labelRef.current);
151
127
 
152
128
  let line = '';
153
129
  for (let i = 0; i < chars.length; i++) {
@@ -157,7 +133,7 @@ export default class Label extends React.Component<Props, State> {
157
133
  else {
158
134
  let processedLine = line;
159
135
  if (breakMode === BreakType.WORD) {
160
- const {validLine, remainingChars} = this.breakLine(line);
136
+ const {validLine, remainingChars} = breakLine(line);
161
137
  processedLine = validLine;
162
138
  line = remainingChars;
163
139
  } else line = '';
@@ -165,8 +141,8 @@ export default class Label extends React.Component<Props, State> {
165
141
  lines.push(processedLine);
166
142
  if (maxLines > 0 && lines.length === maxLines) {
167
143
  if (i < chars.length)
168
- // Truncate last line if chars remaining
169
- lines[lines.length - 1] = this.reduceText(lines[lines.length - 1], pxWidth);
144
+ // Truncate last line if chars remaining
145
+ lines[lines.length - 1] = truncateText(lines[lines.length - 1], pxWidth);
170
146
  line = '';
171
147
  break;
172
148
  }
@@ -174,23 +150,178 @@ export default class Label extends React.Component<Props, State> {
174
150
  }
175
151
  }
176
152
  if (line.length) lines.push(line);
177
- this.setState({text: lines.join('\n')});
178
- };
179
-
180
- componentDidMount = () => {
181
- // Make sure the label has mounted before calculating the text
182
- window.setTimeout(this.renderText, 0);
183
- };
184
-
185
- render = () => {
186
- const {labelType} = this.props, {text} = this.state, labelProps = this.getLabelProps();
187
- switch (labelType) {
188
- case LabelType.LABEL:
189
- return <label {...labelProps}>{text}</label>;
190
- case LabelType.PAR:
191
- return <p {...labelProps}>{text}</p>;
192
- case LabelType.SPAN:
193
- return <span {...labelProps}>{text}</span>;
194
- }
195
- };
196
- }
153
+ setText(lines.join('\n'));
154
+ }
155
+
156
+ useEffect(() => {
157
+ defer(renderText);
158
+ }, []);
159
+
160
+ switch (labelType) {
161
+ case LabelType.LABEL:
162
+ return <label {...labelProps}>{text}</label>;
163
+ case LabelType.PAR:
164
+ return <p {...labelProps}>{text}</p>;
165
+ case LabelType.SPAN:
166
+ return <span {...labelProps}>{text}</span>;
167
+ }
168
+ }
169
+
170
+ // class _Label extends React.Component<Props, State> {
171
+ // declare context: React.ContextType<typeof ControlContext>;
172
+ // static contextType = ControlContext;
173
+ //
174
+ // static defaultProps = {
175
+ // labelType: LabelType.LABEL,
176
+ // className: '',
177
+ // labelProps: {},
178
+ // breakMode: BreakType.CHAR,
179
+ // maxLines: 0,
180
+ // };
181
+ //
182
+ // label: HTMLElement | null = null;
183
+ //
184
+ // state = {
185
+ // text: '',
186
+ // };
187
+ //
188
+ // getColor = (): string => {
189
+ // const {contrastColor} = this.context, {color} = this.props, finalColor: string = color ? color : contrastColor;
190
+ // return convertColor(finalColor);
191
+ // };
192
+ //
193
+ // getWidth = (): string => {
194
+ // const {width} = this.props;
195
+ // let finalWidth: string = 'initial';
196
+ // if (width !== undefined && width !== null) {
197
+ // if (typeof width === 'string') finalWidth = width;
198
+ // else finalWidth = width + 'px';
199
+ // }
200
+ // return finalWidth;
201
+ // };
202
+ //
203
+ // getLabelProps = (): LabelProps => {
204
+ // const {className, labelProps, text} = this.props;
205
+ // return {
206
+ // className: classNames('dvr-label', className),
207
+ // style: {
208
+ // color: this.getColor(),
209
+ // width: this.getWidth(),
210
+ // display: 'inline-block',
211
+ // },
212
+ // title: text,
213
+ // ref: (ref: HTMLElement | null) => {
214
+ // this.label = ref
215
+ // },
216
+ // ...labelProps,
217
+ // };
218
+ // };
219
+ //
220
+ // getFontSize = (): number | undefined => {
221
+ // if (this.label) {
222
+ // let fontSize: number | string = getComputedProperty('font-size', this.label);
223
+ // fontSize = parseInt(fontSize.slice(0, fontSize.length - 2), 10);
224
+ // return fontSize;
225
+ // }
226
+ // return undefined;
227
+ // };
228
+ //
229
+ // /**
230
+ // * Truncate given text to given maxWidth. Truncating is done by removing characters from the end until the text + ...
231
+ // * width is less then the given maxWidth. Trailing white spaces are removed and ... are appended.
232
+ // * @param text Text to truncate
233
+ // * @param maxWidth Max width in px
234
+ // */
235
+ // reduceText = (text: string, maxWidth: number): string => {
236
+ // let fontSize, fontFamily;
237
+ // if (this.label) {
238
+ // fontSize = this.getFontSize();
239
+ // fontFamily = getComputedProperty('font-family', this.label);
240
+ // }
241
+ // while (calculateTextWidth(text + '...', fontSize, fontFamily) >= maxWidth)
242
+ // text = text.slice(0, -1);
243
+ // return text.trimEnd() + '...';
244
+ // };
245
+ //
246
+ // /**
247
+ // * Break a line by removing characters from the end until a whitespace or dash is found. Removed characters are
248
+ // * returned as remainingChars.
249
+ // * @param validLine Text to break
250
+ // * @return Object containing the remainder of the the line and the removed characters.
251
+ // */
252
+ // breakLine = (validLine: string): { validLine: string, remainingChars: string } => {
253
+ // let remainingChars = '';
254
+ // if (/[ -]$/.test(validLine)) {
255
+ // validLine = validLine.slice(0, -1);
256
+ // remainingChars = ' ';
257
+ // }
258
+ //
259
+ // while (!/[ -]$/.test(validLine) && validLine.length) {
260
+ // remainingChars = validLine.slice(-1) + remainingChars;
261
+ // validLine = validLine.slice(0, -1);
262
+ // }
263
+ // return {validLine, remainingChars};
264
+ // };
265
+ //
266
+ // /**
267
+ // * Process the given text. This function divides the text into lines, by breaking the text according to the break
268
+ // * mode given in the props. The width of the text is calculating by using a non-mounted canvas. The font family and
269
+ // * font size are calculated from the computed properties of the rendered label.
270
+ // */
271
+ // renderText = () => {
272
+ // const {text, maxLines, breakMode} = this.props, chars = text.split(''), width = this.getWidth();
273
+ // if (width === 'initial') return text;
274
+ //
275
+ // const pxWidth: number = width.endsWith('px') ? Number(width.slice(0, -2)) : remToPx(Number(width.slice(0, -3))),
276
+ // lines: string[] = new Array<string>();
277
+ // let fontSize, fontFamily;
278
+ // if (this.label) {
279
+ // fontSize = this.getFontSize();
280
+ // fontFamily = getComputedProperty('font-family', this.label);
281
+ // }
282
+ //
283
+ // let line = '';
284
+ // for (let i = 0; i < chars.length; i++) {
285
+ // const char = chars[i];
286
+ // if (calculateTextWidth(line + char, fontSize, fontFamily) < pxWidth)
287
+ // line += char;
288
+ // else {
289
+ // let processedLine = line;
290
+ // if (breakMode === BreakType.WORD) {
291
+ // const {validLine, remainingChars} = this.breakLine(line);
292
+ // processedLine = validLine;
293
+ // line = remainingChars;
294
+ // } else line = '';
295
+ //
296
+ // lines.push(processedLine);
297
+ // if (maxLines > 0 && lines.length === maxLines) {
298
+ // if (i < chars.length)
299
+ // // Truncate last line if chars remaining
300
+ // lines[lines.length - 1] = this.reduceText(lines[lines.length - 1], pxWidth);
301
+ // line = '';
302
+ // break;
303
+ // }
304
+ // line += char;
305
+ // }
306
+ // }
307
+ // if (line.length) lines.push(line);
308
+ // this.setState({text: lines.join('\n')});
309
+ // };
310
+ //
311
+ // componentDidMount = () => {
312
+ // // Make sure the label has mounted before calculating the text
313
+ // window.setTimeout(this.renderText, 0);
314
+ // };
315
+ //
316
+ // render = () => {
317
+ // const {labelType} = this.props, {text} = this.state, labelProps = this.getLabelProps();
318
+ // switch (labelType) {
319
+ // case LabelType.LABEL:
320
+ // return <label {...labelProps}>{text}</label>;
321
+ // case LabelType.PAR:
322
+ // return <p {...labelProps}>{text}</p>;
323
+ // case LabelType.SPAN:
324
+ // return <span {...labelProps}>{text}</span>;
325
+ // }
326
+ // };
327
+ // }
@@ -0,0 +1,150 @@
1
+ import './style/mobileNavigation.scss';
2
+
3
+ import classNames from 'classnames';
4
+ import React, {
5
+ ForwardedRef,
6
+ forwardRef,
7
+ MouseEventHandler,
8
+ ReactElement, useEffect,
9
+ useImperativeHandle,
10
+ useMemo, useRef,
11
+ useState
12
+ } from 'react';
13
+ import {useMatch, useNavigate} from 'react-router';
14
+ import {NavigationItem} from "../util/interfaces";
15
+ import {AwesomeIcon, hasHover} from "../../../index";
16
+ import {defer} from 'lodash';
17
+
18
+ interface Props {
19
+ onClickItem?: (item: NavigationItem) => MouseEventHandler;
20
+ navigationItems: Array<NavigationItem>;
21
+ className?: string;
22
+ menuIcon?: ReactElement;
23
+ id?: string;
24
+ keepOpen?: true;
25
+ }
26
+
27
+ export interface MobileNavigationRef {
28
+ toggle: (forceState?: boolean) => void;
29
+ }
30
+
31
+ function MobileNavigation(props: Props, ref: ForwardedRef<MobileNavigationRef>) {
32
+ const {navigationItems, onClickItem, className, menuIcon, id, keepOpen} = props;
33
+ const [open, setOpen] = useState(false);
34
+ const [topItems, bottomItems] = useMemo(() => {
35
+ const topItems: Array<NavigationItem> = [];
36
+ const bottomItems: Array<NavigationItem> = [];
37
+ for (const item of navigationItems) {
38
+ if (item.placement === 'bottom') bottomItems.push(item);
39
+ // Default placement top
40
+ else topItems.push(item);
41
+ }
42
+ return [topItems, bottomItems];
43
+ }, [navigationItems]);
44
+ const navigate = useNavigate();
45
+ const menuRef = useRef<HTMLDivElement>(null);
46
+
47
+ function toggle(forceState?: boolean) {
48
+ if (forceState !== undefined) setOpen(forceState);
49
+ else setOpen(!open);
50
+ }
51
+
52
+ function onClickIcon() {
53
+ toggle();
54
+ }
55
+
56
+ function _onClick(item: NavigationItem) {
57
+ return function (evt: React.MouseEvent) {
58
+ if (!item.passPropagation) evt.stopPropagation();
59
+ if (!keepOpen) toggle(false);
60
+ if (onClickItem) return onClickItem(item)(evt);
61
+ else if (item.onClick) item.onClick(evt);
62
+ else if (item.route) _navigate(item.route);
63
+ else throw new Error(`Mobile navigation item ${item.id} has no click handler.`)
64
+ }
65
+ }
66
+
67
+ function _navigate(route: string) {
68
+ if (route.startsWith('http'))
69
+ window.open(route, '_blank');
70
+ else navigate(route);
71
+ }
72
+
73
+ function renderNavigationItem(item: NavigationItem) {
74
+ return (
75
+ <MobileNavigationItem key={item.id} item={item} onClick={_onClick}/>
76
+ );
77
+ }
78
+
79
+ function renderIcon() {
80
+ let icon: React.ReactElement;
81
+ if (!!menuIcon) {
82
+ const className = classNames(menuIcon.props.className, 'mobile-navigation-icon');
83
+ icon = React.cloneElement(menuIcon, {...menuIcon.props, className});
84
+ } else icon = <AwesomeIcon name='bars' className='mobile-navigation-icon'/>;
85
+ return (
86
+ <div className='mobile-navigation-icon-container' onClick={onClickIcon}>
87
+ {icon}
88
+ </div>
89
+ )
90
+ }
91
+
92
+ function renderMenu() {
93
+ return (
94
+ <>
95
+ {open && <div className='click-catcher'/>}
96
+ <div className='mobile-navigation-container' ref={menuRef}>
97
+ <AwesomeIcon name='times' className='mobile-navigation-close' onClick={onClickIcon}/>
98
+ <div className='mobile-navigation-items'>
99
+ {topItems.map(renderNavigationItem)}
100
+ </div>
101
+ <div className='mobile-navigation-items'>
102
+ {bottomItems.map(renderNavigationItem)}
103
+ </div>
104
+ </div>
105
+ </>
106
+ )
107
+ }
108
+
109
+ function outsideClickListener() {
110
+ const menu = menuRef.current;
111
+ if (!hasHover(menu)) toggle(false);
112
+ }
113
+
114
+ useEffect(() => {
115
+ defer(() => {
116
+ if (open) document.addEventListener('click', outsideClickListener);
117
+ else document.removeEventListener('click', outsideClickListener);
118
+ });
119
+ return function () {
120
+ document.removeEventListener('click', outsideClickListener);
121
+ }
122
+ }, [open]);
123
+
124
+ useImperativeHandle(ref, () => ({toggle}));
125
+
126
+ return (
127
+ <div className={classNames('dvrd-mobile-navigation', open && 'open', className)} id={id}>
128
+ {renderIcon()}
129
+ {renderMenu()}
130
+ </div>
131
+ )
132
+ }
133
+
134
+ interface ItemProps {
135
+ item: NavigationItem;
136
+ onClick: (item: NavigationItem) => MouseEventHandler;
137
+ }
138
+
139
+ function MobileNavigationItem(props: ItemProps) {
140
+ const {item, onClick} = props;
141
+ const isActive = item.route && Boolean(useMatch(item.route));
142
+ return (
143
+ <div key={item.id} className={classNames('mobile-navigation-item', isActive && 'active', item.className)}
144
+ id={item.id} onClick={onClick(item)}>
145
+ {item.content}
146
+ </div>
147
+ )
148
+ }
149
+
150
+ export default forwardRef<MobileNavigationRef, Props>(MobileNavigation);
@@ -0,0 +1,88 @@
1
+ @import '../../../style/variables';
2
+
3
+ .dvrd-mobile-navigation {
4
+ @media(min-width: 750px) {
5
+ display: none;
6
+ }
7
+
8
+ color: $color-blue-text;
9
+ position: fixed;
10
+ z-index: 2000;
11
+
12
+ .mobile-navigation-icon-container {
13
+ visibility: visible;
14
+ opacity: 1;
15
+ position: fixed;
16
+ top: 1rem;
17
+ left: 1rem;
18
+ padding: .5rem; // For better UX, makes the touch surface bigger
19
+
20
+ .mobile-navigation-icon {
21
+ @include backgroundShadow2;
22
+ font-size: 2rem;
23
+ padding: .25rem;
24
+ background-color: white;
25
+ border-radius: .5rem;
26
+ }
27
+ }
28
+
29
+ .click-catcher {
30
+ position: fixed;
31
+ top: 0;
32
+ left: 0;
33
+ width: 100dvw;
34
+ height: 100dvh;
35
+ }
36
+
37
+ .mobile-navigation-container {
38
+ @include backgroundShadow3($borderRadius: 0);
39
+ position: fixed;
40
+ top: 0;
41
+ left: 0;
42
+ height: 100dvh;
43
+ max-width: 100dvw;
44
+ overflow-y: auto;
45
+ background-color: white;
46
+ transform: translateX(-100%);
47
+ transition: transform .2s ease-in-out;
48
+ display: grid;
49
+ grid-template-rows: fit-content(100%) 1fr fit-content(100%);
50
+
51
+ .mobile-navigation-close {
52
+ position: sticky;
53
+ top: 1rem;
54
+ left: 1rem;
55
+ font-size: 2rem;
56
+ padding: .5rem;
57
+ z-index: 1;
58
+ margin-bottom: 1rem;
59
+ }
60
+
61
+ .mobile-navigation-items {
62
+ display: grid;
63
+ align-content: start;
64
+
65
+ .mobile-navigation-item {
66
+ padding: 1rem;
67
+ font-size: 1.1rem;
68
+
69
+ &.active {
70
+ color: $color-blue-1;
71
+ font-weight: 500;
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ &.open {
78
+ .mobile-navigation-icon-container {
79
+ visibility: hidden;
80
+ opacity: 0;
81
+ transition: visibility .2s ease-in-out, opacity .2s ease-in-out;
82
+ }
83
+
84
+ .mobile-navigation-container {
85
+ transform: none;
86
+ }
87
+ }
88
+ }
@@ -13,7 +13,6 @@ export const addToHistory = (items: PDFElementParams<any, any>[], mainFont: PdfF
13
13
  if (history !== HISTORY[currentIdx]) {
14
14
  HISTORY.push(history);
15
15
  currentIdx++;
16
- console.debug('Current history', HISTORY.map((hist: string) => JSON.parse(hist)));
17
16
  }
18
17
  };
19
18
 
@@ -78,6 +78,14 @@ export default class PDFTemplateCreator extends PureComponent<Props, State> {
78
78
  this.state = {elements, focusedElement: null, canRedo: false, canUndo: false};
79
79
  }
80
80
 
81
+ reloadTemplate(items: DefaultPDFElementParams<any, any>[]) {
82
+ const elements: IndexedObject<{
83
+ element: React.ReactElement,
84
+ config: DefaultPDFElementParams<any, any>
85
+ }> = this.processItems(items);
86
+ this.setState({elements, canRedo: false, canUndo: false, focusedElement: null});
87
+ }
88
+
81
89
  processItems = (items?: DefaultPDFElementParams<any, any>[],
82
90
  convertUnits: boolean = true): IndexedObject<{
83
91
  element: React.ReactElement,
@@ -276,8 +284,7 @@ export default class PDFTemplateCreator extends PureComponent<Props, State> {
276
284
  const supportMultiPage = this.props.options?.supportMultiPage ?? this.props.supportMultiPage;
277
285
  const enableTableHeaderColor = this.props.options?.enableTableHeaderColor;
278
286
  return (
279
- <div className='pdf-creator' onClick={this.onResetElement}
280
- style={{maxWidth: maxWidth ?? '100%'}}>
287
+ <div className='pdf-creator' onClick={this.onResetElement} style={{maxWidth: maxWidth ?? '100%'}}>
281
288
  <div className='footer-container'>
282
289
  <div className='left-buttons'>
283
290
  {onPreview !== undefined ? <DvrdButton onClick={this.onPreview} label='Voorbeeld'/> : <div/>}
@@ -14,6 +14,20 @@ export interface MenuItem {
14
14
  normalCase?: boolean;
15
15
  id?: string;
16
16
  allowPropagation?: boolean;
17
+ index?: number;
18
+ route?: string;
19
+ }
20
+
21
+ export interface NavigationItem {
22
+ content: React.ReactNode;
23
+ children?: Array<NavigationItem>;
24
+ onClick?: MouseEventHandler;
25
+ id: string;
26
+ passPropagation?: true;
27
+ index?: number;
28
+ route?: string;
29
+ placement?: 'top' | 'bottom';
30
+ className?: string;
17
31
  }
18
32
 
19
33
  export interface PasswordRule {
@@ -78,7 +92,7 @@ export interface HeaderItem {
78
92
 
79
93
  export enum ModeEnum {DETAIL = 'detail', EDIT = 'edit', NEW = 'new'}
80
94
 
81
- export enum Breakpoint {XS = 0, SM = 600, MD = 960, LG = 1280, XL = 1920}
95
+ export enum Breakpoint {XS = 0, SM = 750, MD = 960, LG = 1280, XL = 1920}
82
96
 
83
97
  export enum MenuPlacement {TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT}
84
98
 
@@ -220,6 +220,11 @@ $hoverColor: white, $duration: .2s) {
220
220
  border-radius: .5rem;
221
221
  }
222
222
 
223
+ @mixin backgroundShadow3($color: black, $alpha: .1, $borderRadius: .5rem) {
224
+ box-shadow: 0 4px 4px rgba($color, $alpha), 0 0 4px rgba($color, $alpha);
225
+ border-radius: $borderRadius;
226
+ }
227
+
223
228
  @mixin popupContainer() {
224
229
  position: fixed;
225
230
  top: 0;
@@ -11,7 +11,7 @@ Ref: https://medium.com/@catalinbridinel/this-makes-more-sense-than-the-counter-
11
11
  // Material-UI Breakpoints
12
12
  //===========================
13
13
  @mixin break-xs {
14
- @media(max-width: 599px) {
14
+ @media(max-width: 750px) {
15
15
  @content;
16
16
  }
17
17
  }