Skip to content

Commit

Permalink
feat: use overlay to style elements
Browse files Browse the repository at this point in the history
  • Loading branch information
dbajpeyi committed Nov 4, 2024
1 parent 7a69964 commit 65dac43
Showing 1 changed file with 62 additions and 15 deletions.
77 changes: 62 additions & 15 deletions injected/src/features/autofill-password-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const ANIMATION_DURATION_MS = 1000
const ANIMATION_ITERATIONS = Infinity
const BACKGROUND_COLOR_START = 'rgba(85, 127, 243, 0.10)'
const BACKGROUND_COLOR_END = 'rgba(85, 127, 243, 0.25)'
const OVERLAY_ID = 'ddg-password-import-overlay'

/**
* This feature is responsible for animating some buttons passwords.google.com,
Expand Down Expand Up @@ -66,7 +67,7 @@ export default class AutofillPasswordImport extends ContentFeature {
/**
* Takes a path and returns the element and style to animate.
* @param {string} path
* @returns {Promise<{element: HTMLElement|Element, style: any, shouldTap: boolean}|null>}
* @returns {Promise<{element: HTMLElement|Element, style: any, shouldTap: boolean, shouldWatchForRemoval: boolean}|null>}
*/
async getElementAndStyleFromPath (path) {
if (path === '/') {
Expand All @@ -75,7 +76,8 @@ export default class AutofillPasswordImport extends ContentFeature {
? {
style: this.settingsButtonStyle,
element,
shouldTap: this.#settingsButtonSettings?.shouldAutotap ?? false
shouldTap: this.#settingsButtonSettings?.shouldAutotap ?? false,
shouldWatchForRemoval: false
}
: null
} else if (path === '/options') {
Expand All @@ -84,7 +86,8 @@ export default class AutofillPasswordImport extends ContentFeature {
? {
style: this.exportButtonStyle,
element,
shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false
shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false,
shouldWatchForRemoval: true
}
: null
} else if (path === '/intro') {
Expand All @@ -93,40 +96,77 @@ export default class AutofillPasswordImport extends ContentFeature {
? {
style: this.signInButtonStyle,
element,
shouldTap: this.#signInButtonSettings?.shouldAutotap ?? false
shouldTap: this.#signInButtonSettings?.shouldAutotap ?? false,
shouldWatchForRemoval: false
}
: null
} else {
return null
}
}

insertOverlayElement (mainElement, offsetLeft, offsetTop) {
hasNoOtherSiblings (element) {
return element.parentNode && element.parentNode.children.length === 1
}

removeOverlayIfNeeded () {
const existingOverlay = document.getElementById(OVERLAY_ID)
if (existingOverlay != null) {
existingOverlay.style.display = 'none'
existingOverlay.remove()
}
}

insertOverlayElement (mainElement, offsetLeft, offsetTop, isRound = false) {
this.removeOverlayIfNeeded()

const overlay = document.createElement('div')
overlay.setAttribute('id', OVERLAY_ID)
const svgElement = mainElement.parentNode?.querySelector('svg') ?? mainElement.querySelector('svg')
const elementToCenterOn = isRound ? svgElement : mainElement
const { top, left, width, height } = elementToCenterOn.getBoundingClientRect()
overlay.style.position = 'absolute'

// FIXME: Workaround for the overlay not being positioned correctly
overlay.style.top = `calc(${mainElement.offsetTop}px - ${offsetTop}em)`
overlay.style.left = `calc(${mainElement.offsetLeft}px - ${offsetLeft}em)`
const dimensions = mainElement.getBoundingClientRect()
overlay.style.width = `${dimensions.width}px`
overlay.style.height = `${dimensions.height}px`
overlay.style.top = `calc(${top}px + ${window.scrollY}px - ${isRound ? height / 2 : 0}px - 1px - ${offsetTop}em)`
overlay.style.left = `calc(${left}px + ${window.scrollX}px - ${isRound ? width / 2 : 0}px - 1px - ${offsetLeft}em)`

const mainElementRect = mainElement.getBoundingClientRect()
overlay.style.width = `${mainElementRect.width}px`
overlay.style.height = `${mainElementRect.height}px`
overlay.style.zIndex = '999'

// Ensure overlay is non-interactive
overlay.style.pointerEvents = 'none'

// Ensure that the element is injected before the parent to avoid z-index issues
mainElement.parentNode.insertBefore(overlay, mainElement.nextSibling)
// insert in document.body
document.body.appendChild(overlay)
return overlay
}

observeElementRemoval (element, onRemoveCallback) {
// Set up the mutation observer
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Check if the element has been removed from its parent
if (mutation.type === 'childList' && !document.contains(element)) {
// Element has been removed
onRemoveCallback()
observer.disconnect() // Stop observing
}
})
})

// Start observing the parent node for child list changes
observer.observe(document.body, { childList: true, subtree: true })
}

/**
* Moves the element into view and animates it.
* @param {HTMLElement|Element} element
* @param {any} style
*/
animateElement (element, style) {
const overlay = this.insertOverlayElement(element, style.offsetLeft, style.offsetTop)
const overlay = this.insertOverlayElement(element, style.offsetLeft, style.offsetTop, style.borderRadius === '100%')
overlay.scrollIntoView({
behavior: 'smooth',
block: 'center',
Expand Down Expand Up @@ -201,9 +241,16 @@ export default class AutofillPasswordImport extends ContentFeature {
]
if (supportedPaths.includes(path)) {
try {
const { element, style, shouldTap } = await this.getElementAndStyleFromPath(path) ?? {}
this.removeOverlayIfNeeded()
const { element, style, shouldTap, shouldWatchForRemoval } = await this.getElementAndStyleFromPath(path) ?? {}
if (element != null) {
shouldTap ? this.autotapElement(element) : setTimeout(() => this.animateElement(element, style), 300)
if (shouldWatchForRemoval) {
// Sometimes navigation events are not triggered, then we need to watch for removal
this.observeElementRemoval(element, () => {
this.removeOverlayIfNeeded()
})
}
}
} catch {
console.error('password-import: handleElementForPath failed for path:', path)
Expand Down

0 comments on commit 65dac43

Please sign in to comment.