Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ export namespace Components {
}
interface SmoothlyInputDemoStandard {
}
interface SmoothlyInputDemoTypes {
}
interface SmoothlyInputDemoUserInput {
}
interface SmoothlyInputEdit {
Expand Down Expand Up @@ -1607,6 +1609,12 @@ declare global {
prototype: HTMLSmoothlyInputDemoStandardElement;
new (): HTMLSmoothlyInputDemoStandardElement;
};
interface HTMLSmoothlyInputDemoTypesElement extends Components.SmoothlyInputDemoTypes, HTMLStencilElement {
}
var HTMLSmoothlyInputDemoTypesElement: {
prototype: HTMLSmoothlyInputDemoTypesElement;
new (): HTMLSmoothlyInputDemoTypesElement;
};
interface HTMLSmoothlyInputDemoUserInputElement extends Components.SmoothlyInputDemoUserInput, HTMLStencilElement {
}
var HTMLSmoothlyInputDemoUserInputElement: {
Expand Down Expand Up @@ -2268,6 +2276,7 @@ declare global {
"smoothly-input-date-time": HTMLSmoothlyInputDateTimeElement;
"smoothly-input-demo": HTMLSmoothlyInputDemoElement;
"smoothly-input-demo-standard": HTMLSmoothlyInputDemoStandardElement;
"smoothly-input-demo-types": HTMLSmoothlyInputDemoTypesElement;
"smoothly-input-demo-user-input": HTMLSmoothlyInputDemoUserInputElement;
"smoothly-input-edit": HTMLSmoothlyInputEditElement;
"smoothly-input-file": HTMLSmoothlyInputFileElement;
Expand Down Expand Up @@ -2707,6 +2716,8 @@ declare namespace LocalJSX {
}
interface SmoothlyInputDemoStandard {
}
interface SmoothlyInputDemoTypes {
}
interface SmoothlyInputDemoUserInput {
}
interface SmoothlyInputEdit {
Expand Down Expand Up @@ -3087,6 +3098,7 @@ declare namespace LocalJSX {
"smoothly-input-date-time": SmoothlyInputDateTime;
"smoothly-input-demo": SmoothlyInputDemo;
"smoothly-input-demo-standard": SmoothlyInputDemoStandard;
"smoothly-input-demo-types": SmoothlyInputDemoTypes;
"smoothly-input-demo-user-input": SmoothlyInputDemoUserInput;
"smoothly-input-edit": SmoothlyInputEdit;
"smoothly-input-file": SmoothlyInputFile;
Expand Down Expand Up @@ -3203,6 +3215,7 @@ declare module "@stencil/core" {
"smoothly-input-date-time": LocalJSX.SmoothlyInputDateTime & JSXBase.HTMLAttributes<HTMLSmoothlyInputDateTimeElement>;
"smoothly-input-demo": LocalJSX.SmoothlyInputDemo & JSXBase.HTMLAttributes<HTMLSmoothlyInputDemoElement>;
"smoothly-input-demo-standard": LocalJSX.SmoothlyInputDemoStandard & JSXBase.HTMLAttributes<HTMLSmoothlyInputDemoStandardElement>;
"smoothly-input-demo-types": LocalJSX.SmoothlyInputDemoTypes & JSXBase.HTMLAttributes<HTMLSmoothlyInputDemoTypesElement>;
"smoothly-input-demo-user-input": LocalJSX.SmoothlyInputDemoUserInput & JSXBase.HTMLAttributes<HTMLSmoothlyInputDemoUserInputElement>;
"smoothly-input-edit": LocalJSX.SmoothlyInputEdit & JSXBase.HTMLAttributes<HTMLSmoothlyInputEditElement>;
"smoothly-input-file": LocalJSX.SmoothlyInputFile & JSXBase.HTMLAttributes<HTMLSmoothlyInputFileElement>;
Expand Down
74 changes: 50 additions & 24 deletions src/components/input/InputStateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Adjacent } from "./Adjacent"

type Formatter = tidily.Formatter & tidily.Converter<any>
type Handler<E extends Event> = (event: E, unformatted: tidily.State, formatted: tidily.State) => tidily.State
type CommitState = (state: Readonly<tidily.State> & tidily.Settings) => void

export class InputStateHandler {
constructor(private formatter: Formatter, private type: tidily.Type) {}
Expand Down Expand Up @@ -79,56 +80,84 @@ export class InputStateHandler {
input.value = result.value
return result
}

public onInputEvent(event: InputEvent, state: tidily.State): Readonly<tidily.State> & tidily.Settings {
private nextFormattedState?: Readonly<tidily.State> & Readonly<tidily.Settings> // Set in beforeinput event - used in input event
public onBeforeInputEvent(event: InputEvent, state: tidily.State): void {
const input = event.target as HTMLInputElement
state.selection.start = input.selectionStart ?? state.selection.start
state.selection.end = input.selectionEnd ?? state.selection.end
state.selection.direction = input.selectionDirection ?? state.selection.direction
const result =
event.type == "beforeinput" || event.type == "input"
? this.eventHandlers[event.type][event.inputType]?.(event, this.unformatState(state), state) ?? state
: state
const formatted = this.partialFormatState(result)
if (event.defaultPrevented) {
input.value = formatted.value
input.selectionStart = formatted.selection.start
input.selectionEnd = formatted.selection.end
input.selectionDirection = formatted.selection.direction ?? null
const unformatted = this.eventHandlers.beforeinput[event.inputType]?.(event, this.unformatState(state), state)
const formatted = unformatted ? this.partialFormatState(unformatted) : undefined
this.nextFormattedState = formatted
}
private requestAnimationFrameId: number | undefined
private timeoutId: NodeJS.Timeout | undefined
public onInputEvent(event: InputEvent, state: tidily.State, commitState: CommitState): void {
const input = event.target as HTMLInputElement
if (!event.inputType) {
// Chrome will dispatch an input event without inputType on autofill (this event is not reliable, but it's be best Chrome provides)
const newValue = input.value
this.requestAnimationFrameId && cancelAnimationFrame(this.requestAnimationFrameId)
clearTimeout(this.timeoutId)
this.requestAnimationFrameId = requestAnimationFrame(() => {
const newState = { ...state, value: newValue }
const formatted = this.partialFormatState(newState)
commitState(formatted)
})
} else if (this.nextFormattedState) {
const newState = this.nextFormattedState
this.nextFormattedState = undefined
input.value = newState.value
input.selectionStart = newState.selection.start
input.selectionEnd = newState.selection.end
input.selectionDirection = newState.selection.direction ?? null
commitState(newState)
} else {
state.selection.start = input.selectionStart ?? state.selection.start
state.selection.end = input.selectionEnd ?? state.selection.end
state.selection.direction = input.selectionDirection ?? state.selection.direction
const result = this.eventHandlers.input[event.inputType]?.(event, this.unformatState(state), state) ?? state
const formatted = this.partialFormatState(result)
if (
input.value != formatted.value ||
input.selectionStart != formatted.selection.start ||
input.selectionEnd != formatted.selection.end
) {
input.value = formatted.value
input.selectionStart = formatted.selection.start
input.selectionEnd = formatted.selection.end
input.selectionDirection = formatted.selection.direction ?? null
}
commitState(formatted)
}
return formatted
}
private eventHandlers: Record<"beforeinput" | "input", { [inputType: string]: Handler<InputEvent> | undefined }> = {
beforeinput: {
insertText: (event, state) => this.insert(event, state),
insertFromPaste: (event, state) => this.insert(event, state),
insertFromDrop: (event, state) => this.insert(event, state),
deleteContentBackward: (event, state) => {
event.preventDefault()
deleteContentBackward: (_, state) => {
if (state.selection.start == state.selection.end)
this.select(state, state.selection.start - 1, state.selection.end)
this.erase(state)
return state
},
deleteContentForward: (event, state) => {
event.preventDefault()
deleteContentForward: (_, state) => {
if (state.selection.start == state.selection.end)
this.select(state, state.selection.start, state.selection.end + 1)
this.erase(state)
return state
},
deleteWordBackward: (event, unformatted, formattedState) => {
deleteWordBackward: (_, unformatted, formattedState) => {
let result = unformatted
if (this.type != "password") {
event.preventDefault()
result = this.deleteWord(formattedState, "backward")
}
return result
},
deleteWordForward: (event, unformatted, formattedState) => {
deleteWordForward: (_, unformatted, formattedState) => {
let result = unformatted
if (this.type != "password") {
event.preventDefault()
result = this.deleteWord(formattedState, "forward")
}
return result
Expand All @@ -148,13 +177,10 @@ export class InputStateHandler {
this.type == "password" ? { ...state, value: (event.target as HTMLInputElement).value } : state,
deleteWordForward: (event, state) =>
this.type == "password" ? { ...state, value: (event.target as HTMLInputElement).value } : state,
// Chrome will dispatch an input event without inputType when auto-filling
undefined: (event, state) => ({ ...state, value: (event.target as HTMLInputElement).value }),
},
}

private insert(event: InputEvent, unformatted: tidily.State): tidily.State {
event.preventDefault()
if (typeof event.data == "string")
for (const c of event.data)
this.formatter.allowed(c, unformatted) && this.replace(unformatted, c.replace(/(\r|\n|\t)/g, ""))
Expand Down
1 change: 1 addition & 0 deletions src/components/input/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class SmoothlyInputDemo {
<smoothly-input-demo-standard />
<smoothly-input-date-demo />
<smoothly-input-demo-user-input />
<smoothly-input-demo-types />
<div class="inputs">
<h2>Calendar</h2>
<smoothly-input-date name="some-date">Calendar</smoothly-input-date>
Expand Down
35 changes: 35 additions & 0 deletions src/components/input/demo/types/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Component, h, Host } from "@stencil/core"
import { tidily } from "tidily"

@Component({
tag: "smoothly-input-demo-types",
styleUrl: "style.css",
scoped: true,
})
export class SmoothlyInputDemoTypes {
types: tidily.Type[] = [
"text",
"integer",
"price",
"percent",
"password",
"email",
"card-number",
"card-expires",
"card-csc",
] as const

render() {
return (
<Host>
<h3>Input Types</h3>
{this.types.map(type => (
<smoothly-input name={type} type={type} key={type} looks="border">
{type}
<smoothly-input-clear slot="end" />
</smoothly-input>
))}
</Host>
)
}
}
9 changes: 9 additions & 0 deletions src/components/input/demo/types/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:host {
display: block;
max-width: min(calc(100% - 0.5rem), 48rem);
margin: auto;
}
:host smoothly-input {
width: 100%;
}

16 changes: 10 additions & 6 deletions src/components/input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,23 @@ export class SmoothlyInput implements Clearable, Input, Editable {
this.observer.publish()
}
componentDidLoad() {
if (this.inputElement)
this.inputElement.value = this.state.value
// if (this.inputElement)
// this.inputElement.value = this.state.value
}
async disconnectedCallback() {
if (!this.element.isConnected)
await this.unregister()
}
@Listen("input")
@Listen("beforeinput")
onEvent(event: InputEvent) {
this.state = this.stateHandler.onInputEvent(event, this.state)
if (event.type == "input" || event.defaultPrevented)
onBeforeInput(event: InputEvent) {
this.stateHandler.onBeforeInputEvent(event, this.state)
}
@Listen("input")
onInput(event: InputEvent) {
this.stateHandler.onInputEvent(event, this.state, state => {
this.state = state
this.smoothlyUserInput.emit({ name: this.name, value: this.stateHandler.getValue(this.state) })
})
}
copyText(value?: string) {
if (value) {
Expand Down