@@ -52,9 +52,34 @@ interface ImagePreviewAcknowledgement {
viewerId : string
}
interface ImagePreviewView {
centerX : number
centerY : number
scale : number
}
interface ImagePreviewCommandBase {
id : string
requestId : string
sentAt : number
viewerId : string
}
interface ImagePreviewCloseCommand extends ImagePreviewCommandBase {
type : 'close'
}
interface ImagePreviewViewCommand extends ImagePreviewCommandBase {
type : 'view'
view : ImagePreviewView
}
type ImagePreviewCommand = ImagePreviewCloseCommand | ImagePreviewViewCommand
interface EasySlidesSharedState {
easySlidesImagePreview ? : {
acknowledgement ? : ImagePreviewAcknowledgement
command ? : ImagePreviewCommand
request ? : ImagePreviewRequest
}
}
@@ -67,6 +92,15 @@ interface PendingImagePreview {
requestId : string
}
interface ImagePointer {
x : number
y : number
}
const IMAGE _MIN _SCALE = 0.1
const IMAGE _MAX _SCALE = 4
const IMAGE _SCALE _STEP = 0.1
const nav = useNav ( )
const easySharedState = sharedState as typeof sharedState & EasySlidesSharedState
const clientId = createClientId ( )
@@ -78,16 +112,35 @@ const imageInteractions = new Map<HTMLImageElement, ImageInteraction>()
let observer : MutationObserver | undefined
let resizeObserver : ResizeObserver | undefined
let channel : BroadcastChannel | undefined
let imageChannel : BroadcastChannel | undefined
let lightbox : HTMLElement | undefined
let lightboxViewport : HTMLElement | undefined
let lightboxImage : HTMLImageElement | undefined
let lightboxClose : HTMLButtonElement | undefined
let lightboxDecrease : HTMLButtonElement | undefined
let lightboxIncrease : HTMLButtonElement | undefined
let lightboxScaleOutput : HTMLOutputElement | undefined
let activeImage : HTMLImageElement | undefined
let activePreviewRequestId : string | undefined
let pendingImagePreview : PendingImagePreview | undefined
let stopAcknowledgementWatch : ( ( ) => void ) | undefined
let stopCommandWatch : ( ( ) => void ) | undefined
let stopRequestWatch : ( ( ) => void ) | undefined
let fitFrame : number | undefined
let imageViewFrame : number | undefined
let pendingSharedView : ImagePreviewView | undefined
let pendingRemoteView : ImagePreviewView | undefined
let lastHandledRequestId = easySharedState . easySlidesImagePreview ? . request ? . id ? ? ''
let lastHandledCommandId = easySharedState . easySlidesImagePreview ? . command ? . id ? ? ''
let bodyOverflow = ''
let imageScale = 1
let imageCenterX = 0.5
let imageCenterY = 0.5
let imageViewTouched = false
let lightboxPointerMoved = false
let pinchDistance = 0
let pinchMidpoint : ImagePointer | undefined
const imagePointers = new Map < number , ImagePointer > ( )
function createClientId ( ) {
if ( typeof crypto . randomUUID === 'function' )
@@ -227,6 +280,260 @@ function applyScale(key: string, kind: ScalableKind, scale: number) {
} )
}
function clampImageScale ( scale : number ) {
return Math . min ( IMAGE _MAX _SCALE , Math . max ( IMAGE _MIN _SCALE , Math . round ( scale * 10 _000 ) / 10 _000 ) )
}
function imageGeometry ( scale = imageScale ) {
if ( ! lightboxViewport || ! lightboxImage || ! lightboxImage . naturalWidth || ! lightboxImage . naturalHeight )
return undefined
return {
imageHeight : lightboxImage . naturalHeight * scale ,
imageWidth : lightboxImage . naturalWidth * scale ,
viewportHeight : lightboxViewport . clientHeight ,
viewportWidth : lightboxViewport . clientWidth ,
}
}
function clampImageCenter ( center : number , imageSize : number , viewportSize : number ) {
if ( imageSize <= viewportSize || imageSize <= 0 )
return 0.5
const edge = viewportSize / ( 2 * imageSize )
return Math . min ( 1 - edge , Math . max ( edge , center ) )
}
function currentImageView ( ) : ImagePreviewView {
return {
centerX : imageCenterX ,
centerY : imageCenterY ,
scale : imageScale ,
}
}
function renderImageView ( ) {
if ( ! lightboxImage || ! lightboxViewport )
return
const geometry = imageGeometry ( )
if ( ! geometry )
return
imageCenterX = clampImageCenter ( imageCenterX , geometry . imageWidth , geometry . viewportWidth )
imageCenterY = clampImageCenter ( imageCenterY , geometry . imageHeight , geometry . viewportHeight )
lightboxImage . style . width = ` ${ geometry . imageWidth } px `
lightboxImage . style . height = ` ${ geometry . imageHeight } px `
lightboxImage . style . transform = ` translate( ${ - imageCenterX * 100 } %, ${ - imageCenterY * 100 } %) `
lightboxImage . dataset . easyImageScale = String ( imageScale )
lightboxImage . dataset . easyImageCenterX = String ( imageCenterX )
lightboxImage . dataset . easyImageCenterY = String ( imageCenterY )
lightboxViewport . classList . toggle (
'easy-image-lightbox-pannable' ,
geometry . imageWidth > geometry . viewportWidth + 1 || geometry . imageHeight > geometry . viewportHeight + 1 ,
)
const percentage = ` ${ Math . round ( imageScale * 100 ) } % `
if ( lightboxScaleOutput ) {
lightboxScaleOutput . value = percentage
lightboxScaleOutput . textContent = percentage
lightboxScaleOutput . title = ` 当前图片缩放 ${ percentage } `
}
if ( lightboxDecrease )
lightboxDecrease . disabled = imageScale <= IMAGE _MIN _SCALE
if ( lightboxIncrease )
lightboxIncrease . disabled = imageScale >= IMAGE _MAX _SCALE
}
function broadcastImageView ( ) {
if ( ! activePreviewRequestId )
return
pendingSharedView = currentImageView ( )
if ( imageViewFrame !== undefined )
return
imageViewFrame = requestAnimationFrame ( ( ) => {
imageViewFrame = undefined
const view = pendingSharedView
pendingSharedView = undefined
if ( ! activePreviewRequestId || ! view )
return
const command : ImagePreviewViewCommand = {
id : createClientId ( ) ,
requestId : activePreviewRequestId ,
sentAt : Date . now ( ) ,
type : 'view' ,
view ,
viewerId : clientId ,
}
lastHandledCommandId = command . id
imageChannel ? . postMessage ( command )
updateImagePreviewState ( { command } )
} )
}
function applyImageView ( view : ImagePreviewView , shouldBroadcast = true , touched = true ) {
if ( ! Number . isFinite ( view . scale ) || ! Number . isFinite ( view . centerX ) || ! Number . isFinite ( view . centerY ) )
return
imageScale = clampImageScale ( view . scale )
imageCenterX = Math . min ( 1 , Math . max ( 0 , view . centerX ) )
imageCenterY = Math . min ( 1 , Math . max ( 0 , view . centerY ) )
imageViewTouched || = touched
renderImageView ( )
if ( shouldBroadcast )
broadcastImageView ( )
}
function resetImageView ( ) {
const geometry = imageGeometry ( 1 )
if ( ! geometry )
return
const scale = clampImageScale ( Math . min (
1 ,
geometry . viewportWidth / geometry . imageWidth ,
geometry . viewportHeight / geometry . imageHeight ,
) )
imageViewTouched = false
applyImageView ( { centerX : 0.5 , centerY : 0.5 , scale } , false , false )
}
function zoomImageAt ( requestedScale : number , clientX ? : number , clientY ? : number , shouldBroadcast = true ) {
const oldGeometry = imageGeometry ( )
if ( ! oldGeometry || ! lightboxViewport )
return
const scale = clampImageScale ( requestedScale )
let centerX = imageCenterX
let centerY = imageCenterY
if ( clientX !== undefined && clientY !== undefined && scale !== imageScale ) {
const viewportRect = lightboxViewport . getBoundingClientRect ( )
const pointerX = clientX - viewportRect . left
const pointerY = clientY - viewportRect . top
const imageLeft = oldGeometry . viewportWidth / 2 - imageCenterX * oldGeometry . imageWidth
const imageTop = oldGeometry . viewportHeight / 2 - imageCenterY * oldGeometry . imageHeight
const anchorX = ( pointerX - imageLeft ) / oldGeometry . imageWidth
const anchorY = ( pointerY - imageTop ) / oldGeometry . imageHeight
const nextWidth = lightboxImage ! . naturalWidth * scale
const nextHeight = lightboxImage ! . naturalHeight * scale
centerX = ( oldGeometry . viewportWidth / 2 - pointerX + anchorX * nextWidth ) / nextWidth
centerY = ( oldGeometry . viewportHeight / 2 - pointerY + anchorY * nextHeight ) / nextHeight
}
applyImageView ( { centerX , centerY , scale } , shouldBroadcast )
}
function panImageBy ( deltaX : number , deltaY : number , shouldBroadcast = true ) {
const geometry = imageGeometry ( )
if ( ! geometry )
return
applyImageView ( {
centerX : imageCenterX - deltaX / geometry . imageWidth ,
centerY : imageCenterY - deltaY / geometry . imageHeight ,
scale : imageScale ,
} , shouldBroadcast )
}
function pinchMetrics ( ) {
const pointers = Array . from ( imagePointers . values ( ) ) . slice ( 0 , 2 )
if ( pointers . length < 2 )
return undefined
return {
distance : Math . hypot ( pointers [ 1 ] . x - pointers [ 0 ] . x , pointers [ 1 ] . y - pointers [ 0 ] . y ) ,
midpoint : {
x : ( pointers [ 0 ] . x + pointers [ 1 ] . x ) / 2 ,
y : ( pointers [ 0 ] . y + pointers [ 1 ] . y ) / 2 ,
} ,
}
}
function handleImagePointerDown ( event : PointerEvent ) {
if ( ! lightboxViewport || ( event . pointerType === 'mouse' && event . button !== 0 ) )
return
event . preventDefault ( )
event . stopPropagation ( )
if ( imagePointers . size === 0 )
lightboxPointerMoved = false
imagePointers . set ( event . pointerId , { x : event . clientX , y : event . clientY } )
try {
lightboxViewport . setPointerCapture ( event . pointerId )
}
catch {
// Synthetic pointer events used by tests do not participate in native pointer capture.
}
lightboxViewport . classList . add ( 'easy-image-lightbox-dragging' )
const metrics = pinchMetrics ( )
pinchDistance = metrics ? . distance ? ? 0
pinchMidpoint = metrics ? . midpoint
}
function handleImagePointerMove ( event : PointerEvent ) {
const previous = imagePointers . get ( event . pointerId )
if ( ! previous )
return
event . preventDefault ( )
event . stopPropagation ( )
const current = { x : event . clientX , y : event . clientY }
imagePointers . set ( event . pointerId , current )
if ( Math . hypot ( current . x - previous . x , current . y - previous . y ) > 1 )
lightboxPointerMoved = true
if ( imagePointers . size === 1 ) {
panImageBy ( current . x - previous . x , current . y - previous . y )
return
}
const metrics = pinchMetrics ( )
if ( ! metrics )
return
if ( pinchDistance > 0 )
zoomImageAt ( imageScale * metrics . distance / pinchDistance , metrics . midpoint . x , metrics . midpoint . y , false )
if ( pinchMidpoint )
panImageBy ( metrics . midpoint . x - pinchMidpoint . x , metrics . midpoint . y - pinchMidpoint . y )
pinchDistance = metrics . distance
pinchMidpoint = metrics . midpoint
}
function handleImagePointerEnd ( event : PointerEvent ) {
if ( ! imagePointers . has ( event . pointerId ) )
return
event . preventDefault ( )
event . stopPropagation ( )
imagePointers . delete ( event . pointerId )
if ( lightboxViewport ? . hasPointerCapture ( event . pointerId ) )
lightboxViewport . releasePointerCapture ( event . pointerId )
const metrics = pinchMetrics ( )
pinchDistance = metrics ? . distance ? ? 0
pinchMidpoint = metrics ? . midpoint
if ( imagePointers . size === 0 )
lightboxViewport ? . classList . remove ( 'easy-image-lightbox-dragging' )
}
function handleImageWheel ( event : WheelEvent ) {
event . preventDefault ( )
event . stopPropagation ( )
const sensitivity = event . deltaMode === WheelEvent . DOM _DELTA _PIXEL ? 0.002 : 0.05
zoomImageAt ( imageScale * Math . exp ( - event . deltaY * sensitivity ) , event . clientX , event . clientY )
}
function handleLightboxImageLoad ( ) {
if ( ! lightbox || lightbox . hidden )
return
resetImageView ( )
if ( pendingRemoteView ) {
const view = pendingRemoteView
pendingRemoteView = undefined
applyImageView ( view , false )
}
}
function createImageScaleButton ( label : string , text : string , onClick : ( ) => void ) {
const button = document . createElement ( 'button' )
button . type = 'button'
button . className = 'easy-image-scale-button'
button . setAttribute ( 'aria-label' , label )
button . title = label
button . textContent = text
button . addEventListener ( 'pointerdown' , event => event . stopPropagation ( ) )
button . addEventListener ( 'click' , ( event ) => {
event . preventDefault ( )
event . stopPropagation ( )
onClick ( )
} )
return button
}
function createLightbox ( ) {
const overlay = document . createElement ( 'div' )
overlay . className = 'easy-image-lightbox'
@@ -235,9 +542,20 @@ function createLightbox() {
overlay . setAttribute ( 'aria-modal' , 'true' )
overlay . setAttribute ( 'aria-label' , '图片全屏预览' )
const viewport = document . createElement ( 'div' )
viewport . className = 'easy-image-lightbox-viewport'
const preview = document . createElement ( 'img' )
preview . className = 'easy-image-lightbox-preview'
preview . alt = ''
preview . draggable = false
preview . addEventListener ( 'load' , handleLightboxImageLoad )
viewport . addEventListener ( 'pointerdown' , handleImagePointerDown )
viewport . addEventListener ( 'pointermove' , handleImagePointerMove )
viewport . addEventListener ( 'pointerup' , handleImagePointerEnd )
viewport . addEventListener ( 'pointercancel' , handleImagePointerEnd )
viewport . addEventListener ( 'wheel' , handleImageWheel , { passive : false } )
viewport . append ( preview )
const close = document . createElement ( 'button' )
close . type = 'button'
@@ -245,38 +563,66 @@ function createLightbox() {
close . setAttribute ( 'aria-label' , '关闭图片全屏预览' )
close . title = '关闭(Esc) '
close . textContent = '× '
close . addEventListener ( 'pointerdown' , event => event . stopPropagation ( ) )
close . addEventListener ( 'click' , ( event ) => {
event . preventDefault ( )
event . stopPropagation ( )
closeLightbox ( )
} )
const toolbar = document . createElement ( 'nav' )
toolbar . className = 'easy-image-scale-toolbar'
toolbar . setAttribute ( 'aria-label' , '图片缩放' )
toolbar . addEventListener ( 'pointerdown' , event => event . stopPropagation ( ) )
const decrease = createImageScaleButton ( '缩小图片' , '− ' , ( ) => zoomImageAt ( imageScale - IMAGE _SCALE _STEP ) )
const scaleOutput = document . createElement ( 'output' )
scaleOutput . className = 'easy-image-scale-value'
scaleOutput . setAttribute ( 'aria-live' , 'polite' )
scaleOutput . value = '100%'
scaleOutput . textContent = '100%'
const increase = createImageScaleButton ( '放大图片' , '+' , ( ) => zoomImageAt ( imageScale + IMAGE _SCALE _STEP ) )
toolbar . append ( decrease , scaleOutput , increase )
overlay . addEventListener ( 'pointerdown' , event => event . stopPropagation ( ) )
overlay . addEventListener ( 'click' , ( event ) => {
event . preventDefault ( )
event . stopPropagation ( )
if ( event . target === overlay )
const clickedBackdrop = event . target === overlay || event . target === viewport
if ( clickedBackdrop && ! lightboxPointerMoved )
closeLightbox ( )
lightboxPointerMoved = false
} )
overlay . append ( preview , close )
overlay . append ( viewport , close , toolbar )
document . body . append ( overlay )
lightbox = overlay
lightboxViewport = viewport
lightboxImage = preview
lightboxClose = close
lightboxDecrease = decrease
lightboxIncrease = increase
lightboxScaleOutput = scaleOutput
}
function showLightbox ( src : string , alt : string , image ? : HTMLImageElement ) {
function showLightbox ( src : string , alt : string , image ? : HTMLImageElement , requestId ? : string ) {
if ( ! lightbox || ! lightboxImage || ! lightboxClose )
return
const wasHidden = lightbox . hidden
activeImage = image
lightboxImage . src = src
activePreviewRequestId = requestId
pendingRemoteView = undefined
imageViewTouched = false
imageScale = 1
imageCenterX = 0.5
imageCenterY = 0.5
lightboxImage . alt = alt
lightbox . hidden = false
lightboxImage . src = src
if ( wasHidden )
bodyOverflow = document . body . style . overflow
document . body . style . overflow = 'hidden'
document . addEventListener ( 'keydown' , handleLightboxKeydown , true )
if ( lightboxImage . complete && lightboxImage . naturalWidth )
requestAnimationFrame ( handleLightboxImageLoad )
lightboxClose . focus ( )
}
@@ -284,17 +630,54 @@ function openLightbox(image: HTMLImageElement) {
showLightbox ( image . currentSrc || image . src , image . alt , image )
}
function closeLightbox ( restoreFocus = true ) {
function broadcastImageClose ( ) {
if ( ! activePreviewRequestId )
return
const command : ImagePreviewCloseCommand = {
id : createClientId ( ) ,
requestId : activePreviewRequestId ,
sentAt : Date . now ( ) ,
type : 'close' ,
viewerId : clientId ,
}
lastHandledCommandId = command . id
imageChannel ? . postMessage ( command )
updateImagePreviewState ( { command } )
}
function closeLightbox ( restoreFocus = true , shouldBroadcast = true ) {
if ( ! lightbox || lightbox . hidden )
return
if ( shouldBroadcast )
broadcastImageClose ( )
document . removeEventListener ( 'keydown' , handleLightboxKeydown , true )
lightbox . hidden = true
if ( lightboxImage )
if ( imageViewFrame !== undefined ) {
cancelAnimationFrame ( imageViewFrame )
imageViewFrame = undefined
}
pendingSharedView = undefined
pendingRemoteView = undefined
imagePointers . clear ( )
lightboxViewport ? . classList . remove ( 'easy-image-lightbox-dragging' , 'easy-image-lightbox-pannable' )
if ( lightboxImage ) {
lightboxImage . removeAttribute ( 'src' )
lightboxImage . style . removeProperty ( 'width' )
lightboxImage . style . removeProperty ( 'height' )
lightboxImage . style . removeProperty ( 'transform' )
delete lightboxImage . dataset . easyImageScale
delete lightboxImage . dataset . easyImageCenterX
delete lightboxImage . dataset . easyImageCenterY
}
document . body . style . overflow = bodyOverflow
if ( restoreFocus )
activeImage ? . focus ( )
activeImage = undefined
activePreviewRequestId = undefined
imageScale = 1
imageCenterX = 0.5
imageCenterY = 0.5
imageViewTouched = false
}
function handleLightboxKeydown ( event : KeyboardEvent ) {
@@ -304,8 +687,18 @@ function handleLightboxKeydown(event: KeyboardEvent) {
closeLightbox ( )
}
else if ( event . key === 'Tab' ) {
const focusable = [ lightboxClose , lightboxDecrease , lightboxIncrease ] . filter (
( element ) : element is HTMLButtonElement => Boolean ( element && ! element . disabled ) ,
)
if ( ! focusable . length )
return
event . preventDefault ( )
lightboxClose ? . focus ( )
const currentIndex = focusable . indexOf ( document . activeElement as HTMLButtonElement )
const direction = event . shiftKey ? - 1 : 1
const nextIndex = currentIndex < 0
? 0
: ( currentIndex + direction + focusable . length ) % focusable . length
focusable [ nextIndex ] . focus ( )
}
}
@@ -368,13 +761,43 @@ function handleImagePreviewAcknowledgement(acknowledgement: ImagePreviewAcknowle
closeLightbox ( false )
}
function isImagePreviewCommand ( command : ImagePreviewCommand | undefined ) : command is ImagePreviewCommand {
if ( ! command || ! command . id || ! command . requestId || ! command . viewerId || ! Number . isFinite ( command . sentAt ) )
return false
if ( command . type === 'close' )
return true
return command . type === 'view'
&& Number . isFinite ( command . view ? . scale )
&& Number . isFinite ( command . view ? . centerX )
&& Number . isFinite ( command . view ? . centerY )
}
function handleImagePreviewCommand ( command : ImagePreviewCommand | undefined ) {
if ( ! isImagePreviewCommand ( command ) || ! isProjectionRoute ( ) || command . id === lastHandledCommandId )
return
if ( command . requestId !== activePreviewRequestId )
return
if ( command . sentAt < mountedAt - 100 || Date . now ( ) - command . sentAt > 10 _000 )
return
lastHandledCommandId = command . id
if ( command . type === 'close' ) {
closeLightbox ( false , false )
return
}
if ( ! lightboxImage ? . naturalWidth ) {
pendingRemoteView = command . view
return
}
applyImageView ( command . view , false )
}
function handleImagePreviewRequest ( request : ImagePreviewRequest | undefined ) {
if ( ! request || ! isProjectionRoute ( ) || request . id === lastHandledRequestId )
return
if ( request . sentAt < mountedAt - 100 || Date . now ( ) - request . sentAt > 10 _000 )
return
lastHandledRequestId = request . id
showLightbox ( request . src , request . alt )
showLightbox ( request . src , request . alt , undefined , request . id )
updateImagePreviewState ( {
acknowledgement : {
requestId : request . id ,
@@ -574,6 +997,16 @@ function scheduleCodeHeightFit() {
} )
}
function handleViewportResize ( ) {
scheduleCodeHeightFit ( )
if ( ! lightbox || lightbox . hidden || ! lightboxImage ? . naturalWidth )
return
if ( imageViewTouched )
renderImageView ( )
else
resetImageView ( )
}
function cleanupImage ( image : HTMLImageElement , interaction : ImageInteraction ) {
image . removeEventListener ( 'click' , interaction . click )
image . removeEventListener ( 'keydown' , interaction . keydown )
@@ -628,13 +1061,17 @@ function cleanupCodeHosts() {
function cleanupContent ( ) {
clearPendingImagePreview ( )
closeLightbox ( )
closeLightbox ( false , false )
imageInteractions . forEach ( ( interaction , image ) => cleanupImage ( image , interaction ) )
imageInteractions . clear ( )
lightbox ? . remove ( )
lightbox = undefined
lightboxViewport = undefined
lightboxImage = undefined
lightboxClose = undefined
lightboxDecrease = undefined
lightboxIncrease = undefined
lightboxScaleOutput = undefined
cleanupTableHosts ( )
cleanupCodeHosts ( )
}
@@ -659,11 +1096,17 @@ onMounted(() => {
if ( isScaleMessage ( event . data ) )
updateScale ( event . data . key , event . data . kind , event . data . scale , false )
} )
imageChannel = new BroadcastChannel ( ` easy-slides:image-preview: ${ import . meta . env . BASE _URL } ` )
imageChannel . addEventListener ( 'message' , event => handleImagePreviewCommand ( event . data as ImagePreviewCommand ) )
}
stopAcknowledgementWatch = watch (
( ) => easySharedState . easySlidesImagePreview ? . acknowledgement ,
handleImagePreviewAcknowledgement ,
)
stopCommandWatch = watch (
( ) => easySharedState . easySlidesImagePreview ? . command ,
handleImagePreviewCommand ,
)
stopRequestWatch = watch (
( ) => easySharedState . easySlidesImagePreview ? . request ,
handleImagePreviewRequest ,
@@ -676,7 +1119,7 @@ onMounted(() => {
scheduleCodeHeightFit ( )
} )
observer . observe ( document . body , { childList : true , subtree : true } )
window . addEventListener ( 'resize' , scheduleCodeHeightFit )
window . addEventListener ( 'resize' , handleViewportResize )
void document . fonts ? . ready . then ( scheduleCodeHeightFit )
scheduleCodeHeightFit ( )
} )
@@ -685,9 +1128,11 @@ onBeforeUnmount(() => {
observer ? . disconnect ( )
resizeObserver ? . disconnect ( )
channel ? . close ( )
imageChannel ? . close ( )
stopAcknowledgementWatch ? . ( )
stopCommandWatch ? . ( )
stopRequestWatch ? . ( )
window . removeEventListener ( 'resize' , scheduleCodeHeightFit )
window . removeEventListener ( 'resize' , handleViewportResize )
if ( fitFrame !== undefined )
cancelAnimationFrame ( fitFrame )
cleanupContent ( )