Skip to content

Commit

Permalink
FEAT: Improved xpath selector logic to include text and allow for man…
Browse files Browse the repository at this point in the history
…ual modification
  • Loading branch information
ronparkdev committed Jul 15, 2024
1 parent 3e68d4c commit c3111b6
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 112 deletions.
193 changes: 112 additions & 81 deletions src/components/TargetEditLayer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
import { Button, Slider, Modal, Form, Typography, Flex, Mentions } from 'antd'
import type { CollapseProps } from 'antd'
import { Button, Slider, Modal, Typography, Collapse, Alert, Input } from 'antd'
import { useTargetsConfig } from 'hooks/config'
import { showSavedToast } from 'notification'
import type { FC } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import Draggable from 'react-draggable'
import { useHotkeys } from 'react-hotkeys-hook'
import { DomService } from 'services/dom'
import type { HotKey } from 'services/hotKey'
import { HotKeyService } from 'services/hotKey'

const { Title, Text, Paragraph } = Typography
const { Title, Paragraph } = Typography

type Props = {
onChangeHighlight: (xPathSelector: string | null) => void
onChangeHighlight: (element: Element | null) => void
onClose: () => void
defaultSelector?: string
defaultUrl?: string
defaultElement: Element
defaultUrl: string
}

export const TargetEditLayer: FC<Props> = ({ onChangeHighlight, onClose, defaultSelector = '', defaultUrl = '' }) => {
export const TargetEditLayer: FC<Props> = ({ onChangeHighlight, onClose, defaultElement, defaultUrl }) => {
const [open, setOpen] = useState(true)
const [selector, setSelector] = useState<string | null>(null)

const [targets, setTargets] = useTargetsConfig()

const selectors = useMemo(() => defaultSelector.split('/'), [defaultSelector])
const [selectorMaxIndex, setSelectorMaxIndex] = useState(Math.max(0, selectors.length))
const selector = selectors.slice(0, selectorMaxIndex).join('/')
const elements = useMemo(() => {
const elements: Element[] = []

let el: Element | null = defaultElement
while (el !== null) {
elements.unshift(el)
el = el.parentElement
}

return elements
}, [defaultElement])

const [elementIndex, setElementIndex] = useState(elements.length - 1)

const element = elements[elementIndex]

useMemo(() => {
setSelector(DomService.getSafeXPath(element))
onChangeHighlight(element)
}, [element])

const urlParts = useMemo(() => defaultUrl.split('/'), [defaultUrl])
const [urlPartMaxIndex, setUrlPartMaxIndex] = useState(urlParts.length)
Expand All @@ -38,18 +58,14 @@ export const TargetEditLayer: FC<Props> = ({ onChangeHighlight, onClose, default
keyRef.current?.focus()
}, [])

useEffect(() => {
onChangeHighlight(selector)
}, [selector])

const handleClose = () => {
onChangeHighlight(null)
onClose()
setOpen(false)
}

const handleSave = () => {
if (hotKey !== null) {
if (hotKey !== null && selector !== null) {
setTargets([...targets, { selector, url, hotKey }])
}
showSavedToast()
Expand All @@ -61,39 +77,16 @@ export const TargetEditLayer: FC<Props> = ({ onChangeHighlight, onClose, default
setHotKey(HotKeyService.parse(event))
})

const isValid = HotKeyService.isValid(hotKey)
const isSelectorValid = selector !== null && DomService.findElementsByXPath(selector).includes(element)
const isValid = HotKeyService.isValid(hotKey) && isSelectorValid

return (
<Modal
open={open}
okButtonProps={{ disabled: !isValid }}
onOk={handleSave}
onCancel={handleClose}
onClose={handleClose}
mask={false}
maskClosable={false}
title={
<Title level={3} style={{ marginTop: 15 }}>
Set Shortcut for This Item
</Title>
}
modalRender={modal => (
<Draggable>
<div style={{ cursor: 'move' }}>{modal}</div>
</Draggable>
)}
centered>
<Form layout="vertical">
<Form.Item
label={
<Flex vertical>
<Text strong style={{ fontSize: '16px' }}>
Shortcut Key
</Text>
<Paragraph>Press the keys you want to set as a shortcut for this item.</Paragraph>
</Flex>
}
style={{ marginBottom: 12 }}>
const items: CollapseProps['items'] = [
{
key: '1',
label: 'Shortcut Key',
children: (
<div>
<Paragraph>Press the keys you want to set as a shortcut for this item.</Paragraph>
<Button
ref={keyRef}
onFocus={() => setHotKeyListening(true)}
Expand All @@ -105,51 +98,89 @@ export const TargetEditLayer: FC<Props> = ({ onChangeHighlight, onClose, default
? 'Press keys to record shortcut'
: 'Click here to record shortcut'}
</Button>
</Form.Item>
<Form.Item
label={
<Flex vertical>
<Text strong style={{ fontSize: '16px' }}>
Select Item Range
</Text>
<Paragraph>
Adjust the slider to select the specific range of the item you want to set a shortcut for.
</Paragraph>
</Flex>
}
style={{ marginBottom: 12 }}>
{/* <Text>{selector}</Text> */}
</div>
),
},
{
key: '2',
label: 'Select Item',
children: (
<div>
<Paragraph>
Adjust the slider to select the specific range of the item you want to set a shortcut for.
</Paragraph>
<Slider
min={2}
max={selectors.length}
defaultValue={selectorMaxIndex}
onChange={setSelectorMaxIndex}
min={0}
max={elements.length - 1}
value={elementIndex}
onChange={setElementIndex}
tooltip={{ open: false }}
/>
</Form.Item>
<Form.Item
label={
<Flex vertical>
<Text strong style={{ fontSize: '16px' }}>
URL Range
</Text>
<Paragraph>
Adjust the slider to decide if the shortcut should apply to the entire website, this specific page, or a
custom path.
</Paragraph>
</Flex>
}
style={{ marginBottom: 12 }}>
<Mentions readOnly={true} variant="filled" value={url} />
</div>
),
},
{
key: '3',
label: 'URL Range',
children: (
<div>
<Paragraph>
Adjust the slider to decide if the shortcut should apply to the entire website, this specific page, or a
custom path.
</Paragraph>
<Input.TextArea readOnly={true} variant="filled" value={url} autoSize />
<Slider
min={2}
max={urlParts.length}
defaultValue={urlPartMaxIndex}
onChange={setUrlPartMaxIndex}
tooltip={{ open: false }}
/>
</Form.Item>
</Form>
</div>
),
},
{
key: '4',
label: 'Advanced options',
children: (
<div>
<Paragraph>You can modify the XPath selector directly.</Paragraph>
<Input.TextArea
variant="outlined"
value={selector ?? ''}
autoSize
onChange={e => setSelector(e.target.value)}
status={isSelectorValid ? '' : 'error'}
/>
{!isSelectorValid && (
<Alert description="The modified xpath does not match the element." type="error" showIcon />
)}
</div>
),
},
]

return (
<Modal
open={open}
okButtonProps={{ disabled: !isValid }}
onOk={handleSave}
onCancel={handleClose}
onClose={handleClose}
mask={false}
maskClosable={false}
title={
<Title level={3} style={{ marginTop: 15 }}>
Set Shortcut for This Item
</Title>
}
modalRender={modal => (
<Draggable>
<div style={{ cursor: 'move' }}>{modal}</div>
</Draggable>
)}
centered>
<Collapse defaultActiveKey={['1', '2', '3']} items={items} />
</Modal>
)
}
13 changes: 6 additions & 7 deletions src/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,18 @@ void (async () => {
DomHighlightService.injectStyle()

const showDialog = async ({ element }: { element: Element }) => {
const selector = DomService.getXPath(element)
const defaultUrl = UrlUtils.getCurrentUrl()
void render({ visible: true, defaultSelector: selector, defaultUrl })
void render({ visible: true, defaultElement: element, defaultUrl })
}

const render = async ({
visible,
defaultSelector,
defaultElement,
defaultUrl,
}: {
visible: boolean
defaultSelector?: string
defaultUrl?: string
defaultElement: Element
defaultUrl: string
}) => {
const [React, { createRoot }, { TargetEditLayer }] = await Promise.all([
import('react'),
Expand All @@ -154,11 +153,11 @@ void (async () => {
<StrictMode>
{visible && (
<TargetEditLayer
defaultSelector={defaultSelector}
defaultElement={defaultElement}
defaultUrl={defaultUrl}
onChangeHighlight={DomHighlightService.highlight}
onClose={() => {
render({ visible: false })
render({ visible: false, defaultElement, defaultUrl })
}}
/>
)}
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { ConfigService, type TargetConfig } from 'services/config'

import { useChromeStorage } from './chromeStorage'

const DEFAULT_TARGETS: TargetConfig[] = []

const useTargetsConfig = () => {
return useChromeStorage<TargetConfig[]>('local', ConfigService.TARGETS_KEY, [])
return useChromeStorage<TargetConfig[]>('local', ConfigService.TARGETS_KEY, DEFAULT_TARGETS)
}

export { useTargetsConfig }
Loading

0 comments on commit c3111b6

Please sign in to comment.