From 0dd20c0a0dcac1b4cd26ed8502629043bcdbcf83 Mon Sep 17 00:00:00 2001
From: Jan Suwart
Date: Sun, 3 Dec 2017 12:46:10 +0100
Subject: [PATCH] Fix parsing of uppercase PM postfix, add mocha/chai specs and
test page, resolves #13
---
.editorconfig | 20 +++++
README.md | 14 ++--
css/appointment-picker.css | 2 +-
css/appointment-picker.scss | 2 +-
index.html | 4 +
js/appointment-picker.js | 38 ++++-----
js/appointment-picker.min.js | 2 +-
package.json | 2 +-
tests/test.js | 145 +++++++++++++++++++++++++++++++++++
tests/tests.html | 40 ++++++++++
10 files changed, 241 insertions(+), 28 deletions(-)
create mode 100644 .editorconfig
create mode 100644 tests/test.js
create mode 100644 tests/tests.html
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..fc61982
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+# http://EditorConfig.org
+
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{js}]
+indent_style = tab
+indent_size = 4
+
+[*.scss]
+indent_style = space
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false
\ No newline at end of file
diff --git a/README.md b/README.md
index a0e631e..8afb1dd 100644
--- a/README.md
+++ b/README.md
@@ -142,18 +142,22 @@ document.body.addEventListener('change.appo.picker', function(e) { var time = e.
## Styling
All appointment-picker styles are namespaced with `.appo-picker`, i.e. `.appo-picker-list-item`. Depending on your project, you can either overwrite them using your own CSS or by modifying the provided CSS.
+## Accessibility
+
+For screen reader support add both a `aria-label` and `aria-live` properties on the input field
+```html
+
+```
+
## Best practices
- appointment-picker neither installs any event listeners outside of the input nor it adds any dom elements until it is opened by the user
- it can be destroyed using its the exposed destroy method that causes all event listeners and dom elements to be removed (i.e. if used in a single page application)
-- for better screen reader support it is recomended to add both a `aria-label` and `aria-live` properties on the input field
- ```html
-
- ```
+- there is automated testing ([Mocha](https://mochajs.org) and [Chai](http://chaijs.com/api/assert)) to assert that the exposed core functions and the date parser behave correctly ([see specs page](https://jannicz.github.io/appointment-picker/tests/tests.html))
## Browser Support (tested)
- Chrome
- Firefox
-- Safari (macOS 11 & iOS 10)
+- Safari (macOS 10 & iOS 9)
- Edge
- IE11 / IE10
diff --git a/css/appointment-picker.css b/css/appointment-picker.css
index f855c8e..5fb3260 100644
--- a/css/appointment-picker.css
+++ b/css/appointment-picker.css
@@ -1,5 +1,5 @@
/*
- * appointment-picker.css 1.0.4 | MIT License | github.com/jannicz/appointment-picker
+ * appointment-picker.css 1.0.5 | MIT License | github.com/jannicz/appointment-picker
*/
/* Default variation */
.appo-picker {
diff --git a/css/appointment-picker.scss b/css/appointment-picker.scss
index 34ecee8..50f023d 100644
--- a/css/appointment-picker.scss
+++ b/css/appointment-picker.scss
@@ -1,5 +1,5 @@
/*
- * appointment-picker.css 1.0.4 | MIT License | github.com/jannicz/appointment-picker
+ * appointment-picker.css 1.0.5 | MIT License | github.com/jannicz/appointment-picker
*/
/* Default variation */
diff --git a/index.html b/index.html
index 771e90c..1355d1b 100644
--- a/index.html
+++ b/index.html
@@ -124,6 +124,10 @@ More Examples
Calling exposed functions
Rendering into DOM on init
+ Tests
+
Report bugs or feature requests on Github Issues
diff --git a/js/appointment-picker.js b/js/appointment-picker.js
index 0e1a52c..7abda70 100644
--- a/js/appointment-picker.js
+++ b/js/appointment-picker.js
@@ -2,7 +2,7 @@
* Appointment-Picker - a lightweight, accessible and customizable timepicker
*
* @module Appointment-Picker
- * @version 1.0.4
+ * @version 1.0.5
*
* @author Jan Suwart
*/
@@ -59,7 +59,7 @@
this.closeEventFn = this.close.bind(this);
this.openEventFn = this.open.bind(this);
this.keyEventFn = this.onKeyPress.bind(this);
- this.tabKeyUpEventFn = this.onTabKeyUp.bind(this);
+ this.bodyFocusEventFn = this.onBodyFocus.bind(this);
initialize(this, el, options || {});
};
@@ -67,7 +67,7 @@
/**
* Initialize the picker, merge default options and check for errors
* @param {Object} _this - this view reference
- * @param {DOMnode} el - reference to the time input field
+ * @param {HTMLElement} el - reference to the time input field
* @param {Object} options - user defined options
*/
function initialize(_this, el, options) {
@@ -151,7 +151,7 @@
// Delay document click listener to prevent picker flashing
setTimeout(function() {
document.body.addEventListener('click', _this.closeEventFn);
- document.body.addEventListener('focus', _this.tabKeyUpEventFn, true);
+ document.body.addEventListener('focus', _this.bodyFocusEventFn, true);
}, 100);
};
@@ -185,7 +185,7 @@
this.picker.removeEventListener('click', this.selectionEventFn);
this.picker.removeEventListener('keyup', this.keyEventFn);
document.body.removeEventListener('click', this.closeEventFn);
- document.body.removeEventListener('focus', this.tabKeyUpEventFn, true);
+ document.body.removeEventListener('focus', this.bodyFocusEventFn, true);
};
/**
@@ -238,7 +238,7 @@
};
// Close the picker on document focus, usually by hitting TAB
- AppointmentPicker.prototype.onTabKeyUp = function(e) {
+ AppointmentPicker.prototype.onBodyFocus = function(e) {
if (!this.isOpen) return;
this.close(e);
};
@@ -333,26 +333,26 @@
};
/**
- * @param {String} time - string that needs to be parsed, i.e. '11:15PM '
+ * @param {String} time - string that needs to be parsed, i.e. '11:15PM ' or '10:30 am'
* @returns {Object|undefined} containing {h: hour, m: minute} or undefined if unrecognized
- * @see https://regexr.com/3h7bo
+ * @see https://regexr.com/3heaj
*/
function _parseTime(time) {
- var match = time.match(/^([\d]{1,2}):([\d]{2})[\s]*([ap][m])?.*$/);
- var hour;
+ var match = time.match(/^\s*([\d]{1,2}):([\d]{2})[\s]*([ap][m])?.*$/i);
if (match) {
- if (match[3] === 'pm' && match[1] !== '12') {
- hour = Number(match[1]) + 12;
- } else if (match[3] === 'am' && match[1] === '12') {
+ var hour = Number(match[1]);
+ var minute = Number(match[2]);
+ var postfix = match[3];
+
+ if (/pm/i.test(postfix) && hour !== 12) {
+ hour += 12;
+ } else if (/am/i.test(postfix) && hour === 12) {
hour = 0;
- } else {
- hour = match[1]
}
- return { h: Number(hour), m: Number(match[2]) };
+ return { h: hour, m: minute };
}
-
- return undefined;
+ return;
};
/**
@@ -385,7 +385,7 @@
var next = direction < 0 ? item.previousElementSibling : item.nextElementSibling;
if (next && next.className.indexOf('disabled') < 0) {
return next;
- } else { // If .disabled found, try the next sibling
+ } else { // If disabled class found, try the next sibling
return _getNextSibling(next, direction);
}
};
diff --git a/js/appointment-picker.min.js b/js/appointment-picker.min.js
index e06eedb..f80d9b8 100644
--- a/js/appointment-picker.min.js
+++ b/js/appointment-picker.min.js
@@ -1 +1 @@
-!function(e,t){"object"==typeof exports?module.exports=t():"function"==typeof define&&define.amd?define("appointment-picker",[],function(){return t()}):e.AppointmentPicker=t()}(this,function(){"use strict";function e(e,t,i,s,n){var r=!1;return!(ei.maxTime||e>24)&&(!(s.indexOf(t)<0)&&(n.forEach(function(i,s){i.h===e&&i.m===t&&(r=!0)}),!r))}function t(e){var t,i=e.match(/^([\d]{1,2}):([\d]{2})[\s]*([ap][m])?.*$/);if(i)return t="pm"===i[3]&&"12"!==i[1]?Number(i[1])+12:"am"===i[3]&&"12"===i[1]?0:i[1],{h:Number(t),m:Number(i[2])}}function i(e,t,i,s){var n=e;return s&&(e>12?n=e-12:0==e&&(n=12),i=i.replace(e<12?"p":"a","")),i.replace("H",n).replace("M",function(e){return/^[0-9]{1}$/.test(e)?"0"+e:e}(t))}function s(e,t){if(!e)return null;var i=t<0?e.previousElementSibling:e.nextElementSibling;return i&&i.className.indexOf("disabled")<0?i:s(i,t)}var n=function(e,i){this.options={interval:60,minTime:0,maxTime:24,startTime:0,endTime:24,disabled:[],mode:"24h",large:!1,static:!1,title:"Timepicker"},this.template={inner:' ',outer:'{{title}} ',time12:"H:M apm",time24:"H:M"},this.el=e,this.picker=null,this.isOpen=!1,this.isInDom=!1,this.time={},this.intervals=[],this.disabledArr=[],this.displayTime="",this.selectionEventFn=this.select.bind(this),this.changeEventFn=this.onchange.bind(this),this.closeEventFn=this.close.bind(this),this.openEventFn=this.open.bind(this),this.keyEventFn=this.onKeyPress.bind(this),this.tabKeyUpEventFn=this.onTabKeyUp.bind(this),function(e,i,s){for(var n in s)e.options[n]=s[n];if(e.el)if(void 0===e.el.length)if(e.options.interval>60)console.warn("appointment-picker: the maximal interval is 60");else{e.options.disabled.forEach(function(i,s){e.disabledArr[s]=t(e.options.disabled[s])});for(var r=0;r<60/e.options.interval;r++)e.intervals[r]=r*e.options.interval;e.options.static?(e.picker=e.build(),e.picker.classList.add("is-position-static"),e.picker.addEventListener("click",e.selectionEventFn),e.isInDom=!0):i.addEventListener("focus",e.openEventFn),e.setTime(e.el.value),i.addEventListener("keyup",e.keyEventFn),i.addEventListener("change",e.changeEventFn)}else console.warn("appointment-picker: pass only one dom element as argument")}(this,e,i||{})};return n.prototype.render=function(){if(this.isOpen){var e=this.el.offsetTop+this.el.offsetHeight,t=this.el.offsetLeft,i=this.picker.querySelector("input.is-selected");if(this.picker.classList.add("is-open"),i&&i.classList.remove("is-selected"),this.time.hasOwnProperty("h")){var s=this.picker.querySelector('[value="'+this.displayTime+'"]');s&&s.classList.add("is-selected")}this.picker.style.top=e+"px",this.picker.style.left=t+"px"}else this.picker.classList.remove("is-open")},n.prototype.open=function(){var e=this;this.isOpen||(this.isInDom||(this.picker=this.build(),this.isInDom=!0),this.isOpen=!0,this.render(),this.picker.addEventListener("click",this.selectionEventFn),this.picker.addEventListener("keyup",this.keyEventFn),setTimeout(function(){document.body.addEventListener("click",e.closeEventFn),document.body.addEventListener("focus",e.tabKeyUpEventFn,!0)},100))},n.prototype.close=function(e){if(this.isOpen){if(Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),e){var t=e.target;if(t.isEqualNode(this.el))return;for(;t;){if(t.matches(".appo-picker"))return;t=t.parentElement}}this.isOpen=!1,this.render(),this.picker.removeEventListener("click",this.selectionEventFn),this.picker.removeEventListener("keyup",this.keyEventFn),document.body.removeEventListener("click",this.closeEventFn),document.body.removeEventListener("focus",this.tabKeyUpEventFn,!0)}},n.prototype.select=function(e){var t=this;e.target.value&&(this.setTime(e.target.value),this.el.focus(),setTimeout(function(){t.close(null)},100))},n.prototype.onchange=function(e){this.setTime(this.el.value)},n.prototype.onKeyPress=function(e){var t=this.picker.querySelector('input[type="button"]:not([disabled])'),i=this.picker.querySelector("input.is-selected"),n=null;switch(e.keyCode){case 13:case 27:this.close(null);break;case 38:n=i?s(i.parentNode,-1):t.parentNode;break;case 40:n=i?s(i.parentNode,1):t.parentNode}n&&!n.firstChild.disabled&&(n.firstChild.classList.add("is-selected"),i&&i.classList.remove("is-selected"),this.setTime(n.firstChild.value))},n.prototype.onTabKeyUp=function(e){this.isOpen&&this.close(e)},n.prototype.build=function(){var t=document.createElement("div");return t.innerHTML=function(t,s,n,r){for(var o=t.startTime,a=t.endTime,l="",p="12h"===t.mode,c=p?s.time12:s.time24,h=o;hi.maxTime||e>24)&&(!(s.indexOf(t)<0)&&(n.forEach(function(i,s){i.h===e&&i.m===t&&(r=!0)}),!r))}function t(e){var t=e.match(/^\s*([\d]{1,2}):([\d]{2})[\s]*([ap][m])?.*$/i);if(t){var i=Number(t[1]),s=Number(t[2]),n=t[3];return/pm/i.test(n)&&12!==i?i+=12:/am/i.test(n)&&12===i&&(i=0),{h:i,m:s}}}function i(e,t,i,s){var n=e;return s&&(e>12?n=e-12:0==e&&(n=12),i=i.replace(e<12?"p":"a","")),i.replace("H",n).replace("M",function(e){return/^[0-9]{1}$/.test(e)?"0"+e:e}(t))}function s(e,t){if(!e)return null;var i=t<0?e.previousElementSibling:e.nextElementSibling;return i&&i.className.indexOf("disabled")<0?i:s(i,t)}var n=function(e,i){this.options={interval:60,minTime:0,maxTime:24,startTime:0,endTime:24,disabled:[],mode:"24h",large:!1,static:!1,title:"Timepicker"},this.template={inner:' ',outer:'{{title}} ',time12:"H:M apm",time24:"H:M"},this.el=e,this.picker=null,this.isOpen=!1,this.isInDom=!1,this.time={},this.intervals=[],this.disabledArr=[],this.displayTime="",this.selectionEventFn=this.select.bind(this),this.changeEventFn=this.onchange.bind(this),this.closeEventFn=this.close.bind(this),this.openEventFn=this.open.bind(this),this.keyEventFn=this.onKeyPress.bind(this),this.bodyFocusEventFn=this.onBodyFocus.bind(this),function(e,i,s){for(var n in s)e.options[n]=s[n];if(e.el)if(void 0===e.el.length)if(e.options.interval>60)console.warn("appointment-picker: the maximal interval is 60");else{e.options.disabled.forEach(function(i,s){e.disabledArr[s]=t(e.options.disabled[s])});for(var r=0;r<60/e.options.interval;r++)e.intervals[r]=r*e.options.interval;e.options.static?(e.picker=e.build(),e.picker.classList.add("is-position-static"),e.picker.addEventListener("click",e.selectionEventFn),e.isInDom=!0):i.addEventListener("focus",e.openEventFn),e.setTime(e.el.value),i.addEventListener("keyup",e.keyEventFn),i.addEventListener("change",e.changeEventFn)}else console.warn("appointment-picker: pass only one dom element as argument")}(this,e,i||{})};return n.prototype.render=function(){if(this.isOpen){var e=this.el.offsetTop+this.el.offsetHeight,t=this.el.offsetLeft,i=this.picker.querySelector("input.is-selected");if(this.picker.classList.add("is-open"),i&&i.classList.remove("is-selected"),this.time.hasOwnProperty("h")){var s=this.picker.querySelector('[value="'+this.displayTime+'"]');s&&s.classList.add("is-selected")}this.picker.style.top=e+"px",this.picker.style.left=t+"px"}else this.picker.classList.remove("is-open")},n.prototype.open=function(){var e=this;this.isOpen||(this.isInDom||(this.picker=this.build(),this.isInDom=!0),this.isOpen=!0,this.render(),this.picker.addEventListener("click",this.selectionEventFn),this.picker.addEventListener("keyup",this.keyEventFn),setTimeout(function(){document.body.addEventListener("click",e.closeEventFn),document.body.addEventListener("focus",e.bodyFocusEventFn,!0)},100))},n.prototype.close=function(e){if(this.isOpen){if(Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),e){var t=e.target;if(t.isEqualNode(this.el))return;for(;t;){if(t.matches(".appo-picker"))return;t=t.parentElement}}this.isOpen=!1,this.render(),this.picker.removeEventListener("click",this.selectionEventFn),this.picker.removeEventListener("keyup",this.keyEventFn),document.body.removeEventListener("click",this.closeEventFn),document.body.removeEventListener("focus",this.bodyFocusEventFn,!0)}},n.prototype.select=function(e){var t=this;e.target.value&&(this.setTime(e.target.value),this.el.focus(),setTimeout(function(){t.close(null)},100))},n.prototype.onchange=function(e){this.setTime(this.el.value)},n.prototype.onKeyPress=function(e){var t=this.picker.querySelector('input[type="button"]:not([disabled])'),i=this.picker.querySelector("input.is-selected"),n=null;switch(e.keyCode){case 13:case 27:this.close(null);break;case 38:n=i?s(i.parentNode,-1):t.parentNode;break;case 40:n=i?s(i.parentNode,1):t.parentNode}n&&!n.firstChild.disabled&&(n.firstChild.classList.add("is-selected"),i&&i.classList.remove("is-selected"),this.setTime(n.firstChild.value))},n.prototype.onBodyFocus=function(e){this.isOpen&&this.close(e)},n.prototype.build=function(){var t=document.createElement("div");return t.innerHTML=function(t,s,n,r){for(var o=t.startTime,l=t.endTime,a="",c="12h"===t.mode,p=c?s.time12:s.time24,h=o;h {
+ pickerInstance = new AppointmentPicker(document.getElementById('time-spec'), {
+ interval: 30,
+ mode: '12h',
+ maxTime: 18,
+ startTime: 09,
+ endTime: 21,
+ disabled: ['1:30 pm', '2:00 pm', '5:30 pm']
+ });
+
+ // Give focus to the input and therefore creating the picker dom elements
+ pickerInstance.el.focus();
+
+ console.log('pickerInstance =>', pickerInstance);
+ });
+
+ describe("initial value recognition", function() {
+
+ it("applies the initial input value correctly as time", function() {
+ assert.isNumber(pickerInstance.getTime().h);
+ assert.isNumber(pickerInstance.getTime().m);
+ assert.equal(pickerInstance.getTime().h, 14);
+ assert.equal(pickerInstance.getTime().m, 30);
+ });
+
+ });
+
+ describe("dom element creation", function() {
+
+ this.retries(4);
+
+ it("creates the dom element after a focusin", function() {
+ assert.isNotNull(pickerInstance.picker);
+ assert.isNotNull(pickerInstance.picker.innerHTML);
+ });
+
+ it("opens the picker after a focusin", function() {
+ assert.include(pickerInstance.picker.classList.toString(), 'appo-picker');
+ assert.include(pickerInstance.picker.classList.toString(), 'is-open');
+ });
+
+ });
+
+ describe("time manipulation and parsing", function() {
+
+ var testsAccept = [
+ { args: " 10:00AM", expected: { h: 10, m: 0 } },
+ { args: " 10:30 ", expected: { h: 10, m: 30 } },
+ { args: "11:00 foo", expected: { h: 11, m: 0 } },
+ { args: "12:00 pm", expected: { h: 12, m: 0 } },
+ { args: "1:00pm", expected: { h: 13, m: 0 } },
+ { args: "4:30 PM", expected: { h: 16, m: 30 } },
+ { args: "18:00", expected: { h: 18, m: 0 } },
+ { args: "12:00am", expected: { h: 0, m: 0 } }
+ ];
+
+ testsAccept.forEach(function(test) {
+ it('correctly parses "' + test.args + '" into time ' + JSON.stringify(test.expected), function(done) {
+ this.slow(500);
+ pickerInstance.setTime(test.args);
+ pickerInstance.render();
+ var result = pickerInstance.getTime();
+ assert.deepEqual(result, test.expected);
+ setTimeout(done, 100);
+ });
+ });
+
+ });
+
+ describe("time validation and rejection", function() {
+
+ before(() => {
+ pickerInstance.setTime("18:00");
+ });
+
+ var testsReject = [
+ { args: "20:00", expected: { h: 18, m: 0 } },
+ { args: "8:30 pm", expected: { h: 18, m: 0 } },
+ { args: "1:30 PM", expected: { h: 18, m: 0 } },
+ { args: "2:00PM", expected: { h: 18, m: 0 } },
+ { args: "dh4kj6", expected: { h: 18, m: 0 } }
+ ];
+
+ testsReject.forEach(function(test) {
+ it('rejects "' + test.args + '" and kepps its old value of 18:00', function() {
+ pickerInstance.setTime(test.args);
+ var result = pickerInstance.getTime();
+ assert.deepEqual(result, test.expected);
+ });
+ });
+
+ });
+
+ describe("exposed behaviour functions", function() {
+
+ before(() => {
+ pickerInstance.setTime("12:00");
+ pickerInstance.render();
+ });
+
+ it("closes the picker", function(done) {
+ this.slow(1000);
+ pickerInstance.close();
+ assert.notInclude(pickerInstance.picker.classList.toString(), 'is-open');
+ setTimeout(done, 300);
+ });
+
+ it("opens the picker", function(done) {
+ this.slow(500);
+ pickerInstance.open();
+ assert.include(pickerInstance.picker.classList.toString(), 'is-open');
+ setTimeout(done, 100);
+ });
+
+ it("moves time selection by calling keyboard event function", function(done) {
+ this.slow(500);
+ // Simulate event with keyCode 40 = down arrow
+ pickerInstance.onKeyPress({ keyCode: 40 });
+ var result = pickerInstance.getTime();
+ assert.deepEqual(result, { h: 12, m: 30 });
+ setTimeout(done, 100);
+ });
+
+ it("moves selection and skips disabled times using keyboard event function", function(done) {
+ this.slow(500);
+ // Simulate two events with keyCode 40 = down arrow
+ pickerInstance.onKeyPress({ keyCode: 40 });
+ pickerInstance.onKeyPress({ keyCode: 40 });
+
+ var result = pickerInstance.getTime();
+ assert.deepEqual(result, { h: 14, m: 30 });
+ setTimeout(done, 100);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/tests.html b/tests/tests.html
new file mode 100644
index 0000000..0be6803
--- /dev/null
+++ b/tests/tests.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Time
+
+
+
+
+
+
+
+
+
+
+
+
+ Copyright Jan Suwart, MIT license
+
+
+
+
\ No newline at end of file