UNPKG

20.9 kBJavaScriptView Raw
1/*!
2 * (C) Ionic http://ionicframework.com - MIT License
3 */
4import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client';
5import { g as getElementRoot } from './helpers.js';
6
7const pickerInternalIosCss = ":host{display:-ms-flexbox;display:flex;position:relative;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:100%;height:200px;direction:ltr;z-index:0}:host .picker-before,:host .picker-after{position:absolute;width:100%;z-index:1;pointer-events:none}:host .picker-before{left:0;top:0;height:83px}:host-context([dir=rtl]){left:unset;right:unset;right:0}:host .picker-after{left:0;top:116px;height:84px}:host-context([dir=rtl]){left:unset;right:unset;right:0}:host .picker-highlight{border-radius:8px;left:0;right:0;top:50%;bottom:0;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;position:absolute;width:calc(100% - 16px);height:34px;-webkit-transform:translateY(-50%);transform:translateY(-50%);z-index:-1}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){:host .picker-highlight{margin-left:unset;margin-right:unset;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto}}:host input{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;clip:rect(0 0 0 0);opacity:0;overflow:hidden;-webkit-appearance:none;-moz-appearance:none}:host ::slotted(ion-picker-column-internal:first-of-type){text-align:start}:host ::slotted(ion-picker-column-internal:last-of-type){text-align:end}:host .picker-before{background:-webkit-gradient(linear, left top, left bottom, color-stop(20%, var(--background, var(--ion-background-color, #fff))), to(rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8)));background:linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8) 100%)}:host .picker-after{background:-webkit-gradient(linear, left bottom, left top, color-stop(20%, var(--background, var(--ion-background-color, #fff))), to(rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8)));background:linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8) 100%)}:host .picker-highlight{background:var(--ion-color-step-150, #eeeeef)}";
8
9const pickerInternalMdCss = ":host{display:-ms-flexbox;display:flex;position:relative;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:100%;height:200px;direction:ltr;z-index:0}:host .picker-before,:host .picker-after{position:absolute;width:100%;z-index:1;pointer-events:none}:host .picker-before{left:0;top:0;height:83px}:host-context([dir=rtl]){left:unset;right:unset;right:0}:host .picker-after{left:0;top:116px;height:84px}:host-context([dir=rtl]){left:unset;right:unset;right:0}:host .picker-highlight{border-radius:8px;left:0;right:0;top:50%;bottom:0;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;position:absolute;width:calc(100% - 16px);height:34px;-webkit-transform:translateY(-50%);transform:translateY(-50%);z-index:-1}@supports ((-webkit-margin-start: 0) or (margin-inline-start: 0)) or (-webkit-margin-start: 0){:host .picker-highlight{margin-left:unset;margin-right:unset;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto}}:host input{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;clip:rect(0 0 0 0);opacity:0;overflow:hidden;-webkit-appearance:none;-moz-appearance:none}:host ::slotted(ion-picker-column-internal:first-of-type){text-align:start}:host ::slotted(ion-picker-column-internal:last-of-type){text-align:end}:host .picker-before{background:-webkit-gradient(linear, left top, left bottom, color-stop(20%, var(--background, var(--ion-background-color, #fff))), color-stop(90%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0)));background:linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0) 90%)}:host .picker-after{background:-webkit-gradient(linear, left bottom, left top, color-stop(30%, var(--background, var(--ion-background-color, #fff))), color-stop(90%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0)));background:linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 30%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0) 90%)}";
10
11const PickerInternal = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
12 constructor() {
13 super();
14 this.__registerHost();
15 this.__attachShadow();
16 this.ionInputModeChange = createEvent(this, "ionInputModeChange", 7);
17 this.useInputMode = false;
18 this.isInHighlightBounds = (ev) => {
19 const { highlightEl } = this;
20 if (!highlightEl) {
21 return false;
22 }
23 const bbox = highlightEl.getBoundingClientRect();
24 /**
25 * Check to see if the user clicked
26 * outside the bounds of the highlight.
27 */
28 const outsideX = ev.clientX < bbox.left || ev.clientX > bbox.right;
29 const outsideY = ev.clientY < bbox.top || ev.clientY > bbox.bottom;
30 if (outsideX || outsideY) {
31 return false;
32 }
33 return true;
34 };
35 /**
36 * If we are no longer focused
37 * on a picker column, then we should
38 * exit input mode. An exception is made
39 * for the input in the picker since having
40 * that focused means we are still in input mode.
41 */
42 this.onFocusOut = (ev) => {
43 const { relatedTarget } = ev;
44 if (!relatedTarget ||
45 relatedTarget.tagName !== 'ION-PICKER-COLUMN-INTERNAL' && relatedTarget !== this.inputEl) {
46 this.exitInputMode();
47 }
48 };
49 /**
50 * When picker columns receive focus
51 * the parent picker needs to determine
52 * whether to enter/exit input mode.
53 */
54 this.onFocusIn = (ev) => {
55 const { target } = ev;
56 /**
57 * Due to browser differences in how/when focus
58 * is dispatched on certain elements, we need to
59 * make sure that this function only ever runs when
60 * focusing a picker column.
61 */
62 if (target.tagName !== 'ION-PICKER-COLUMN-INTERNAL') {
63 return;
64 }
65 /**
66 * If we have actionOnClick
67 * then this means the user focused
68 * a picker column via mouse or
69 * touch (i.e. a PointerEvent). As a result,
70 * we should not enter/exit input mode
71 * until the click event has fired, which happens
72 * after the `focusin` event.
73 *
74 * Otherwise, the user likely focused
75 * the column using their keyboard and
76 * we should enter/exit input mode automatically.
77 */
78 if (!this.actionOnClick) {
79 const columnEl = target;
80 const allowInput = columnEl.numericInput;
81 if (allowInput) {
82 this.enterInputMode(columnEl, false);
83 }
84 else {
85 this.exitInputMode();
86 }
87 }
88 };
89 /**
90 * On click we need to run an actionOnClick
91 * function that has been set in onPointerDown
92 * so that we enter/exit input mode correctly.
93 */
94 this.onClick = () => {
95 const { actionOnClick } = this;
96 if (actionOnClick) {
97 actionOnClick();
98 this.actionOnClick = undefined;
99 }
100 };
101 /**
102 * Clicking a column also focuses the column on
103 * certain browsers, so we use onPointerDown
104 * to tell the onFocusIn function that users
105 * are trying to click the column rather than
106 * focus the column using the keyboard. When the
107 * user completes the click, the onClick function
108 * runs and runs the actionOnClick callback.
109 */
110 this.onPointerDown = (ev) => {
111 const { useInputMode, inputModeColumn, el } = this;
112 if (this.isInHighlightBounds(ev)) {
113 /**
114 * If we were already in
115 * input mode, then we should determine
116 * if we tapped a particular column and
117 * should switch to input mode for
118 * that specific column.
119 */
120 if (useInputMode) {
121 /**
122 * If we tapped a picker column
123 * then we should either switch to input
124 * mode for that column or all columns.
125 * Otherwise we should exit input mode
126 * since we just tapped the highlight and
127 * not a column.
128 */
129 if (ev.target.tagName === 'ION-PICKER-COLUMN-INTERNAL') {
130 /**
131 * If user taps 2 different columns
132 * then we should just switch to input mode
133 * for the new column rather than switching to
134 * input mode for all columns.
135 */
136 if (inputModeColumn && inputModeColumn === ev.target) {
137 this.actionOnClick = () => {
138 this.enterInputMode();
139 };
140 }
141 else {
142 this.actionOnClick = () => {
143 this.enterInputMode(ev.target);
144 };
145 }
146 }
147 else {
148 this.actionOnClick = () => {
149 this.exitInputMode();
150 };
151 }
152 /**
153 * If we were not already in
154 * input mode, then we should
155 * enter input mode for all columns.
156 */
157 }
158 else {
159 /**
160 * If there is only 1 numeric input column
161 * then we should skip multi column input.
162 */
163 const columns = el.querySelectorAll('ion-picker-column-internal.picker-column-numeric-input');
164 const columnEl = (columns.length === 1) ? ev.target : undefined;
165 this.actionOnClick = () => {
166 this.enterInputMode(columnEl);
167 };
168 }
169 return;
170 }
171 this.actionOnClick = () => {
172 this.exitInputMode();
173 };
174 };
175 /**
176 * Enters input mode to allow
177 * for text entry of numeric values.
178 * If on mobile, we focus a hidden input
179 * field so that the on screen keyboard
180 * is brought up. When tabbing using a
181 * keyboard, picker columns receive an outline
182 * to indicate they are focused. As a result,
183 * we should not focus the hidden input as it
184 * would cause the outline to go away, preventing
185 * users from having any visual indication of which
186 * column is focused.
187 */
188 this.enterInputMode = (columnEl, focusInput = true) => {
189 const { inputEl, el } = this;
190 if (!inputEl) {
191 return;
192 }
193 /**
194 * Only active input mode if there is at
195 * least one column that accepts numeric input.
196 */
197 const hasInputColumn = el.querySelector('ion-picker-column-internal.picker-column-numeric-input');
198 if (!hasInputColumn) {
199 return;
200 }
201 /**
202 * If columnEl is undefined then
203 * it is assumed that all numeric pickers
204 * are eligible for text entry.
205 * (i.e. hour and minute columns)
206 */
207 this.useInputMode = true;
208 this.inputModeColumn = columnEl;
209 /**
210 * Users with a keyboard and mouse can
211 * activate input mode where the input is
212 * focused as well as when it is not focused,
213 * so we need to make sure we clean up any
214 * old listeners.
215 */
216 if (focusInput) {
217 if (this.destroyKeypressListener) {
218 this.destroyKeypressListener();
219 this.destroyKeypressListener = undefined;
220 }
221 inputEl.focus();
222 }
223 else {
224 el.addEventListener('keypress', this.onKeyPress);
225 this.destroyKeypressListener = () => {
226 el.removeEventListener('keypress', this.onKeyPress);
227 };
228 }
229 this.emitInputModeChange();
230 };
231 this.exitInputMode = () => {
232 const { inputEl, useInputMode } = this;
233 if (!useInputMode || !inputEl) {
234 return;
235 }
236 this.useInputMode = false;
237 this.inputModeColumn = undefined;
238 inputEl.blur();
239 inputEl.value = '';
240 if (this.destroyKeypressListener) {
241 this.destroyKeypressListener();
242 this.destroyKeypressListener = undefined;
243 }
244 this.emitInputModeChange();
245 };
246 this.onKeyPress = (ev) => {
247 const { inputEl } = this;
248 if (!inputEl) {
249 return;
250 }
251 const parsedValue = parseInt(ev.key, 10);
252 /**
253 * Only numbers should be allowed
254 */
255 if (!Number.isNaN(parsedValue)) {
256 inputEl.value += ev.key;
257 this.onInputChange();
258 }
259 };
260 this.selectSingleColumn = () => {
261 const { inputEl, inputModeColumn, singleColumnSearchTimeout } = this;
262 if (!inputEl || !inputModeColumn) {
263 return;
264 }
265 const values = inputModeColumn.items;
266 /**
267 * If users pause for a bit, the search
268 * value should be reset similar to how a
269 * <select> behaves. So typing "34", waiting,
270 * then typing "5" should select "05".
271 */
272 if (singleColumnSearchTimeout) {
273 clearTimeout(singleColumnSearchTimeout);
274 }
275 this.singleColumnSearchTimeout = setTimeout(() => {
276 inputEl.value = '';
277 this.singleColumnSearchTimeout = undefined;
278 }, 1000);
279 /**
280 * For values that are longer than 2 digits long
281 * we should shift the value over 1 character
282 * to the left. So typing "456" would result in "56".
283 * TODO: If we want to support more than just
284 * time entry, we should update this value to be
285 * the max length of all of the picker items.
286 */
287 if (inputEl.value.length >= 3) {
288 const startIndex = inputEl.value.length - 2;
289 const newString = inputEl.value.substring(startIndex);
290 inputEl.value = newString;
291 this.selectSingleColumn();
292 return;
293 }
294 /**
295 * Checking the value of the input gets priority
296 * first. For example, if the value of the input
297 * is "1" and we entered "2", then the complete value
298 * is "12" and we should select hour 12.
299 *
300 * Regex removes any leading zeros from values like "02".
301 */
302 const findItemFromCompleteValue = values.find(({ text }) => text.replace(/^0+/, '') === inputEl.value);
303 if (findItemFromCompleteValue) {
304 inputModeColumn.value = findItemFromCompleteValue.value;
305 return;
306 }
307 /**
308 * If we typed "56" to get minute 56, then typed "7",
309 * we should select "07" as "567" is not a valid minute.
310 */
311 if (inputEl.value.length === 2) {
312 const changedCharacter = inputEl.value.substring(inputEl.value.length - 1);
313 inputEl.value = changedCharacter;
314 this.selectSingleColumn();
315 }
316 };
317 /**
318 * Searches a list of column items for a particular
319 * value. This is currently used for numeric values.
320 * The zeroBehavior can be set to account for leading
321 * or trailing zeros when looking at the item text.
322 */
323 this.searchColumn = (colEl, value, zeroBehavior = 'start') => {
324 let item;
325 const behavior = zeroBehavior === 'start' ? /^0+/ : /0$/;
326 item = colEl.items.find(({ text }) => text.replace(behavior, '') === value);
327 if (item) {
328 colEl.value = item.value;
329 }
330 };
331 this.selectMultiColumn = () => {
332 const { inputEl, el } = this;
333 if (!inputEl) {
334 return;
335 }
336 const numericPickers = Array.from(el.querySelectorAll('ion-picker-column-internal')).filter(col => col.numericInput);
337 const firstColumn = numericPickers[0];
338 const lastColumn = numericPickers[1];
339 let value = inputEl.value;
340 let minuteValue;
341 switch (value.length) {
342 case 1:
343 this.searchColumn(firstColumn, value);
344 break;
345 case 2:
346 /**
347 * If the first character is `0` or `1` it is
348 * possible that users are trying to type `09`
349 * or `11` into the hour field, so we should look
350 * at that first.
351 */
352 const firstCharacter = inputEl.value.substring(0, 1);
353 value = (firstCharacter === '0' || firstCharacter === '1') ? inputEl.value : firstCharacter;
354 this.searchColumn(firstColumn, value);
355 /**
356 * If only checked the first value,
357 * we can check the second value
358 * for a match in the minutes column
359 */
360 if (value.length === 1) {
361 minuteValue = inputEl.value.substring(inputEl.value.length - 1);
362 this.searchColumn(lastColumn, minuteValue, 'end');
363 }
364 break;
365 case 3:
366 /**
367 * If the first character is `0` or `1` it is
368 * possible that users are trying to type `09`
369 * or `11` into the hour field, so we should look
370 * at that first.
371 */
372 const firstCharacterAgain = inputEl.value.substring(0, 1);
373 value = (firstCharacterAgain === '0' || firstCharacterAgain === '1') ? inputEl.value.substring(0, 2) : firstCharacterAgain;
374 this.searchColumn(firstColumn, value);
375 /**
376 * If only checked the first value,
377 * we can check the second value
378 * for a match in the minutes column
379 */
380 minuteValue = (value.length === 1) ? inputEl.value.substring(1) : inputEl.value.substring(2);
381 this.searchColumn(lastColumn, minuteValue, 'end');
382 break;
383 case 4:
384 /**
385 * If the first character is `0` or `1` it is
386 * possible that users are trying to type `09`
387 * or `11` into the hour field, so we should look
388 * at that first.
389 */
390 const firstCharacterAgainAgain = inputEl.value.substring(0, 1);
391 value = (firstCharacterAgainAgain === '0' || firstCharacterAgainAgain === '1') ? inputEl.value.substring(0, 2) : firstCharacterAgainAgain;
392 this.searchColumn(firstColumn, value);
393 /**
394 * If only checked the first value,
395 * we can check the second value
396 * for a match in the minutes column
397 */
398 const minuteValueAgain = (value.length === 1) ? inputEl.value.substring(1, inputEl.value.length) : inputEl.value.substring(2, inputEl.value.length);
399 this.searchColumn(lastColumn, minuteValueAgain, 'end');
400 break;
401 default:
402 const startIndex = inputEl.value.length - 4;
403 const newString = inputEl.value.substring(startIndex);
404 inputEl.value = newString;
405 this.selectMultiColumn();
406 break;
407 }
408 };
409 /**
410 * Searches the value of the active column
411 * to determine which value users are trying
412 * to select
413 */
414 this.onInputChange = () => {
415 const { useInputMode, inputEl, inputModeColumn } = this;
416 if (!useInputMode || !inputEl) {
417 return;
418 }
419 if (inputModeColumn) {
420 this.selectSingleColumn();
421 }
422 else {
423 this.selectMultiColumn();
424 }
425 };
426 /**
427 * Emit ionInputModeChange. Picker columns
428 * listen for this event to determine whether
429 * or not their column is "active" for text input.
430 */
431 this.emitInputModeChange = () => {
432 const { useInputMode, inputModeColumn } = this;
433 this.ionInputModeChange.emit({
434 useInputMode,
435 inputModeColumn
436 });
437 };
438 }
439 componentWillLoad() {
440 getElementRoot(this.el).addEventListener('focusin', this.onFocusIn);
441 getElementRoot(this.el).addEventListener('focusout', this.onFocusOut);
442 }
443 render() {
444 return (h(Host, { onPointerDown: (ev) => this.onPointerDown(ev), onClick: () => this.onClick() }, h("input", { "aria-hidden": "true", tabindex: -1, inputmode: "numeric", type: "number", ref: el => this.inputEl = el, onInput: () => this.onInputChange(), onBlur: () => this.exitInputMode() }), h("div", { class: "picker-before" }), h("div", { class: "picker-after" }), h("div", { class: "picker-highlight", ref: el => this.highlightEl = el }), h("slot", null)));
445 }
446 get el() { return this; }
447 static get style() { return {
448 ios: pickerInternalIosCss,
449 md: pickerInternalMdCss
450 }; }
451}, [33, "ion-picker-internal"]);
452function defineCustomElement() {
453 if (typeof customElements === "undefined") {
454 return;
455 }
456 const components = ["ion-picker-internal"];
457 components.forEach(tagName => { switch (tagName) {
458 case "ion-picker-internal":
459 if (!customElements.get(tagName)) {
460 customElements.define(tagName, PickerInternal);
461 }
462 break;
463 } });
464}
465
466export { PickerInternal as P, defineCustomElement as d };