Appearance
useSpotlightOverlay Composable ​
javascript
import { ref, reactive, computed } from 'vue'
// Initial state
const createInitialState = () => ({
positionX: -100000,
positionY: -100000,
previousPositionX: -100000,
previousPositionY: -100000,
arrowPosition: 'top',
previousArrowPosition: 'top',
showFloatingInstructionBar: false,
barConfig: {},
highlightX: -1,
highlightY: -1,
highlightRadius: 0,
clipPathX: 0,
clipPathY: 0,
clipPathHeight: 10000,
clipPathWidth: 10000,
highlightedElements: [],
showRenderReadyInstructionBar: false,
highlightAreaBorderStyles: {
borderRadius: '0',
borderWidth: '0',
},
isAnimating: false,
animationDuration: 300 // milliseconds
})
// Reactive state
const state = reactive(createInitialState())
// Utility functions
const calculateHighlightPosition = (rect) => {
const highlightX = rect.left + rect.width / 2
const highlightY = rect.top + rect.height / 2
const highlightRadius = Math.max(rect.width / 2, rect.height / 2)
return { highlightX, highlightY, highlightRadius }
}
const calculateClipPathPosition = (rect) => {
const clipPathX = rect.left
const clipPathY = rect.top
const clipPathHeight = rect.height
const clipPathWidth = rect.width
return { clipPathX, clipPathY, clipPathHeight, clipPathWidth }
}
const calculateArrowPosition = (rect, arrowPosition) => {
let positionX = rect.left + rect.width / 2
let positionY = rect.top + rect.height / 2
if (arrowPosition.startsWith('top')) {
positionY = rect.bottom
} else if (arrowPosition.startsWith('bottom')) {
positionY = rect.top
} else if (arrowPosition === 'right') {
positionX = rect.left
} else if (arrowPosition === 'left') {
positionX = rect.right
} else if (arrowPosition === 'leftTop') {
positionY = rect.top + 16
positionX = rect.right
} else if (arrowPosition === 'leftBottom') {
positionY = rect.bottom - 16
positionX = rect.right
} else if (arrowPosition === 'RightTop') {
positionY = rect.top + 16
positionX = rect.left
} else if (arrowPosition === 'RightBottom') {
positionY = rect.bottom - 16
positionX = rect.left
}
return { positionX, positionY }
}
// Actions
const setPosition = (data) => {
// Store previous position before updating
state.previousPositionX = state.positionX
state.previousPositionY = state.positionY
state.positionX = data.positionX
state.positionY = data.positionY
}
const setPositionWithAnimation = (data) => {
// Store previous position before updating
state.previousPositionX = state.positionX
state.previousPositionY = state.positionY
// Set animation flag
state.isAnimating = true
// Update position
state.positionX = data.positionX
state.positionY = data.positionY
// Clear animation flag after duration
setTimeout(() => {
state.isAnimating = false
}, state.animationDuration)
}
const setHighlight = (data) => {
state.highlightX = data.highlightX
state.highlightY = data.highlightY
state.highlightRadius = data.highlightRadius
}
const setClipPath = (data) => {
state.clipPathX = data.clipPathX
state.clipPathY = data.clipPathY
state.clipPathHeight = data.clipPathHeight
state.clipPathWidth = data.clipPathWidth
}
const addHighlightedElements = ({ elementId, className }) => {
// Add element to highlighted elements array
state.highlightedElements.push({ elementId, className })
// Apply the className to the element by elementId
const element = document.getElementById(elementId)
if (element) {
element.classList.add(className)
}
}
const resetHighlightedElements = () => {
// Reset the array and remove the class from all highlighted elements
state.highlightedElements.forEach(({ elementId, className }) => {
const element = document.getElementById(elementId)
if (element) {
element.classList.remove(className)
}
})
// Clear the highlighted elements array
state.highlightedElements = []
}
const setInstructionBarConfig = (data) => {
// Store previous arrow position
state.previousArrowPosition = state.arrowPosition
if (data.position) {
// Use animation if the bar is already visible and position is changing
if (state.showFloatingInstructionBar &&
(state.positionX !== data.position.positionX || state.positionY !== data.position.positionY)) {
setPositionWithAnimation(data.position)
} else {
setPosition(data.position)
}
}
if (data.clipPath) {
setClipPath(data.clipPath)
} else {
setClipPath({
clipPathX: 0,
clipPathY: 0,
clipPathHeight: 0,
clipPathWidth: 0,
})
}
if (data.highlight) {
setHighlight(data.highlight)
} else {
setHighlight({ highlightX: -1, highlightY: -1, highlightRadius: 0 })
}
state.arrowPosition = data.arrowPosition
state.showFloatingInstructionBar = data.isVisible
state.barConfig = { ...data.config, elementId: data.elementId }
}
const resetInstructionBarConfig = () => {
setPosition({
positionX: -100000,
positionY: -100000,
previousPositionX: -100000,
previousPositionY: -100000,
})
state.showFloatingInstructionBar = false
state.barConfig = {}
state.clipPathX = 0,
state.clipPathY = 0,
state.clipPathHeight = 10000,
state.clipPathWidth = 10000,
resetHighlightedElements()
}
const setHighlightAreaBorderStyles = (data) => {
state.highlightAreaBorderStyles = {
...createInitialState().highlightAreaBorderStyles,
...data
}
}
const setInstructionBarConfigByElementId = (data) => {
if (!data || !data.elementId) return
let payload = { ...data }
const element = document.getElementById(data.elementId)
if (!element) return
// Extract border styles from the element and set them in the state
const computedStyles = window.getComputedStyle(element)
const elementBorderRadius = computedStyles.getPropertyValue('border-radius')
let borderRadius = '0'
if(elementBorderRadius.includes('%')) {
borderRadius = `calc(${elementBorderRadius})`
} else {
borderRadius = `calc(${elementBorderRadius} + 9999px)`
}
setHighlightAreaBorderStyles({
// borderRadius: `calc(${computedStyles.getPropertyValue('border-radius')}px + 10000px)`,
borderRadius,
borderWidth: '9999px',
})
const rect = element.getBoundingClientRect()
const { highlightX, highlightY, highlightRadius } = calculateHighlightPosition(rect)
const { positionX, positionY } = calculateArrowPosition(rect, data.arrowPosition)
const { clipPathX, clipPathY, clipPathHeight, clipPathWidth } = calculateClipPathPosition(rect)
payload.position = { positionX, positionY }
payload.highlight = { highlightX, highlightY, highlightRadius }
payload.clipPath = { clipPathX, clipPathY, clipPathHeight, clipPathWidth }
highlightElement({ elementId: data.elementId, className: (data.elementHighlightClassName || 'tour-highlight') })
if (payload.config && payload.config.button) {
let buttonCallback = () => {}
if (payload.config.button.callback) {
buttonCallback = payload.config.button.callback
}
payload.config.button.callback = () => {
element.click()
buttonCallback()
resetInstructionBarConfig()
}
}
if (data.elementToHighLightId) {
resetHighlightedElements()
const elementToHighlight = document.getElementById(data.elementToHighLightId)
if (elementToHighlight) {
const highlightRect = elementToHighlight.getBoundingClientRect()
const { highlightX, highlightY, highlightRadius } = calculateHighlightPosition(highlightRect)
const { positionX, positionY } = calculateArrowPosition(highlightRect, data.arrowPosition)
payload.position = { positionX, positionY }
payload.highlight = { highlightX, highlightY, highlightRadius }
highlightElement({ elementId: data.elementToHighLightId, className: (data.elementHighlightClassName || 'tour-highlight') })
}
}
setInstructionBarConfig(payload)
}
const highlightElement = ({ elementId, className }) => {
addHighlightedElements({ elementId, className })
}
// Computed properties (getters)
const getters = {
positionX: computed(() => state.positionX),
positionY: computed(() => state.positionY),
previousPositionX: computed(() => state.previousPositionX),
previousPositionY: computed(() => state.previousPositionY),
arrowPosition: computed(() => state.arrowPosition),
previousArrowPosition: computed(() => state.previousArrowPosition),
showFloatingInstructionBar: computed(() => state.showFloatingInstructionBar),
barConfig: computed(() => state.barConfig),
highlightX: computed(() => state.highlightX),
highlightY: computed(() => state.highlightY),
highlightRadius: computed(() => state.highlightRadius),
clipPathX: computed(() => state.clipPathX),
clipPathY: computed(() => state.clipPathY),
clipPathHeight: computed(() => state.clipPathHeight),
clipPathWidth: computed(() => state.clipPathWidth),
highlightedElements: computed(() => state.highlightedElements),
showRenderReadyInstructionBar: computed(() => state.showRenderReadyInstructionBar),
highlightAreaBorderStyles: computed(() => state.highlightAreaBorderStyles),
isAnimating: computed(() => state.isAnimating),
animationDuration: computed(() => state.animationDuration)
}
// Main composable function
export default function useSpotlightOverlay() {
return {
// State
state,
// Getters
...getters,
// Actions
setInstructionBarConfig,
resetInstructionBarConfig,
setInstructionBarConfigByElementId,
highlightElement,
// Utility functions
setPosition,
setPositionWithAnimation,
setHighlight,
setClipPath,
addHighlightedElements,
resetHighlightedElements
}
}