Skip to content

Commit

Permalink
fix: using shadow DOM to prevent style pollution issues
Browse files Browse the repository at this point in the history
  • Loading branch information
yetone committed Mar 6, 2023
1 parent db87f82 commit 89b2597
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 89 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-icons": "^3.0.0",
"@types/xregexp": "^4.4.0",
"@webcomponents/webcomponentsjs": "^2.7.0",
"baseui": "^12.2.0",
"rc-field-form": "^1.27.4",
"react": "^17.0.1",
"react": "^18.0.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.1",
"react-dom": "^18.0.1",
"react-hot-toast": "^2.4.0",
"react-icons": "^4.8.0",
"react-jss": "^10.10.0",
Expand Down
14 changes: 5 additions & 9 deletions src/content/PopupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Select, Value, Option } from 'baseui/select'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { RxCopy } from 'react-icons/rx'
import { HiOutlineSpeakerWave } from 'react-icons/hi2'
import { queryPopupCardElement } from './utils'

const langOptions: Value = supportLanguages.reduce((acc, [id, label]) => {
return [
Expand All @@ -24,8 +25,6 @@ const langOptions: Value = supportLanguages.reduce((acc, [id, label]) => {
]
}, [] as Value)

const engine = new Styletron()

const useStyles = createUseStyles({
popupCard: {},
popupCardHeaderContainer: {
Expand Down Expand Up @@ -121,6 +120,7 @@ const useStyles = createUseStyles({

export interface IPopupCardProps {
text: string
engine: Styletron
}

const loadingIcons = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']
Expand Down Expand Up @@ -169,11 +169,6 @@ export function PopupCard(props: IPopupCardProps) {
return undefined
}

const $popupCard: HTMLDivElement | null = document.querySelector(`#${popupCardID}`)
if (!$popupCard) {
return
}

let closed = true

const dragMouseDown = (e: MouseEvent) => {
Expand All @@ -184,7 +179,8 @@ export function PopupCard(props: IPopupCardProps) {
document.addEventListener('mousemove', elementDrag)
}

const elementDrag = (e: MouseEvent) => {
const elementDrag = async (e: MouseEvent) => {
const $popupCard = await queryPopupCardElement()
if (!$popupCard) {
return
}
Expand Down Expand Up @@ -257,7 +253,7 @@ export function PopupCard(props: IPopupCardProps) {
}, [])

return (
<StyletronProvider value={engine}>
<StyletronProvider value={props.engine}>
<BaseProvider theme={LightTheme}>
<div className={styles.popupCard}>
<div>
Expand Down
1 change: 1 addition & 0 deletions src/content/consts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const zIndex = '2147483647'
export const popupThumbID = '__yetone-openai-translator-popup-thumb'
export const popupCardID = '__yetone-openai-translator-popup-card'
export const containerTagName = 'yetone-openai-translator'
116 changes: 57 additions & 59 deletions src/content/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import '@webcomponents/webcomponentsjs'
import React from 'react'
import ReactDOM from 'react-dom'
import icon from './assets/images/icon.png'
import { popupCardID, popupThumbID, zIndex } from './consts'
import { containerTagName, popupCardID, popupThumbID, zIndex } from './consts'
import { PopupCard } from './PopupCard'
import { getContainer, queryPopupCardElement, queryPopupThumbElement } from './utils'
import { create } from 'jss'
import preset from 'jss-preset-default'
import { JssProvider, createGenerateId } from 'react-jss'
import { Client as Styletron } from 'styletron-engine-atomic'

let hidePopupThumbTimer: number | null = null
const generateId = createGenerateId()
const hidePopupThumbTimer: number | null = null

function popupThumbClickHandler(event: MouseEvent) {
async function popupThumbClickHandler(event: MouseEvent) {
event.stopPropagation()
event.preventDefault()
const $popupThumb: HTMLDivElement | null = document.querySelector(`#${popupThumbID}`)
const $popupThumb: HTMLDivElement | null = await queryPopupThumbElement()
if (!$popupThumb) {
return
}
Expand All @@ -18,84 +25,48 @@ function popupThumbClickHandler(event: MouseEvent) {
showPopupCard(x, y, $popupThumb.dataset['text'] || '')
}

async function tryToRemoveContainer() {
const $popupThumb: HTMLDivElement | null = document.querySelector(`#${popupThumbID}`)
const $popupCard: HTMLDivElement | null = document.querySelector(`#${popupCardID}`)
if (
$popupThumb &&
$popupThumb.style.display === 'none' &&
$popupCard &&
$popupCard.style.display === 'none'
) {
const $container = await getContainer()
$container.remove()
}
async function removeContainer() {
const $container = await getContainer()
$container.remove()
}

async function hidePopupThumb() {
if (hidePopupThumbTimer) {
clearTimeout(hidePopupThumbTimer)
const $popupThumb: HTMLDivElement | null = await queryPopupThumbElement()
if (!$popupThumb) {
return
}
hidePopupThumbTimer = window.setTimeout(() => {
const $popupThumb: HTMLDivElement | null = document.querySelector(`#${popupThumbID}`)
if (!$popupThumb) {
return
}
$popupThumb.style.display = 'none'
tryToRemoveContainer()
}, 100)
removeContainer()
}

async function hidePopupCard() {
const $popupCard: HTMLDivElement | null = document.querySelector(`#${popupCardID}`)
const $popupCard: HTMLDivElement | null = await queryPopupCardElement()
if (!$popupCard) {
return
}
chrome.runtime.sendMessage({
type: 'stopSpeaking',
})
$popupCard.style.display = 'none'
ReactDOM.unmountComponentAtNode($popupCard)
await tryToRemoveContainer()
}

async function getContainer(): Promise<HTMLElement> {
const containerTagName = 'yetone-openai-translator'
let $container: HTMLElement | null = document.querySelector(containerTagName)
if (!$container) {
$container = document.createElement(containerTagName)
$container.style.zIndex = zIndex
return new Promise((resolve) => {
setTimeout(() => {
const $html = document.body.parentElement
if ($html) {
$html.appendChild($container as HTMLElement)
} else {
document.appendChild($container as HTMLElement)
}
resolve($container as HTMLElement)
}, 100)
})
}
return new Promise((resolve) => {
resolve($container as HTMLElement)
})
removeContainer()
}

async function showPopupCard(x: number, y: number, text: string) {
if (!text) {
return
}
hidePopupThumb()
let $popupCard: HTMLDivElement | null = document.querySelector(`#${popupCardID}`)
const $popupThumb: HTMLDivElement | null = await queryPopupThumbElement()
if ($popupThumb) {
$popupThumb.style.display = 'none'
}
let $popupCard: HTMLDivElement | null = await queryPopupCardElement()
if (!$popupCard) {
$popupCard = document.createElement('div')
$popupCard.id = popupCardID
$popupCard.style.position = 'absolute'
$popupCard.style.zIndex = zIndex
$popupCard.style.background = '#fff'
$popupCard.style.borderRadius = '4px'
$popupCard.style.boxShadow = '0 0 6px rgba(0,0,0,.3)'
$popupCard.style.boxShadow = '0 0 8px rgba(0,0,0,.3)'
$popupCard.style.minWidth = '200px'
$popupCard.style.maxWidth = '600px'
$popupCard.style.lineHeight = '1.6'
Expand All @@ -110,17 +81,32 @@ async function showPopupCard(x: number, y: number, text: string) {
event.stopPropagation()
})
const $container = await getContainer()
$container.appendChild($popupCard)
$container.shadowRoot?.querySelector('div')?.appendChild($popupCard)
}
$popupCard.style.display = 'block'
$popupCard.style.width = 'auto'
$popupCard.style.height = 'auto'
$popupCard.style.opacity = '100'
$popupCard.style.left = `${x}px`
$popupCard.style.top = `${y}px`
const engine = new Styletron({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
container: $popupCard.parentElement as any,
})
const jss = create().setup({
...preset(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
insertionPoint: $popupCard.parentElement as any,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const JSS = JssProvider as any
ReactDOM.render(
<React.StrictMode>
<PopupCard text={text} />
<div>
<JSS jss={jss} generateId={generateId}>
<PopupCard text={text} engine={engine} />
</JSS>
</div>
</React.StrictMode>,
$popupCard,
)
Expand All @@ -133,7 +119,7 @@ async function showPopupThumb(text: string, x: number, y: number) {
if (hidePopupThumbTimer) {
clearTimeout(hidePopupThumbTimer)
}
let $popupThumb: HTMLDivElement | null = document.querySelector(`#${popupThumbID}`)
let $popupThumb: HTMLDivElement | null = await queryPopupThumbElement()
if (!$popupThumb) {
$popupThumb = document.createElement('div')
$popupThumb.id = popupThumbID
Expand Down Expand Up @@ -165,7 +151,7 @@ async function showPopupThumb(text: string, x: number, y: number) {
$img.style.height = '100%'
$popupThumb.appendChild($img)
const $container = await getContainer()
$container.appendChild($popupThumb)
$container.shadowRoot?.querySelector('div')?.appendChild($popupThumb)
}
$popupThumb.dataset['text'] = text
$popupThumb.style.display = 'block'
Expand All @@ -174,6 +160,18 @@ async function showPopupThumb(text: string, x: number, y: number) {
$popupThumb.style.top = `${y}px`
}

customElements.define(
containerTagName,
class extends HTMLElement {
constructor() {
super()
const shadowRoot = this.attachShadow({ mode: 'open' })
const $container = document.createElement('div')
shadowRoot.appendChild($container)
}
},
)

document.addEventListener('mouseup', (event: MouseEvent) => {
window.setTimeout(() => {
const text = (window.getSelection()?.toString() ?? '').trim()
Expand Down
38 changes: 38 additions & 0 deletions src/content/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { containerTagName, popupCardID, popupThumbID, zIndex } from './consts'

export async function getContainer(): Promise<HTMLElement> {
let $container: HTMLElement | null = document.querySelector(containerTagName)
if (!$container) {
$container = document.createElement(containerTagName)
$container.style.zIndex = zIndex
return new Promise((resolve) => {
setTimeout(() => {
const $container_: HTMLElement | null = document.querySelector(containerTagName)
if ($container_) {
resolve($container_)
return
}
const $html = document.body.parentElement
if ($html) {
$html.appendChild($container as HTMLElement)
} else {
document.appendChild($container as HTMLElement)
}
resolve($container as HTMLElement)
}, 100)
})
}
return new Promise((resolve) => {
resolve($container as HTMLElement)
})
}

export async function queryPopupThumbElement(): Promise<HTMLDivElement | null> {
const $container = await getContainer()
return $container.shadowRoot?.getElementById(popupThumbID) as HTMLDivElement | null
}

export async function queryPopupCardElement(): Promise<HTMLDivElement | null> {
const $container = await getContainer()
return $container.shadowRoot?.getElementById(popupCardID) as HTMLDivElement | null
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
"jsx": "react",
"typeRoots": [ "node_modules/@types" ]
}
}
}
Loading

0 comments on commit 89b2597

Please sign in to comment.