* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { KEY_CODE, IPTHRESHOLD } from '@opentiny/utils'
import type { IIpAddressProps, IIpAddressApi, IIpAddressRenderlessParamUtils, IIpAddressState } from '@/types'
export const isIP6 = (str: string) => /^IPv6$/i.test(str)
export const isIP4 = (str: string) => /^IPv4$/i.test(str)
export const ipValidator =
({ props, api }: { props: IIpAddressProps; api: IIpAddressApi }) =>
(value: string) => {
let result = true
if (props.type) {
if (api.isIP6(props.type)) {
result =
/^(((([\da-fA-F]{1,4}):){7}([\da-fA-F]{1,4}))|(((([\da-fA-F]{1,4}):){1,7}:)|((([\da-fA-F]{1,4}):){6}:([\da-fA-F]{1,4}))|((([\da-fA-F]{1,4}):){5}:(([\da-fA-F]{1,4}):)?([\da-fA-F]{1,4}))|((([\da-fA-F]{1,4}):){4}:(([\da-fA-F]{1,4}):){0,2}([\da-fA-F]{1,4}))|((([\da-fA-F]{1,4}):){3}:(([\da-fA-F]{1,4}):){0,3}([\da-fA-F]{1,4}))|((([\da-fA-F]{1,4}):){2}:(([\da-fA-F]{1,4}):){0,4}([\da-fA-F]{1,4}))|((([\da-fA-F]{1,4}):){1}:(([\da-fA-F]{1,4}):){0,5}([\da-fA-F]{1,4}))|(::(([\da-fA-F]{1,4}):){0,6}([\da-fA-F]{1,4}))|(::([\da-fA-F]{1,4})?))|(((([\da-fA-F]{1,4}):){6}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-fA-F]{1,4}):){5}:(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-fA-F]{1,4}):){4}:(([\da-fA-F]{1,4}):)?(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-fA-F]{1,4}):){3}:(([\da-fA-F]{1,4}):){0,2}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-fA-F]{1,4}):){2}:(([\da-fA-F]{1,4}):){0,3}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(([\da-fA-F]{1,4})::(([\da-fA-F]{1,4}):){0,4}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(::(([\da-fA-F]{1,4}):){0,5}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))))$/.test(
value
)
} else if (api.isIP4(props.type)) {
result = /^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$/.test(value)
}
}
return result
}
export const getCursorPosition = (el) => {
let cursorPos = 0
let selectRange: any | null = null
type NewDocument = Document & { selection: Document }
if ((document as NewDocument | null)?.selection) {
selectRange = (document as NewDocument | null)?.selection?.createRange()
selectRange?.moveStart('character', -el.value.length)
cursorPos = selectRange?.text?.length
}
if (el.selectionStart || el.selectionStart === '0') {
cursorPos = el.selectionStart
}
return cursorPos
}
export const getValue =
({ api, props, state }: { api: IIpAddressApi; props: IIpAddressProps; state: IIpAddressState }) =>
() => {
const valueArr: string[] = []
let result = ''
if (api.isIP6(props.type)) {
state.address.forEach((item) => {
if (item.value) {
item.value = item.value.replace(/[^a-fA-F\d]/g, '')
}
item.value && valueArr.push(item.value)
})
result = state.address.filter((item) => item.value).length === 8 ? valueArr.join(':') : ''
} else {
state.address.forEach((item) => {
if (api.isIP4(props.type) && item.value) {
item.value = item.value.replace(/\D/g, '')
}
item.value && valueArr.push(item.value)
})
result = state.address.filter((item) => item.value).length === 4 ? valueArr.join('.') : ''
}
return result
}
export const setValue =
({ api, props, state }: { api: IIpAddressApi; props: IIpAddressProps; state: IIpAddressState }) =>
(value: string) => {
if (!value || !api?.ipValidator?.(value)) {
const createValue = () => ({ value: '' })
state.address = api.isIP6(props.type)
? Array.from({ length: 8 }, createValue)
: Array.from({ length: 4 }, createValue)
return
}
if (api.isIP6(props.type)) {
state.address = value.split(':').map((item) => ({ value: item }))
if (state.address.length < 8) {
const missingCount = 8 - state.address.length
const emptyIndex = state.address.findIndex((item) => item.value === '')
const insertIndex = emptyIndex >= 0 ? emptyIndex : 0
if (emptyIndex >= 0) {
state.address[emptyIndex].value = '0000'
}
const newItems = Array(missingCount).fill({ value: '0000' })
state.address.splice(insertIndex, 0, ...newItems)
}
} else {
state.address = value.split('.').map((item) => ({ value: item }))
}
}
const activeEvent = ({
emit,
parent,
state,
index,
event,
type
}: {
emit: IIpAddressRenderlessParamUtils['emit']
parent: IIpAddressRenderlessParamUtils['parent']
state: IIpAddressState
index: number
event: any
type: string
}) => {
const target = event && event.target ? event.target : parent.$el.querySelectorAll('input')[index || 0]
type === 'focus' && (state.active = true)
if (!event) {
if (target && !state.isSelected) {
state.isSelected = true
target[type]()
setTimeout(() => {
state.isSelected = false
}, 10)
emit(type, target.value, index)
}
} else {
!state.isSelected && emit(type, target.value, index)
}
}
export const focus =
({
emit,
parent,
state
}: {
emit: IIpAddressRenderlessParamUtils['emit']
parent: IIpAddressRenderlessParamUtils['parent']
state: IIpAddressState
props?: IIpAddressProps
}) =>
({ index, event }: { index: number; event?: any }) => {
activeEvent({ emit, parent, state, index, event, type: 'focus' })
}
export const select =
({
emit,
parent,
state
}: {
emit: IIpAddressRenderlessParamUtils['emit']
parent: IIpAddressRenderlessParamUtils['parent']
state: IIpAddressState
props?: IIpAddressProps
}) =>
({ index, event }: { index: number; event?: any }) => {
activeEvent({ emit, parent, state, index, event, type: 'select' })
}
export const inputEvent =
({ api, componentName, emit, eventName, props }: { api: IIpAddressApi; componentName; emit; eventName; props }) =>
({ item, index }) => {
const val = item.value.trim()
let value: string | undefined = ''
if (api.isIP4(props.type)) {
if (!index && api?.ipValidator?.(val)) {
api?.setValue?.(val)
} else if (isNaN(val) || val < IPTHRESHOLD.Min || val > IPTHRESHOLD.Max) {
item.value = ''
}
} else {
if (!index && api?.ipValidator?.(val)) {
api?.setValue?.(val)
} else if (val.length > 4) {
item.value = item.value.slice(0, 4)
}
}
value = api?.getValue?.()
emit('update:modelValue', value, index)
emit('input', value, index)
api.dispatch(componentName, eventName, [value])
}
export const change =
({ api, emit }: { api: IIpAddressApi; emit: IIpAddressRenderlessParamUtils['emit'] }) =>
() => {
const value = api?.getValue?.()
emit('change', value)
}
export const blur =
({
api,
componentName,
emit,
eventName,
props,
state
}: {
api: IIpAddressApi
componentName: string
emit: IIpAddressRenderlessParamUtils['emit']
eventName: string
props: IIpAddressProps
state: IIpAddressState
}) =>
({ item, index }) => {
state.active = false
state.isDel = false
if (api.isIP4(props.type)) {
item.value = item.value.replace(/\D/g, '')
}
emit('blur', item.value, index)
api.dispatch(componentName, eventName, [item.value])
}
export const keyup =
({ api, props }: { api: IIpAddressApi; props: IIpAddressProps; parent?: IIpAddressRenderlessParamUtils['parent'] }) =>
({ item, index, event }) => {
const { keyCode } = event
const value = item.value.trim()
const nextIndex = index + 1
if (api.isIP4(props.type)) {
if (isNaN(item.value)) {
item.value = item.value.replace(/\D/g, '')
return false
}
if ([KEY_CODE.Tab, KEY_CODE.Space, KEY_CODE.NumpadDecimal, KEY_CODE.NumpadComma].includes(keyCode) && value) {
api.select({ index: nextIndex })
return false
}
if ((value === '0' || value > IPTHRESHOLD.NonNumeric || value.length === 3) && !isNaN(event.key)) {
api.focus({ index: nextIndex })
api.select({ index: nextIndex })
return false
}
}
if (api.isIP6(props.type)) {
if ([KEY_CODE.Tab, KEY_CODE.Space, KEY_CODE.NumpadDecimal, KEY_CODE.NumpadComma].includes(keyCode) && value) {
api.select({ index: nextIndex })
return false
}
if (
(value.length === 4 || value === '0000') &&
(!isNaN(event.key) || (keyCode >= KEY_CODE.KeyA && keyCode <= KEY_CODE.KeyF))
) {
api.focus({ index: nextIndex })
api.select({ index: nextIndex })
return false
}
}
}
const checkError1 = ({
Tab,
Space,
NumpadDecimal,
NumpadComma,
keyCode,
value
}: {
Tab: number
Space: number
NumpadDecimal: number
NumpadComma: number
keyCode: number
value: string
}) => [Tab, Space, NumpadDecimal, NumpadComma].includes(keyCode) && value
const checkError2 = (newValue) => newValue && (isNaN(newValue) || newValue > IPTHRESHOLD.Max)
const checkError3 = ({ isfilterKeyCodes, isSelected, value }) => !isfilterKeyCodes && !isSelected && value === '0'
const checkError4 = ({ isfilterKeyCodes, isSelected, value, key }) =>
!isfilterKeyCodes && !isSelected && value + key > IPTHRESHOLD.Max
const checkError5 = ({ key, isfilterKeyCodes, value, ctrlKey, keyCode, KeyV }) =>
isNaN(key) && !isfilterKeyCodes && !(!value && ctrlKey && keyCode === KeyV)
const isError = ({
key,
value,
isSelected,
isfilterKeyCodes,
ctrlKey,
keyCode,
newValue
}: {
key: string
value: string
isSelected: boolean
isfilterKeyCodes: boolean
ctrlKey: boolean
keyCode: number
newValue: string | false
api?: IIpAddressApi
props?: IIpAddressProps
}) => {
const { Tab, Space, NumpadDecimal, NumpadComma, KeyV } = KEY_CODE
return (
checkError1({ Tab, Space, NumpadDecimal, NumpadComma, keyCode, value }) ||
checkError2(newValue) ||
checkError3({ isfilterKeyCodes, isSelected, value }) ||
checkError4({ isfilterKeyCodes, isSelected, value, key }) ||
checkError5({ key, isfilterKeyCodes, value, ctrlKey, keyCode, KeyV })
)
}
export const keydown =
({ api, props, state }: { api: IIpAddressApi; props: IIpAddressProps; state: IIpAddressState }) =>
({ item, index, event }: { item; index: number; event: KeyboardEvent }) => {
const { target, key, keyCode, ctrlKey } = event
const value = item.value
const selectionStart = (target as KeyboardEvent['target'] & { selectionStart: number })?.selectionStart
const selectionEnd = (target as KeyboardEvent['target'] & { selectionEnd: number })?.selectionEnd
const isSelected = selectionStart !== selectionEnd
const cursorPosition = api.getCursorPosition(target)
const isfilterKeyCodes = state.filterKeyCodes.includes(keyCode)
const nextIndex = index + 1
const lastIndex = index - 1
const newValue =
isSelected && !isfilterKeyCodes && value.substr(0, selectionStart) + key + value.substr(selectionEnd)
state.isDel = keyCode === KEY_CODE.Backspace || keyCode === KEY_CODE.Delete
if (keyCode === KEY_CODE.Backspace && cursorPosition === 0 && !selectionEnd) {
api.focus({ index: lastIndex })
return false
}
if (keyCode === KEY_CODE.ArrowLeft && cursorPosition === 0) {
api.focus({ index: lastIndex })
event.preventDefault()
return false
}
if (keyCode === KEY_CODE.ArrowRight && cursorPosition === value.length) {
api.focus({ index: nextIndex })
event.preventDefault()
return false
}
if (
isError({
key,
value,
isSelected,
isfilterKeyCodes,
ctrlKey,
keyCode,
newValue,
api,
props
})
) {
event.preventDefault()
return false
}
}