@lemonadejs/calendar 5.2.0 → 5.3.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/README.md CHANGED
@@ -1,88 +1,90 @@
1
- # JavaScript Calendar
2
-
3
- [Official JavaScript Calendar Documenation](https://lemonadejs.com/docs/plugins/calendar)
4
-
5
- Compatible with Vanilla JavaScript, LemonadeJS, React, VueJS or Angular.
6
-
7
- # JavaScript Calendar
8
-
9
- Leverage the power of the LemonadeJS Calendar, a lightweight and versatile JavaScript component compatible with React, VueJS, and Angular. Designed to enhance web applications, it offers an embeddable calendar for easy date, time, and range selections. Key features include:
10
-
11
- - Intuitive keyboard navigation.
12
- - A reactive and responsive design for seamless device adaptation.
13
- - Flexible range selection for various applications.
14
- - Customizable options to match your project needs.
15
-
16
- ## Getting Started
17
-
18
- You can install using NPM or using directly from a CDN.
19
-
20
- ### npm Installation
21
-
22
- To install it in your project using npm, run the following command:
23
-
24
- ```bash
25
- $ npm install @lemonadejs/calendar
26
- ```
27
-
28
- ### CDN
29
-
30
- For immediate use without NPM, include these script tags in your HTML for access to the calendar and its dependencies:
31
-
32
- ```html
33
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lemonadejs/modal/dist/style.min.css" />
34
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lemonadejs/calendar/dist/style.min.css" />
35
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />
36
- <script src="https://cdn.jsdelivr.net/npm/lemonadejs/dist/lemonade.min.js"></script>
37
- <script src="https://cdn.jsdelivr.net/npm/@lemonadejs/modal/dist/index.min.js"></script>
38
- <script src="https://cdn.jsdelivr.net/npm/@lemonadejs/calendar/dist/index.min.js"></script>
39
- ```
40
-
41
- ### Usage
42
-
43
- Quick example with ReactJS.
44
-
45
- ```javascript
46
- import React, { useRef } from 'react';
47
- import Calendar from '@lemonadejs/calendar/dist/react';
48
-
49
- import "@lemonadejs/calendar/dist/style.css";
50
- import "@lemonadejs/modal/dist/style.css";
51
-
52
- export default function App() {
53
- const calendarRef = useRef();
54
-
55
- return (<>
56
- <Calendar type={'inline'} ref={calendarRef} value={new Date()} />
57
- </>);
58
- }
59
- ```
60
-
61
- ### Configuration
62
-
63
- You can configure things such as calendar starting date, calendar events, and customize functions.
64
-
65
- #### Calendar Properties
66
-
67
- | Attribute | Type | Description |
68
- | --------- |------------------|-------------------------------------------------------------------------------------------------------------------|
69
- | type? | string | Determines the rendering type for the calendar. Options: 'inline', 'default'. |
70
- | range? | boolean | Enables the range mode for date selection. |
71
- | value? | number or string | Represents the currently selected date. |
72
- | numeric? | boolean | Enables the use of numeric dates, treating them as serial numbers. |
73
- | input? | HTML element | An optional reference to control the calendar opening. The value is automatically bound when using this property. |
74
-
75
- ### Calendar Events
76
-
77
- | Event | Description |
78
- |----------------------------------|-------------------------------------|
79
- | onchange?: (self, value) => void | Called when a new date is selected. |
80
-
81
- ## License
82
-
83
- The LemonadeJS [Reactive JavaScript Calendar](https://lemonadejs.com/docs/plugins/calendar) is released under the MIT.
84
-
85
- ## Other Tools
86
-
87
- - [jSuites Plugins - JavaScript Calendar](https://jsuites.net/docs/javascript-calendar)
88
- - [Jspreadsheet - JavaScript Spreadsheet](https://jspreadsheet.com/)
1
+ # JavaScript Calendar
2
+
3
+ [Official JavaScript Calendar Documenation](https://lemonadejs.com/docs/plugins/calendar)
4
+
5
+ Compatible with Vanilla JavaScript, LemonadeJS, React, VueJS or Angular.
6
+
7
+ # JavaScript Calendar
8
+
9
+ Leverage the power of the LemonadeJS Calendar, a lightweight and versatile JavaScript component compatible with React, VueJS, and Angular. Designed to enhance web applications, it offers an embeddable calendar for easy date, time, and range selections. Key features include:
10
+
11
+ - Intuitive keyboard navigation.
12
+ - A reactive and responsive design for seamless device adaptation.
13
+ - Flexible range selection for various applications.
14
+ - Customizable options to match your project needs.
15
+
16
+ ## Getting Started
17
+
18
+ You can install using NPM or using directly from a CDN.
19
+
20
+ ### npm Installation
21
+
22
+ To install it in your project using npm, run the following command:
23
+
24
+ ```bash
25
+ $ npm install @lemonadejs/calendar
26
+ ```
27
+
28
+ ### CDN
29
+
30
+ For immediate use without NPM, include these script tags in your HTML for access to the calendar and its dependencies:
31
+
32
+ ```html
33
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lemonadejs/modal/dist/style.min.css" />
34
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lemonadejs/calendar/dist/style.min.css" />
35
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />
36
+ <script src="https://cdn.jsdelivr.net/npm/lemonadejs/dist/lemonade.min.js"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/@lemonadejs/modal/dist/index.min.js"></script>
38
+ <script src="https://cdn.jsdelivr.net/npm/@lemonadejs/calendar/dist/index.min.js"></script>
39
+ ```
40
+
41
+ ### Usage
42
+
43
+ Quick example with ReactJS.
44
+
45
+ ```javascript
46
+ import React, { useRef } from 'react';
47
+ import Calendar from '@lemonadejs/calendar/dist/react';
48
+
49
+ import '@lemonadejs/calendar/dist/style.css';
50
+ import '@lemonadejs/modal/dist/style.css';
51
+
52
+ export default function App() {
53
+ const calendarRef = useRef();
54
+
55
+ return (
56
+ <>
57
+ <Calendar type={'inline'} ref={calendarRef} value={new Date()} />
58
+ </>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ### Configuration
64
+
65
+ You can configure things such as calendar starting date, calendar events, and customize functions.
66
+
67
+ #### Calendar Properties
68
+
69
+ | Attribute | Type | Description |
70
+ | --------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
71
+ | type? | string | Determines the rendering type for the calendar. Options: 'inline', 'default'. |
72
+ | range? | boolean | Enables the range mode for date selection. |
73
+ | value? | number or string | Represents the currently selected date. |
74
+ | numeric? | boolean | Enables the use of numeric dates, treating them as serial numbers. |
75
+ | input? | HTML element | An optional reference to control the calendar opening. The value is automatically bound when using this property. |
76
+
77
+ ### Calendar Events
78
+
79
+ | Event | Description |
80
+ | -------------------------------- | ----------------------------------- |
81
+ | onchange?: (self, value) => void | Called when a new date is selected. |
82
+
83
+ ## License
84
+
85
+ The LemonadeJS [Reactive JavaScript Calendar](https://lemonadejs.com/docs/plugins/calendar) is released under the MIT.
86
+
87
+ ## Other Tools
88
+
89
+ - [jSuites Plugins - JavaScript Calendar](https://jsuites.net/docs/javascript-calendar)
90
+ - [Jspreadsheet - JavaScript Spreadsheet](https://jspreadsheet.com/)
package/dist/index.d.ts CHANGED
@@ -37,7 +37,7 @@ declare namespace Calendar {
37
37
  /** Update view on mouse wheel. Default: true */
38
38
  wheel?: boolean;
39
39
  /** Bind the calendar to na HTML input element */
40
- input?: HTMLElement | 'auto';
40
+ input?: HTMLInputElement | 'auto';
41
41
  /** Create events and assing the calendar classes for the input. Default true */
42
42
  initInput?: boolean;
43
43
  /** Change value event */
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ if (! Modal && typeof (require) === 'function') {
51
51
  }
52
52
  }
53
53
 
54
- const Helpers = (function() {
54
+ const Helpers = (function () {
55
55
  const component = {};
56
56
 
57
57
  // Excel like dates
@@ -60,22 +60,22 @@ if (! Modal && typeof (require) === 'function') {
60
60
  const millisecondsPerDay = 86400000;
61
61
 
62
62
  // Transform in two digits
63
- component.two = function(value) {
63
+ component.two = function (value) {
64
64
  value = '' + value;
65
65
  if (value.length === 1) {
66
66
  value = '0' + value;
67
67
  }
68
68
  return value;
69
- }
69
+ };
70
70
 
71
- component.isValidDate = function(d) {
71
+ component.isValidDate = function (d) {
72
72
  return d instanceof Date && !isNaN(d.getTime());
73
- }
73
+ };
74
74
 
75
- component.isValidDateFormat = function(date, format) {
75
+ component.isValidDateFormat = function(date) {
76
76
  if (typeof date === 'string') {
77
77
  // Check format: YYYY-MM-DD using regex
78
- const match = date.match(/^(\d{4})-(\d{2})-(\d{2})$/);
78
+ const match = date.substring(0, 10).match(/^(\d{4})-(\d{2})-(\d{2})$/);
79
79
  if (match) {
80
80
  const year = Number(match[1]);
81
81
  const month = Number(match[2]) - 1;
@@ -105,7 +105,7 @@ if (! Modal && typeof (require) === 'function') {
105
105
  i = date[4];
106
106
  s = date[5];
107
107
  } else {
108
- if (! date) {
108
+ if (!date) {
109
109
  date = new Date();
110
110
  }
111
111
  y = date.getUTCFullYear();
@@ -119,12 +119,14 @@ if (! Modal && typeof (require) === 'function') {
119
119
  if (dateOnly === true) {
120
120
  return component.two(y) + '-' + component.two(m) + '-' + component.two(d);
121
121
  } else {
122
- return component.two(y) + '-' + component.two(m) + '-' + component.two(d) + ' ' + component.two(h) + ':' + component.two(i) + ':' + component.two(s);
122
+ return (
123
+ component.two(y) + '-' + component.two(m) + '-' + component.two(d) + ' ' + component.two(h) + ':' + component.two(i) + ':' + component.two(s)
124
+ );
123
125
  }
124
- }
126
+ };
125
127
 
126
128
  component.toArray = function (value) {
127
- let date = value.split(((value.indexOf('T') !== -1) ? 'T' : ' '));
129
+ let date = value.split(value.indexOf('T') !== -1 ? 'T' : ' ');
128
130
  let time = date[1];
129
131
 
130
132
  date = date[0].split('-');
@@ -140,14 +142,14 @@ if (! Modal && typeof (require) === 'function') {
140
142
  i = parseInt(time[1]);
141
143
  }
142
144
  return [y, m, d, h, i, 0];
143
- }
145
+ };
144
146
 
145
- component.arrayToStringDate = function(arr) {
147
+ component.arrayToStringDate = function (arr) {
146
148
  return component.toString(arr, false);
147
- }
149
+ };
148
150
 
149
- component.dateToNum = function(jsDate) {
150
- if (typeof(jsDate) === 'string') {
151
+ component.dateToNum = function (jsDate) {
152
+ if (typeof jsDate === 'string') {
151
153
  jsDate = new Date(jsDate + ' GMT+0');
152
154
  }
153
155
  let jsDateInMilliseconds = jsDate.getTime();
@@ -157,9 +159,9 @@ if (! Modal && typeof (require) === 'function') {
157
159
  jsDateInMilliseconds -= excelInitialTime;
158
160
 
159
161
  return jsDateInMilliseconds / millisecondsPerDay;
160
- }
162
+ };
161
163
 
162
- component.numToDate = function(excelSerialNumber, asArray) {
164
+ component.numToDate = function (excelSerialNumber, asArray) {
163
165
  // allow 0; only bail on null/undefined/empty
164
166
  if (excelSerialNumber === null || excelSerialNumber === undefined || excelSerialNumber === '') {
165
167
  return '';
@@ -196,10 +198,10 @@ if (! Modal && typeof (require) === 'function') {
196
198
  ];
197
199
 
198
200
  return asArray ? arr : component.toString(arr, false);
199
- }
201
+ };
200
202
 
201
203
  component.prettify = function (d, texts) {
202
- if (! texts) {
204
+ if (!texts) {
203
205
  texts = {
204
206
  justNow: 'Just now',
205
207
  xMinutesAgo: '{0}m ago',
@@ -208,7 +210,7 @@ if (! Modal && typeof (require) === 'function') {
208
210
  xWeeksAgo: '{0}w ago',
209
211
  xMonthsAgo: '{0} mon ago',
210
212
  xYearsAgo: '{0}y ago',
211
- }
213
+ };
212
214
  }
213
215
 
214
216
  if (d.indexOf('GMT') === -1 && d.indexOf('Z') === -1) {
@@ -221,24 +223,29 @@ if (! Modal && typeof (require) === 'function') {
221
223
 
222
224
  const format = (t, o) => {
223
225
  return t.replace('{0}', o);
224
- }
226
+ };
225
227
 
226
- if (! total) {
228
+ if (!total) {
227
229
  return texts.justNow;
228
230
  } else if (total < 90) {
229
231
  return format(texts.xMinutesAgo, total);
230
- } else if (total < 1440) { // One day
232
+ } else if (total < 1440) {
233
+ // One day
231
234
  return format(texts.xHoursAgo, Math.round(total / 60));
232
- } else if (total < 20160) { // 14 days
235
+ } else if (total < 20160) {
236
+ // 14 days
233
237
  return format(texts.xDaysAgo, Math.round(total / 1440));
234
- } else if (total < 43200) { // 30 days
238
+ } else if (total < 43200) {
239
+ // 30 days
235
240
  return format(texts.xWeeksAgo, Math.round(total / 10080));
236
- } else if (total < 1036800) { // 24 months
241
+ } else if (total < 1036800) {
242
+ // 24 months
237
243
  return format(texts.xMonthsAgo, Math.round(total / 43200));
238
- } else { // 24 months+
244
+ } else {
245
+ // 24 months+
239
246
  return format(texts.xYearsAgo, Math.round(total / 525600));
240
247
  }
241
- }
248
+ };
242
249
 
243
250
  component.prettifyAll = function () {
244
251
  let elements = document.querySelectorAll('.prettydate');
@@ -253,7 +260,7 @@ if (! Modal && typeof (require) === 'function') {
253
260
  }
254
261
  }
255
262
  }
256
- }
263
+ };
257
264
 
258
265
  // Compatibility with jSuites
259
266
  component.now = component.toString;
@@ -261,17 +268,17 @@ if (! Modal && typeof (require) === 'function') {
261
268
  const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
262
269
  const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
263
270
 
264
- const translate = function(t) {
265
- if (typeof(document) !== "undefined" && document.dictionary) {
271
+ const translate = function (t) {
272
+ if (typeof document !== 'undefined' && document.dictionary) {
266
273
  return document.dictionary[t] || t;
267
274
  } else {
268
275
  return t;
269
276
  }
270
- }
277
+ };
271
278
 
272
279
  Object.defineProperty(component, 'weekdays', {
273
280
  get: function () {
274
- return weekdays.map(function(v) {
281
+ return weekdays.map(function (v) {
275
282
  return translate(v);
276
283
  });
277
284
  },
@@ -279,15 +286,15 @@ if (! Modal && typeof (require) === 'function') {
279
286
 
280
287
  Object.defineProperty(component, 'weekdaysShort', {
281
288
  get: function () {
282
- return weekdays.map(function(v) {
283
- return translate(v).substring(0,3);
289
+ return weekdays.map(function (v) {
290
+ return translate(v).substring(0, 3);
284
291
  });
285
292
  },
286
293
  });
287
294
 
288
295
  Object.defineProperty(component, 'months', {
289
296
  get: function () {
290
- return months.map(function(v) {
297
+ return months.map(function (v) {
291
298
  return translate(v);
292
299
  });
293
300
  },
@@ -295,8 +302,8 @@ if (! Modal && typeof (require) === 'function') {
295
302
 
296
303
  Object.defineProperty(component, 'monthsShort', {
297
304
  get: function () {
298
- return months.map(function(v) {
299
- return translate(v).substring(0,3);
305
+ return months.map(function (v) {
306
+ return translate(v).substring(0, 3);
300
307
  });
301
308
  },
302
309
  });
@@ -304,7 +311,7 @@ if (! Modal && typeof (require) === 'function') {
304
311
  return component;
305
312
  })();
306
313
 
307
- const Mask = (function() {
314
+ const Mask = (function Mask() {
308
315
  // Currency
309
316
  const tokens = {
310
317
  // Text
@@ -325,6 +332,33 @@ if (! Modal && typeof (require) === 'function') {
325
332
  general: [ 'A', '0', '\\?', '\\*', ',,M', ',,,B', '[0-9a-zA-Z\\$]+', '_\\(', '_\\)', '\\(', '\\)', '_-', '.']
326
333
  }
327
334
 
335
+ // All expressions
336
+ const allExpressions = [].concat(tokens.fraction, tokens.currency, tokens.datetime, tokens.percentage, tokens.scientific, tokens.numeric, tokens.text, tokens.general).join('|');
337
+
338
+ // Pre-compile all regexes once at initialization for better performance
339
+ const compiledTokens = {};
340
+ const tokenPriority = ['fraction', 'currency', 'scientific', 'percentage', 'numeric', 'datetime', 'text', 'general'];
341
+
342
+ // Initialize compiled regexes
343
+ for (const type of tokenPriority) {
344
+ compiledTokens[type] = tokens[type].map(pattern => ({
345
+ regex: new RegExp('^' + pattern + '$', 'gi'),
346
+ method: pattern
347
+ }));
348
+ }
349
+
350
+ // Pre-compile regex for getTokens function
351
+ const allExpressionsRegex = new RegExp(allExpressions, 'gi');
352
+
353
+ // Pre-compile currency symbol regexes for autoCastingCurrency
354
+ const knownSymbols = ['$', '€', '£', '¥', '₹', '₽', '₩', '₫', 'R$', 'CHF', 'AED'];
355
+ const currencyRegexes = knownSymbols.map(s => ({
356
+ symbol: s,
357
+ regex: new RegExp(`^${s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}(\\s?)`)
358
+ }));
359
+
360
+ const hiddenCaret = "\u200B";
361
+
328
362
  // Labels
329
363
  const weekDaysFull = Helpers.weekdays;
330
364
  const weekDays = Helpers.weekdaysShort;
@@ -333,6 +367,29 @@ if (! Modal && typeof (require) === 'function') {
333
367
 
334
368
  // Helpers
335
369
 
370
+ const focus = function(el) {
371
+ if (el.textContent.length) {
372
+ // Handle contenteditable elements
373
+ const range = document.createRange();
374
+ const sel = window.getSelection();
375
+
376
+ let node = el;
377
+ // Go as deep as possible to the last text node
378
+ while (node.lastChild) node = node.lastChild;
379
+ // Ensure it's a text node
380
+ if (node.nodeType === Node.TEXT_NODE) {
381
+ range.setStart(node, node.length);
382
+ } else {
383
+ range.setStart(node, node.childNodes.length);
384
+ }
385
+ range.collapse(true);
386
+ sel.removeAllRanges();
387
+ sel.addRange(range);
388
+
389
+ el.scrollLeft = el.scrollWidth;
390
+ }
391
+ }
392
+
336
393
  /**
337
394
  * Returns if the given value is considered blank
338
395
  */
@@ -461,7 +518,6 @@ if (! Modal && typeof (require) === 'function') {
461
518
  const setCaret = function(index) {
462
519
  if (typeof index !== 'number') index = Number(index) || 0;
463
520
 
464
- // Inputs/Textareas
465
521
  if (this.tagName !== 'DIV' || this.isContentEditable !== true) {
466
522
  const n = this.value ?? '';
467
523
  if (index < 0) index = 0;
@@ -648,14 +704,18 @@ if (! Modal && typeof (require) === 'function') {
648
704
  }
649
705
  } else {
650
706
  if (this.values[this.index] == 1 && parseInt(v) < 3) {
651
- this.date[1] = this.values[this.index] += v;
707
+ this.values[this.index] += v;
652
708
  commit();
653
709
  } else if (this.values[this.index] == 0 && parseInt(v) > 0 && parseInt(v) < 10) {
654
- this.date[1] = this.values[this.index] += v;
710
+ this.values[this.index] += v;
655
711
  commit();
656
712
  } else {
657
713
  let test = parseInt(this.values[this.index]);
658
714
  if (test > 0 && test <= 12) {
715
+ if (! single) {
716
+ test = '0' + test;
717
+ }
718
+ this.values[this.index] = test;
659
719
  commit();
660
720
  return false;
661
721
  }
@@ -706,6 +766,10 @@ if (! Modal && typeof (require) === 'function') {
706
766
  } else {
707
767
  let test = parseInt(this.values[this.index]);
708
768
  if (test > 0 && test <= 31) {
769
+ if (! single) {
770
+ test = '0' + test;
771
+ }
772
+ this.values[this.index] = test;
709
773
  commit();
710
774
  return false;
711
775
  }
@@ -935,10 +999,7 @@ if (! Modal && typeof (require) === 'function') {
935
999
  }
936
1000
  this.values[this.index] += this.decimal;
937
1001
  }
938
- } else if (v === "\u200B") {
939
- this.values[this.index] += v;
940
1002
  }
941
-
942
1003
  },
943
1004
  '[0#]+([.,]{1}0*#*)?%': function(v) {
944
1005
  parseMethods['[0#]+([.,]{1}0*#*)?'].call(this, v);
@@ -1094,7 +1155,7 @@ if (! Modal && typeof (require) === 'function') {
1094
1155
  // Add the value
1095
1156
  this.values[this.index] += v;
1096
1157
  // Only if caret is before the change
1097
- let current = this.values[this.index].replace('\u200B','');
1158
+ let current = this.values[this.index];
1098
1159
  // Add token to the values
1099
1160
  if (current !== word.substring(0,current.length)) {
1100
1161
  this.values[this.index] = word;
@@ -1121,7 +1182,7 @@ if (! Modal && typeof (require) === 'function') {
1121
1182
  this.index++;
1122
1183
  }
1123
1184
  },
1124
- '\\*': function(v) {
1185
+ '\\*': function() {
1125
1186
  this.values[this.index] = '';
1126
1187
  this.index++;
1127
1188
  return false;
@@ -1160,17 +1221,17 @@ if (! Modal && typeof (require) === 'function') {
1160
1221
  }
1161
1222
  this.values[this.index] += v;
1162
1223
  },
1163
- '_\\(': function(v) {
1224
+ '_\\(': function() {
1164
1225
  this.values[this.index] = ' ';
1165
1226
  this.index++;
1166
1227
  return false;
1167
1228
  },
1168
- '_\\)': function(v) {
1229
+ '_\\)': function() {
1169
1230
  this.values[this.index] = ' ';
1170
1231
  this.index++;
1171
1232
  return false;
1172
1233
  },
1173
- '\\(': function(v) {
1234
+ '\\(': function() {
1174
1235
  if (this.type === 'currency' && this.parenthesisForNegativeNumbers) {
1175
1236
  this.values[this.index] = '';
1176
1237
  } else {
@@ -1179,7 +1240,7 @@ if (! Modal && typeof (require) === 'function') {
1179
1240
  this.index++;
1180
1241
  return false;
1181
1242
  },
1182
- '\\)': function(v) {
1243
+ '\\)': function() {
1183
1244
  if (this.type === 'currency' && this.parenthesisForNegativeNumbers) {
1184
1245
  this.values[this.index] = '';
1185
1246
  } else {
@@ -1188,17 +1249,17 @@ if (! Modal && typeof (require) === 'function') {
1188
1249
  this.index++;
1189
1250
  return false;
1190
1251
  },
1191
- '_-': function(v) {
1252
+ '_-': function() {
1192
1253
  this.values[this.index] = ' ';
1193
1254
  this.index++;
1194
1255
  return false;
1195
1256
  },
1196
- ',,M': function(v) {
1257
+ ',,M': function() {
1197
1258
  this.values[this.index] = 'M';
1198
1259
  this.index++;
1199
1260
  return false;
1200
1261
  },
1201
- ',,,B': function(v) {
1262
+ ',,,B': function() {
1202
1263
  this.values[this.index] = 'B';
1203
1264
  this.index++;
1204
1265
  return false;
@@ -1243,9 +1304,8 @@ if (! Modal && typeof (require) === 'function') {
1243
1304
  // Types TODO: Generate types so we can garantee that text,scientific, numeric,percentage, current are not duplicates. If they are, it will be general or broken.
1244
1305
 
1245
1306
  const getTokens = function(str) {
1246
- const expressions = [].concat(tokens.fraction, tokens.currency, tokens.datetime, tokens.percentage, tokens.scientific, tokens.numeric, tokens.text, tokens.general);
1247
- // Expression to extract all tokens from the string
1248
- return str.match(new RegExp(expressions.join('|'), 'gi'));
1307
+ allExpressionsRegex.lastIndex = 0; // Reset for global regex
1308
+ return str.match(allExpressionsRegex);
1249
1309
  }
1250
1310
 
1251
1311
  /**
@@ -1253,35 +1313,23 @@ if (! Modal && typeof (require) === 'function') {
1253
1313
  */
1254
1314
  const getMethod = function(str, temporary) {
1255
1315
  str = str.toString().toUpperCase();
1256
- const types = Object.keys(tokens);
1257
1316
 
1258
1317
  // Check for datetime mask
1259
- let datetime = true;
1260
- for (let i = 0; i < temporary.length; i++) {
1261
- let type = temporary[i].type;
1262
- if (! (type === 'datetime' || type === 'general')) {
1263
- datetime = false;
1264
- }
1265
- }
1266
-
1267
- // Remove date time from the possible types
1268
- if (datetime !== true) {
1269
- let index = types.indexOf('datetime');
1270
- types.splice(index, 1);
1271
- }
1318
+ const datetime = temporary.every(t => t.type === 'datetime' || t.type === 'general');
1272
1319
 
1273
- // Get the method based on the token
1274
- for (let i = 0; i < types.length; i++) {
1275
- let type = types[i];
1320
+ // Use priority order for faster matching with pre-compiled regexes
1321
+ for (const type of tokenPriority) {
1322
+ if (!datetime && type === 'datetime') continue;
1276
1323
 
1277
- for (let j = 0; j < tokens[type].length; j++) {
1278
- let e = new RegExp('^' + tokens[type][j] + '$', 'gi'); // Anchor regex
1279
- let r = str.match(e);
1280
- if (r) {
1281
- return { type: type, method: tokens[type][j] }
1324
+ for (const compiled of compiledTokens[type]) {
1325
+ let regex = compiled.regex;
1326
+ regex.lastIndex = 0; // Reset regex state
1327
+ if (regex.test(str)) {
1328
+ return { type: type, method: compiled.method };
1282
1329
  }
1283
1330
  }
1284
1331
  }
1332
+ return null;
1285
1333
  }
1286
1334
 
1287
1335
  const fixMinuteToken = function(t) {
@@ -1417,7 +1465,7 @@ if (! Modal && typeof (require) === 'function') {
1417
1465
 
1418
1466
  const isNumber = function(num) {
1419
1467
  if (typeof(num) === 'string') {
1420
- num = num.replace("\u200B", "").trim();
1468
+ num = num.trim();
1421
1469
  }
1422
1470
  return !isNaN(num) && num !== null && num !== '';
1423
1471
  }
@@ -1711,7 +1759,7 @@ if (! Modal && typeof (require) === 'function') {
1711
1759
  }
1712
1760
 
1713
1761
  const extractDateAndTime = function(value) {
1714
- value = '' + value.substring(0,19);
1762
+ value = value.toString().substring(0,19);
1715
1763
  let splitStr = (value.indexOf('T') !== -1) ? 'T' : ' ';
1716
1764
  value = value.split(splitStr);
1717
1765
 
@@ -1759,11 +1807,20 @@ if (! Modal && typeof (require) === 'function') {
1759
1807
  // Walk every character on the value
1760
1808
  let method;
1761
1809
  while (method = getMethodByPosition(control)) {
1762
- // Get the method name to handle the current token
1763
- let ret = method.call(control, control.value[control.position]);
1764
- // Next position
1765
- if (ret !== false) {
1810
+ let char = control.value[control.position];
1811
+ if (char === hiddenCaret) {
1812
+ control.caret = {
1813
+ index: control.index,
1814
+ position: control.values[control.index]?.length ?? 0,
1815
+ }
1766
1816
  control.position++;
1817
+ } else {
1818
+ // Get the method name to handle the current token
1819
+ let ret = method.call(control, char);
1820
+ // Next position
1821
+ if (ret !== false) {
1822
+ control.position++;
1823
+ }
1767
1824
  }
1768
1825
  }
1769
1826
 
@@ -1785,6 +1842,13 @@ if (! Modal && typeof (require) === 'function') {
1785
1842
  }
1786
1843
  }
1787
1844
  }
1845
+ if (control.caret) {
1846
+ let index = control.caret.index;
1847
+ let position = control.caret.position;
1848
+ let value = control.values[index] ?? '';
1849
+ // Re-apply the caret to the original position
1850
+ control.values[index] = value.substring(0, position) + hiddenCaret + value.substring(position);
1851
+ }
1788
1852
 
1789
1853
  control.value = getValue(control);
1790
1854
 
@@ -1897,7 +1961,6 @@ if (! Modal && typeof (require) === 'function') {
1897
1961
  const parts = str.split('-');
1898
1962
  const p1 = parseInt(parts[0]);
1899
1963
  const p2 = parseInt(parts[1]);
1900
- const p3 = parseInt(parts[2]);
1901
1964
 
1902
1965
  if (p1 <= 12 && p2 <= 31 && p2 > 12) {
1903
1966
  patterns.push('mm-dd-yyyy', 'mm-dd-yy', 'm-d-yyyy', 'm-d-yy');
@@ -2085,13 +2148,10 @@ if (! Modal && typeof (require) === 'function') {
2085
2148
  const hasParens = /^\s*\(.+\)\s*$/.test(original);
2086
2149
  let value = original.replace(/[()\-]/g, '').trim();
2087
2150
 
2088
- // Known symbols
2089
- const knownSymbols = ['$', '€', '£', '¥', '₹', '₽', '₩', '₫', 'R$', 'CHF', 'AED'];
2151
+ // Use pre-compiled currency regexes
2090
2152
  let symbol = '';
2091
2153
 
2092
- for (let s of knownSymbols) {
2093
- const escaped = s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
2094
- const regex = new RegExp(`^${escaped}(\\s?)`);
2154
+ for (let {symbol: s, regex} of currencyRegexes) {
2095
2155
  const match = value.match(regex);
2096
2156
  if (match) {
2097
2157
  symbol = s + (match[1] || '');
@@ -2441,8 +2501,6 @@ if (! Modal && typeof (require) === 'function') {
2441
2501
  return returnObject ? o : result;
2442
2502
  };
2443
2503
 
2444
- // TODO: We have a large number like 1000000 and I want format it to 1,00 or 1M or… (display million/thousands/full numbers). In the excel we can do that with custom format cell “0,00..” However, when I tried applying similar formatting with the mask cell of Jspreadsheet, it didn't work. Could you advise how we can achieve this?
2445
-
2446
2504
  Component.render = function(value, options, fullMask) {
2447
2505
  // Nothing to render
2448
2506
  if (value === '' || value === undefined || value === null) {
@@ -2531,7 +2589,7 @@ if (! Modal && typeof (require) === 'function') {
2531
2589
  } else {
2532
2590
  options.input.value = value;
2533
2591
  }
2534
- jSuites.focus(options.input);
2592
+ focus(options.input);
2535
2593
  }
2536
2594
 
2537
2595
  return value;
@@ -2564,6 +2622,11 @@ if (! Modal && typeof (require) === 'function') {
2564
2622
  return '';
2565
2623
  }
2566
2624
 
2625
+ // Tokens
2626
+ const dateTokens = ['DAY', 'WD', 'DDDD', 'DDD', 'DD', 'D', 'Q', 'HH24', 'HH12', 'HH', '\\[H\\]', 'H', 'AM/PM', 'MI', 'SS', 'MS', 'YYYY', 'YYY', 'YY', 'Y', 'MONTH', 'MON', 'MMMMM', 'MMMM', 'MMM', 'MM', 'M', '.'];
2627
+ // All date tokens
2628
+ const allDateTokens = dateTokens.join('|')
2629
+
2567
2630
  Component.getDateString = function(value, options) {
2568
2631
  if (! options) {
2569
2632
  options = {};
@@ -2595,11 +2658,9 @@ if (! Modal && typeof (require) === 'function') {
2595
2658
  value = Helpers.numToDate(value);
2596
2659
  }
2597
2660
 
2598
- // Tokens
2599
- let tokens = ['DAY', 'WD', 'DDDD', 'DDD', 'DD', 'D', 'Q', 'HH24', 'HH12', 'HH', '\\[H\\]', 'H', 'AM/PM', 'MI', 'SS', 'MS', 'YYYY', 'YYY', 'YY', 'Y', 'MONTH', 'MON', 'MMMMM', 'MMMM', 'MMM', 'MM', 'M', '.'];
2600
2661
 
2601
2662
  // Expression to extract all tokens from the string
2602
- let e = new RegExp(tokens.join('|'), 'gi');
2663
+ let e = new RegExp(allDateTokens, 'gi');
2603
2664
  // Extract
2604
2665
  let t = format.match(e);
2605
2666
 
@@ -2793,7 +2854,7 @@ if (! Modal && typeof (require) === 'function') {
2793
2854
  // Keep the current caret position
2794
2855
  let caret = getCaret(element);
2795
2856
  if (caret) {
2796
- value = value.substring(0, caret) + "\u200B" + value.substring(caret);
2857
+ value = value.substring(0, caret) + hiddenCaret + value.substring(caret);
2797
2858
  }
2798
2859
 
2799
2860
  // Run mask
@@ -2804,15 +2865,17 @@ if (! Modal && typeof (require) === 'function') {
2804
2865
  // Apply the result back to the element
2805
2866
  if (newValue !== value && ! e.inputType.includes('delete')) {
2806
2867
  // Set the caret to the position before transformation
2807
- let caret = newValue.indexOf("\u200B");
2868
+ let caret = newValue.indexOf(hiddenCaret);
2808
2869
  if (caret !== -1) {
2809
2870
  // Apply value
2810
- element[property] = newValue.replace("\u200B", "");
2871
+ element[property] = newValue.replace(hiddenCaret, "");
2811
2872
  // Set caret
2812
2873
  setCaret.call(element, caret);
2813
2874
  } else {
2814
2875
  // Apply value
2815
2876
  element[property] = newValue;
2877
+ // Make sure the caret is positioned in the end
2878
+ focus(element);
2816
2879
  }
2817
2880
  }
2818
2881
  }
@@ -2822,7 +2885,10 @@ if (! Modal && typeof (require) === 'function') {
2822
2885
  Component.adjustPrecision = adjustPrecision;
2823
2886
 
2824
2887
  return Component;
2825
- })();
2888
+ }());
2889
+
2890
+ // Aditional Helpers
2891
+ Helpers.getDate = Mask.getDate;
2826
2892
 
2827
2893
  const isNumber = function (num) {
2828
2894
  if (typeof(num) === 'string') {
@@ -3098,13 +3164,26 @@ if (! Modal && typeof (require) === 'function') {
3098
3164
  // Update the headers of the calendar
3099
3165
  let value = d.toISOString().substring(0,10).split('-');
3100
3166
  let time = d.toISOString().substring(11,19).split(':');
3101
- // Update the month label
3102
- self.month = Helpers.months[parseInt(value[1])-1];
3103
- // Update the year label
3104
- self.year = parseInt(value[0]);
3105
- // Hour
3106
- self.hour = parseInt(time[0]);
3107
- self.minute = parseInt(time[1]);
3167
+ let month = Helpers.months[parseInt(value[1])-1];
3168
+ let year = parseInt(value[0]);
3169
+ let hour = parseInt(time[0]);
3170
+ let minute = parseInt(time[1]);
3171
+
3172
+ if (self.month !== month) {
3173
+ // Update the month label
3174
+ self.month = month;
3175
+ }
3176
+ if (self.year !== year) {
3177
+ // Update the year label
3178
+ self.year = year;
3179
+
3180
+ }
3181
+ if (self.hour !== hour) {
3182
+ self.hour = hour;
3183
+ }
3184
+ if (self.minute !== minute) {
3185
+ self.minute = minute;
3186
+ }
3108
3187
 
3109
3188
  // Load data
3110
3189
  if (! self.view) {
@@ -3304,7 +3383,7 @@ if (! Modal && typeof (require) === 'function') {
3304
3383
 
3305
3384
  const renderValue = function() {
3306
3385
  let value = null;
3307
- if (self.range) {
3386
+ if (self.range === true) {
3308
3387
  if (Array.isArray(self.rangeValues)) {
3309
3388
  if (self.numeric) {
3310
3389
  value = self.rangeValues;
@@ -3325,7 +3404,7 @@ if (! Modal && typeof (require) === 'function') {
3325
3404
  }
3326
3405
 
3327
3406
  const updateValue = function(v) {
3328
- if (self.range) {
3407
+ if (self.range === true) {
3329
3408
  if (v) {
3330
3409
  if (! Array.isArray(v)) {
3331
3410
  v = v.toString().split(',');
@@ -3385,12 +3464,30 @@ if (! Modal && typeof (require) === 'function') {
3385
3464
  let input = getInput();
3386
3465
  let value = self.value;
3387
3466
  if (input) {
3467
+ let v = value;
3388
3468
  if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
3389
3469
  isEditable = !input.hasAttribute('readonly') && !input.hasAttribute('disabled');
3390
- value = input.value;
3470
+ v = input.value;
3391
3471
  } else if (input.isContentEditable) {
3392
3472
  isEditable = true;
3393
- value = input.textContent;
3473
+ v = input.textContent;
3474
+ }
3475
+
3476
+ let ret = v;
3477
+ if (ret && Number(ret) == ret) {
3478
+ ret = Helpers.numToDate(ret);
3479
+ }
3480
+
3481
+ // Try a formatted date
3482
+ if (ret && ! Helpers.isValidDateFormat(ret)) {
3483
+ let tmp = Mask.extractDateFromString(ret, self.format);
3484
+ if (tmp) {
3485
+ ret = tmp;
3486
+ }
3487
+ }
3488
+
3489
+ if (ret !== value) {
3490
+ value = ret;
3394
3491
  }
3395
3492
  }
3396
3493
 
@@ -3625,30 +3722,46 @@ if (! Modal && typeof (require) === 'function') {
3625
3722
  }
3626
3723
 
3627
3724
  self.setValue = function(v) {
3628
- // Destroy range
3629
- destroyRange();
3630
- // Update the internal controllers
3631
- updateValue(v);
3632
3725
  // Events
3633
3726
  if (v !== self.value) {
3634
3727
  // Update value
3635
3728
  self.value = v;
3636
- // Events
3637
- Dispatch.call(self, change, 'change', {
3729
+ }
3730
+ }
3731
+
3732
+ const dispatchOnChangeEvent = function() {
3733
+ // Destroy range
3734
+ destroyRange();
3735
+ // Update the internal controllers
3736
+ updateValue(self.value);
3737
+ // Events
3738
+ Dispatch.call(self, change, 'change', {
3739
+ instance: self,
3740
+ value: self.value,
3741
+ });
3742
+ // Update input
3743
+ let input = getInput();
3744
+ if (input) {
3745
+ let v = self.value;
3746
+ if (self.format && v) {
3747
+ if (self.range === true) {
3748
+ if (v[0]) {
3749
+ v[0] = Mask.render(v[0], self.format);
3750
+ }
3751
+ if (v[1]) {
3752
+ v[1] = Mask.render(v[1], self.format);
3753
+ }
3754
+ } else {
3755
+ v = Mask.render(v, self.format);
3756
+ }
3757
+ }
3758
+ // Update input value
3759
+ input.value = v;
3760
+ // Dispatch event
3761
+ Dispatch.call(input, null, 'change', {
3638
3762
  instance: self,
3639
3763
  value: self.value,
3640
3764
  });
3641
- // Update input
3642
- let input = getInput();
3643
- if (input) {
3644
- // Update input value
3645
- input.value = Mask.render(v, self.format);
3646
- // Dispatch event
3647
- Dispatch.call(input, null, 'change', {
3648
- instance: self,
3649
- value: self.value,
3650
- });
3651
- }
3652
3765
  }
3653
3766
  }
3654
3767
 
@@ -3660,6 +3773,8 @@ if (! Modal && typeof (require) === 'function') {
3660
3773
  }
3661
3774
  }
3662
3775
 
3776
+ self.helpers = Helpers;
3777
+
3663
3778
  onchange(function(prop) {
3664
3779
  if (prop === 'view') {
3665
3780
  if (typeof(views[self.view]) === 'function') {
@@ -3667,12 +3782,18 @@ if (! Modal && typeof (require) === 'function') {
3667
3782
  self.options = views[self.view].call(self, date);
3668
3783
  }
3669
3784
  } else if (prop === 'value') {
3670
- self.setValue(self.value);
3785
+ dispatchOnChangeEvent();
3671
3786
  } else if (prop === 'startingDay') {
3672
- self.weekdays = getWeekdays(self.startingDay);
3787
+ self.weekdays = getWeekdays(self.startingDay ?? 0);
3673
3788
  }
3674
3789
  });
3675
3790
 
3791
+ // Input
3792
+ if (self.input === 'auto') {
3793
+ self.input = document.createElement('input');
3794
+ self.input.type = 'text';
3795
+ }
3796
+
3676
3797
  onload(function() {
3677
3798
  if (self.type !== "inline") {
3678
3799
  // Create modal instance
@@ -3695,11 +3816,11 @@ if (! Modal && typeof (require) === 'function') {
3695
3816
  self.range = true;
3696
3817
  }
3697
3818
 
3819
+ let ret;
3820
+
3698
3821
  // Create input controls
3699
3822
  if (self.input && self.initInput !== false) {
3700
- if (self.input === 'auto') {
3701
- self.input = document.createElement('input');
3702
- self.input.type = 'text';
3823
+ if (! self.input.parentNode) {
3703
3824
  self.el.parentNode.insertBefore(self.input, self.el);
3704
3825
  }
3705
3826
 
@@ -3723,13 +3844,21 @@ if (! Modal && typeof (require) === 'function') {
3723
3844
  if (self.value) {
3724
3845
  input.value = self.value;
3725
3846
  } else if (input.value && input.value !== self.value) {
3726
- self.value = input.value;
3847
+ // Correct format
3848
+ if (self.format) {
3849
+ input.value = Helpers.getDate(input.value, self.format);
3850
+ }
3851
+ ret = input.value;
3727
3852
  }
3728
3853
  }
3729
3854
  }
3730
3855
 
3731
3856
  // Update the internal date values
3732
- updateValue(self.value);
3857
+ if (ret) {
3858
+ self.value = ret;
3859
+ } else {
3860
+ updateValue(self.value);
3861
+ }
3733
3862
 
3734
3863
  /**
3735
3864
  * Handler keyboard
@@ -3812,7 +3941,7 @@ if (! Modal && typeof (require) === 'function') {
3812
3941
  const hours = views.hours();
3813
3942
  const minutes = views.minutes();
3814
3943
 
3815
- return render => render`<div class="lm-calendar" :value="self.value" data-grid="{{self.grid}}" data-type="{{self.type}}" data-disabled="{{self.disabled}}">
3944
+ return render => render`<div class="lm-calendar" :value="self.value" data-grid="{{self.grid}}" data-type="{{self.type}}" data-disabled="{{self.disabled}}" data-starting-day="{{self.startingDay}}">
3816
3945
  <div class="lm-calendar-options">
3817
3946
  <button type="button" onclick="self.reset">${T('Reset')}</button>
3818
3947
  <button type="button" onclick="${update}">${T('Done')}</button>
@@ -3833,7 +3962,7 @@ if (! Modal && typeof (require) === 'function') {
3833
3962
  </div>
3834
3963
  <div class="lm-calendar-footer" data-visible="{{self.footer}}">
3835
3964
  <div class="lm-calendar-time" data-visible="{{self.time}}"><select :loop="${hours}" :bind="self.hour" class="lm-calendar-control"><option value="{{self.value}}">{{self.title}}</option></select>:<select :loop="${minutes}" :bind="self.minute" class="lm-calendar-control"><option value="{{self.value}}">{{self.title}}</option></select></div>
3836
- <div class="lm-calendar-update"><input type="button" value="${T('Update')}" onclick="${update}" class="lm-ripple"></div>
3965
+ <div class="lm-calendar-update"><input type="button" value="${T('Update')}" onclick="${update}" class="lm-ripple lm-input"></div>
3837
3966
  </div>
3838
3967
  </div>
3839
3968
  </div>`
package/dist/style.css CHANGED
@@ -69,6 +69,7 @@
69
69
  padding: 4px;
70
70
  background-color: transparent;
71
71
  font-weight: bold;
72
+ color: var(--lm-font-color);
72
73
  }
73
74
 
74
75
  .lm-calendar-navigation {
@@ -78,7 +79,7 @@
78
79
  }
79
80
 
80
81
  .lm-calendar-navigation i:hover {
81
- background-color: var(--lm-background-color-hover, #ebebeb);
82
+ background-color: var(--lm-background-color-highlight, #ebebeb);
82
83
  color: #000;
83
84
  }
84
85
 
@@ -126,6 +127,7 @@
126
127
  cursor: pointer;
127
128
  border-radius: 100px;
128
129
  background-origin: padding-box;
130
+ min-width: 38px;
129
131
  }
130
132
 
131
133
  .lm-calendar-content > div[data-disabled="true"] {
@@ -242,8 +244,6 @@
242
244
  }
243
245
 
244
246
  .lm-calendar-footer input {
245
- border: transparent;
246
- padding: 8px;
247
247
  width: 100%;
248
248
  cursor: pointer;
249
249
  background-color: var(--lm-border-color-light, #e9e9e9);
@@ -324,7 +324,7 @@
324
324
  }
325
325
 
326
326
  .lm-ripple:hover {
327
- background: var(--lm-background-color-hover, #ebebeb) radial-gradient(circle, transparent 1%, var(--lm-background-color-hover, #ebebeb) 1%) center/15000%;
327
+ background: var(--lm-background-color-highlight, #ebebeb) radial-gradient(circle, transparent 1%, var(--lm-background-color-highlight, #ebebeb) 1%) center/15000%;
328
328
  }
329
329
 
330
330
  .lm-ripple:active {
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "name": "@lemonadejs/calendar",
3
- "title": "LemonadeJS calendar",
4
- "description": "LemonadeJS reactive JavaScript calendar plugin",
5
- "author": {
6
- "name": "Contact <contact@lemonadejs.net>",
7
- "url": "https://lemonadejs.net"
8
- },
9
- "keywords": [
10
- "javascript calendar",
11
- "lemonadejs plugins",
12
- "js",
13
- "library",
14
- "javascript plugins"
15
- ],
16
- "dependencies": {
17
- "lemonadejs": "^5.2.0",
18
- "@lemonadejs/modal": "^5.2.0"
19
- },
20
- "main": "dist/index.js",
21
- "types": "dist/index.d.ts",
22
- "version": "5.2.0"
23
- }
1
+ {
2
+ "name": "@lemonadejs/calendar",
3
+ "title": "LemonadeJS calendar",
4
+ "description": "LemonadeJS reactive JavaScript calendar plugin",
5
+ "author": {
6
+ "name": "Contact <contact@lemonadejs.net>",
7
+ "url": "https://lemonadejs.net"
8
+ },
9
+ "keywords": [
10
+ "javascript calendar",
11
+ "lemonadejs plugins",
12
+ "js",
13
+ "library",
14
+ "javascript plugins"
15
+ ],
16
+ "dependencies": {
17
+ "lemonadejs": "^5.3.1",
18
+ "@lemonadejs/modal": "^5.3.0"
19
+ },
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "version": "5.3.0"
23
+ }