Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations SkipVought on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

non jquery date picker with limitations 1

Status
Not open for further replies.

Leozack

MIS
Oct 25, 2002
867
0
0
GB
Hi all - so I'm being asked to make a date picker that will somehow stop more than 3 people picking the same day before that day becomes unpickable for anyone else. I assume I can at least wait until a submit button is pressed in order for someone to count as one of the 3. So I will have to find a way to record the date to a flatfile or db and then check it (probably more ajax) and apply any future limitations to the datepicker when it's opened.
Ignoring those complications, I've just been trying to find a basic datepicker that works nicely and can be default disable 2 days a week (sun/mon) before I get into the (only 3 per day) limitations.
I've looked at this one
but it's started to open my div one about 200px lower than the div it's supposed to appear by for no reason.
So I've started looking for others but have just ended up with lots of options and many don't have the limitation power I need or the pages are dead or they use jquery or they're not free :/
Any thoughts anyone?

_________________________________
Leozack
Code:
MakeUniverse($infinity,1,42);
 
to the datepicker when it's opened
not just when it is opened, but I suspect you will have to check every few seconds to make the user experience ok. and of course when a date is actually selected (before the submit button is pressed).

what is the business requirement to avoid jQuery? or rather why does such a requirement exist? Without some kind of framework you risk a greater development and testing cycle to ensure cross-browser compatibility. Given cacheing and browser speed, most people find that is not a sensible trade off these days.

If the concern is over clashing with another library/framework then this is easily avoided with compatibility mode.

In terms of springboarding your design - an idea might be to take the jquery date picker, render a page and then capture the source. that would give you a starting block with a known good css + html structure. you could then add on the javascript functionality depending on what you actually need. That would be pretty straight forward. Do make sure you include appropriate copyright acknowledgments.

Ignoring the complexities of cross-browser compatibility, it's relatively simple to craft a calendar object that renders nicely. You need to take an early design choice as to whether you should use a table or not, for the calendar. As a calendar is at least quasi-tabular probably the purists would not be too put out by using a calendar. But it is perfectly feasible to do so. check this out for an example.

If you don't add too many runtime options in, you could probably get a fully functional version sorted in an hour or so. post back if you want me to post a proof of concept for you to build on.

Tips:

1. If you use rem for the outer div measurement and em for all measurements inside the outer layer, you end up with a very flexible design that will scale with the font-size of the page. Assuming you are using a strict doctype, anyway.

2. encode the year, month and day of each calendar entry in the data attributes of the box. that way you can get at them more easily than parsing a complex id.

3. consider using moment.js as the base for your date manipulation and output. It has neat functions that will make iterating and plotting the dates easier.


 
i had a spare hour so knocked up something that the OP/others may find useful.

the server is a very trivial counter written in php

Code:
<?php
$pdo = new pdo('sqlite:myDatabase.sqlite');
class availability{
	
	public function getAvailability($year, $month, $day){
		global $pdo;
		$s = $pdo->prepare('select ifnull(clicks,0) as c from clickTable where d=?');
		if($s === false):
			print_r($pdo->errorInfo());
		endif;
		$result = $s->execute(array("$year-$month-$day"));
		if($result === false):
			print_r($s->errorInfo());
		endif;
		$obj = $s->fetchObject();
		return $obj ? intval($obj->c) : 0;
	}
	function getMonthAvailability($year,$month){
		$data = array();
		$date = new datetime();
		$date->setDate((int) $year, (int) $month, 1);
		$month = $date->format('m');
		while($month == $date->format('m')):
			$data[$date->format('Y')][$date->format('n') - 1][$date->format('j')]
								= $this->getAvailability(
													$date->format('Y'),
													$date->format('m'),
													$date->format('d'));
			$date->modify('+1 day');
		endwhile;
		return $data;
	}
	public function addClick($year,$month,$day){
		global $pdo;
		$c = $this->getAvailability(	$year,
										str_pad($month,2,"0", STR_PAD_LEFT),
										str_pad($day,2,"0",STR_PAD_LEFT)
										);
		if($c >= 3):
			return false;
		endif;
		$s = $pdo->prepare(<<<SQL
INSERT OR REPLACE
INTO clickTable ( clicks, d)
VALUES(?, date(?))
SQL
);
		$s->execute(array($c+1,	"$year-" .
								str_pad($month,2,"0", STR_PAD_LEFT) . '-' .
								str_pad($day,2,"0",STR_PAD_LEFT)
						)
				);
		return true;
	}
}
date_default_timezone_set('UTC');
$return = array();
$a = new availability();
switch($_REQUEST['action']):
case 'getAvailability':
	$data = $a->getMonthAvailability($_POST['year'], $_POST['month'] + 1);
	$return = array('result'=>'ok', 'data'=>$data);
break;
case 'addClick':
	$result = $a->addClick($_POST['year'],$_POST['month'] + 1, $_POST['day']);
	if($result):
		$data = $a->getMonthAvailability($_POST['year'], $_POST['month'] + 1);
		$return = array('result'=>'ok', 'data'=>$data);
	else:
		$return = array('result'=>'error', 'data'=>$data);
	endif;
break;
endswitch;
echo json_encode($return);
die;
?>

the ui relies only on moment.js which is loaded via a cdn. you can see that the mark up and styling are very minimal so the css can be very easily tweaked to make it look how you like. To change the size of the calendar just change the font-size of the container.

you can, of course, have multiple calendars but then you will want to tweak the ajax methods so that the ui identifies the calendar being clicked to the server. each calendar needs to be instantiated separately.

nb not all browsers are compliant with calc() in css. so you might want to move that style declaration to the js plugin instead if you are worried about old browsers.

i've not included a 'key' but hopefully obvious that reddish squares are not available and greenish squares are ok. the script updates every 30 seconds - which can be parameterised through the options object.

Code:
<!DOCTYPE HTML>
	<meta charset="utf-8" />
	<head>
		<script src="[URL unfurl="true"]https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment.min.js"></script>[/URL]
		<script>
Element.prototype.datepicker = function( option ){
	
	var _this;
	var options = {
		ajaxServer: 'debug.php',
		refreshInterval: 30000,
		maskDays:[6,7],
		alreadyTakenMessage: 'Sorry, that day is no longer available',
		successMessage: 'Thank you, your click has been logged',
		startMonth: -24,
		endMonth: 24
	}
	var timer = {
		start: function(){
			timer.timer = window.setInterval(events.tick, im.getOption('refreshInterval'));
		},
		stop: function(){
			window.clearInterval(timer.timer);
		},
		timer: ''
	}
	
	var ajaxRequest; 
	
	var ajax = {
		r: '',
		post: function(data, callback, async){
			if(typeof async == 'undefined'){
				async = true;
			}
			ajax.connect();
			ajax.r.onreadystatechange = function(){
				if(ajax.r.readyState == 4){
					callback(JSON.parse(ajax.r.responseText));
				}
			}
			ajax.r.open('POST', im.getOption('ajaxServer'), async);
			ajax.r.setRequestHeader("Content-type","application/x-[URL unfurl="true"]www-form-urlencoded");[/URL]
			ajax.r.send(im.serialize(data));
		},
		get: function(data, callback, async){
			if(typeof async == 'undefined'){
				async = true;
			}
			ajax.connect();
			ajax.r.onreadystatechange = function(){
				if(ajax.r.readyState == 4){
					callback(JSON.parse(ajax.r.responseText));
				}
			}
			ajax.r.open('GET', im.getOption('ajaxServer') + '?' + im.serialize(data), async);		
			ajax.r.send();
		},
		connect: function(){
			try{
				ajax.r = new XMLHttpRequest();
			} catch (e){
				try{
					ajax.r = new ActiveXObject("Msxml2.XMLHTTP");
				} catch (e) {
					try{
						ajax.r = new ActiveXObject("Microsoft.XMLHTTP");
					} catch (e){
						// Something went wrong
						alert("Your browser broke!");
						return false;
					}
				}
			}
		}
	}
	var events = {
		dayClick: function(e){
			var that = e.target;
			if(!im.isAvailable(that)){
				return;
			}
			im.addClick(that.dataset.year, that.dataset.month, that.dataset.day, that);
		},
		mouseover: function(e){
			var that = e.target;
			that.className = that.className + " hover";
			that.addEventListener('mouseout', events.mouseleave);
		},
		mouseleave: function(e){
			var that = e.target;
			that.classList.remove('hover');
			that.removeEventListener('mouseout', events.mouseover);
		},
		buttonClick: function(e){
			var that = e.target;
			that.preventDefault();
		},
		tick: function(){
			im.refreshAvailability();
		},
		prevClick: function(e){
			timer.stop();
			e.preventDefault();
			var cM = im.getCurrentMonth();
			var m = new moment({year:cM.year, month:cM.month, day:1});
			m.add(-1,'months');
			methods.load( m.get('year'), m.get('month'));
		},
		nextClick: function(e){
			timer.stop();
			e.preventDefault();
			var cM = im.getCurrentMonth();
			var m = new moment({year:cM.year, month:cM.month, day:1});
			m.add(1,'months');
			methods.load(m.get('year'), m.get('month'));
		},
		selectChange: function(e){
			var that = e.target;
			var bits = that.value.split('-');
			methods.load(bits[0],bits[1]);
		}
	}
	
	var im = {
		setOptions: function(opts){
			for(var i in opts) options[i] = opts[i];
		}
	}
	
	var methods = {
		init: function(){
			_this = this;
			im.setOption('fontSize', parseFloat(window.getComputedStyle(_this,null).getPropertyValue('font-size')));
			im.doSelect();
			methods.load();
		},
		load: function(year, month, day ){
			im.empty(_this);
			container = im.getContainer();
			var div = document.createElement('div');
			div.classList.add('heading');
			div.appendChild(im.getOption('prev'));
			div.appendChild(im.getOption('select'));
			div.appendChild(im.getOption('next'))
			container.appendChild(div);
			var days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
			for (var i = 0; i < 7; i++) {
				var cell = im.getBlankDay();
				var t = document.createTextNode(days[i ]);
				cell.appendChild(t);
				cell.classList.remove('blankDay');
				cell.classList.add(days[i].toLowerCase());
				cell.classList.add('heading')
				container.appendChild(cell);
			}	 
			if(year){
				var m = new moment({year:year, month:month, day:1});
			} else {
				var m = new moment().set({date:1});
				year = m.get('year');
				month = m.get('month');
				day = m.get('date');
			}
			var d = m.isoWeekday();
			var startingMonth = m.month();
			first = true;
			while (m.month() == startingMonth) {
				for (var i = 1; i <= 7; i++) {
					if (i < d && first){
						var cell = im.getBlankDay();
					} else if(m.month() == startingMonth){
						var cell = im.getDay(m.year(), m.month(), m.date());
						m.add(1, 'days');
					} else {
						var cell = im.getBlankDay();
					}
					container.appendChild(cell);
				}
				first = false;
			}
			_this.appendChild(container);
			im.setSelectValue(year,month);
			im.refreshAvailability(year,month);
		}	
	}
	
	var im = { //internal only methods
		serialize: function(obj){
			var str = [];
			for (var p in obj) {
				if (obj.hasOwnProperty(p)) {
					str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
				}
			}
			return str.join("&");
		},
		s4: function(){
			return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
		},
		empty: function(elem){
			while (elem.firstChild) {
				elem.removeChild(elem.firstChild);
			}
		},
		guid: function(){
			return im.s4() + im.s4() + '-' + im.s4() + '-' + im.s4() + '-' + im.s4() + '-' + im.s4() + im.s4() + im.s4();
		},
		setOption: function(option, value){
			options[option] = value;
		},
		getOption: function(option){
			return options[option] ? options[option] : null;
		},
		getContainer: function(){
			var c = document.createElement('div');
			c.className = "calendarContainer";
			c.id = im.guid();
			im.setOption('container', c.id);
			var width = (im.convertToRem(im.getOption('fontSize')) * 17) + 'rem';
			console.log(width);
			c.style.width = width;
			return c;
		},
		convertToRem: function(value){
			console.log('value',value);
			var aRem = im.convertFromRem(1);
			var m = parseFloat(getComputedStyle(document.documentElement).fontSize);
			console.log('conversion', value/m);
			return value / m;
		},
		convertFromRem: function(value) {
			var v = parseFloat(getComputedStyle(document.documentElement).fontSize);
    		return value * v;
		},
		getDay: function(year, month, day){
			var m = new moment({
				year: year,
				month: month,
				day: day
			});
			var d = document.createElement('span');
			d.dataset.year = year;
			d.dataset.month = month;
			d.dataset.day = day;
			d.dataset.blank = false;
			d.classList.add("day");
			d.classList.add(m.format('ddd').toLowerCase());
			if (m.isSame(new Date(), "day")) {
				d.classList.add('today');
			}
			d.appendChild(document.createTextNode(day));
			d.addEventListener('click', events.dayClick);
			d.addEventListener('mouseover', events.mouseover);
			return d;
		},
		getBlankDay: function(){
			var d = document.createElement('span');
			d.text = '&nbsp;';
			d.className = 'day blankDay';
			d.dataset.blank = true;
			return d;
		},
		addClick: function(year, month, day, elem){
			timer.stop();
			ajax.post({
				action: 'addClick',
				year: year,
				month: month,
				day: day
			}, function(data){
				if (data.result == 'ok') {
					im.setAvailability(data);
					alert(im.getOption('successMessage'));
				}
				else {
					im.setAvailability(data);
					alert(im.getOption('alreadyTakenMessage'));
				}
			}, false);
		},
		checkAvailability: function(year, month, elem){
			im.refreshAvailability(year, month, true);
			if (!im.isAvailable(elem)) {
				alert('Too late.  That date currently has no availability');
			}
		},
		isAvailable: function(elem){
			if (elem.dataset.clicks >= 3) {
				return false;
			}
			else {
				var m = new moment({
					year: elem.dataset.year,
					month: elem.dataset.month,
					day: elem.dataset.day
				});
				if (im.getOption('maskDays').indexOf(m.isoWeekday()) > -1) {
					return false;
				}
				else {
					return true;
				}
			}
		},
		
		refreshAvailability: function(year, month){
			timer.stop();
			if (typeof year == 'undefined') {
				var e = _this.querySelector('.day:not(.blankDay):not(.heading)');
				year = e.dataset.year;
				month = e.dataset.month;
			}
			ajax.post({
				action: 'getAvailability',
				year: year,
				month: month
			}, im.setAvailability, true);
		},
		setAvailability: function(obj){
			if (obj.data) {
				[].forEach.call(_this.querySelectorAll('.day:not(.blankDay):not(.heading)'), function(el){
					el.dataset.clicks = obj.data[el.dataset.year][el.dataset.month][el.dataset.day];
					var a = im.isAvailable(el);
					if (a) {
						el.classList.add('available');
						el.classList.remove('unavailable');
					}
					else {
						el.classList.add('unavailable');
						el.classList.remove('available');
					}
				});
			}
			timer.start();
		},
		doSelect: function(){
			var m = new moment().add(im.getOption('startMonth'), 'months');
			var cM = im.getCurrentMonth();
			var t = new moment({
				year: cM.year,
				month: cM.month,
				day: cM.day
			});
			var s = document.createElement('select');
			for (var i = 0; i < Math.abs(im.getOption('startMonth')) + im.getOption('endMonth'); i++) {
				var opt = document.createElement('option');
				opt.text = m.format('MMMM, YYYY');
				opt.value = m.format('YYYY-MM');
				if (m.isSame(t, 'month')) {
					opt.selected = true;
				}
				m.add(1, 'months');
				s.add(opt);
			}
			s.id = im.guid();
			s.classList.add('selectBox');
			s.addEventListener('change', events.selectChange);
			
			var prev = document.createElement('a');
			prev.classList.add('button');
			prev.text = '<<';
			prev.addEventListener('click', events.prevClick);
			var next = document.createElement('a');
			next.classList.add('button');
			next.text = '>>';
			next.addEventListener('click', events.nextClick);
			
			im.setOption('select', s);
			im.setOption('prev', prev);
			im.setOption('next', next);
		},
		getCurrentMonth: function(){
			var elem = _this.querySelector('.day:not(.blankDay):not(.heading)');
			if (elem == null) {
				var m = new moment();
				return {
					year: m.get('year'),
					month: m.get('month')
				}
			}
			return {
				year: elem.dataset.year,
				month: elem.dataset.month
			}
		},
		setSelectValue: function(year, month){
			month = month + 1;
			if (month <= 9) 
				month = '0' + month;
			var val = year + '-' + month;
			var elem = _this.querySelector("select");
			elem.value = val;
		}
	}
	if(methods[option]){
		return methods[option].apply( this, Array.prototype.slice.call( arguments, 1 ));
	} else if ( typeof option === 'object' || ! option ) {
		return methods.init.apply( this, arguments );
    } else {
		return false;
	}
}


		</script>
		<style>
#calendar{
	font-size:0.8rem;
}
.calendarContainer {
    padding-top: 1rem; 
}
.calendarContainer .day {
	box-sizing:border-box;
	-moz-box-sizing:border-box;
	-webkit-box-sizing: border-box;
	font-size: inherit;
    float: left;
    width: calc((100%/7) - 0.25rem);
    padding: 0;
    margin: 0.125rem;
    text-align: center;
    border: 1px solid silver;
	display:block;
	vertical-align: middle;
}
.calendarContainer .today{
	border-color: #5881fc;
}
.calendarContainer .blankDay{
	border-color: transparent;
}
.calendarContainer, calendarContainer .sun {
    clear: left;
}
.calendarContainer hover{
	backgroundColor: #fbffd2;
}
.calendarContainer .available{
	background-color: #b3ffd7;
	cursor:pointer;
}
.calendarContainer .unavailable{
	background-color: #ffccc9;
}

.calendarContainer .heading{
	text-align:center;
	margin-bottom: 0.5rem;
}
.calendarContainer .heading .button{
	margin-left: 0.5rem;
	margin-right: 0.5rem;
	text-decoration: none;
	cursor: pointer;
}
		</style>
	</head>
	<body>
	<div id="calendar"></div>	
	</body>
	<script>
		document.querySelector('#calendar').datepicker();
	</script>
 
Wow that's a lot you've knocked up there mate! I look forward to grinding through it as soon as I get the chance - just looked at your demo page and it seems you've got it all working nicely first time - always a winner! Thanks again I'll let you know how I get on

_________________________________
Leozack
Code:
MakeUniverse($infinity,1,42);
 
Definitely use the demo page version as I have not reposted some of the js tweaks and css fixes here.
All js and css is on one page. I may have slightly changed the php and will post the source on the demo page too.
 
Tbh I've not had the time to work on this which is annoying and tonight doesn't look free yet either but I'll let you know. Process-wise my only concern for the intended use is that anyone viewing can click-happy on the calendar and use up all the days. I'm wondering considering limiting click-booking based on IP perhaps or tie it into some form submission for an order - which they only want 3 to be allowed per day. But since they've got a disconnected method of payment that has no part in this process, it is all a little fluid for my liking. But I'll see what I can come up with when I get some actual time to work on this :eek:

_________________________________
Leozack
Code:
MakeUniverse($infinity,1,42);
 
you can limit by session id more easily perhaps than id.
you'd need to add a table column and tweak the two sql queries slightly.

but first you'd need the business rules articulated by your client as it's a mug's game developing any solution without a hard input specification.

 
Yeah it turns out that he's reviewed the process and has tried to streamline payment into it more without the calendar booking - so now you just choose a date then get the payment options and if there is a problem with the order he'll contact them and hopefully won't have more than 3 a day.
I don't mind as it means I don't have to implement this and thus keeps it DB free.
But thanks for your input and well done on making such a great date picker which I'm sure people will be glad to find when searching online!

_________________________________
Leozack
Code:
MakeUniverse($infinity,1,42);
 
thanks for posting back @Leozack.

I will rehash the script into a pure date picker and perhaps post here or as a FAQ.

as an aside - you didn't give any business rationale as to why jQuery was banned for this task. It would be interesting to hear the answer as I have come across the 'no Frameworks' shtick twice in recent weeks. Were I an IT director I'd be saying the exact opposite. I'm eager to learn why I might be wrong.
 
The answer is simply that I've made do without JS and not taken the time to learn it - not because it's a problem :p I'm aware that nowadays I might aswell learn it and utilise all the stuff people have made in it but hey, I prefer to just get stuff done without if I can #oldfashioned

_________________________________
Leozack
Code:
MakeUniverse($infinity,1,42);
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top