fix(datetime): ionChange event

This commit is contained in:
Manu Mtz.-Almeida
2018-08-07 01:08:30 +02:00
parent efd77c93a4
commit 8b35e37a43
6 changed files with 3868 additions and 5398 deletions

View File

@ -27,7 +27,7 @@
"ionicons": "4.3.0" "ionicons": "4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@stencil/core": "0.11.0", "@stencil/core": "0.11.1",
"@stencil/dev-server": "latest", "@stencil/dev-server": "latest",
"@stencil/sass": "0.1.0", "@stencil/sass": "0.1.0",
"@stencil/utils": "latest", "@stencil/utils": "latest",

8827
core/src/components.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -173,7 +173,7 @@ export function isLeapYear(year: number): boolean {
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 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}))?)?)?$/; const TIME_REGEXP = /^((\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
export function parseDate(val: any): DatetimeData | null { export function parseDate(val: string | undefined): DatetimeData | null {
// manually parse IS0 cuz Date.parse cannot be trusted // manually parse IS0 cuz Date.parse cannot be trusted
// ISO 8601 format: 1994-12-15T13:47:20Z // ISO 8601 format: 1994-12-15T13:47:20Z
let parse: any[] | null = null; let parse: any[] | null = null;
@ -254,8 +254,9 @@ export function updateDate(existingData: DatetimeData, newData: any): boolean {
// merge new values from the picker's selection // merge new values from the picker's selection
// to the existing DatetimeData values // to the existing DatetimeData values
Object.assign(existingData, newData); for (const key of Object.keys(newData)) {
existingData[key] = newData[key].value;
}
return true; return true;
} }

View File

@ -163,10 +163,7 @@ export class Datetime {
* Any additional options that the picker interface can accept. * Any additional options that the picker interface can accept.
* See the [Picker API docs](../../picker/Picker) for the picker options. * See the [Picker API docs](../../picker/Picker) for the picker options.
*/ */
@Prop() pickerOptions: PickerOptions = { @Prop() pickerOptions?: PickerOptions;
buttons: [],
columns: []
};
/** /**
* The text to display when there's no date selected yet. * The text to display when there's no date selected yet.
@ -186,6 +183,7 @@ export class Datetime {
protected valueChanged() { protected valueChanged() {
this.updateValue(); this.updateValue();
this.emitStyle(); this.emitStyle();
this.ionChange.emit();
} }
/** /**
@ -193,6 +191,11 @@ export class Datetime {
*/ */
@Event() ionCancel!: EventEmitter<void>; @Event() ionCancel!: EventEmitter<void>;
/**
* Emitted when the checked property has changed.
*/
@Event() ionChange!: EventEmitter<void>;
/** /**
* Emitted when the styles change. * Emitted when the styles change.
*/ */
@ -232,7 +235,23 @@ export class Datetime {
this.updateText(); this.updateText();
} }
private buildPicker(pickerOptions: PickerOptions) { private async open() {
if (this.disabled) {
return;
}
const pickerOptions = this.generatePicketOptions();
this.picker = await this.pickerCtrl.create(pickerOptions);
this.validate();
await this.picker!.present();
}
private generatePicketOptions(): PickerOptions {
const pickerOptions: PickerOptions = {
...this.pickerOptions,
columns: this.generateColumns()
};
// If the user has not passed in picker buttons, // If the user has not passed in picker buttons,
// add a cancel and ok button to the picker // add a cancel and ok button to the picker
const buttons = pickerOptions.buttons; const buttons = pickerOptions.buttons;
@ -245,36 +264,20 @@ export class Datetime {
}, },
{ {
text: this.doneText, text: this.doneText,
handler: (data: any) => this.value = data, handler: (data: any) => this.value = data
}];
} }
];
pickerOptions.columns = this.generateColumns();
const picker = this.pickerCtrl.create(pickerOptions);
return picker;
} }
return pickerOptions;
private async open() {
// TODO check this.isFocus() || this.disabled
if (this.disabled) {
return;
}
const pickerOptions = { ...this.pickerOptions };
this.picker = await this.buildPicker(pickerOptions);
this.validate();
await this.picker!.present();
} }
private generateColumns(): PickerColumn[] { private generateColumns(): PickerColumn[] {
let columns: PickerColumn[] = [];
// if a picker format wasn't provided, then fallback // if a picker format wasn't provided, then fallback
// to use the display format // to use the display format
let template = this.pickerFormat || this.displayFormat || DEFAULT_FORMAT; let template = this.pickerFormat || this.displayFormat || DEFAULT_FORMAT;
if (!template) {
if (template) { return [];
}
// make sure we've got up to date sizing information // make sure we've got up to date sizing information
this.calcMinMax(); this.calcMinMax();
@ -290,7 +293,7 @@ export class Datetime {
template = template.replace(/{~}/g, ''); template = template.replace(/{~}/g, '');
// parse apart the given template into an array of "formats" // parse apart the given template into an array of "formats"
parseTemplate(template).forEach((format: any) => { const columns = parseTemplate(template).map((format: any) => {
// loop through each format in the template // loop through each format in the template
// create a new picker column to build up with data // create a new picker column to build up with data
const key = convertFormatToKey(format)!; const key = convertFormatToKey(format)!;
@ -302,28 +305,23 @@ export class Datetime {
? convertToArrayOfNumbers(this[key + 'Values'], key) ? convertToArrayOfNumbers(this[key + 'Values'], key)
: dateValueRange(format, this.datetimeMin, this.datetimeMax); : dateValueRange(format, this.datetimeMin, this.datetimeMax);
const column: PickerColumn = { const colOptions = values.map(val => {
name: key,
selectedIndex: 0,
options: values.map(val => {
return { return {
value: val, value: val,
text: renderTextFormat(format, val, null, this.locale), text: renderTextFormat(format, val, null, this.locale),
}; };
}) });
};
// cool, we've loaded up the columns with options // cool, we've loaded up the columns with options
// preselect the option for this column // preselect the option for this column
const optValue = getValueFromFormat(this.datetimeValue, format); const optValue = getValueFromFormat(this.datetimeValue, format);
const selectedIndex = column.options.findIndex(opt => opt.value === optValue); const selectedIndex = colOptions.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 return {
columns.push(column); name: key,
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
options: colOptions
};
}); });
// Normalize min/max // Normalize min/max
@ -336,10 +334,7 @@ export class Datetime {
max[name] = 0; max[name] = 0;
}); });
columns = this.divyColumns(columns); return divyColumns(columns);
}
return columns;
} }
private validate() { private validate() {
@ -484,42 +479,6 @@ export class Datetime {
return 0; return 0;
} }
private divyColumns(columns: PickerColumn[]): PickerColumn[] {
const pickerColumns = columns;
const columnsWidth: number[] = [];
let col: PickerColumn;
let width: number;
for (let i = 0; i < pickerColumns.length; i++) {
col = pickerColumns[i];
columnsWidth.push(0);
for (const option of col.options) {
width = option.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;
}
/**
*/
private updateText() { private updateText() {
// create the text of the formatted data // create the text of the formatted data
const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT; const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT;
@ -527,8 +486,6 @@ export class Datetime {
this.text = renderDatetime(template, this.datetimeValue, this.locale); this.text = renderDatetime(template, this.datetimeValue, this.locale);
} }
/**
*/
hasValue(): boolean { hasValue(): boolean {
const val = this.datetimeValue; const val = this.datetimeValue;
return val return val
@ -580,6 +537,38 @@ export class Datetime {
} }
} }
function divyColumns(columns: PickerColumn[]): PickerColumn[] {
const columnsWidth: number[] = [];
let col: PickerColumn;
let width: number;
for (let i = 0; i < columns.length; i++) {
col = columns[i];
columnsWidth.push(0);
for (const option of col.options) {
width = option.text!.length;
if (width > columnsWidth[i]) {
columnsWidth[i] = width;
}
}
}
if (columnsWidth.length === 2) {
width = Math.max(columnsWidth[0], columnsWidth[1]);
columns[0].align = 'right';
columns[1].align = 'left';
columns[0].optionsWidth = columns[1].optionsWidth = `${width * 17}px`;
} else if (columnsWidth.length === 3) {
width = Math.max(columnsWidth[0], columnsWidth[2]);
columns[0].align = 'right';
columns[1].columnWidth = `${columnsWidth[1] * 17}px`;
columns[0].optionsWidth = columns[2].optionsWidth = `${width * 17}px`;
columns[2].align = 'left';
}
return columns;
}
let datetimeIds = 0; let datetimeIds = 0;
const DEFAULT_FORMAT = 'MMM D, YYYY'; const DEFAULT_FORMAT = 'MMM D, YYYY';

View File

@ -1,9 +1,8 @@
import { Component, Element, Prop, QueueApi } from '@stencil/core'; import { Component, Element, Event, EventEmitter, Prop, QueueApi } from '@stencil/core';
import { Gesture, GestureDetail, Mode, PickerColumn } from '../../interface'; import { Gesture, GestureDetail, Mode, PickerColumn } from '../../interface';
import { hapticSelectionChanged } from '../../utils'; import { hapticSelectionChanged } from '../../utils';
import { clamp } from '../../utils/helpers'; import { clamp } from '../../utils/helpers';
import { createThemedClasses } from '../../utils/theme';
/** @hidden */ /** @hidden */
@Component({ @Component({
@ -14,7 +13,6 @@ export class PickerColumnCmp {
private bounceFrom!: number; private bounceFrom!: number;
private lastIndex?: number; private lastIndex?: number;
private lastTempIndex?: number;
private minY!: number; private minY!: number;
private maxY!: number; private maxY!: number;
private optHeight = 0; private optHeight = 0;
@ -22,6 +20,7 @@ export class PickerColumnCmp {
private scaleFactor = 1; private scaleFactor = 1;
private velocity = 0; private velocity = 0;
private y = 0; private y = 0;
private optsEl?: HTMLElement;
private gesture?: Gesture; private gesture?: Gesture;
private rafId: any; private rafId: any;
@ -31,6 +30,8 @@ export class PickerColumnCmp {
@Prop() col!: PickerColumn; @Prop() col!: PickerColumn;
@Event() ionChange!: EventEmitter<void>;
componentWillLoad() { componentWillLoad() {
let pickerRotateFactor = 0; let pickerRotateFactor = 0;
let pickerScaleFactor = 0.81; let pickerScaleFactor = 0.81;
@ -46,7 +47,7 @@ export class PickerColumnCmp {
async componentDidLoad() { async componentDidLoad() {
// get the scrollable element within the column // get the scrollable element within the column
const colEl = this.el.querySelector('.picker-opts')!; const colEl = this.optsEl!;
// get the height of one option // get the height of one option
this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0); this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0);
@ -69,39 +70,35 @@ export class PickerColumnCmp {
private setSelected(selectedIndex: number, duration: number) { private setSelected(selectedIndex: number, duration: number) {
// if there is a selected index, then figure out it's y position // if there is a selected index, then figure out it's y position
// if there isn't a selected index, then just use the top y position // if there isn't a selected index, then just use the top y position
const y = (selectedIndex > -1) ? ((selectedIndex * this.optHeight) * -1) : 0; const y = (selectedIndex > -1) ? -(selectedIndex * this.optHeight) : 0;
this.velocity = 0; this.velocity = 0;
// set what y position we're at // set what y position we're at
cancelAnimationFrame(this.rafId); cancelAnimationFrame(this.rafId);
this.update(y, duration, true, true); this.update(y, duration, true);
this.ionChange.emit();
} }
private update(y: number, duration: number, saveY: boolean, emitChange: boolean) { private update(y: number, duration: number, saveY: boolean) {
// ensure we've got a good round number :) // ensure we've got a good round number :)
y = Math.round(y);
let translateY = 0; let translateY = 0;
let translateZ = 0; let translateZ = 0;
const { col, rotateFactor } = this;
const parent = this.el.querySelector('.picker-opts')!; const selectedIndex = col.selectedIndex = this.indexForY(-y);
const children = parent.children;
const length = children.length;
const selectedIndex = this.col.selectedIndex = Math.min(Math.max(Math.round(-y / this.optHeight), 0), length - 1);
const durationStr = (duration === 0) ? null : duration + 'ms'; const durationStr = (duration === 0) ? null : duration + 'ms';
const scaleStr = `scale(${this.scaleFactor})`; const scaleStr = `scale(${this.scaleFactor})`;
for (let i = 0; i < length; i++) { const children = this.optsEl!.children;
for (let i = 0; i < children.length; i++) {
const button = children[i] as HTMLElement; const button = children[i] as HTMLElement;
const opt = this.col.options[i]; const opt = col.options[i];
const optOffset = (i * this.optHeight) + y; const optOffset = (i * this.optHeight) + y;
let visible = true; let visible = true;
let transform = ''; let transform = '';
if (this.rotateFactor !== 0) { if (rotateFactor !== 0) {
const rotateX = optOffset * this.rotateFactor; const rotateX = optOffset * rotateFactor;
if (Math.abs(rotateX) > 90) { if (Math.abs(rotateX) > 90) {
visible = false; visible = false;
} else { } else {
@ -152,32 +149,15 @@ export class PickerColumnCmp {
this.y = y; this.y = y;
} }
if (emitChange) { if (this.lastIndex !== selectedIndex) {
if (this.lastIndex === undefined) {
// have not set a last index yet // have not set a last index yet
this.lastIndex = this.col.selectedIndex; hapticSelectionChanged();
this.lastIndex = selectedIndex;
} else if (this.lastIndex !== this.col.selectedIndex) {
// new selected index has changed from the last index
// update the lastIndex and emit that it has changed
this.lastIndex = this.col.selectedIndex;
// TODO ionChange event
// var ionChange = this.ionChange;
// if (ionChange.observers.length > 0) {
// this._zone.run(ionChange.emit.bind(ionChange, this.col.options[this.col.selectedIndex]));
// }
}
} }
} }
private decelerate() { private decelerate() {
let y = 0; if (this.velocity !== 0) {
if (isNaN(this.y) || !this.optHeight) {
// fallback in case numbers get outta wack
this.update(y, 0, true, true);
} else if (Math.abs(this.velocity) > 0) {
// still decelerating // still decelerating
this.velocity *= DECELERATION_FRICTION; this.velocity *= DECELERATION_FRICTION;
@ -186,7 +166,7 @@ export class PickerColumnCmp {
? Math.max(this.velocity, 1) ? Math.max(this.velocity, 1)
: Math.min(this.velocity, -1); : Math.min(this.velocity, -1);
y = Math.round(this.y + this.velocity); let y = this.y + this.velocity;
if (y > this.minY) { if (y > this.minY) {
// whoops, it's trying to scroll up farther than the options we have! // whoops, it's trying to scroll up farther than the options we have!
@ -199,13 +179,13 @@ export class PickerColumnCmp {
this.velocity = 0; this.velocity = 0;
} }
const notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1); this.update(y, 0, true);
const notLockedIn = (Math.round(y) % this.optHeight !== 0) || (Math.abs(this.velocity) > 1);
this.update(y, 0, true, !notLockedIn);
if (notLockedIn) { if (notLockedIn) {
// isn't locked in yet, keep decelerating until it is // isn't locked in yet, keep decelerating until it is
this.rafId = requestAnimationFrame(() => this.decelerate()); this.rafId = requestAnimationFrame(() => this.decelerate());
} else {
this.ionChange.emit();
} }
} else if (this.y % this.optHeight !== 0) { } else if (this.y % this.optHeight !== 0) {
@ -217,14 +197,10 @@ export class PickerColumnCmp {
this.decelerate(); this.decelerate();
} }
const currentIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0);
if (currentIndex !== this.lastTempIndex) {
// Trigger a haptic event for physical feedback that the index has changed
hapticSelectionChanged();
} }
this.lastTempIndex = currentIndex;
private indexForY(y: number) {
return Math.min(Math.max(Math.abs(Math.round(y / this.optHeight)), 0), this.col.options.length - 1);
} }
// TODO should this check disabled? // TODO should this check disabled?
@ -250,8 +226,8 @@ export class PickerColumnCmp {
} }
} }
this.minY = (minY * this.optHeight * -1); this.minY = -(minY * this.optHeight);
this.maxY = (maxY * this.optHeight * -1); this.maxY = -(maxY * this.optHeight);
} }
private onDragMove(detail: GestureDetail) { private onDragMove(detail: GestureDetail) {
@ -277,22 +253,17 @@ export class PickerColumnCmp {
this.bounceFrom = 0; this.bounceFrom = 0;
} }
this.update(y, 0, false, false); this.update(y, 0, false);
const currentIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0);
if (currentIndex !== this.lastTempIndex) {
this.lastTempIndex = currentIndex;
}
} }
private onDragEnd(detail: GestureDetail) { private onDragEnd(detail: GestureDetail) {
if (this.bounceFrom > 0) { if (this.bounceFrom > 0) {
// bounce back up // bounce back up
this.update(this.minY, 100, true, true); this.update(this.minY, 100, true);
return; return;
} else if (this.bounceFrom < 0) { } else if (this.bounceFrom < 0) {
// bounce back down // bounce back down
this.update(this.maxY, 100, true, true); this.update(this.maxY, 100, true);
return; return;
} }
@ -304,10 +275,7 @@ export class PickerColumnCmp {
} }
} else { } else {
if (Math.abs(detail.deltaY) > 3) { this.y += detail.deltaY;
const y = this.y + detail.deltaY;
this.update(y, 0, true, true);
}
this.decelerate(); this.decelerate();
} }
} }
@ -327,15 +295,14 @@ export class PickerColumnCmp {
if (this.col.prevSelected !== selectedIndex) { if (this.col.prevSelected !== selectedIndex) {
const y = (selectedIndex * this.optHeight) * -1; const y = (selectedIndex * this.optHeight) * -1;
this.velocity = 0; this.velocity = 0;
this.update(y, 150, true, false); this.update(y, 150, true);
} }
} }
hostData() { hostData() {
return { return {
class: { class: {
...createThemedClasses(this.mode, 'picker-col'), 'picker-col': true,
'picker-opts-left': this.col.align === 'left', 'picker-opts-left': this.col.align === 'left',
'picker-opts-right': this.col.align === 'right' 'picker-opts-right': this.col.align === 'right'
}, },
@ -347,14 +314,6 @@ export class PickerColumnCmp {
render() { render() {
const col = this.col; const col = this.col;
const options = col.options.map(o => {
return (typeof o === 'string')
? { text: o }
: o;
})
.filter(o => o !== null);
const Button = 'button' as any; const Button = 'button' as any;
return [ return [
col.prefix && ( col.prefix && (
@ -362,8 +321,11 @@ export class PickerColumnCmp {
{col.prefix} {col.prefix}
</div> </div>
), ),
<div class="picker-opts" style={{ maxWidth: col.optionsWidth! }}> <div
{options.map((o, index) => class="picker-opts"
style={{ maxWidth: col.optionsWidth! }}
ref={ el => this.optsEl = el }>
{ col.options.map((o, index) =>
<Button <Button
class={{ 'picker-opt': true, 'picker-opt-disabled': !!o.disabled }} class={{ 'picker-opt': true, 'picker-opt-disabled': !!o.disabled }}
disable-activated disable-activated

View File

@ -205,22 +205,6 @@ export class Picker implements OverlayInterface {
return eventMethod(this.el, 'ionPickerWillDismiss', callback); return eventMethod(this.el, 'ionPickerWillDismiss', callback);
} }
/**
* Add a new PickerButton to the picker
*/
@Method()
addButton(button: PickerButton) {
this.buttons.push(button);
}
/**
* Add a new PickerColumn to the picker
*/
@Method()
addColumn(column: PickerColumn) {
this.columns.push(column);
}
/** /**
* Returns the column the matches the specified name * Returns the column the matches the specified name
*/ */
@ -229,14 +213,6 @@ export class Picker implements OverlayInterface {
return this.columns.find(column => column.name === name); return this.columns.find(column => column.name === name);
} }
/**
* Returns all the PickerColumns
*/
@Method()
getColumns(): PickerColumn[] {
return this.columns;
}
private buttonClick(button: PickerButton) { private buttonClick(button: PickerButton) {
// if (this.disabled) { // if (this.disabled) {
// return; // return;
@ -295,41 +271,14 @@ export class Picker implements OverlayInterface {
const columns = this.columns; const columns = this.columns;
// // clean up dat data
// data.columns = data.columns.map(column => {
// if (!isPresent(column.options)) {
// column.options = [];
// }
// column.selectedIndex = column.selectedIndex || 0;
// column.options = column.options.map(inputOpt => {
// let opt: PickerColumnOption = {
// text: '',
// value: '',
// disabled: inputOpt.disabled,
// };
// if (isPresent(inputOpt)) {
// if (isString(inputOpt) || isNumber(inputOpt)) {
// opt.text = inputOpt.toString();
// opt.value = inputOpt;
// } else {
// opt.text = isPresent(inputOpt.text) ? inputOpt.text : inputOpt.value;
// opt.value = isPresent(inputOpt.value) ? inputOpt.value : inputOpt.text;
// }
// }
// return opt;
// });
// return column;
// });
return [ return [
<ion-backdrop <ion-backdrop
visible={this.showBackdrop} visible={this.showBackdrop}
tappable={this.enableBackdropDismiss} tappable={this.enableBackdropDismiss}
/>, />,
<div class="picker-wrapper" role="dialog"> <div class="picker-wrapper" role="dialog">
<div class="picker-toolbar"> <div class="picker-toolbar">
{buttons.map(b => ( {buttons.map(b => (
<div class={buttonWrapperClass(b)}> <div class={buttonWrapperClass(b)}>
@ -342,11 +291,13 @@ export class Picker implements OverlayInterface {
</div> </div>
))} ))}
</div> </div>
<div class="picker-columns"> <div class="picker-columns">
<div class="picker-above-highlight" /> <div class="picker-above-highlight" />
{columns.map(c => <ion-picker-column col={c} />)} {columns.map(c => <ion-picker-column col={c} />)}
<div class="picker-below-highlight" /> <div class="picker-below-highlight" />
</div> </div>
</div> </div>
]; ];
} }