* @copyright (c) 2006 - 2016 Stefan Gabos * @package Controls */ class Zebra_Form_Date extends Zebra_Form_Control { /** * Adds a date control to the form. * * Do not instantiate this class directly! Use the {@link Zebra_Form::add() add()} method instead! * * The output of this control will be a {@link Zebra_Form_Text textbox} control with an icon to the right of it.
* Clicking the icon will open an inline JavaScript date picker.
* * * // create a new form * $form = new Zebra_Form('my_form'); * * // add a date control to the form * $mydate = $form->add('date', 'my_date', date('Y-m-d')); * * // you *have* to set the "date" rule * $mydate->set_rule(array( * 'date' => array('error', 'Invalid date specified!'), * )); * * // set the date's format * $mydate->format('M d, Y'); * * // don't forget to always call this method before rendering the form * if ($form->validate()) { * * // get the date in YYYY-MM-DD format so you can play with is easily * $date = $mydate->get_date(); * * } * * // output the form using an automatically generated template * $form->render(); * * * @param string $id Unique name to identify the control in the form. * * The control's name attribute will be the same as the id attribute! * * This is the name to be used when referring to the control's value in the * POST/GET superglobals, after the form is submitted. * * This is also the name of the variable to be used in custom template files, in * order to display the control. * * * // in a template file, in order to print the generated HTML * // for a control named "my_date", one would use: * echo $my_date; * * * @param string $default (Optional) Default date, formatted according to {@link format() format}. * * @param array $attributes (Optional) An array of attributes valid for * {@link http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.4 input} * controls (size, readonly, style, etc) * * Must be specified as an associative array, in the form of attribute => value. * * // setting the "readonly" attribute * $obj = $form->add( * 'date', * 'my_date', * '', * array( * 'readonly' => 'readonly' * ) * ); * * * See {@link Zebra_Form_Control::set_attributes() set_attributes()} on how to set * attributes, other than through the constructor. * * The following attributes are automatically set when the control is created and * should not be altered manually:
* * type, id, name, value, class * * @return void */ function __construct($id, $default = '', $attributes = '') { // call the constructor of the parent class parent::__construct(); // set the private attributes of this control // these attributes are private for this control and are for internal use only // and will not be rendered by the _render_attributes() method $this->private_attributes = array( 'locked', 'disable_xss_filters', 'disable_zebra_datepicker', 'date', 'always_visible', 'container', 'custom_classes', 'days', 'days_abbr', 'default_position', 'direction', 'disabled_dates', 'enabled_dates', 'first_day_of_week', 'format', 'header_captions', 'header_navigation', 'icon_position', 'inside_icon', 'lang_clear_date', 'months', 'months_abbr', 'offset', 'open_icon_only', 'pair', 'readonly_element', 'show_clear_date', 'show_icon', 'show_other_months', 'show_select_today', 'show_week_number', 'select_other_months', 'start_date', 'strict', 'view', 'weekend_days', 'zero_pad', ); // set the javascript attributes of this control // these attributes will be used by the JavaScript date picker object $this->javascript_attributes = array( 'always_visible', 'container', 'custom_classes', 'days', 'days_abbr', 'default_position', 'direction', 'disabled_dates', 'enabled_dates', 'first_day_of_week', 'format', 'header_captions', 'header_navigation', 'icon_position', 'inside_icon', 'lang_clear_date', 'months', 'months_abbr', 'offset', 'pair', 'readonly_element', 'show_clear_date', 'show_icon', 'show_other_months', 'show_select_today', 'show_week_number', 'select_other_months', 'start_date', 'strict', 'view', 'weekend_days', 'zero_pad', ); // set the default attributes for the text control // put them in the order you'd like them rendered $this->set_attributes( array( 'type' => 'text', 'name' => $id, 'id' => $id, 'value' => $default, 'class' => 'control text date', 'always_visible' => null, 'days' => null, 'days_abbr' => null, 'direction' => null, 'disable_zebra_datepicker' => false, 'disabled_dates' => null, 'enabled_dates' => null, 'first_day_of_week' => null, 'format' => 'Y-m-d', 'header_captions' => null, 'header_navigation' => null, 'inside_icon' => null, 'months' => null, 'months_abbr' => null, 'offset' => null, 'pair' => null, 'readonly_element' => null, 'show_clear_date' => null, 'show_other_months' => null, 'show_select_today' => null, 'show_week_number' => null, 'select_other_months' => null, 'start_date' => null, 'strict' => null, 'view' => null, 'weekend_days' => null, 'zero_pad' => null, ) ); // if "class" is amongst user specified attributes if (is_array($attributes) && isset($attributes['class'])) { // we need to set the "class" attribute like this, so it doesn't overwrite previous values $this->set_attributes(array('class' => $attributes['class']), false); // make sure we don't set it again below unset($attributes['class']); } // sets user specified attributes for the control $this->set_attributes($attributes); } /** * Should the date picker be always visible? * * Setting this property to a jQuery element will result in the date picker being always visible, the indicated * element acting as the date picker's container; * * Note that when this property is set to TRUE, the {@link always_show_clear()} will be automatically set to TRUE. * * * $date = $form->add('date', 'my_date'); * * // an element having the ID "container" * // will be the date picker's container * $date->always_visible('$("#container")'); * * * @param string $element A jQuery selector pointing to an existing element from the page to be used as the * date picker's container. * * @return void */ function always_visible($element) { // set the date picker's attribute $this->set_attributes(array('always_visible' => $element)); } /** * Use this method to instruct the library to open a date picker inside a specific element - useful when you want * the date picker to open at a specific position. * * * $date = $form->add('date', 'my_date'); * * // the date picker will open inside this element * $date->container('$("#container")'); * * * @param string $element A jQuery selector pointing to an existing element from the page to be used as the * date picker's container. * * By default, all date pickers are placed at the end of the element * * @since 2.9.8 * * @return void */ function container($element) { // set the date picker's attribute $this->set_attributes(array('container' => $element)); } /** * Dates that should have custom classes applied to them. * * * $date = $form->add('date', 'my_date'); * * // apply "myclass1" custom class to the first day, of every month, of every year * $date->custom_classes(array( * 'myclass1' => array('01 * *'), * )); * * * @param array $custom_classes An array in the form of * * * array( * 'myclass1': array(dates_to_apply_myclass1_to), * 'myclass2': array(dates_to_apply_myclass2_to), * ) * * * ...where "dates_to_apply_myclassx_to" is an array of dates in the same format as * required for {@link disabled_dates} property. * * Custom classes will be applied only in the day picker view and not on * month/year views! Also note that the class name will have the “_disabled” suffix * added if the day the class is applied to is disabled. * * In order for the styles in your custom classes to be applied, make sure you are * using the following syntax: * * * .Zebra_DatePicker .dp_daypicker td.myclass1 { .. } * .Zebra_DatePicker .dp_daypicker td.myclass1_disabled { .. } * * * Default is FALSE, no custom classes * * @since 2.9.8 * * @return void */ function custom_classes($custom_classes) { // set the date picker's attribute $this->set_attributes(array('custom_classes' => $custom_classes)); } /** * The position of the date picker relative to the element it is attached to. * * Note that, regardless of this setting, the date picker's position will be automatically adjusted to fit in the * view port, if needed. * * This property will be ignored if {@link always_visible} or {@link container} properties are set * * * $date = $form->add('date', 'my_date'); * * // the date picker will open *below* the element is attached to * $date->default_position('below'); * * * @param string $position The position of the date picker relative to the element it is attached to. * * Possible values are "above" and "below". * * Default is "above" * * @since 2.9.8 * * @return void */ function default_position($position) { // set the date picker's attribute $this->set_attributes(array('default_position' => $position)); } /** * Direction of the calendar. * * * $obj = $form->add('date', 'mydate') * * // calendar starts tomorrow and seven days after that are selectable * $obj->direction(array(1, 7)); * * // calendar starts today and seven days after that are selectable * $obj->direction(array(true, 7)); * * // calendar starts on January 1st 2013 and has no ending date * // (assuming "format" is YYYY-MM-DD) * $obj->direction(array('2013-01-01', false)); * * // calendar ends today and starts on January 1st 2012 * // assuming "format" is YYYY-MM-DD) * $obj->direction(array(false, '2012-01-01')); * * * @param mixed $direction A positive or negative integer: * * - n (a positive integer) creates a future-only calendar beginning at n days * after today; * * - -n (a negative integer) creates a past-only calendar ending at n days * before today; * * - if n is 0, the calendar has no restrictions. * * Use boolean TRUE for a future-only calendar starting with today and use boolean * FALSE for a past-only calendar ending today. * * You may also set this property to an array with two elements in the following * combinations: * * - first item is boolean TRUE (calendar starts today), an integer > 0 (calendar * starts n days after today), or a valid date given in the format defined by * the "format" attribute (calendar starts at the specified date), and the second * item is boolean FALSE (the calendar has no ending date), an integer > 0 (calendar * ends n days after the starting date), or a valid date given in the format * defined by the "format" attribute and which occurs after the starting date * (calendar ends at the specified date) * * - first item is boolean FALSE (calendar ends today), an integer < 0 (calendar * ends n days before today), or a valid date given in the format defined by the * "format" attribute (calendar ends at the specified date), and the second item * is an integer > 0 (calendar ends n days before the ending date), or a valid * date given in the format defined by the "format" attribute and which occurs * before the starting date (calendar starts at the specified date) * * * Note that {@link disabled_dates()} will still apply! * * Default is 0 (no restrictions). * * @return void */ function direction($direction) { // set the date picker's attribute $this->set_attributes(array('direction' => $direction)); } /** * Disables selection of specific dates or range of dates in the calendar. * * * $obj = $form->add('date', 'mydate') * * // disable January 1, 2012 * $obj->disabled_dates(array('1 1 2012')); * * // disable all days in January 2012 * $obj->disabled_dates(array('* 1 2012')); * * // disable January 1 through 10 in 2012 * $obj->disabled_dates(array('1-10 1 2012')); * * // disable January 1 and 10 in 2012 * $obj->disabled_dates(array('1,10 1 2012')); * * // disable 1 through 10, and then 20th, 22nd and 24th * // of January through March for every year * $obj->disabled_dates(array('1-10,20,22,24 1-3 *')); * * // disable all Saturdays and Sundays * $obj->disabled_dates(array('* * * 0,6')); * * // disable 1st and 2nd of July 2012, * // and all of August of 2012; * $obj->disabled_dates(array('01 07 2012', '02 07 2012', '* 08 2012')); * * * @param array $disabled_dates An array of strings representing disabled dates. Values in the string have * to be in the following format: "day month year weekday" where "weekday" is * optional and can be 0-6 (Saturday to Sunday); The syntax is similar to * cron's syntax: the values are separated by spaces and may contain * (asterisk) * - (dash) and , (comma) delimiters. * * Default is FALSE, no disabled dates. * * @return void */ function disabled_dates($disabled_dates) { // set the date picker's attribute $this->set_attributes(array('disabled_dates' => $disabled_dates)); } /** * By default, Zebra_Form relies on {@link http://stefangabos.ro/jquery/zebra-datepicker/ Zebra_DatePicker} for * {@link Zebra_Form_Date Date} controls. If you want to use a different date picker, you have to disable the * built-in one by calling this method. * * Make sure the language used by the custom date picker is the same as the {@link language() language} of the * library, or validation of the date will fail! * * Also, note that {@link format() format}, {@link direction() direction} and {@link disabled_dates() disabled dates} * will still apply and will be taken into account when validating the date, but the other properties will be ignored * as are specific to Zebra_DatePicker! * * @since 2.8.7 * * @return void */ function disable_zebra_datepicker() { $this->set_attributes(array('disable_zebra_datepicker' => true)); } /** * Enables selection of specific dates or range of dates in the calendar, after dates have been previously disabled * via {@link disabled_dates()}. * * @param array $enabled_dates An array of enabled dates in the same format as required for as argument for * the {@link disabled_dates()} method. To be used together with * {@link disabled_dates()} by first setting "disabled_dates" to something like * array('* * * *') (which will disable everything) and then setting "enabled_dates" * to, say, array('* * * 0,6') to enable just weekends. * * Default is FALSE, all dates are enabled (unless, specificaly disabled via * {@link disabled_dates()}). * * @since 2.9.3 * * @return void */ function enabled_dates($enabled_dates) { // set the date picker's attribute $this->set_attributes(array('enabled_dates' => $enabled_dates)); } /** * Week's starting day. * * @param integer $day Valid values are 0 to 6, Sunday to Saturday. * * Default is 1, Monday. * * @return void */ function first_day_of_week($day) { // set the date picker's attribute $this->set_attributes(array('first_day_of_week' => $day)); } /** * Sets the format of the returned date. * * @param string $format Format of the returned date. * * Accepts the following characters for date formatting: d, D, j, l, N, w, S, F, m, M, * n, Y, y borrowing syntax from ({@link http://www.php.net/manual/en/function.date.php * PHP's date function}) * * Note that when setting a date format without days (‘d’, ‘j’), the users will be able * to select only years and months, and when setting a format without months and days * (‘F’, ‘m’, ‘M’, ‘n’, ‘t’, ‘d’, ‘j’), the users will be able to select only years. * * Also note that the value of the "view" property (see below) may be overridden if it * is the case: a value of "days" for the "view" property makes no sense if the date * format doesn’t allow the selection of days. * * Default format is Y-m-d * * @return void */ function format($format) { // set the date picker's attribute $this->set_attributes(array('format' => $format)); } /** * To be used after the form is submitted! * * Returns submitted date in the YYYY-MM-DD format so that it's directly usable with a database engine or with * PHP's {@link http://php.net/manual/en/function.strtotime.php strtotime} function. * * @return string Returns submitted date in the YYYY-MM-DD format, or an empty string if control was * submitted with no value (empty). */ function get_date() { $result = $this->get_attributes('date'); // if control had a value return it, or return an empty string otherwise return (isset($result['date'])) ? $result['date'] : ''; } /** * Captions in the datepicker's header, for the 3 possible views: days, months, years. * * For each of the 3 views the following special characters may be used borrowing from PHP's "date" function's * syntax: m, n, F, M, y and Y; any of these will be replaced at runtime with the appropriate date fragment, depending * on the currently viewed date. two more special characters are also available Y1 and Y2 (upper case representing * years with 4 digits, lowercase representing years with 2 digits) which represent "currently selected year - 7" * and "currently selected year + 4" and which only make sense used in the "years" view. * * Even though any of these special characters may be used in any of the 3 views, you should use m, n, F, M for the * "days" view and y, Y, Y1, Y2, y1, y2 for the "months" and "years" view or you may get unexpected results! * * Text and HTML can also be used, and will be rendered as it is, as in the example below (the library is smart * enough to not replace special characters when used in words or HTML tags): * * * header_captions(array( * 'days' => 'Departure:
F, Y', * 'months' => 'Departure:
Y', * 'years' => 'Departure:
Y1 - Y2' * )); *
* * Default is * * * header_captions(array( * 'days' => 'F, Y', * 'months' => 'Y', * 'years' => 'Y1 - Y2' * )); * * * @param $captions An associative array containing captions in the datepicker's header, for the 3 possible * views: days, months, years. * * @return void */ function header_captions($captions) { // set the date picker's attribute $this->set_attributes(array('header_captions' => $captions)); } /** * Sets the HTML to be used for the previous month/next month buttons. * * @param $navigation An array with 2 elements containing the HTML to be used for the previous month/next month * buttons. * * Default is array('«','»') * * @return void */ function header_navigation($navigation) { // set the date picker's attribute $this->set_attributes(array('header_navigation' => $navigation)); } /** * The position of the date picker's inside the element it is attached to. * * * $date = $form->add('date', 'my_date'); * * // position the date picker's icon to the left * $date->icon_position('left'); * * * @param string $position The position of the date picker's inside the element it is attached to. * * Possible values are "left" and "right". * * Default is "right" * * @since 2.9.8 * * @return void */ function icon_position($position) { // set the date picker's attribute $this->set_attributes(array('icon_position' => $position)); } /** * Sets whether the icon for opening the datepicker should be inside or outside the element. * * @param boolean $value If set to FALSE, the icon will be placed to the right of the parent element, while * if set to TRUE it will be placed to the right of the parent element, but *inside* the * element itself. * * Default is TRUE. * * @return void */ function inside($value) { // set the date picker's attribute // ("inside" is a "reserved" attribute so we'll pick something else) $this->set_attributes(array('inside_icon' => $value)); } /** * Sets the offset, in pixels (x, y), to shift the date picker’s position relative to the top-left of the icon that * toggles the date picker. * * @param array $value An array indicating the offset, in pixels (x, y), to shift the date picker’s position * relative to the top-left of the icon that toggles the date picker. * * Default is array(5, -5). * * @return void */ function offset($value) { // set the date picker's attribute $this->set_attributes(array('offset' => $value)); } /** * Sets whether the date picker should be shown *only* when clicking the icon. * * @param array $value An array indicating the offset, in pixels (x, y), to shift the date picker’s position * relative to the top-left of the icon that toggles the date picker. * * Default is FALSE. * * @return void */ function open_icon_only($value) { // set the date picker's attribute $this->set_attributes(array('open_icon_only' => $value)); } /** * Pairs the date element with another date element from the page, so that the other date element will use the current * date element’s value as starting date. * * * // let's assume this will be the starting date * $date1 = $form->add('date', 'starting_date'); * * // dates are selectable in the future, starting with today * $date1->direction(true); * * // indicate another date element that will use this * // element's value as starting date * $date1->pair('ending_date'); * * // the other date element * $date2 = $form->add('date', 'ending_date'); * * // start one day after the reference date * // (that is, one day after whaterver is selected in the first element) * $date2->direction(1); * * * @param string $value The ID of another "date" element which will use the current date element's value as * starting date. * * Note that the rules set in the "direction" property will still apply, only that the * reference date will not be the current system date but the value selected in the * current date picker. * * Default is FALSE (not paired with another date picker) * * @return void */ function pair($value) { // set the date picker's attribute $this->set_attributes(array('pair' => '$(\'#' . $value . '\')')); } /** * Sets whether the element the calendar is attached to should be read-only. * * @param boolean $value The setting's value * * If set to TRUE, a date can be set only through the date picker and cannot be enetered * manually. * * Default is TRUE. * * @return void */ function readonly_element($value) { // set the date picker's attribute $this->set_attributes(array('readonly_element' => $value)); } /** * Should days from previous and/or next month be selectable when visible? * * @param string $value The setting's value * * Note that if set to TRUE, the value of {@link show_other_months()} will be considered * TRUE regardless of the actual value! * * Default is TRUE. * * @since 2.9.3 * * @return void */ function select_other_months($value) { // set the date picker's attribute $this->set_attributes(array('select_other_months' => $value)); } /** * Should the "Clear date" button be visible? * * @param string $value The setting's value * * Accepted values are: * * - 0 (zero) – the button for clearing a previously selected date is shown only if a * previously selected date already exists; this means that if the input the date * picker is attached to is empty, and the user selects a date for the first time, * this button will not be visible; once the user picked a date and opens the date * picker again, this time the button will be visible. * * - TRUE will make the button visible all the time * * - FALSE will disable the button * * Default is "0" (without quotes) * * @return void */ function show_clear_date($value = 0) { // set the date picker's attribute $this->set_attributes(array('show_clear_date' => $value)); } /** * Should a calendar icon be added to the elements the plugin is attached to? * * * $date = $form->add('date', 'my_date'); * * // do not show the icon * $date->show_icon(false); * * * @param boolean $visible When set to TRUE the plugin will attach a calendar icon to the elements the plugin * is attached to. * * Default is TRUE * * @since 2.9.8 * * @return void */ function show_icon($visible) { // set the date picker's attribute $this->set_attributes(array('show_icon' => $visible)); } /** * Should days from previous and/or next month be visible? * * @param string $value The setting's value * * Default is TRUE. * * @since 2.9.3 * * @return void */ function show_other_months($value = true) { // set the date picker's attribute $this->set_attributes(array('show_other_months' => $value)); } /** * Should the "Today" button be visible? * * @param string $value The setting's value * * Setting this property to anything but a boolean FALSE will enable the button and * will use the property's value as caption for the button; setting it to FALSE will * disable the button. * * Default is "Today" * * @since 2.9.4 * * @return void */ function show_select_today($value = 'Today') { // set the date picker's attribute $this->set_attributes(array('show_select_today' => $value)); } /** * Sets whether an extra column should be shown, showing the number of each week. * * @param string $value Anything other than FALSE will enable this feature, and use the given value as column * title. For example, show_week_number: ‘Wk’ would enable this feature and have "Wk" as * the column’s title. * * Default is FALSE. * * @return void */ function show_week_number($value) { // set the date picker's attribute $this->set_attributes(array('show_week_number' => $value)); } /** * Sets a default date to start the date picker with. * * @param date $value A default date to start the date picker with, * * Must be specified in the format defined by the "format" property, or it will be * ignored! * * Note that this value is used only if there is no value in the field the date picker * is attached to! * * Default is FALSE. * * @return void */ function start_date($value) { // set the date picker's attribute $this->set_attributes(array('start_date' => $value)); } /** * Sets whether default values, in the input field the date picker is attached to, be deleted if they are not valid * according to {@link direction() direction} and/or {@link disabled_dates() disabled_dates}. * * @param boolean $value If set to TRUE, default values, in the input field the date picker is attached to, * will be deleted if they are not valid according to {@link direction() direction} * and/or {@link disabled_dates() disabled_dates}. * * Default is FALSE. * * @return void */ function strict($value) { // set the date picker's attribute $this->set_attributes(array('strict' => $value)); } /** * Sets how should the date picker start. * * @param string $view How should the date picker start. * * Valid values are "days", "months" and "years". * * Note that the date picker is always cycling days-months-years when clicking in the * date picker's header, and years-months-days when selecting dates (unless one or more * of the views are missing due to the date's format) * * Also note that the value of the "view" property may be overridden if the date's format * requires so! (i.e. "days" for the "view" property makes no sense if the date format * doesn't allow the selection of days) * * Default is "days". * * @return void */ function view($view) { // set the date picker's attribute $this->set_attributes(array('view' => $view)); } /** * Sets the days of the week that are to be considered as "weekend days". * * @param array $days An array of days of the week that are to be considered as "weekend days". * * Valid values are 0 to 6 (Sunday to Saturday). * * Default is array(0,6) (Saturday and Sunday). * * @return void */ function weekend_days($days) { // set the date picker's attribute $this->set_attributes(array('weekend_days' => $days)); } /** * Should day numbers < 10 be padded with zero? * * @param boolean $state When set to TRUE, day numbers < 10 will be prefixed with 0. * * Default is FALSE. * * @return void */ function zero_pad($state) { // set the date picker's attribute $this->set_attributes(array('zero_pad' => $state)); } /** * Generates the control's HTML code. * * This method is automatically called by the {@link Zebra_Form::render() render()} method! * * @return string The control's HTML code */ function toHTML() { // all date controls must have the "date" rule set or we trigger an error if (!isset($this->rules['date'])) _zebra_form_show_error('The control named "' . $this->attributes['name'] . '" in form "' . $this->form_properties['name'] . '" must have the "date" rule set', E_USER_ERROR); return '
_render_attributes() . ($this->form_properties['doctype'] == 'xhtml' ? '/' : '') . '>
'; } /** * Initializes the datepicker's data so calculations for disabled dates can be done. * * Returns an array with two values: the first and the last selectable dates, as UNIX timestamps. * * If a value does not apply (i.e. no starting or no ending date), the value will be "0". * * @return array Returns an array with two values: the first and the last selectable dates, * as UNIX timestamps. * * @access private */ function _init() { // do these calculations only once if (!isset($this->limits)) { // get current system date $system_date = time(); // check if the calendar has any restrictions // calendar is future-only, starting today // it means we have a starting date (the current system date), but no ending date if ($this->attributes['direction'] === true) $this->first_selectable_date = $system_date; // calendar is past only, ending today // it means we have an ending date (the reference date), but no starting date else if ($this->attributes['direction'] === false) $this->last_selectable_date = $system_date; else if ( // if direction is not given as an array and the value is an integer > 0 (!is_array($this->attributes['direction']) && (int)($this->attributes['direction']) > 0) || // or direction is given as an array (is_array($this->attributes['direction']) && ( // and first entry is boolean TRUE $this->attributes['direction'][0] === true || // or a valid date ($tmp_start_date = $this->_is_format_valid($this->attributes['direction'][0])) || // or an integer > 0 (is_numeric($this->attributes['direction'][0]) && $this->attributes['direction'][0] > 0) ) && ( // and second entry is boolean FALSE $this->attributes['direction'][1] === false || // or a valid date ($tmp_end_date = $this->_is_format_valid($this->attributes['direction'][1])) || // or integer >= 0 (is_numeric($this->attributes['direction'][1]) && $this->attributes['direction'][1] >= 0) )) ) { // if an exact starting date was given, use that as a starting date if (isset($tmp_start_date)) $this->first_selectable_date = $tmp_start_date; // otherwise else // figure out the starting date $this->first_selectable_date = strtotime('+' . (!is_array($this->attributes['direction']) ? (int)($this->attributes['direction']) : (int)($this->attributes['direction'][0] === true ? 0 : $this->attributes['direction'][0])) . ' day', $system_date); // if an exact ending date was given and the date is after the starting date, use that as a ending date if (isset($tmp_end_date) && $tmp_end_date >= $this->first_selectable_date) $this->last_selectable_date = $tmp_end_date; // if have information about the ending date else if (!isset($tmp_end_date) && $this->attributes['direction'][1] !== false && is_array($this->attributes['direction'])) // figure out the ending date $this->last_selectable_date = strtotime('+' . (int)($this->attributes['direction'][1]) . ' day', $system_date); } else if ( // if direction is not given as an array and the value is an integer < 0 (!is_array($this->attributes['direction']) && is_numeric($this->attributes['direction']) && $this->attributes['direction'] < 0) || // or direction is given as an array (is_array($this->attributes['direction']) && ( // and first entry is boolean FALSE $this->attributes['direction'][0] === false || // or an integer < 0 (is_numeric($this->attributes['direction'][0]) && $this->attributes['direction'][0] < 0) ) && ( // and second entry is integer >= 0 (is_numeric($this->attributes['direction'][1]) && $this->attributes['direction'][1] >= 0) || // or a valid date ($tmp_start_date = $this->_is_format_valid($this->attributes['direction'][1])) )) ) { // figure out the ending date $this->last_selectable_date = strtotime('+' . (!is_array($this->attributes['direction']) ? (int)($this->attributes['direction']) : (int)($this->attributes['direction'][0] === false ? 0 : $this->attributes['direction'][0])) . ' day', $system_date); // if an exact starting date was given, and the date is before the ending date, use that as a starting date if (isset($tmp_start_date) && $tmp_start_date < $this->last_selectable_date) $this->first_selectable_date = $tmp_start_date; // if have information about the starting date else if (!isset($tmp_start_date) && is_array($this->attributes['direction'])) // figure out the staring date $this->first_selectable_date = strtotime('-' . (int)($this->attributes['direction'][1]) . ' day'); } // if a first selectable date exists if (isset($this->first_selectable_date)) { // extract the date parts $first_selectable_year = date('Y', $this->first_selectable_date); $first_selectable_month = date('m', $this->first_selectable_date); $first_selectable_day = date('d', $this->first_selectable_date); } // if a last selectable date exists if (isset($this->last_selectable_date)) { // extract the date parts $last_selectable_year = date('Y', $this->last_selectable_date); $last_selectable_month = date('m', $this->last_selectable_date); $last_selectable_day = date('d', $this->last_selectable_date); } // if a first selectable date exists but is disabled, find the actual first selectable date if (isset($this->first_selectable_date) && $this->_is_disabled($first_selectable_year, $first_selectable_month, $first_selectable_day)) { // loop until we find the first selectable year while ($this->_is_disabled($first_selectable_year)) { // if calendar is past-only, decrement the year if ($this->first_selectable_date < 0 || $this->first_selectable_date === false) $first_selectable_year--; // otherwise, increment the year else $first_selectable_year++; // because we've changed years, reset the month to January $first_selectable_month = 1; } // loop until we find the first selectable month while ($this->_is_disabled($first_selectable_year, $first_selectable_month)) { // if calendar is past-only, decrement the month if ($this->first_selectable_date < 0 || $this->first_selectable_date === false) $first_selectable_month--; // otherwise, increment the month else $first_selectable_month++; // if we moved to a following year if ($first_selectable_month > 12) { // increment the year $first_selectable_year++; // reset the month to January $first_selectable_month = 1; // if we moved to a previous year } else if ($first_selectable_month < 1) { // decrement the year $first_selectable_year--; // reset the month to January $first_selectable_month = 1; } // because we've changed months, reset the day to the first day of the month $first_selectable_day = 1; } // loop until we find the first selectable day while ($this->_is_disabled($first_selectable_year, $first_selectable_month, $first_selectable_day)) // if calendar is past-only, decrement the day if ($this->first_selectable_date < 0 || $this->first_selectable_date === false) $first_selectable_day--; // otherwise, increment the day else $first_selectable_day++; // use mktime() to normalize the date // for example, 2011 05 33 will be transformed to 2011 06 02 $this->first_selectable_date = mktime(12, 0, 0, $first_selectable_month, $first_selectable_day, $first_selectable_year); // re-extract date parts from the normalized date // as we use them in the current loop // extract the date parts $first_selectable_year = date('Y', $this->first_selectable_date); $first_selectable_month = date('m', $this->first_selectable_date); $first_selectable_day = date('d', $this->first_selectable_date); } // save first and last selectable dates, as UNIX timestamps (or "0" if does not apply) $this->limits = array(isset($this->first_selectable_date) ? $this->first_selectable_date : 0, isset($this->last_selectable_date) ? $this->last_selectable_date : 0); } // return first and last selectable dates, as UNIX timestamps (or "0" if does not apply) return $this->limits; } /** * Checks if the enetered value is a valid date in the right format. * * @return mixed Returns the UNIX timestamp of the checked date, if the date has the correct format, * or FALSE otherwise. * * @access private */ function _is_format_valid($date) { // the format we expect the date to be // escape characters that would make sense as regular expression $format = preg_quote($this->attributes['format']); // parse the format and extract the characters that define the format // (note that we're also capturing the offsets) preg_match_all('/[dDjlNSwFmMnYyGHghaAisU]{1}/', $format, $matches, PREG_OFFSET_CAPTURE); $regexp = array(); // iterate through the found characters // and create the regular expression that we will use to see if the entered date is ok foreach ($matches[0] as $match) { switch ($match[0]) { // day of the month, 2 digits with leading zeros, 01 to 31 case 'd': $regexp[] = '0[1-9]|[12][0-9]|3[01]'; break; // a textual representation of a day, three letters, mon through sun case 'D': $regexp[] = '[a-z]{3}'; break; // day of the month without leading zeros, 1 to 31 case 'j': $regexp[] = '[1-9]|[12][0-9]|3[01]'; break; // a full textual representation of the day of the week, sunday through saturday case 'l': $regexp[] = '[a-z]+'; break; // ISO-8601 numeric representation of the day of the week (added in PHP 5.1.0), 1 (for Monday) through 7 (for Sunday) case 'N': $regexp[] = '[1-7]'; break; // english ordinal suffix for the day of the month, 2 characters: st, nd, rd or th. works well with j case 'S': $regexp[] = 'st|nd|rd|th'; break; // numeric representation of the day of the week, 0 (for sunday) through 6 (for saturday) case 'w': $regexp[] = '[0-6]'; break; // a full textual representation of a month, such as january or march case 'F': $regexp[] = '[a-z]+'; break; // numeric representation of a month, with leading zeros, 01 through 12 case 'm': $regexp[] = '0[1-9]|1[012]+'; break; // a short textual representation of a month, three letters, jan through dec case 'M': $regexp[] = '[a-z]{3}'; break; // numeric representation of a month, without leading zeros, 1 through 12 case 'n': $regexp[] = '[1-9]|1[012]'; break; // a full numeric representation of a year, 4 digits examples: 1999 or 2003 case 'Y': $regexp[] = '[0-9]{4}'; break; // a two digit representation of a year examples: 99 or 03 case 'y': $regexp[] = '[0-9]{2}'; break; // 24-hour format of an hour without leading zeros, 0 through 23 case 'G': $regexp[] = '[0-9]|1[0-9]|2[0-3]'; break; // 24-hour format of an hour with leading zeros, 00 through 23 case 'H': $regexp[] = '0[0-9]|1[0-9]|2[0-3]'; break; // 12-hour format of an hour without leading zeros, 1 through 12 case 'g': $regexp[] = '[0-9]|1[0-2]'; break; // 12-hour format of an hour with leading zeros, 01 through 12 case 'h': $regexp[] = '0[0-9]|1[0-2]'; break; // lowercase ante meridiem and post meridiem am or pm case 'a': case 'A': $regexp[] = '(am|pm)'; break; // minutes with leading zeros, 00 to 59 case 'i': $regexp[] = '[0-5][0-9]'; break; // seconds, with leading zeros 00 through 59 case 's': $regexp[] = '[0-5][0-9]'; break; } } // if format is defined if (!empty($regexp)) { // we will replace every format-related character in the format expression with // the appropriate regular expression in order to see that valid data was entered // as required by the character // we are replacing from finish to start so that we don't mess up the offsets // therefore, we need to reverse the array first $matches[0] = array_reverse($matches[0]); // how many characters to replace $chars = count($matches[0]); // iterate through the characters foreach ($matches[0] as $index => $char) // and replace them with the appropriate regular expression $format = substr_replace($format, '(' . $regexp[$chars - $index - 1] . ')', $matches[0][$index][1], 1); // the final regular expression to math the date against $format = '/^' . str_replace('/', '\/', $format) . '$/i'; // if entered value seems to be ok if (preg_match($format, $date, $segments)) { $original_day = $original_month = $original_year = 0; $original_hour = $original_minute = $original_second = $original_format = -1; // english names for days and months $english_days = array('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'); $english_months = array('January','February','March','April','May','June','July','August','September','October','November','December'); // reverse the characters in the format (remember that we reversed them above) $matches[0] = array_reverse($matches[0]); $valid = true; // iterate through the characters in the format // to see if years, months, days, hours, minutes and seconds are correct // i.e. if for month we entered "abc" it would pass our regular expression but // now we will check if the three letter text is an actual month foreach ($matches[0] as $index => $match) { switch ($match[0]) { // numeric representation of a month, with leading zeros, 01 through 12 case 'm': // numeric representation of a month, without leading zeros, 1 through 12 case 'n': $original_month = (int)($segments[$index + 1] - 1); break; // day of the month, 2 digits with leading zeros, 01 to 31 case 'd': // day of the month without leading zeros, 1 to 31 case 'j': $original_day = (int)($segments[$index + 1]); break; // a textual representation of a day, three letters, mon through sun case 'D': // a full textual representation of the day of the week, sunday through saturday case 'l': // a full textual representation of a month, such as january or march case 'F': // a short textual representation of a month, three letters, jan through dec case 'M': // by default, we assume that the text is invalid $valid = false; // iterate through the values in the language file foreach ($this->form_properties['language'][($match[0] == 'F' || $match[0] == 'M' ? 'months' : 'days')] as $key => $value) { // if value matches the value from the language file if (strtolower($segments[$index + 1]) == strtolower(substr($value, 0, ($match[0] == 'D' || $match[0] == 'M' ? 3 : strlen($value))))) { // replace with the english value // this is because later on we'll run strtotime of the entered value and strtotime parses english dates switch ($match[0]) { case 'D': $segments[$index + 1] = substr($english_days[$key], 0, 3); break; case 'l': $segments[$index + 1] = $english_days[$key]; break; case 'F': $segments[$index + 1] = $english_months[$key]; $original_month = $key; break; case 'M': $segments[$index + 1] = substr($english_months[$key], 0, 3); $original_month = $key; break; } // flag the value as valid $valid = true; // don't look further break; } } // if an invalid was found don't look any further if (!$valid) break 2; break; // a full numeric representation of a year, 4 digits examples: 1999 or 2003 case 'Y': $original_year = (int)($segments[$index + 1]); break; // a two digit representation of a year examples: 99 or 03 case 'y': $original_year = (int)('19' . $segments[$index + 1]); break; // 24-hour format of an hour without leading zeros, 0 through 23 case 'G': // 24-hour format of an hour with leading zeros, 00 through 23 case 'H': // 12-hour format of an hour without leading zeros, 1 through 12 case 'g': // 12-hour format of an hour with leading zeros, 01 through 12 case 'h': $original_hour = (int)($segments[$index + 1]); break; // lowercase ante meridiem and post meridiem am or pm case 'a': case 'A': $original_format = $segments[$index + 1]; break; // minutes with leading zeros, 00 to 59 case 'i': $original_minute = (int)($segments[$index + 1]); break; // seconds, with leading zeros 00 through 59 case 's': $original_second = (int)($segments[$index + 1]); break; } } // if entered value seems valid if ($valid) { // if date format does not include day, make day = 1 if ($original_day == 0) $original_day = 1; // if date format does not include month, make month = 0 (January) if ($original_month == 0) $original_month = 0; // if date format does not include year, use the current year if ($original_year == 0) $original_year = date('Y'); // if date is still valid after we process it with strtotime // (we do this because, so far, a date like "Feb 31 2010" would be valid // but strtotime would turn that to "Mar 03 2010") if ( $english_months[$original_month] . ' ' . str_pad($original_day, 2, '0', STR_PAD_LEFT) . ', ' . $original_year == date('F d, Y', strtotime($english_months[$original_month] . ' ' . $original_day . ', ' . $original_year)) ) { // make sure we also return the date as YYYY-MM-DD so that it can be // easily used with a database or with PHP's strtotime function $this->attributes['date'] = $original_year . '-' . str_pad($original_month + 1, 2, '0', STR_PAD_LEFT) . '-' . str_pad($original_day, 2, '0', STR_PAD_LEFT); return strtotime($original_year . '-' . ($original_month + 1) . '-' . $original_day); } } } } // if script gets this far, return FALSE as something must've been wrong return false; } /** * Checks if, according to the restrictions of the calendar and/or the values defined by the "disabled_dates" * property, a day, a month or a year needs to be disabled. * * @param integer $year The year to check * @param integer $month The month to check * @param integer $day The day to check * * @return boolean Returns TRUE if the given value is not disabled or FALSE otherwise * * @access private */ function _is_disabled($year, $month = '', $day = '') { // parse the rules for disabling dates and turn them into arrays of arrays if (!isset($this->disabled_dates)) { // array that will hold the rules for disabling dates $this->disabled_dates = array(); // if disabled dates is an array and is not empty if (is_array($this->attributes['disabled_dates']) && !empty($this->attributes['disabled_dates'])) // iterate through the rules for disabling dates foreach ($this->attributes['disabled_dates'] as $value) { // split the values in rule by white space $rules = explode(' ', $value); // there can be a maximum of 4 rules (days, months, years and, optionally, day of the week) for ($i = 0; $i < 4; $i++) { // if one of the values is not available // replace it with a * (wildcard) if (!isset($rules[$i])) $rules[$i] = '*'; // if rule contains a comma, create a new array by splitting the rule by commas // if there are no commas create an array containing the rule's string $rules[$i] = (strpos($rules[$i], ',') !== false ? explode(',', $rules[$i]) : (array)$rules[$i]); // iterate through the items in the rule for ($j = 0; $j < count($rules[$i]); $j++) // if item contains a dash (defining a range) if (strpos($rules[$i][$j], '-') !== false) { // get the lower and upper limits of the range // if range is valid if (preg_match('/^([0-9]+)\-([0-9]+)/', $rules[$i][$j], $limits) > 0) { // remove the range indicator array_splice($rules[$i], $j, 1); // iterate through the range for ($k = $limits[1]; $k <= $limits[2]; $k++) // if value is not already among the values of the rule // add it to the rule if (!in_array($k, $rules[$i])) $rules[$i][] = (int)$k; } // make sure to convert things like "01" to "1" } elseif ($rules[$i][$j] != '*') $rules[$i][$j] = (int)$rules[$i][$j]; } // add to the list of processed rules $this->disabled_dates[] = $rules; } } // if calendar has direction restrictions if (!(!is_array($this->attributes['direction']) && (int)($this->attributes['direction']) === 0)) { // if a first selectable date exists if (isset($this->first_selectable_date)) { // extract the date parts $first_selectable_year = date('Y', $this->first_selectable_date); $first_selectable_month = date('m', $this->first_selectable_date); $first_selectable_day = date('d', $this->first_selectable_date); } // if a last selectable date exists if (isset($this->last_selectable_date)) { // extract the date parts $last_selectable_year = date('Y', $this->last_selectable_date); $last_selectable_month = date('m', $this->last_selectable_date); $last_selectable_day = date('d', $this->last_selectable_date); } // normalize and merge arguments then transform the result to an integer $now = $year . ($month != '' ? str_pad($month, 2, '0', STR_PAD_LEFT) : '') . ($day != '' ? str_pad($day, 2, '0', STR_PAD_LEFT) : ''); // if we're checking days if (strlen($now) == 8 && ( // day is before the first selectable date (isset($this->first_selectable_date) && $now < $first_selectable_year . $first_selectable_month . $first_selectable_day) || // or day is after the last selectable date (isset($this->last_selectable_date) && $now > $last_selectable_year . $last_selectable_month . $last_selectable_day) // day needs to be disabled )) return true; // if we're checking months else if (strlen($now) == 6 && ( // month is before the first selectable month (isset($this->first_selectable_date) && $now < $first_selectable_year . $first_selectable_month) || // or month is after the last selectable month (isset($this->last_selectable_date) && $now > $last_selectable_year . $last_selectable_month) // month needs to be disabled )) return true; // if we're checking years else if (strlen($now) == 4 && ( // year is before the first selectable year (isset($this->first_selectable_date) && $now < $first_selectable_year) || // or year is after the last selectable year (isset($this->last_selectable_date) && $now > $last_selectable_year) // year needs to be disabled )) return true; } // if there are rules for disabling dates if (isset($this->disabled_dates)) { // by default, we assume the day/month/year is not to be disabled $disabled = false; // iterate through the rules for disabling dates foreach ($this->disabled_dates as $rule) { // if the date is to be disabled, don't look any further if ($disabled) return; // if the rules apply for the current year if (in_array($year, $rule[2]) || in_array('*', $rule[2], true)) // if the rules apply for the current month if (($month != '' && in_array($month, $rule[1])) || in_array('*', $rule[1], true)) // if the rules apply for the current day if (($day != '' && in_array($day, $rule[0])) || in_array('*', $rule[0], true)) { // if day is to be disabled whatever the day // don't look any further if (in_array('*', $rule[3], true)) return ($disabled = true); // get the weekday $weekday = date('w', mktime(12, 0, 0, $month, $day, $year)); // if weekday is to be disabled // don't look any further if (in_array($weekday, $rule[3])) return ($disabled = true); } } // if the day/month/year needs to be disabled if ($disabled) return true; } // if script gets this far it means that the day/month/year doesn't need to be disabled return false; } } ?>