diff --git a/packages/core/src/components/datetime/datetime-util.ts b/packages/core/src/components/datetime/datetime-util.ts index 6a18f83e0e..07b0e2cb34 100644 --- a/packages/core/src/components/datetime/datetime-util.ts +++ b/packages/core/src/components/datetime/datetime-util.ts @@ -1,3 +1,475 @@ +import { isArray, isBlank, isString } from '../../utils/helpers'; + + +export function renderDateTime(template: string, value: DateTimeData, locale: LocaleData) { + if (isBlank(value)) { + return ''; + } + + let tokens: string[] = []; + let hasText = false; + FORMAT_KEYS.forEach((format, index) => { + if (template.indexOf(format.f) > -1) { + var token = '{' + index + '}'; + var text = renderTextFormat(format.f, (value)[format.k], value, locale); + + if (!hasText && text && (value)[format.k]) { + hasText = true; + } + + tokens.push(token, text); + + template = template.replace(format.f, token); + } + }); + + if (!hasText) { + return ''; + } + + for (var i = 0; i < tokens.length; i += 2) { + template = template.replace(tokens[i], tokens[i + 1]); + } + + return template; +} + + +export function renderTextFormat(format: string, value: any, date: DateTimeData, locale: LocaleData): string { + + if (format === FORMAT_DDDD || format === FORMAT_DDD) { + try { + value = (new Date(date.year, date.month - 1, date.day)).getDay(); + + if (format === FORMAT_DDDD) { + return (locale.dayNames ? locale.dayNames : DAY_NAMES)[value]; + } + + return (locale.dayShortNames ? locale.dayShortNames : DAY_SHORT_NAMES)[value]; + + } catch (e) {} + + return ''; + } + + if (format === FORMAT_A) { + return date ? date.hour < 12 ? 'AM' : 'PM' : value ? value.toUpperCase() : ''; + } + + if (format === FORMAT_a) { + return date ? date.hour < 12 ? 'am' : 'pm' : value ? value : ''; + } + + if (isBlank(value)) { + return ''; + } + + if (format === FORMAT_YY || format === FORMAT_MM || + format === FORMAT_DD || format === FORMAT_HH || + format === FORMAT_mm || format === FORMAT_ss) { + return twoDigit(value); + } + + if (format === FORMAT_YYYY) { + return fourDigit(value); + } + + if (format === FORMAT_MMMM) { + return (locale.monthNames ? locale.monthNames : MONTH_NAMES)[value - 1]; + } + + if (format === FORMAT_MMM) { + return (locale.monthShortNames ? locale.monthShortNames : MONTH_SHORT_NAMES)[value - 1]; + } + + if (format === FORMAT_hh || format === FORMAT_h) { + if (value === 0) { + return '12'; + } + if (value > 12) { + value -= 12; + } + if (format === FORMAT_hh && value < 10) { + return ('0' + value); + } + } + + return value.toString(); +} + + +export function dateValueRange(format: string, min: DateTimeData, max: DateTimeData): any[] { + let opts: any[] = []; + let i: number; + + if (format === FORMAT_YYYY || format === FORMAT_YY) { + // year + i = max.year; + while (i >= min.year) { + opts.push(i--); + } + + } else if (format === FORMAT_MMMM || format === FORMAT_MMM || + format === FORMAT_MM || format === FORMAT_M || + format === FORMAT_hh || format === FORMAT_h) { + // month or 12-hour + for (i = 1; i < 13; i++) { + opts.push(i); + } + + } else if (format === FORMAT_DDDD || format === FORMAT_DDD || + format === FORMAT_DD || format === FORMAT_D) { + // day + for (i = 1; i < 32; i++) { + opts.push(i); + } + + } else if (format === FORMAT_HH || format === FORMAT_H) { + // 24-hour + for (i = 0; i < 24; i++) { + opts.push(i); + } + + } else if (format === FORMAT_mm || format === FORMAT_m) { + // minutes + for (i = 0; i < 60; i++) { + opts.push(i); + } + + } else if (format === FORMAT_ss || format === FORMAT_s) { + // seconds + for (i = 0; i < 60; i++) { + opts.push(i); + } + + } else if (format === FORMAT_A || format === FORMAT_a) { + // AM/PM + opts.push('am', 'pm'); + } + + return opts; +} + +export function dateSortValue(year: number, month: number, day: number, hour: number = 0, minute: number = 0): number { + return parseInt(`1${fourDigit(year)}${twoDigit(month)}${twoDigit(day)}${twoDigit(hour)}${twoDigit(minute)}`, 10); +} + +export function dateDataSortValue(data: DateTimeData): number { + if (data) { + return dateSortValue(data.year, data.month, data.day, data.hour, data.minute); + } + return -1; +} + +export function daysInMonth(month: number, year: number): number { + return (month === 4 || month === 6 || month === 9 || month === 11) ? 30 : (month === 2) ? isLeapYear(year) ? 29 : 28 : 31; +} + +export function isLeapYear(year: number): boolean { + return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); +} + + +const ISO_8601_REGEXP = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/; +const TIME_REGEXP = /^((\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/; + +export function parseDate(val: any): DateTimeData { + // manually parse IS0 cuz Date.parse cannot be trusted + // ISO 8601 format: 1994-12-15T13:47:20Z + let parse: any[]; + + if (val && val !== '') { + // try parsing for just time first, HH:MM + parse = TIME_REGEXP.exec(val); + if (parse) { + // adjust the array so it fits nicely with the datetime parse + parse.unshift(undefined, undefined); + parse[2] = parse[3] = undefined; + + } else { + // try parsing for full ISO datetime + parse = ISO_8601_REGEXP.exec(val); + } + } + + if (isBlank(parse)) { + // wasn't able to parse the ISO datetime + return null; + } + + // ensure all the parse values exist with at least 0 + for (var i = 1; i < 8; i++) { + parse[i] = (parse[i] !== undefined ? parseInt(parse[i], 10) : null); + } + + var tzOffset = 0; + if (parse[9] && parse[10]) { + // hours + tzOffset = parseInt(parse[10], 10) * 60; + if (parse[11]) { + // minutes + tzOffset += parseInt(parse[11], 10); + } + if (parse[9] === '-') { + // + or - + tzOffset *= -1; + } + } + + return { + year: parse[1], + month: parse[2], + day: parse[3], + hour: parse[4], + minute: parse[5], + second: parse[6], + millisecond: parse[7], + tzOffset: tzOffset, + }; +} + + +export function updateDate(existingData: DateTimeData, newData: any): boolean { + if (newData && newData !== '') { + + if (isString(newData)) { + // new date is a string, and hopefully in the ISO format + // convert it to our DateTimeData if a valid ISO + newData = parseDate(newData); + if (newData) { + // successfully parsed the ISO string to our DateTimeData + Object.assign(existingData, newData); + return true; + } + + } else if ((newData.year || newData.hour || newData.month || newData.day || newData.minute || newData.second)) { + // newData is from of a datetime picker's selected values + // update the existing DateTimeData data with the new values + + // do some magic for 12-hour values + if (newData.ampm && newData.hour) { + if (newData.ampm.value === 'pm') { + newData.hour.value = (newData.hour.value === 12 ? 12 : newData.hour.value + 12); + + } else { + newData.hour.value = (newData.hour.value === 12 ? 0 : newData.hour.value); + } + } + + // merge new values from the picker's selection + // to the existing DateTimeData values + for (var k in newData) { + (existingData)[k] = newData[k].value; + } + + return true; + } + + // eww, invalid data + console.warn(`Error parsing date: "${newData}". Please provide a valid ISO 8601 datetime format: https://www.w3.org/TR/NOTE-datetime`); + + } else { + // blank data, clear everything out + for (var k in existingData) { + delete (existingData)[k]; + } + } + return false; +} + + +export function parseTemplate(template: string): string[] { + const formats: string[] = []; + + template = template.replace(/[^\w\s]/gi, ' '); + + FORMAT_KEYS.forEach(format => { + if (format.f.length > 1 && template.indexOf(format.f) > -1 && template.indexOf(format.f + format.f.charAt(0)) < 0) { + template = template.replace(format.f, ' ' + format.f + ' '); + } + }); + + const words = template.split(' ').filter(w => w.length > 0); + words.forEach((word, i) => { + FORMAT_KEYS.forEach(format => { + if (word === format.f) { + if (word === FORMAT_A || word === FORMAT_a) { + // this format is an am/pm format, so it's an "a" or "A" + if ((formats.indexOf(FORMAT_h) < 0 && formats.indexOf(FORMAT_hh) < 0) || + VALID_AMPM_PREFIX.indexOf(words[i - 1]) === -1) { + // template does not already have a 12-hour format + // or this am/pm format doesn't have a hour, minute, or second format immediately before it + // so do not treat this word "a" or "A" as the am/pm format + return; + } + } + formats.push(word); + } + }); + }); + + return formats; +} + + +export function getValueFromFormat(date: DateTimeData, format: string) { + if (format === FORMAT_A || format === FORMAT_a) { + return (date.hour < 12 ? 'am' : 'pm'); + } + if (format === FORMAT_hh || format === FORMAT_h) { + return (date.hour > 12 ? date.hour - 12 : date.hour); + } + return (date)[convertFormatToKey(format)]; +} + + +export function convertFormatToKey(format: string): string { + for (var k in FORMAT_KEYS) { + if (FORMAT_KEYS[k].f === format) { + return FORMAT_KEYS[k].k; + } + } + return null; +} + + +export function convertDataToISO(data: DateTimeData): string { + // https://www.w3.org/TR/NOTE-datetime + let rtn = ''; + + if (data) { + if (data.year) { + // YYYY + rtn = fourDigit(data.year); + + if (data.month) { + // YYYY-MM + rtn += '-' + twoDigit(data.month); + + if (data.day) { + // YYYY-MM-DD + rtn += '-' + twoDigit(data.day); + + if (data.hour) { + // YYYY-MM-DDTHH:mm:SS + rtn += `T${twoDigit(data.hour)}:${twoDigit(data.minute)}:${twoDigit(data.second)}`; + + if (data.millisecond > 0) { + // YYYY-MM-DDTHH:mm:SS.SSS + rtn += '.' + threeDigit(data.millisecond); + } + + if (isBlank(data.tzOffset) || data.tzOffset === 0) { + // YYYY-MM-DDTHH:mm:SSZ + rtn += 'Z'; + + } else { + // YYYY-MM-DDTHH:mm:SS+/-HH:mm + rtn += (data.tzOffset > 0 ? '+' : '-') + twoDigit(Math.floor(data.tzOffset / 60)) + ':' + twoDigit(data.tzOffset % 60); + } + } + } + } + + } else if (data.hour) { + // HH:mm + rtn = twoDigit(data.hour) + ':' + twoDigit(data.minute); + + if (data.second) { + // HH:mm:SS + rtn += ':' + twoDigit(data.second); + + if (data.millisecond) { + // HH:mm:SS.SSS + rtn += '.' + threeDigit(data.millisecond); + } + } + } + } + + return rtn; +} + + +/** + * Use to convert a string of comma separated strings or + * an array of strings, and clean up any user input + */ +export function convertToArrayOfStrings(input: any, type: string): string[] { + if (!input) { + return null; + } + + if (isString(input)) { + // convert the string to an array of strings + // auto remove any [] characters + input = input.replace(/\[|\]/g, '').split(','); + } + + var values: string[]; + if (isArray(input)) { + // trim up each string value + values = input.map((val: string) => val.trim()); + } + + if (!values || !values.length) { + console.warn(`Invalid "${type}Names". Must be an array of strings, or a comma separated string.`); + } + + return values; +} + + +/** + * Use to convert a string of comma separated numbers or + * an array of numbers, and clean up any user input + */ +export function convertToArrayOfNumbers(input: any, type: string): number[] { + if (isString(input)) { + // convert the string to an array of strings + // auto remove any whitespace and [] characters + input = input.replace(/\[|\]|\s/g, '').split(','); + } + + let values: number[]; + if (isArray(input)) { + // ensure each value is an actual number in the returned array + values = input + .map((num: any) => parseInt(num, 10)) + .filter(isFinite); + } + + if (!values || !values.length) { + console.warn(`Invalid "${type}Values". Must be an array of numbers, or a comma separated string of numbers.`); + } + + return values; +} + + +function twoDigit(val: number): string { + return ('0' + (val ? Math.abs(val) : '0')).slice(-2); +} + +function threeDigit(val: number): string { + return ('00' + (val ? Math.abs(val) : '0')).slice(-3); +} + +function fourDigit(val: number): string { + return ('000' + (val ? Math.abs(val) : '0')).slice(-4); +} + + +export interface DateTimeData { + year?: number; + month?: number; + day?: number; + hour?: number; + minute?: number; + second?: number; + millisecond?: number; + tzOffset?: number; +} export interface LocaleData { @@ -6,3 +478,102 @@ export interface LocaleData { dayNames?: string[]; dayShortNames?: string[]; } + + +const FORMAT_YYYY = 'YYYY'; +const FORMAT_YY = 'YY'; +const FORMAT_MMMM = 'MMMM'; +const FORMAT_MMM = 'MMM'; +const FORMAT_MM = 'MM'; +const FORMAT_M = 'M'; +const FORMAT_DDDD = 'DDDD'; +const FORMAT_DDD = 'DDD'; +const FORMAT_DD = 'DD'; +const FORMAT_D = 'D'; +const FORMAT_HH = 'HH'; +const FORMAT_H = 'H'; +const FORMAT_hh = 'hh'; +const FORMAT_h = 'h'; +const FORMAT_mm = 'mm'; +const FORMAT_m = 'm'; +const FORMAT_ss = 'ss'; +const FORMAT_s = 's'; +const FORMAT_A = 'A'; +const FORMAT_a = 'a'; + +const FORMAT_KEYS = [ + { f: FORMAT_YYYY, k: 'year' }, + { f: FORMAT_MMMM, k: 'month' }, + { f: FORMAT_DDDD, k: 'day' }, + { f: FORMAT_MMM, k: 'month' }, + { f: FORMAT_DDD, k: 'day' }, + { f: FORMAT_YY, k: 'year' }, + { f: FORMAT_MM, k: 'month' }, + { f: FORMAT_DD, k: 'day' }, + { f: FORMAT_HH, k: 'hour' }, + { f: FORMAT_hh, k: 'hour' }, + { f: FORMAT_mm, k: 'minute' }, + { f: FORMAT_ss, k: 'second' }, + { f: FORMAT_M, k: 'month' }, + { f: FORMAT_D, k: 'day' }, + { f: FORMAT_H, k: 'hour' }, + { f: FORMAT_h, k: 'hour' }, + { f: FORMAT_m, k: 'minute' }, + { f: FORMAT_s, k: 'second' }, + { f: FORMAT_A, k: 'ampm' }, + { f: FORMAT_a, k: 'ampm' }, +]; + +const DAY_NAMES = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; + +const DAY_SHORT_NAMES = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', +]; + +const MONTH_NAMES = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + +const MONTH_SHORT_NAMES = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + +const VALID_AMPM_PREFIX = [ + FORMAT_hh, FORMAT_h, FORMAT_mm, FORMAT_m, FORMAT_ss, FORMAT_s +]; diff --git a/packages/core/src/components/datetime/datetime.tsx b/packages/core/src/components/datetime/datetime.tsx index 8ecaebb4f0..b89ac084c6 100644 --- a/packages/core/src/components/datetime/datetime.tsx +++ b/packages/core/src/components/datetime/datetime.tsx @@ -1,8 +1,10 @@ -import { Component, CssClassMap, Event, EventEmitter, Prop } from '@stencil/core'; +import { Component, CssClassMap, Event, EventEmitter, Prop, PropDidChange } from '@stencil/core'; -import { LocaleData } from './datetime-util'; +import { convertFormatToKey, convertToArrayOfNumbers, convertToArrayOfStrings, dateDataSortValue, dateSortValue, DateTimeData, dateValueRange, daysInMonth, getValueFromFormat, LocaleData, parseDate, parseTemplate, renderTextFormat, renderDateTime } from './datetime-util'; -import { isArray, isString } from '../../utils/helpers'; +import { clamp, isBlank, isObject } from '../../utils/helpers'; + +import { Picker, PickerColumn, PickerController, PickerOptions } from '../../index'; /** * @name DateTime @@ -253,10 +255,17 @@ import { isArray, isString } from '../../utils/helpers'; } }) export class DateTime { + private datetimeId: string; + private labelId: string; + private picker: Picker; + text: any; - id: string; - labelId: string; locale: LocaleData = {}; + dateTimeMin: DateTimeData = {}; + dateTimeMax: DateTimeData = {}; + dateTimeValue: DateTimeData = {}; + + @Prop({ connect: 'ion-picker-controller' }) pickerCtrl: PickerController; /** * @input {boolean} If true, the user cannot interact with this element. Defaults to `false`. @@ -271,7 +280,7 @@ export class DateTime { * datetime. For example, the minimum could just be the year, such as `1994`. * Defaults to the beginning of the year, 100 years ago from today. */ - @Prop() min: string; + @Prop({ mutable: true }) min: string; /** * @input {string} The maximum datetime allowed. Value must be a date string @@ -281,7 +290,7 @@ export class DateTime { * datetime. For example, the maximum could just be the year, such as `1994`. * Defaults to the end of this year. */ - @Prop() max: string; + @Prop({ mutable: true }) max: string; /** * @input {string} The display format of the date and time as text that shows @@ -383,10 +392,13 @@ export class DateTime { @Prop() dayShortNames: any; /** - * @input {any} Any additional options that the picker interface can accept. + * @input {PickerOptions} Any additional options that the picker interface can accept. * See the [Picker API docs](../../picker/Picker) for the picker options. */ - @Prop() pickerOptions: any = {}; + @Prop() pickerOptions: PickerOptions = { + buttons: [], + columns: [] + }; /** * @input {string} The text to display when there's no date selected yet. @@ -394,64 +406,384 @@ export class DateTime { */ @Prop() placeholder: string; + /** + * @input {string} the value of the datetime. + */ + @Prop({ mutable: true }) value: string; + + /** + * @hidden + * Update the datetime text when the value changes + */ + @PropDidChange('value') + valueChanged() { + this.updateText(); + } + /** * @output {any} Emitted when the datetime selection was cancelled. */ @Event() ionCancel: EventEmitter; - ionViewDidLoad() { + protected ionViewDidLoad() { // first see if locale names were provided in the inputs // then check to see if they're in the config // if neither were provided then it will use default English names - ['monthNames', 'monthShortNames', 'dayNames', 'dayShortNames'].forEach(type => { + this.locale = { // this.locale[type] = convertToArrayOfStrings((this[type] ? this[type] : this.config.get(type), type); - console.log('this[type]', this[type]); - this.locale[type] = convertToArrayOfStrings(this[type], type); - }); + monthNames: convertToArrayOfStrings(this.monthNames, 'monthNames'), + monthShortNames: convertToArrayOfStrings(this.monthShortNames, 'monthShortNames'), + dayNames: convertToArrayOfStrings(this.dayNames, 'dayNames'), + dayShortNames: convertToArrayOfStrings(this.dayShortNames, 'dayShortNames') + }; - // this.initialize(); + this.updateText(); } + buildPicker(pickerOptions: PickerOptions) { + console.debug('Build Datetime: Picker with', pickerOptions); - - - open() { - // TODO check this.isFocus() || this.disabled - if (this.disabled) { - return; + // If the user has not passed in picker buttons, + // add a cancel and ok button to the picker + if (pickerOptions.buttons.length === 0) { + pickerOptions.buttons = [{ + text: this.cancelText, + role: 'cancel', + handler: () => this.ionCancel.emit(this) + },{ + text: this.doneText, + handler: (data: any) => this.value = data, + }]; } - console.debug('datetime, open picker'); - // // the user may have assigned some options specifically for the alert - // const pickerOptions = deepCopy(this.pickerOptions); + pickerOptions.columns = this.generateColumns(); - // // Configure picker under the hood - // const picker = this._picker = this._pickerCtrl.create(pickerOptions); - // picker.addButton({ - // text: this.cancelText, - // role: 'cancel', - // handler: () => this.ionCancel.emit(this) - // }); - // picker.addButton({ - // text: this.doneText, - // handler: (data: any) => this.value = data, - // }); + const picker = this.pickerCtrl.create(pickerOptions); // picker.ionChange.subscribe(() => { // this.validate(); // picker.refresh(); // }); - // // Update picker status before presenting - // this.generate(); - // this.validate(); - - // // Present picker - // this._fireFocus(); - // picker.present(pickerOptions); // picker.onDidDismiss(() => { // this._fireBlur(); // }); + + console.debug('Built Datetime: Picker with', pickerOptions); + return picker; + } + + open() { + console.debug('datetime, open picker'); + + const pickerOptions = {...this.pickerOptions}; + + // TODO check this.isFocus() || this.disabled + if (this.disabled) { + return; + } + + let controller: Promise; + + controller = this.buildPicker(pickerOptions); + + controller.then((component: any) => { + // Update picker status before presenting + this.picker = component; + + this.validate(); + + component.present(); + }); + } + + + /** + * @hidden + */ + generateColumns(): PickerColumn[] { + let columns: PickerColumn[] = []; + + // if a picker format wasn't provided, then fallback + // to use the display format + let template = this.pickerFormat || this.displayFormat || DEFAULT_FORMAT; + + if (template) { + // make sure we've got up to date sizing information + this.calcMinMax(); + + // does not support selecting by day name + // automaticallly remove any day name formats + template = template.replace('DDDD', '{~}').replace('DDD', '{~}'); + if (template.indexOf('D') === -1) { + // there is not a day in the template + // replace the day name with a numeric one if it exists + template = template.replace('{~}', 'D'); + } + // make sure no day name replacer is left in the string + template = template.replace(/{~}/g, ''); + + // parse apart the given template into an array of "formats" + parseTemplate(template).forEach((format: any) => { + // loop through each format in the template + // create a new picker column to build up with data + let key = convertFormatToKey(format); + let values: any[]; + + // first see if they have exact values to use for this input + if (this[key + 'Values']) { + // user provide exact values for this date part + values = convertToArrayOfNumbers(this[key + 'Values'], key); + } else { + // use the default date part values + values = dateValueRange(format, this.dateTimeMin, this.dateTimeMax); + } + + const column: PickerColumn = { + name: key, + selectedIndex: 0, + options: values.map(val => { + return { + value: val, + text: renderTextFormat(format, val, null, this.locale), + }; + }) + }; + + // cool, we've loaded up the columns with options + // preselect the option for this column + const optValue = getValueFromFormat(this.dateTimeValue, format); + const selectedIndex = column.options.findIndex(opt => opt.value === optValue); + if (selectedIndex >= 0) { + // set the select index for this column's options + column.selectedIndex = selectedIndex; + } + + // add our newly created column to the picker + columns.push(column); + }); + + // Normalize min/max + const min = this.dateTimeMin; + const max = this.dateTimeMax; + ['month', 'day', 'hour', 'minute'] + .filter(name => !columns.find(column => column.name === name)) + .forEach(name => { + min[name] = 0; + max[name] = 0; + }); + + columns = this.divyColumns(columns); + } + + return columns; + } + + /** + * @private + */ + validate() { + const today = new Date(); + const minCompareVal = dateDataSortValue(this.dateTimeMin); + const maxCompareVal = dateDataSortValue(this.dateTimeMax); + const yearCol = this.picker.getColumn('year'); + + let selectedYear: number = today.getFullYear(); + if (yearCol) { + // default to the first value if the current year doesn't exist in the options + if (!yearCol.options.find(col => col.value === today.getFullYear())) { + selectedYear = yearCol.options[0].value; + } + + var yearOpt = yearCol.options[yearCol.selectedIndex]; + if (yearOpt) { + // they have a selected year value + selectedYear = yearOpt.value; + } + } + + const selectedMonth = this.validateColumn( + 'month', 1, + minCompareVal, maxCompareVal, + [selectedYear, 0, 0, 0, 0], + [selectedYear, 12, 31, 23, 59] + ); + + const numDaysInMonth = daysInMonth(selectedMonth, selectedYear); + const selectedDay = this.validateColumn( + 'day', 2, + minCompareVal, maxCompareVal, + [selectedYear, selectedMonth, 0, 0, 0], + [selectedYear, selectedMonth, numDaysInMonth, 23, 59] + ); + + const selectedHour = this.validateColumn( + 'hour', 3, + minCompareVal, maxCompareVal, + [selectedYear, selectedMonth, selectedDay, 0, 0], + [selectedYear, selectedMonth, selectedDay, 23, 59] + ); + + this.validateColumn( + 'minute', 4, + minCompareVal, maxCompareVal, + [selectedYear, selectedMonth, selectedDay, selectedHour, 0], + [selectedYear, selectedMonth, selectedDay, selectedHour, 59] + ); + } + + + /** + * @hidden + */ + calcMinMax(now?: Date) { + const todaysYear = (now || new Date()).getFullYear(); + + if (this.yearValues) { + var years = convertToArrayOfNumbers(this.yearValues, 'year'); + if (isBlank(this.min)) { + this.min = Math.min.apply(Math, years); + } + if (isBlank(this.max)) { + this.max = Math.max.apply(Math, years); + } + } else { + if (isBlank(this.min)) { + this.min = (todaysYear - 100).toString(); + } + if (isBlank(this.max)) { + this.max = todaysYear.toString(); + } + } + const min = this.dateTimeMin = parseDate(this.min); + const max = this.dateTimeMax = parseDate(this.max); + + min.year = min.year || todaysYear; + max.year = max.year || todaysYear; + + min.month = min.month || 1; + max.month = max.month || 12; + min.day = min.day || 1; + max.day = max.day || 31; + min.hour = min.hour || 0; + max.hour = max.hour || 23; + min.minute = min.minute || 0; + max.minute = max.minute || 59; + min.second = min.second || 0; + max.second = max.second || 59; + + // Ensure min/max constraits + if (min.year > max.year) { + console.error('min.year > max.year'); + min.year = max.year - 100; + } + if (min.year === max.year) { + if (min.month > max.month) { + console.error('min.month > max.month'); + min.month = 1; + } else if (min.month === max.month && min.day > max.day) { + console.error('min.day > max.day'); + min.day = 1; + } + } + } + + + /** + * @hidden + */ + validateColumn(name: string, index: number, min: number, max: number, lowerBounds: number[], upperBounds: number[]): number { + const column = this.picker.getColumn(name); + if (!column) { + return 0; + } + + const lb = lowerBounds.slice(); + const ub = upperBounds.slice(); + const options = column.options; + let indexMin = options.length - 1; + let indexMax = 0; + + for (var i = 0; i < options.length; i++) { + var opt = options[i]; + var value = opt.value; + lb[index] = opt.value; + ub[index] = opt.value; + + var disabled = opt.disabled = ( + value < lowerBounds[index] || + value > upperBounds[index] || + dateSortValue(ub[0], ub[1], ub[2], ub[3], ub[4]) < min || + dateSortValue(lb[0], lb[1], lb[2], lb[3], lb[4]) > max + ); + if (!disabled) { + indexMin = Math.min(indexMin, i); + indexMax = Math.max(indexMax, i); + } + } + let selectedIndex = column.selectedIndex = clamp(indexMin, column.selectedIndex, indexMax); + opt = column.options[selectedIndex]; + if (opt) { + return opt.value; + } + return 0; + } + + + /** + * @hidden + */ + divyColumns(columns: PickerColumn[]): PickerColumn[] { + const pickerColumns = columns; + let columnsWidth: number[] = []; + let col: PickerColumn; + let width: number; + for (var i = 0; i < pickerColumns.length; i++) { + col = pickerColumns[i]; + columnsWidth.push(0); + + for (var j = 0; j < col.options.length; j++) { + width = col.options[j].text.length; + if (width > columnsWidth[i]) { + columnsWidth[i] = width; + } + } + } + + if (columnsWidth.length === 2) { + width = Math.max(columnsWidth[0], columnsWidth[1]); + pickerColumns[0].align = 'right'; + pickerColumns[1].align = 'left'; + pickerColumns[0].optionsWidth = pickerColumns[1].optionsWidth = `${width * 17}px`; + + } else if (columnsWidth.length === 3) { + width = Math.max(columnsWidth[0], columnsWidth[2]); + pickerColumns[0].align = 'right'; + pickerColumns[1].columnWidth = `${columnsWidth[1] * 17}px`; + pickerColumns[0].optionsWidth = pickerColumns[2].optionsWidth = `${width * 17}px`; + pickerColumns[2].align = 'left'; + } + + return columns; + } + + /** + * @hidden + */ + updateText() { + // create the text of the formatted data + const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT; + this.text = renderDateTime(template, this.dateTimeValue, this.locale); + } + + /** + * @hidden + */ + hasValue(): boolean { + const val = this.dateTimeValue; + return val + && isObject(val) + && Object.keys(val).length > 0; } hostData() { @@ -462,8 +794,7 @@ export class DateTime { }; } - render() { - console.log('rendering', this); + protected render() { let addPlaceholderClass = false; // If selected text has been passed in, use that first @@ -482,7 +813,7 @@ export class DateTime {
{ dateTimeText }
,