ES6: port datepicker and util to ES6
Change-Id: I203aa47229e6c4dacd3a33bea39c512f4576a182
diff --git a/Gruntfile.js b/Gruntfile.js
index 106e241..34cdbba 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -144,6 +144,11 @@
timestamp: true
},
{
+ src: 'public/css/kalamar-<%= pkg.version %>.css',
+ dest: 'public/css/kalamar-latest.css',
+ timestamp: true
+ },
+ {
src: 'public/css/kalamar-plugin-<%= pkg.pluginVersion %>.css',
dest: 'public/css/kalamar-plugin-latest.css',
timestamp: true
@@ -159,7 +164,7 @@
watch: {
css: {
files: ['dev/scss/**/*.scss'],
- tasks: ['sass'],
+ tasks: ['sass'],
options: {
spawn: false
}
diff --git a/dev/js/src-lib/datepicker/datepicker.js b/dev/js/src-lib/datepicker/datepicker.js
new file mode 100644
index 0000000..859bd75
--- /dev/null
+++ b/dev/js/src-lib/datepicker/datepicker.js
@@ -0,0 +1,543 @@
+/**
+ * Date picker for the
+ * Virtual Collection builder.
+ *
+ * @author Nils Diewald
+ */
+
+import KorAP from '../util/util.js';
+
+const validDateMatchRE = new RegExp("^(?:[lg]?eq|ne)$");
+const validDateRE = new RegExp("^(?:\\d{4})(?:-\\d\\d(?:-\\d\\d)?)?$");
+
+KorAP._validDateMatchRE = validDateMatchRE;
+KorAP._validDateRE = validDateRE;
+
+/*
+ * Localizations
+ */
+const loc = KorAP.Locale;
+loc.WDAY = loc.WDAY || [
+ 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'
+];
+loc.MONTH = loc.MONTH || [
+ 'January', 'February', 'March', 'April',
+ 'May', 'June', 'July', 'August',
+ 'September', 'October', 'November',
+ 'December'
+];
+
+const d = document;
+
+// The datepicker class
+class KalamarDatepicker {
+
+ // Init datepicker
+ constructor() {
+ this._selected = [];
+ }
+
+
+ /**
+ * Get or select a specific date.
+ */
+ select(year, month, day) {
+ const t = this;
+ if (arguments.length >= 1) {
+ t._selected = {'year' : year};
+ t._showYear = year;
+ if (arguments.length >= 2) {
+ t._selected['month'] = month;
+ t._showMonth = month;
+ if (arguments.length >= 3) {
+ t._selected['day'] = day;
+ t._showDay = day;
+ };
+ };
+
+ return t;
+ };
+
+ return t._selected;
+ }
+
+
+ /**
+ * Select a specific date and
+ * init the accompanied action.
+ */
+ set(year, month, day) {
+ this.select(year, month, day);
+ this._store();
+ }
+
+
+ // Store the selected value
+ _store() {
+ if (this._click !== undefined)
+ this._click(this._selected);
+ else
+ console.dir(this._selected);
+ }
+
+
+ /**
+ * Set the action for clicking as a callback.
+ * The callback will retrieve a an object with
+ * an optional year attribute,
+ * an optional month attribute,
+ * and an optional day attribute
+ */
+ onclick(cb) {
+ this._click = cb;
+ }
+
+
+ /**
+ * The associated input field.
+ */
+ input() {
+ return this._input;
+ }
+
+
+ /**
+ * Show the datepicker.
+ * Will either show the selected year/month
+ * or the current date.
+ * Will return the element for appending to the dom.
+ */
+ show(year, month) {
+
+ const e = this._el = d.createElement('div');
+ e.setAttribute('tabindex', 0);
+ e.style.outline = 0;
+ e.classList.add('datepicker');
+
+ const today = new Date();
+ const t = this;
+
+ // Show year
+ t._showYear = (year !== undefined) ? year :
+ (t._selected['year'] ? this._selected['year'] :
+ today.getYear() + 1900);
+
+ // Show month
+ t._showMonth = month ? month :
+ (t._selected['month'] ? t._selected['month'] :
+ (today.getMonth() + 1));
+
+ // Append all helpers
+ e.appendChild(t._monthHelper());
+ e.appendChild(t._yearHelper());
+ e.appendChild(t._dayHelper());
+ t._input = e.appendChild(t._stringHelper());
+
+ // Always focus
+ e.addEventListener(
+ 'mousedown',
+ function (ev) {
+ this._inField = true
+ }.bind(t)
+ );
+
+ e.addEventListener(
+ 'mouseup',
+ function (ev) {
+ this._inField = false;
+ this._input.focus();
+ }.bind(t)
+ );
+
+ t._input.addEventListener(
+ 'blur',
+ function (ev) {
+ if (!this._inField) {
+ if (this.fromString(this._input.value)) {
+ this._store();
+ };
+ };
+ ev.halt();
+ }.bind(t)
+ );
+
+ t._input.focus();
+
+ return t._el;
+ }
+
+ _stringHelper() {
+
+ // Create element
+ // Add input field
+ const input = d.createElement('input');
+ input.value = this.toString();
+ input.setAttribute('tabindex', 0);
+
+ input.addEventListener(
+ 'keyup',
+ function (e) {
+ if (this.fromString(input.value)) {
+ this._updateYear();
+ this._updateMonth();
+ this._updateDay();
+ };
+ }.bind(this)
+ );
+
+ input.addEventListener(
+ 'keypress',
+ function (e) {
+ if (e.keyCode == 13) {
+ if (this.fromString(input.value))
+ this._store();
+
+ e.halt();
+ return false;
+ }
+ }.bind(this)
+ )
+
+ return input;
+ }
+
+
+ /**
+ * Get the HTML element associated with the datepicker.
+ */
+ element() {
+ return this._el;
+ }
+
+
+ /**
+ * Get the current date in string format.
+ */
+ today() {
+ const today = new Date();
+ let str = today.getYear() + 1900;
+ const m = today.getMonth() + 1;
+ const d = today.getDate();
+ str += '-' + (m < 10 ? '0' + m : m);
+ str += '-' + (d < 10 ? '0' + d : d);
+ return str;
+ }
+
+
+ /**
+ * Stringification
+ */
+ toString() {
+ // There are values selected
+ let v = '';
+ const s = this._selected;
+ if (s['year']) {
+ v += s['year'];
+ if (s['month']) {
+ v += '-';
+ v += s['month'] < 10 ? '0' + s['month'] : s['month'];
+ if (s['day']) {
+ v += '-';
+ v += s['day'] < 10 ? '0' + s['day'] : s['day'];
+ };
+ };
+ };
+ return v;
+ }
+
+
+ /**
+ * Increment the year.
+ */
+ incrYear() {
+ const t = this;
+ if (t._showYear < 9999) {
+ t._showYear++;
+ t._updateYear();
+ t._updateMonth();
+ t._updateDay();
+ return t;
+ };
+ return;
+ }
+
+
+ /**
+ * Decrement the year.
+ */
+ decrYear() {
+ const t = this;
+ if (t._showYear > 0) {
+ t._showYear--;
+ t._updateYear();
+ t._updateMonth();
+ t._updateDay();
+ return t;
+ };
+ return;
+ }
+
+
+ /**
+ * Increment the month.
+ */
+ incrMonth() {
+ const t = this;
+ t._showMonth++;
+ if (t._showMonth > 12) {
+ t._showMonth = 1;
+ t.incrYear();
+ }
+ else {
+ t._updateMonth();
+ t._updateDay();
+ };
+ return t;
+ }
+
+
+ /**
+ * Decrement the month.
+ */
+ decrMonth() {
+ const t = this;
+ t._showMonth--;
+ if (t._showMonth < 1) {
+ t._showMonth = 12;
+ t.decrYear();
+ }
+ else {
+ t._updateMonth();
+ t._updateDay();
+ };
+
+ return t;
+ }
+
+
+ // Create the year helper element.
+ _yearHelper() {
+ const t = this;
+ const year = d.createElement('div');
+ year.classList.add('year');
+
+ // Decrement year
+ year.addE('span')
+ .onclick = t.decrYear.bind(t);
+
+ t._yElement = year.addE('span');
+ t._yElement.addT(t._showYear);
+
+ t._yElement.onclick = function () {
+ t.set(t._showYear);
+ }.bind(t);
+ t._selectYear();
+
+ // Increment year
+ year.addE('span')
+ .onclick = t.incrYear.bind(t);
+
+ return year;
+ }
+
+
+ // Update the year helper view.
+ _updateYear() {
+ this._yElement.firstChild.data = this._showYear;
+ this._selectYear();
+ }
+
+
+ // Check if the viewed year is current
+ _selectYear() {
+ if (this._showYear === this.select()['year'])
+ this._yElement.classList.add('selected');
+ else
+ this._yElement.classList.remove('selected');
+ }
+
+
+ // Create the month helper element.
+ _monthHelper() {
+ const t = this;
+ const month = d.createElement('div');
+ month.classList.add('month');
+
+ // Decrement month
+ month.addE('span')
+ .onclick = t.decrMonth.bind(t);
+
+ t._mElement = month.addE('span');
+ t._mElement.addT(loc.MONTH[t._showMonth-1]);
+ t._mElement.onclick = function () {
+ this.set(this._showYear, this._showMonth);
+ }.bind(t);
+
+ t._selectMonth();
+
+ // Increment month
+ month.addE('span')
+ .onclick = t.incrMonth.bind(t);
+
+ return month;
+ }
+
+ // Update the month helper view.
+ _updateMonth() {
+ if (this._showMonth === undefined || this._showMonth > 12)
+ this._showMonth = 1;
+
+ this._mElement.firstChild.data = loc.MONTH[this._showMonth-1];
+ this._selectMonth();
+ }
+
+
+ // Check if the viewed month is current
+ _selectMonth() {
+ const t = this;
+ if (t._showYear === t.select()['year'] &&
+ t._showMonth === t.select()['month'])
+ t._mElement.classList.add('selected');
+ else
+ t._mElement.classList.remove('selected');
+ }
+
+
+ // Create the day (calendar) helper element.
+ _dayHelper() {
+ const table = d.createElement('table');
+
+ // Localized day view
+ const tr = table.addE('thead').addE('tr');
+ for (let i = 0; i < 7; i++) {
+ tr.addE('th').addT(loc.WDAY[i]);
+ };
+
+ this._dBElement = this._dayBody();
+
+ table.appendChild(this._dBElement);
+ return table;
+ }
+
+
+ // Create day body for calendar table
+ _dayBody() {
+ const showDate = new Date(
+ this._showYear,
+ this._showMonth - 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0
+ );
+ const date = new Date(
+ this._showYear,
+ this._showMonth - 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0
+ );
+ const today = new Date();
+ const that = this;
+
+ // What happens, in case someone clicks
+ // on a date
+ const click = function () {
+ that.set(
+ that._showYear,
+ that._showMonth,
+ parseInt(this.firstChild.data)
+ );
+ };
+
+ // Skip back to the previous monday (may be in the last month)
+ date.setDate(date.getDate() - ((date.getDay() + 6) % 7));
+
+ const tb = d.createElement('tbody');
+
+ const s = this.select();
+
+ let tr, i, td;
+
+ // Iterate over all days of the table
+ while (1) {
+
+ // Loop through the week
+ tr = tb.addE('tr');
+ for (i = 0; i < 7; i++) {
+ td = tr.addE('td');
+
+ // Not part of the current month
+ if (date.getMonth() !== showDate.getMonth()) {
+ td.classList.add('out');
+ }
+ else {
+ td.onclick = click;
+ };
+
+ // This is the current day
+ if (date.getDate() === today.getDate() &&
+ date.getMonth() === today.getMonth() &&
+ date.getFullYear() === today.getFullYear()) {
+ td.classList.add('today');
+ };
+
+ // This is the day selected
+ if (s && s['day']) {
+ if (date.getDate() === s['day'] &&
+ date.getMonth() === s['month']-1 &&
+ date.getFullYear() === s['year']) {
+ td.classList.add('selected');
+ };
+ };
+
+ // Add the current day to the table
+ td.addT(date.getDate());
+
+ // Next day
+ date.setDate(date.getDate() + 1);
+ };
+
+ if (date.getMonth() !== showDate.getMonth())
+ break;
+ };
+ return tb;
+ }
+
+ // Update the calendar view
+ _updateDay() {
+ const newBody = this._dayBody();
+ this._dBElement.parentNode.replaceChild(
+ newBody,
+ this._dBElement
+ );
+ this._dBElement = newBody;
+ }
+
+
+ /**
+ * Parse date from string.
+ */
+ fromString(v) {
+ if (v === undefined)
+ return false;
+
+ if (!KorAP._validDateRE.test(v))
+ return false;
+
+ const d = v.split('-', 3);
+ d[0] = parseInt(d[0]);
+ if (d[1]) d[1] = parseInt(d[1]);
+ if (d[2]) d[2] = parseInt(d[2]);
+
+ // Select values
+ this.select(d[0], d[1], d[2]);
+ return true;
+ }
+}
+
+export default KalamarDatepicker;
+export { validDateMatchRE, validDateRE };
diff --git a/dev/js/src-lib/datepicker/demo/datepickerdemo.js b/dev/js/src-lib/datepicker/demo/datepickerdemo.js
new file mode 100644
index 0000000..eeafd6f
--- /dev/null
+++ b/dev/js/src-lib/datepicker/demo/datepickerdemo.js
@@ -0,0 +1,18 @@
+import KalamarDatepicker from '../datepicker.js';
+import '../../../../scss/kalamar.scss';
+
+function mountDatepicker() {
+ const host = document.getElementById('dp');
+ if (!host) {
+ return;
+ }
+
+ const dp = new KalamarDatepicker();
+ host.appendChild(dp.select(2015, 4, 12).show(2015, 4));
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', mountDatepicker);
+} else {
+ mountDatepicker();
+}
diff --git a/dev/js/src-lib/datepicker/demo/index.html b/dev/js/src-lib/datepicker/demo/index.html
new file mode 100644
index 0000000..0ac7e0c
--- /dev/null
+++ b/dev/js/src-lib/datepicker/demo/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Demo: Datepicker</title>
+</head>
+<body>
+ <h1>Datepicker</h1>
+ <script type="module" src="./datepickerdemo.js"></script>
+ <div class="vc">
+ <div id="dp"></div>
+ </div>
+</body>
+</html>
diff --git a/dev/js/src-lib/datepicker/package.json b/dev/js/src-lib/datepicker/package.json
new file mode 100644
index 0000000..e74fb87
--- /dev/null
+++ b/dev/js/src-lib/datepicker/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "kalamar-datepicker",
+ "version": "0.0.1",
+ "description": "Datepicker for Kalmar",
+ "main": "datepicker.js",
+ "scripts": {
+ "dev": "vite --open /demo/index.html"
+ },
+ "author": "",
+ "license": "ISC"
+}
diff --git a/dev/js/src-lib/datepicker/test/datepickerSpec.js b/dev/js/src-lib/datepicker/test/datepickerSpec.js
new file mode 100644
index 0000000..711fd45
--- /dev/null
+++ b/dev/js/src-lib/datepicker/test/datepickerSpec.js
@@ -0,0 +1,176 @@
+import { describe, it, expect, beforeAll } from 'vitest';
+let KalamarDatepicker;
+
+describe('KorAP.Datepicker', function () {
+
+ beforeAll(async () => {
+ const module = await import('../datepicker.js');
+ KalamarDatepicker = module.default;
+ });
+
+ it('should be initializable', function () {
+ var dp = new KalamarDatepicker();
+ var e = dp.show();
+ expect(e.nodeName).toEqual('DIV');
+ expect(e.classList.contains('datepicker')).toBeTruthy();
+ expect(e.getAttribute('tabindex')).toEqual('0');
+ });
+
+ it('should generate valid dates', function () {
+ var dp = new KalamarDatepicker();
+ expect(dp.today()).toMatch(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/);
+ });
+
+ it('should have year and month helpers', function () {
+ var dp = new KalamarDatepicker();
+ var e = dp.show(2013, 2);
+ expect(e.nodeName).toEqual('DIV');
+ expect(e.classList.contains('datepicker')).toBeTruthy();
+ expect(e.getAttribute('tabindex')).toEqual('0');
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2013');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+ });
+
+ it('should have modifyable year', function () {
+ var dp = new KalamarDatepicker();
+ var e = dp.show(2013, 2);
+ expect(e.nodeName).toEqual('DIV');
+ expect(e.classList.contains('datepicker')).toBeTruthy();
+ expect(e.getAttribute('tabindex')).toEqual('0');
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2013');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.incrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2014');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.incrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2015');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.decrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2014');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ // Max value
+ e = dp.show(9998, 2);
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('9998');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.incrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('9999');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.incrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('9999');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ // Min value
+ e = dp.show(2, 2);
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.decrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('1');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.decrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('0');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.decrYear();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('0');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+ });
+
+ it('should have modifyable month', function () {
+ var dp = new KalamarDatepicker();
+ var e = dp.show(2012, 9);
+
+ expect(e.nodeName).toEqual('DIV');
+ expect(e.classList.contains('datepicker')).toBeTruthy();
+
+ expect(e.getAttribute('tabindex')).toEqual('0');
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2012');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('September');
+
+ dp.incrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2012');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('October');
+
+ dp.incrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2012');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('November');
+
+ dp.incrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2012');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('December');
+
+ dp.incrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2013');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('January');
+
+ dp.decrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2012');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('December');
+
+ // Max value
+ e = dp.show(9999, 12);
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('9999');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('December');
+
+ dp.incrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('9999');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('December');
+
+ // Min value
+ e = dp.show(1, 2);
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('1');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.decrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('1');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('January');
+
+ dp.decrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('0');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('December');
+
+ e = dp.show(0, 2);
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('0');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('February');
+
+ dp.decrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('0');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('January');
+
+ dp.decrMonth();
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('0');
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('January');
+
+ });
+
+ it('should parse from string', function () {
+ var dp = new KalamarDatepicker();
+ expect(dp.fromString()).toBeFalsy();
+ expect(dp.fromString("2020-September")).toBeFalsy();
+ expect(dp.fromString("2020")).toBeTruthy();
+ expect(dp.fromString("2020-10")).toBeTruthy();
+ expect(dp.fromString("2020-10-9")).toBeFalsy();
+ expect(dp.fromString("2020-10-09")).toBeTruthy();
+
+ expect(dp._selected['year']).toEqual(2020);
+ expect(dp._selected['month']).toEqual(10);
+ expect(dp._selected['day']).toEqual(9);
+
+ var e = dp.show(2020, 11);
+
+ expect(e.querySelector('div.year > span:nth-child(2)').firstChild.data).toEqual('2020');
+ expect(e.querySelector('div.year > span:nth-child(2)').classList.contains('selected')).toBeTruthy();
+ expect(e.querySelector('div.month > span:nth-child(2)').firstChild.data).toEqual('November');
+ expect(e.querySelector('div.month > span:nth-child(2)').classList.contains('selected')).toBeFalsy();
+ expect(e.querySelector('div.month > span:nth-child(2)').classList.contains('selected')).toBeFalsy();
+
+ expect(dp.toString()).toEqual("2020-10-09");
+ });
+});
diff --git a/dev/js/src-lib/datepicker/vite.config.js b/dev/js/src-lib/datepicker/vite.config.js
new file mode 100644
index 0000000..1edc2f0
--- /dev/null
+++ b/dev/js/src-lib/datepicker/vite.config.js
@@ -0,0 +1,29 @@
+import { defineConfig } from 'vite';
+import path from 'path';
+
+const scssDir = path.resolve(__dirname, '../../../scss'); // Path to your SCSS directory
+const fontDir = path.resolve(__dirname, '../../../font');
+
+export default defineConfig({
+
+ server: {
+ fs: {
+ allow: [
+ scssDir,
+ fontDir,
+ __dirname
+ ]
+ },
+ port: 3000
+ },
+
+
+ css: {
+ preprocessorOptions: {
+ scss: {
+ loadPaths: [scssDir]
+ }
+ }
+ }
+});
+
diff --git a/dev/js/src-lib/util/test/utilSpec.js b/dev/js/src-lib/util/test/utilSpec.js
new file mode 100644
index 0000000..f7bcf54
--- /dev/null
+++ b/dev/js/src-lib/util/test/utilSpec.js
@@ -0,0 +1,66 @@
+import { describe, it, expect, beforeAll } from 'vitest';
+
+let initTogglePwdVisibility;
+let initCopyToClipboard;
+
+beforeAll(async function () {
+ const utilModule = await import('../util.js');
+ initTogglePwdVisibility = utilModule.initTogglePwdVisibility;
+ initCopyToClipboard = utilModule.initCopyToClipboard;
+});
+
+describe('KorAP.util', function () {
+
+ it('should quote', function () {
+ expect('Baum'.quote()).toEqual('"Baum"');
+ expect('B"a"um'.quote()).toEqual('"B\\"a\\"um"');
+ });
+
+ it('should escape regex', function () {
+ expect('aaa/bbb\/ccc'.escapeRegex()).toEqual('aaa\\/bbb\\/ccc');
+ });
+
+ it('should slugify', function () {
+ expect('/korap/test'.slugify()).toEqual('koraptest');
+ expect('korap test'.slugify()).toEqual('korap-test');
+ expect('Korap Test'.slugify()).toEqual('korap-test');
+ });
+});
+
+describe('KorAP.util.initTogllePwdVisibility', function () {
+ it('should toggle', function () {
+ const div = document.createElement('div');
+ let input = div.addE('input');
+ input.setAttribute('type', 'password');
+ input.setAttribute('class', 'show-pwd');
+
+ expect(div.children.length).toEqual(1);
+ initTogglePwdVisibility(div);
+ expect(div.children.length).toEqual(2);
+ expect(div.lastChild.tagName).toEqual("A");
+ expect(div.lastChild.classList.contains("hide")).toBeFalsy();
+ expect(input.getAttribute("type")).toEqual("password");
+
+ div.lastChild.click();
+
+ expect(input.getAttribute("type")).toEqual("text");
+ expect(div.lastChild.classList.contains("hide")).toBeTruthy();
+ });
+});
+
+describe('KorAP.util.initCopyToClipboard', function () {
+ it('should be initializable', function () {
+ const div = document.createElement('div');
+ let input = div.addE('input');
+ input.value = "abcde";
+ input.setAttribute('type', 'text');
+ input.setAttribute('class', 'copy-to-clipboard');
+ expect(div.children.length).toEqual(1);
+ initCopyToClipboard(div);
+ expect(div.children.length).toEqual(2);
+ expect(div.lastChild.tagName).toEqual("A");
+ });
+
+ // document.execCommand() can't be tested without user
+ // intervention.
+});
diff --git a/dev/js/src-lib/util/util.js b/dev/js/src-lib/util/util.js
new file mode 100644
index 0000000..3a8c65e
--- /dev/null
+++ b/dev/js/src-lib/util/util.js
@@ -0,0 +1,230 @@
+window.KorAP = window.KorAP || {};
+const KorAP = window.KorAP;
+
+// Don't let events bubble up
+if (Event.halt === undefined) {
+ // Don't let events bubble up
+ Event.prototype.halt = function () {
+ this.stopPropagation();
+ this.preventDefault();
+ };
+};
+
+const _quoteRE = new RegExp("([\"\\\\])", 'g');
+String.prototype.quote = function () {
+ return '"' + this.replace(_quoteRE, '\\$1') + '"';
+};
+
+const _escapeRE = new RegExp("([\/\\\\])", 'g');
+String.prototype.escapeRegex = function () {
+ return this.replace(_escapeRE, '\\$1');
+};
+
+const _slug1RE = new RegExp("[^-a-zA-Z0-9_\\s]+", 'g');
+const _slug2RE = new RegExp("[-\\s]+", 'g');
+String.prototype.slugify = function () {
+ return this.toLowerCase().replace(_slug1RE, '').replace(_slug2RE, '-');
+};
+
+/**
+ * Upgrade this object to another object,
+ * while private data stays intact.
+ *
+ * @param {Object} An object with properties.
+ */
+Object.defineProperty(Object.prototype, 'upgradeTo', {
+ value: function (props) {
+ for (let prop in props) {
+ this[prop] = props[prop];
+ };
+ return this;
+ },
+ enumerable: false,
+ configurable: true,
+ writable: true
+});
+
+
+// Add toggleClass method similar to jquery
+HTMLElement.prototype.toggleClass = function (c1, c2) {
+ const cl = this.classList;
+ if (cl.contains(c1)) {
+ cl.add(c2);
+ cl.remove(c1);
+ }
+ else {
+ cl.remove(c2);
+ cl.add(c1);
+ };
+};
+
+// Append element by tag name
+HTMLElement.prototype.addE = function (tag) {
+ return this.appendChild(document.createElement(tag));
+};
+
+// Append text node
+HTMLElement.prototype.addT = function (text) {
+ return this.appendChild(document.createTextNode(text));
+};
+
+
+// Utility for removing all children of a node
+function _removeChildren (node) {
+ // Remove everything underneath
+ while (node.firstChild)
+ node.removeChild(node.firstChild);
+};
+
+
+// Utility to get either the charCode
+// or the keyCode of an event
+function _codeFromEvent (e) {
+ if ((e.charCode) && (e.keyCode==0))
+ return e.charCode
+ return e.keyCode;
+};
+
+function _dec2hex (dec) {
+ return ('0' + dec.toString(16)).substr(-2)
+};
+
+
+/**
+ * Create random identifiers
+ */
+/*
+ * code based on
+ * https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript#8084248
+ */
+function randomID (len) {
+ const arr = new Uint8Array((len || 40) / 2)
+ window.crypto.getRandomValues(arr)
+ return Array.from(arr, _dec2hex).join('')
+};
+
+
+/**
+ * Add option to show passwords.
+ */
+function initTogglePwdVisibility (element) {
+ const el = element.querySelectorAll("input[type=password].show-pwd");
+ for (let x = 0; x < el.length; x++) {
+ const pwd = el[x];
+
+ const a = document.createElement('a');
+ a.classList.add('show-pwd');
+ a.addEventListener('click', function () {
+ if (pwd.getAttribute("type") === "password") {
+ pwd.setAttribute("type", "text");
+ a.classList.add('hide');
+ return;
+ };
+ pwd.setAttribute("type", "password");
+ a.classList.remove('hide');
+ });
+ pwd.parentNode.insertBefore(a, pwd.nextSibling);
+ };
+};
+
+
+/**
+ * Add option to copy to clipboard.
+ */
+function initCopyToClipboard (element) {
+ const el = element.querySelectorAll("input.copy-to-clipboard");
+ for (let x = 0; x < el.length; x++) {
+ const text = el[x];
+ const a = document.createElement('a');
+ a.classList.add('copy-to-clipboard');
+ a.addEventListener('click', function () {
+ let back = false;
+ if (text.getAttribute("type") === 'password') {
+ text.setAttribute("type", "text");
+ back = true;
+ };
+ text.select();
+ text.setSelectionRange(0, 99999);
+ document.execCommand("copy");
+ if (back) {
+ text.setAttribute("type", "password");
+ };
+ });
+ text.parentNode.insertBefore(a, text.nextSibling);
+ };
+};
+
+
+// Todo: That's double now!
+KorAP.API = KorAP.API || {};
+KorAP.Locale = KorAP.Locale || {};
+
+const loc = KorAP.Locale;
+loc.OR = loc.OR || 'or';
+loc.AND = loc.AND || 'and';
+
+// Add new stylesheet object lazily to document
+KorAP.newStyleSheet = function () {
+ if (KorAP._sheet === undefined) {
+ const sElem = document.createElement('style');
+ document.head.appendChild(sElem);
+ KorAP._sheet = sElem.sheet;
+ };
+ return KorAP._sheet;
+};
+
+
+// Default log message
+KorAP.log = KorAP.log || function (type, msg, src) {
+ if (src)
+ msg += ' from ' + src;
+ console.log(type + ": " + msg);
+};
+
+/**
+ * A Method for generating an array of nodes, that are direct descendants of the passed
+ * element node, using a tag tagName as a parameter. Supposed to be used by the specification only.
+ * @param {HTMLNode} element The HTMLNode / element object whose children we are fetching
+ * @param {String} tagName The tag the children are looked for by
+ * @returns An array of children nodes with tag tagName
+ */
+function directElementChildrenByTagName (element, tagName) {
+ const tagElementsCollection=element.getElementsByTagName(tagName);
+ //var tagElements = Array.from(tagElementsCollection);
+ //var tagElements = [...tagElementsCollection];
+ //This one has the best compatability:
+ var tagElements = Array.prototype.slice.call(tagElementsCollection);
+ //filter by actually being direct child node
+ tagElements = tagElements.filter(subElement => subElement.parentNode === element);
+ return tagElements;
+};
+
+/**
+ * A Method for generating an array of nodes, that are direct descendants of the passed
+ * element node, using a class className as a parameter. Supposed to be used by the specification only.
+ * @param {HTMLNode} element The HTMLNode / element object whose children we are fetching
+ * @param {String} className The class the children are looked for by
+ * @returns An array of children nodes with class className
+ */
+ function directElementChildrenByClassName (element, className) {
+ const classElementsCollection=element.getElementsByTagName(className);
+ //var classElements = Array.from(classElementsCollection);
+ //var classElements = [...classElementsCollection];
+ //This one has the best compatability:
+ var classElements = Array.prototype.slice.call(classElementsCollection);
+ //filter by actually being direct child node
+ classElements = classElements.filter(subElement => subElement.parentNode === element);
+ return classElements;
+};
+
+export {
+ _removeChildren as removeChildren,
+ _codeFromEvent as codeFromEvent,
+ randomID,
+ initTogglePwdVisibility,
+ initCopyToClipboard,
+ directElementChildrenByTagName,
+ directElementChildrenByClassName
+};
+
+export default KorAP;
\ No newline at end of file
diff --git a/package.json b/package.json
index 6b8fda6..bf2b976 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,13 @@
"license": "BSD-2-Clause",
"version": "0.66.0",
"pluginVersion": "0.2.3",
+ "scripts": {
+ "dev": "vite --config vite.config.js",
+ "build": "vite build --config vite.config.js",
+ "test": "vitest --config vitest.config.js --run"
+ },
"engines": {
- "node": ">=6.0.0"
+ "node": ">=20.19.0"
},
"repository": {
"type": "git",
@@ -22,7 +27,10 @@
"imagemin": "^9.0.1",
"optipng-bin": "^9.0.0",
"sass": "^1.99.0",
- "vinyl-fs": "^4.0.2"
+ "vinyl-fs": "^4.0.2",
+ "vite": "^8.1.4",
+ "vitest": "^2.0.0",
+ "jsdom": "^24.0.0"
},
"optionalDependencies": {
"fsevents": "*"
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..7467b5b
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite';
+import path from 'path';
+
+const scssDir = path.resolve(__dirname, 'dev/scss'); // Path to your SCSS directory
+
+export default defineConfig({
+ css: {
+ preprocessorOptions: {
+ scss: {
+ loadPaths: [scssDir]
+ }
+ }
+ },
+ server: {
+ port: 3000
+ }
+});
+
diff --git a/vitest.config.js b/vitest.config.js
new file mode 100644
index 0000000..c63c71e
--- /dev/null
+++ b/vitest.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ root: 'dev/js/src-lib',
+ environment: 'jsdom',
+ include: ['*/test/**/*Spec.js'],
+ globals: true
+ }
+});