CMS Project Sync

This commit is contained in:
2026-04-15 15:59:53 -04:00
parent 015ea75186
commit a747e2a1d9
11220 changed files with 2590467 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

@@ -0,0 +1,833 @@
import { SMUSH_BEFORE_SIZES, LAZY_BEFORE_SIZES, AutoResizing } from '../frontend/lazy-load/auto-resizing';
import { isSmushLazySizesInstance } from '../frontend/lazy-load/helper/lazysizes';
jest.mock( '../frontend/lazy-load/helper/lazysizes', () => ( {
isSmushLazySizesInstance: jest.fn(),
} ) );
describe( 'AutoResizing', () => {
let instance;
let originalDevicePixelRatio;
beforeEach( () => {
document.body.innerHTML = ''; // Clear DOM
originalDevicePixelRatio = window.devicePixelRatio;
isSmushLazySizesInstance.mockReset();
instance = new AutoResizing( { precision: 5, skipAutoWidth: true } );
} );
afterEach( () => {
window.matchMedia = undefined;
window.devicePixelRatio = originalDevicePixelRatio;
} );
test( 'initializes with default values', () => {
const auto = new AutoResizing();
expect( auto.precision ).toBe( 0 );
expect( auto.skipAutoWidth ).toBe( false );
} );
test.each([
[
'Smush LazySizes instance is valid',
true,
'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=500x0 500w'
],
[
'Smush LazySizes instance is invalid',
false,
'https://smushcdn.com/img-300x300.jpg 300w'
]
])( 'Should handle LAZY_BEFORE_SIZES event based on Smush LazySizes instance: %s', ( description, isValidLazyInstance, expectedSrcset ) => {
isSmushLazySizesInstance.mockReturnValue( isValidLazyInstance );
const imageMarkup = `<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px">`;
const container = document.createElement( 'div' );
container.innerHTML = imageMarkup;
const img = container.firstElementChild;
// Appending the image into document to ensures the event can propagate through the DOM tree,
// allowing listeners on document to catch it.
document.body.appendChild( img );
const event = new CustomEvent( LAZY_BEFORE_SIZES, {
detail: { instance: {}, width: 500, dataAttr: true },
bubbles: true,
} );
Object.defineProperty( event, 'target', { value: img } );
document.dispatchEvent( event );
const srcset = img.getAttribute( 'data-srcset' );
expect( srcset ).toBe( expectedSrcset );
} );
test.each( [
[
'skips resizing when width is invalid',
'img', null, true, 'https://smushcdn.com/img-300x300.jpg 300w'
],
[
'skips resizing when not initial render',
'img', 500, false, 'https://smushcdn.com/img-300x300.jpg 300w'
],
[
'performs resizing for valid image and initial render',
'img', 500, true, 'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=500x0 500w'
],
[
'skips resizing when element is not an image',
'div', 500, true, 'https://smushcdn.com/img-300x300.jpg 300w'
],
] )( 'skips processing when element is not image or missing width, dataAttr: %s', ( description, tagName, resizeWidth, isInitialRender, expectedSrcset ) => {
const element = document.createElement( tagName );
element.setAttribute( 'data-src', 'https://smushcdn.com/img.jpg' );
element.setAttribute( 'data-srcset', 'https://smushcdn.com/img-300x300.jpg 300w' );
element.setAttribute( 'data-original-sizes', '(max-width: 1024px) 100vw, 1024px' );
document.body.appendChild( element );
const event = new CustomEvent( LAZY_BEFORE_SIZES, {
detail: { instance: {}, width: resizeWidth, dataAttr: isInitialRender },
bubbles: true,
} );
Object.defineProperty( event, 'target', { value: element } );
isSmushLazySizesInstance.mockReturnValue( true );
document.dispatchEvent( event );
const srcset = element.getAttribute( 'data-srcset' );
expect( srcset ).toBe( expectedSrcset );
} );
test.each( [
[
'Image element eligible for resizing',
`<img data-src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 300w"
data-sizes="auto"
data-original-sizes="(max-width: 400px) 100vw, 400px"
width="350" >`,
true
],
[
'Image element NOT eligible for resizing due to missing data-src',
`<img src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 300w"
data-sizes="auto"
data-original-sizes="(max-width: 400px) 100vw, 400px"
width="350" >`,
false
],
[
'Image element NOT eligible for resizing due to missing data-srcset',
`<img data-src="https://example.com/img.jpg"
srcset="https://example.com/img.jpg 300w"
data-sizes="auto"
data-original-sizes="(max-width: 400px) 100vw, 400px"
width="350" >`,
false
],
[
'Image element NOT eligible for resizing due to missing data-original-sizes',
`<img data-src="https://example.com/img.jpg"
srcset="https://example.com/img.jpg 300w"
data-sizes="auto"
width="350" >`,
false
],
] )( 'test isElementEligibleForResizing: %s', ( description, imageMarkup, expectedResult ) => {
const container = document.createElement( 'div' );
container.innerHTML = imageMarkup;
const img = container.firstElementChild;
expect( instance.isElementEligibleForResizing( img ) ).toBe( expectedResult );
} );
test.each( [
[
'Image has max width smaller than resizing width, revert to original sizes.',
`<img data-src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 300w"
data-sizes="auto"
data-original-sizes="(max-width: 300px) 100vw, 300px">`,
'(max-width: 300px) 100vw, 300px'
],
[
'Image has max width equal resizing width does not revert sizes.',
`<img data-src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 900w"
data-sizes="auto"
data-original-sizes="(max-width: 350px) 100vw, 350px">`,
null
],
[
'Image has width auto, revert to original sizes.',
`<img data-src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 400w"
data-sizes="auto"
data-original-sizes="(max-width: 400px) 100vw, 400px"
style="width:auto" >`,
'(max-width: 400px) 100vw, 400px'
],
[
'Valid resizing image, keep using data-size="auto".',
`<img data-src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 400w"
data-sizes="auto"
data-original-sizes="(max-width: 400px) 100vw, 400px" >`,
null
],
[
'Image with no data-original-sizes does not revert sizes.',
`<img data-src="https://example.com/img.jpg"
data-srcset="https://example.com/img.jpg 300w"
data-sizes="auto" style="width:auto">`,
null
],
] )( 'reverts to original sizes if applicable: %s', ( description, imageMarkup, expectedSizes ) => {
const container = document.createElement( 'div' );
container.innerHTML = imageMarkup;
const img = container.firstElementChild;
const isRevertedToOriginalSizes = Boolean( expectedSizes );
const preventDefault = jest.fn();
const event = {
detail: { instance: {}, width: 350, dataAttr: true },
target: img,
preventDefault,
};
instance.maybeAutoResize( event );
expect( img.getAttribute( 'sizes' ) ).toBe( expectedSizes );
expect( preventDefault ).toHaveBeenCalledTimes( isRevertedToOriginalSizes ? 1 : 0 );
} );
test( 'Revert original sizes via custom event', () => {
isSmushLazySizesInstance.mockReturnValue( true );
const img = document.createElement( 'img' );
img.setAttribute( 'data-original-sizes', '(max-width: 400px) 100vw, 400px' );
img.setAttribute( 'data-src', 'https://example.com/img.jpg?size=400x0' );
img.setAttribute( 'data-srcset', 'https://example.com/img.jpg 400w' );
img.setAttribute( 'data-sizes', 'auto' );
// Appending the image into document to ensures the event can propagate through the DOM tree,
// allowing listeners on document to catch it.
document.body.appendChild( img );
// Listen and prevent default on the custom event
const handler = function( e ) {
e.preventDefault();
};
document.addEventListener( SMUSH_BEFORE_SIZES, handler );
// Create a real CustomEvent
const event = new CustomEvent( LAZY_BEFORE_SIZES, {
detail: { instance: {}, width: 350, dataAttr: true },
bubbles: true,
cancelable: true,
} );
Object.defineProperty( event, 'target', { value: img } );
// Call the method that triggers the event internally
document.dispatchEvent( event );
expect( img.getAttribute( 'sizes' ) ).toBe( '(max-width: 400px) 100vw, 400px' );
expect( img.getAttribute( 'data-srcset' ) ).toBe( 'https://example.com/img.jpg 400w' );
document.removeEventListener( SMUSH_BEFORE_SIZES, handler );
} );
const skippedImages = [
[
'Image missing data-src',
`<img src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
'https://smushcdn.com/img-300x300.jpg 300w'
],
[
'Image missing data-srcset',
`<img data-src="https://smushcdn.com/img.jpg"
srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
''
],
[
'Image skipped due to source is not from Smush CDN',
`<img src="https://example.com/img.jpg"
data-srcset="https://example.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
'https://example.com/img-300x300.jpg 300w'
],
[
'Image skipped due to has similar source in srcset',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=605x0 605w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=605x0 605w'
],
];
const autoResizedImages = [
[
'Resizes CDN image and appends new srcset entry for requested width',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w'
],
[
'Adds new CDN srcset entry for 600w since it is not within precision (5) of existing 594w',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=594x0 594w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=594x0 594w, https://smushcdn.com/img.jpg?size=600x0 600w'
],
[
'Adds new CDN srcset entry for 600w since similar source in srcset smaller than requested width',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w, https://smushcdn.com/img.jpg?size=600x0 600w'
],
];
test.each( [
...skippedImages,
...autoResizedImages,
] )( 'test resizeImageWithCDN: %s', ( description, imageMarkup, expectedSrcset ) => {
const container = document.createElement( 'div' );
container.innerHTML = imageMarkup;
const img = container.firstElementChild;
const event = {
detail: { width: 600, instance: {}, dataAttr: true },
target: img,
preventDefault: jest.fn(),
};
instance.maybeAutoResize( event );
const newSrcset = img.getAttribute( 'data-srcset' ) || '';
expect( newSrcset ).toBe( expectedSrcset );
} );
const autoResizedImagesWithRetina = [
[
'Resizes CDN image and appends new srcset entry for requested width',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=315x0 315w, https://smushcdn.com/img.jpg?size=552x0 552w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
],
[
'Appends new CDN srcset entry for requested width, preserving existing retina source',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=552x0 552w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=552x0 552w, https://smushcdn.com/img.jpg?size=315x0 315w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >`,
],
[
'Adds both standard and retina srcset entries when original sizes match requested width',
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 315px) 100vw, 315px" >`,
`<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=315x0 315w, https://smushcdn.com/img.jpg?size=552x0 552w"
data-original-sizes="(max-width: 315px) 100vw, 315px" >`,
],
[
'Appends new CDN srcset entry to all responsive <source> elements in a <picture> element',
`<picture>
<source
media="(min-width: 800px)" type="image/webp"
data-srcset="https://smushcdn.com/medium-large.webp 800w, https://smushcdn.com/large.webp 1024w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(min-width: 800px)" type="image/jpeg"
data-srcset="https://smushcdn.com/medium-large.jpg 800w, https://smushcdn.com/large.jpg 1024w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(max-width: 799px)" type="image/webp"
data-srcset="https://smushcdn.com/medium.webp 500w, https://smushcdn.com/medium-smaller.webp 400w"
data-sizes="100vw">
<source
media="(max-width: 799px)" type="image/jpeg"
data-srcset="https://smushcdn.com/medium.jpg 500w, https://smushcdn.com/medium-smaller.jpg 400w"
data-sizes="100vw">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 300px) 100vw, 300px" >
</picture>`,
`<picture>
<source media="(min-width: 800px)"
type="image/webp"
data-srcset="https://smushcdn.com/medium-large.webp 800w, https://smushcdn.com/large.webp 1024w, https://smushcdn.com/large.webp?size=315x0 315w, https://smushcdn.com/large.webp?size=552x0 552w"
data-sizes="(min-width: 800px) 100vw">
<source media="(min-width: 800px)"
type="image/jpeg"
data-srcset="https://smushcdn.com/medium-large.jpg 800w, https://smushcdn.com/large.jpg 1024w, https://smushcdn.com/large.jpg?size=315x0 315w, https://smushcdn.com/large.jpg?size=552x0 552w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(max-width: 799px)"
type="image/webp"
data-srcset="https://smushcdn.com/medium.webp 500w, https://smushcdn.com/medium-smaller.webp 400w, https://smushcdn.com/medium.webp?size=315x0 315w, https://smushcdn.com/medium.webp?size=552x0 552w"
data-sizes="100vw">
<source
media="(max-width: 799px)"
type="image/jpeg"
data-srcset="https://smushcdn.com/medium.jpg 500w, https://smushcdn.com/medium-smaller.jpg 400w, https://smushcdn.com/medium.jpg?size=315x0 315w, https://smushcdn.com/medium.jpg?size=552x0 552w"
data-sizes="100vw">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=315x0 315w, https://smushcdn.com/img.jpg?size=552x0 552w"
data-original-sizes="(max-width: 300px) 100vw, 300px">
</picture>`,
'media="(max-width: 799px)"'
],
[
'For non-responsive <source> elements in a <picture>, only appends new CDN srcset entry only to the selected source.',
`<picture>
<source data-srcset="https://smushcdn.com/img-1024x1024.webp" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=300x300" type="image/jpeg">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-1024x1024.webp" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=315x0 315w, https://smushcdn.com/img.jpg?size=552x0 552w" type="image/jpeg">
<img data-src="https://smushcdn.com/img.jpg" data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=315x0 315w, https://smushcdn.com/img.jpg?size=552x0 552w\" data-original-sizes="(max-width: 1024px) 100vw, 1024px">
</picture>`,
'type="image/jpeg"',
],
];
test.each(
autoResizedImagesWithRetina
)( 'test resizeImageWithCDN with retina: %s', ( description, imageMarkup, expectedMarkup, selectedSourceString = 'source' ) => {
jest.spyOn(instance, 'isSourceActive').mockImplementation((sourceElement) => {
const markup = sourceElement.outerHTML;
return markup.includes( selectedSourceString );
});
window.devicePixelRatio = 1.75; // Simulate retina display
const container = document.createElement( 'div' );
container.innerHTML = imageMarkup;
const img = container.firstElementChild;
document.body.appendChild( container );
const event = {
detail: { width: 315, instance: {}, dataAttr: true },
target: container.querySelector('img'),
preventDefault: jest.fn(),
};
instance.maybeAutoResize( event );
const updatedMarkup = (document.body.querySelector('picture') || document.body.querySelector('img')).outerHTML;
expect( normalizeHtml( updatedMarkup ) ).toBe( normalizeHtml( expectedMarkup ) );
} );
test( 'resizeImageWithCDN with custom resizing width', () => {
const imageMarkup = `<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px">`;
const container = document.createElement( 'div' );
container.innerHTML = imageMarkup;
const img = container.firstElementChild;
// Appending the image into document to ensures the event can propagate through the DOM tree,
// allowing listeners on document to catch it.
document.body.appendChild( img );
// Listen and prevent default on the custom event.
const handler = function( e ) {
e.detail.resizeWidth = 400;
};
document.addEventListener( SMUSH_BEFORE_SIZES, handler );
// Create a real CustomEvent
const event = new CustomEvent( LAZY_BEFORE_SIZES, {
detail: { instance: {}, width: 600, dataAttr: true },
bubbles: true,
cancelable: true,
} );
Object.defineProperty( event, 'target', { value: img } );
isSmushLazySizesInstance.mockReturnValue( true );
document.dispatchEvent( event );
const newSrcset = img.getAttribute( 'data-srcset' );
const expectedSrcset = 'https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=400x0 400w';
expect( newSrcset ).toBe( expectedSrcset );
document.removeEventListener( SMUSH_BEFORE_SIZES, handler );
} );
const autoResizedPictures = [
[
'New srcset entry appended to source with webp type',
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.webp 300w, https://smushcdn.com/img.webp 1024w" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=300x300 300w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.webp 300w, https://smushcdn.com/img.webp 1024w, https://smushcdn.com/img.webp?size=600x0 600w" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=300x300 300w, https://smushcdn.com/img.jpg?size=600x0 600w">
<img data-src="https://smushcdn.com/img.jpg" data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w" data-original-sizes="(max-width: 1024px) 100vw, 1024px">
</picture>`
],
[
'Adds new CDN srcset entry for 600w in <picture> since it is not within precision (5) of existing 594w',
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=594x0 594w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=594x0 594w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=594x0 594w, https://smushcdn.com/img.jpg?size=600x0 600w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=594x0 594w, https://smushcdn.com/img.jpg?size=600x0 600w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`
],
[
'Adds new CDN srcset entry for 600w in <picture> since similar source in srcset smaller than requested width',
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w, https://smushcdn.com/img.jpg?size=600x0 600w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w, https://smushcdn.com/img.jpg?size=600x0 600w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`
],
[
'Adds new CDN srcset entry for 600w in <picture> since similar source in srcset smaller than requested width',
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w, https://smushcdn.com/img.jpg?size=600x0 600w">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=595x0 595w, https://smushcdn.com/img.jpg?size=600x0 600w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`
],
[
'Multi-Breakpoint <Picture> template using srcset and sizes',
`<picture>
<source media="(min-width: 800px)"
data-srcset="https://smushcdn.com/medium-large.jpg 800w, https://smushcdn.com/large.jpg 1024w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(min-width: 500px)"
data-srcset="https://smushcdn.com/medium.jpg 500w, https://smushcdn.com/medium-smaller.jpg 400w"
data-sizes="(min-width: 500px) 100vw">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 300px) 100vw, 300px" >
</picture>`,
`<picture>
<source media="(min-width: 800px)"
data-srcset="https://smushcdn.com/medium-large.jpg 800w, https://smushcdn.com/large.jpg 1024w, https://smushcdn.com/large.jpg?size=600x0 600w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(min-width: 500px)"
data-srcset="https://smushcdn.com/medium.jpg 500w, https://smushcdn.com/medium-smaller.jpg 400w, https://smushcdn.com/medium.jpg?size=600x0 600w"
data-sizes="(min-width: 500px) 100vw">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w"
data-original-sizes="(max-width: 300px) 100vw, 300px">
</picture>`
],
[
'Responsive <source> with type and media attributes in <picture> element',
`<picture>
<source media="(min-width: 800px)"
type="image/webp"
data-srcset="https://smushcdn.com/medium-large.webp 800w, https://smushcdn.com/large.webp 1024w"
data-sizes="(min-width: 800px) 100vw">
<source media="(min-width: 800px)"
type="image/jpeg"
data-srcset="https://smushcdn.com/medium-large.jpg 800w, https://smushcdn.com/large.jpg 1024w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(max-width: 799px)"
type="image/webp"
data-srcset="https://smushcdn.com/medium.webp 500w, https://smushcdn.com/medium-smaller.webp 400w"
data-sizes="100vw">
<source
media="(max-width: 799px)"
type="image/jpeg"
data-srcset="https://smushcdn.com/medium.jpg 500w, https://smushcdn.com/medium-smaller.jpg 400w"
data-sizes="100vw">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 300px) 100vw, 300px" >
</picture>`,
`<picture>
<source media="(min-width: 800px)"
type="image/webp"
data-srcset="https://smushcdn.com/medium-large.webp 800w, https://smushcdn.com/large.webp 1024w, https://smushcdn.com/large.webp?size=600x0 600w"
data-sizes="(min-width: 800px) 100vw">
<source media="(min-width: 800px)"
type="image/jpeg"
data-srcset="https://smushcdn.com/medium-large.jpg 800w, https://smushcdn.com/large.jpg 1024w, https://smushcdn.com/large.jpg?size=600x0 600w"
data-sizes="(min-width: 800px) 100vw">
<source
media="(max-width: 799px)"
type="image/webp"
data-srcset="https://smushcdn.com/medium.webp 500w, https://smushcdn.com/medium-smaller.webp 400w, https://smushcdn.com/medium.webp?size=600x0 600w"
data-sizes="100vw">
<source
media="(max-width: 799px)"
type="image/jpeg"
data-srcset="https://smushcdn.com/medium.jpg 500w, https://smushcdn.com/medium-smaller.jpg 400w, https://smushcdn.com/medium.jpg?size=600x0 600w"
data-sizes="100vw">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w"
data-original-sizes="(max-width: 300px) 100vw, 300px">
</picture>`
],
[
'Multi-format <source> elements in the <picture> element',
`<picture>
<source data-srcset="https://smushcdn.com/img-1024x1024.webp" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=300x300" type="image/jpeg">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-1024x1024.webp" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=600x0" type="image/jpeg">
<img data-src="https://smushcdn.com/img.jpg" data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w" data-original-sizes="(max-width: 1024px) 100vw, 1024px">
</picture>`,
'type="image/jpeg"',
],
[
'<source> elements with descending min-widths in the <picture> element',
`<picture>
<source media="(min-width: 800px)" data-srcset="https://smushcdn.com/large.jpg">
<source media="(min-width: 500px)" data-srcset="https://smushcdn.com/medium.jpg">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 300px) 100vw, 300px" >
</picture>`,
`<picture>
<source media="(min-width: 800px)" data-srcset="https://smushcdn.com/large.jpg?size=600x0">
<source media="(min-width: 500px)" data-srcset="https://smushcdn.com/medium.jpg">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w"
data-original-sizes="(max-width: 300px) 100vw, 300px">
</picture>`,
'media="(min-width: 800px)"',
],
];
const skippedPictures = [
[
'Srcset not changed in <source> elements when x descriptor is found',
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.webp 1x, https://smushcdn.com/img.webp 2x" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=300x300 1x">
<img data-src="https://smushcdn.com/img.jpg"
data-srcset="https://smushcdn.com/img-300x300.jpg 300w"
data-original-sizes="(max-width: 1024px) 100vw, 1024px" >
</picture>`,
`<picture>
<source data-srcset="https://smushcdn.com/img-300x300.webp 1x, https://smushcdn.com/img.webp 2x" type="image/webp">
<source data-srcset="https://smushcdn.com/img.jpg?size=300x300 1x">
<img data-src="https://smushcdn.com/img.jpg" data-srcset="https://smushcdn.com/img-300x300.jpg 300w, https://smushcdn.com/img.jpg?size=600x0 600w" data-original-sizes="(max-width: 1024px) 100vw, 1024px">
</picture>`
]
];
test.each([
...skippedPictures,
... autoResizedPictures
] )( 'test auto resize picture element: %s', ( description, pictureMarkup, expectedMarkup, selectedSourceString = 'source' ) => {
jest.spyOn(instance, 'isSourceActive').mockImplementation((sourceElement) => {
const markup = sourceElement.outerHTML;
return markup.includes( selectedSourceString );
});
const container = document.createElement( 'div' );
container.innerHTML = pictureMarkup;
document.body.appendChild( container );
const event = {
detail: { width: 600, instance: {}, dataAttr: true },
target: container.querySelector('img'),
preventDefault: jest.fn(),
};
instance.maybeAutoResize( event );
const updatedMarkup = document.body.querySelector('picture').outerHTML;
expect( normalizeHtml( updatedMarkup ) ).toBe( normalizeHtml( expectedMarkup ) );
} );
function normalizeHtml(html) {
return html
.replace(/\s+/g, ' ') // collapse whitespace
.replace(/\s+>/g, '>') // remove space(s) before '>'
.trim();
}
test.each(
[
[
// 1
'No match: all sources are smaller than required width',
[
{ value: 300, unit: 'w' },
{ value: 600, unit: 'w' },
],
602,
undefined,
],
[
// 2
'No match: units do not match',
[
{ value: 300, unit: 'w' },
{ value: 604, unit: 'h' },
],
602,
undefined,
],
[
// 3
'Match found: source within precision range of required width',
[
{ value: 300, unit: 'w' },
{ value: 604, unit: 'w' },
],
602,
{ value: 604 },
],
]
)( 'findSimilarSource returns matching source within precision: %s', ( description, sources, resizeWidth, expectedMatch ) => {
const unit = 'w';
const precision = 5;
const found = instance.findSimilarSource( sources, resizeWidth, unit, precision );
const expectedResult = expectedMatch ? expect.objectContaining( expectedMatch ) : undefined;
expect( found ).toEqual( expectedResult );
} );
test( 'getElementWidth returns correctly.', () => {
const img = document.createElement( 'img' );
// Mock getComputedStyle to return a non-numeric width
window.getComputedStyle = () => ( { width: 'auto' } );
expect( instance.getElementWidth( img ) ).toBe( 'auto' );
window.getComputedStyle = () => ( { width: '100px' } );
expect( instance.getElementWidth( img ) ).toBe( 100 );
} );
test( 'parseSrcSet sorts sources in descending order', () => {
const srcset = 'img-200.jpg 200w, img-400.jpg 400w, img-300.jpg 300w';
const sources = instance.parseSrcSet( srcset );
expect( sources[ 0 ].value ).toBe( 400 );
expect( sources[ 1 ].value ).toBe( 300 );
expect( sources[ 2 ].value ).toBe( 200 );
} );
test( 'parseSrcSet parses retina descriptors correctly', () => {
const srcset = 'img-1x.jpg 1x, img-2x.jpg 2x, img-400.jpg 400w';
const sources = instance.parseSrcSet( srcset );
// Should parse the 'x' descriptors as floats and unit as 'x'
expect( sources.find( ( s ) => s.unit === 'x' && s.value === 2 ).src ).toBe( 'img-2x.jpg' );
expect( sources.find( ( s ) => s.unit === 'x' && s.value === 1 ).src ).toBe( 'img-1x.jpg' );
// Should also parse the 'w' descriptor
expect( sources.find( ( s ) => s.unit === 'w' && s.value === 400 ).src ).toBe( 'img-400.jpg' );
} );
test.each( [
[
'Not a thumbnail',
'https://example.com/image.jpg',
[
{ value: 500, src: 'https://example.com/image-500x500.jpg' },
{ value: 300, src: 'https://example.com/image-300x300.jpg' },
{ value: 200, src: 'https://example.com/image-200x200.jpg' },
],
400,
'https://example.com/image.jpg',
],
[
'Is a thumbnail but no larger source',
'https://example.com/image.jpg',
[
{ value: 300, src: 'https://example.com/image-300x300.jpg' },
{ value: 200, src: 'https://example.com/image-200x200.jpg' },
],
400,
'https://example.com/image.jpg',
],
[
'Is a thumbnail and larger source exists',
'https://example.com/image-400x400.jpg',
[
{ value: 500, src: 'https://example.com/image-500x500.jpg' },
{ value: 300, src: 'https://example.com/image-300x300.jpg' },
{ value: 200, src: 'https://example.com/image-200x200.jpg' },
],
400,
'https://example.com/image-500x500.jpg',
]
] )(
'getBaseImageSrcForResize %s',
( desc, src, sortedSources, resizeWidth, expected ) => {
expect( instance.getBaseImageSrcForResize( src, sortedSources, resizeWidth ) ).toBe( expected );
}
);
test( 'updateElementSrcset sets attribute only if changed', () => {
const img = document.createElement( 'img' );
const originalSrcset = 'img-300.jpg 300w, img-600.jpg 600w';
const newSrcset = 'img-300.jpg 300w, img-600.jpg 600w, img-900.jpg 900w';
// Should set attribute because newSrcset !== originalSrcset
instance.updateElementSrcset( img, originalSrcset, newSrcset );
expect( img.getAttribute( 'data-srcset' ) ).toBe( newSrcset );
// Should NOT set attribute because newSrcset === originalSrcset
img.removeAttribute( 'data-srcset' );
instance.updateElementSrcset( img, originalSrcset, originalSrcset );
expect( img.getAttribute( 'data-srcset' ) ).toBe( null );
} );
test( 'parseSrcSet handles sources with equal values and sorts descending', () => {
const srcset = 'img-200.jpg 200w, img-400.jpg 400w, img-400b.jpg 400w, img-300.jpg 300w';
const sources = instance.parseSrcSet( srcset );
// Should be sorted descending, and equal values retain their relative order
expect( sources[ 0 ].value ).toBe( 400 );
expect( sources[ 1 ].value ).toBe( 400 );
expect( sources[ 2 ].value ).toBe( 300 );
expect( sources[ 3 ].value ).toBe( 200 );
// The two 400w sources should both be present and in the order they appeared in the srcset
expect( sources[ 0 ].src ).toBe( 'img-400.jpg' );
expect( sources[ 1 ].src ).toBe( 'img-400b.jpg' );
} );
} );
@@ -0,0 +1,168 @@
import { describe, expect, test, it } from '@jest/globals';
import { LCPDetector, SmushLCPDetector } from '../frontend/detector';
describe( 'background data from property value', () => {
const dataSet = [
[
// background-image: url
"url('http://localhost/wp-content/uploads/2024/08/image1.jpeg')",
'background-image',
[
'http://localhost/wp-content/uploads/2024/08/image1.jpeg'
]
],
[
// background-image: relative url
"url('/wp-content/uploads/2024/08/image1.jpeg')",
'background-image',
[
'/wp-content/uploads/2024/08/image1.jpeg'
]
],
[
// background-image: image-set
'image-set(' +
"'http://localhost/wp-content/uploads/2024/08/image1-768x437.jpeg' 1x, " +
"'http://localhost/wp-content/uploads/2024/08/image1.jpeg' 2x" +
');',
'background-image-set',
[
'http://localhost/wp-content/uploads/2024/08/image1-768x437.jpeg',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
]
],
[
// background-image: image-set with relative URL.
'image-set(' +
"'/wp-content/uploads/2024/08/image1-768x437.jpeg' 1x, " +
"'/wp-content/uploads/2024/08/image1.jpeg' 2x" +
');',
'background-image-set',
[
'/wp-content/uploads/2024/08/image1-768x437.jpeg',
'/wp-content/uploads/2024/08/image1.jpeg',
]
],
[
// background-image: image-set with url
'image-set(' +
"url('http://localhost/wp-content/uploads/2024/08/image1-768x437.jpeg') 1x, " +
"url('http://localhost/wp-content/uploads/2024/08/image1.jpeg') 2x" +
');',
'background-image-set',
[
'http://localhost/wp-content/uploads/2024/08/image1-768x437.jpeg',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
]
],
[
// background-image: image-set with url and relative URL
'image-set(' +
"url('/wp-content/uploads/2024/08/image1-768x437.jpeg') 1x, " +
"url('/wp-content/uploads/2024/08/image1.jpeg') 2x" +
');',
'background-image-set',
[
'/wp-content/uploads/2024/08/image1-768x437.jpeg',
'/wp-content/uploads/2024/08/image1.jpeg',
]
],
[
// background-image: image-set query params
'image-set(' +
'http://localhost/wp-content/uploads/2024/08/image1-768x437.jpeg?hello=world 1x, ' +
'http://localhost/wp-content/uploads/2024/08/image1.jpeg?yellow=world 2x' +
');',
'background-image-set',
[
'http://localhost/wp-content/uploads/2024/08/image1-768x437.jpeg?hello=world',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg?yellow=world',
]
],
[
// background-image: image-set query params with relative URL
'image-set(' +
'/wp-content/uploads/2024/08/image1-768x437.jpeg?hello=world 1x, ' +
'/wp-content/uploads/2024/08/image1.jpeg?yellow=world 2x' +
');',
'background-image-set',
[
'/wp-content/uploads/2024/08/image1-768x437.jpeg?hello=world',
'/wp-content/uploads/2024/08/image1.jpeg?yellow=world',
]
],
];
it.each( dataSet )( 'returns correct data for given property value', ( propertyValue, type, urls ) => {
const lcpDetector = new SmushLCPDetector();
lcpDetector.shouldUseRelativeImageURL = ! propertyValue.includes( 'http://localhost' );
const backgroundDataForElement = lcpDetector.getBackgroundDataForPropertyValue( propertyValue );
expect( backgroundDataForElement ).toStrictEqual( {
type,
urls,
} );
} );
} );
describe( 'shouldUseRelativeImageURL', () => {
const dataSet = [
[
// Case: Element contains absolute URL in src attribute
'<img src="http://localhost/wp-content/uploads/2024/08/image1.jpeg">',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
false,
],
[
// Case: Element contains relative URL in src attribute
'<img src="/wp-content/uploads/2024/08/image1.jpeg">',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
true,
],
[
// Case: Element does not contain the URL
'<img src="/wp-content/uploads/2024/08/other-image.jpeg">',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
false,
],
[
// Case: Element contains absolute URL in background style
'<div style="background-image: url(http://localhost/wp-content/uploads/2024/08/image1.jpeg);"></div>',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
false,
],
[
// Case: Element contains relative URL in background style
'<div style="background-image: url(/wp-content/uploads/2024/08/image1.jpeg);"></div>',
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
true,
],
[
// Case: Element is null
null,
'http://localhost/wp-content/uploads/2024/08/image1.jpeg',
false,
],
];
it.each( dataSet )(
'returns %s when element is %s and absoluteImageUrl is %s',
( elementHTML, absoluteImageUrl, expected ) => {
const lcpDetector = new SmushLCPDetector();
const element = elementHTML
? document.createElement( 'div' )
: null;
if ( element ) {
element.innerHTML = elementHTML;
}
const result = lcpDetector.shouldUseRelativeImageURL(
element?.firstChild || element,
absoluteImageUrl
);
expect( result ).toBe( expected );
}
);
} );
@@ -0,0 +1,59 @@
import '../scss/app.scss';
/**
* Admin modules
*/
const WP_Smush = WP_Smush || {};
window.WP_Smush = WP_Smush;
/**
* IE polyfill for includes.
*
* @since 3.1.0
* @param {string} search
* @param {number} start
* @return {boolean} Returns true if searchString appears as a substring of the result of converting this
* object to a String, at one or more positions that are
* greater than or equal to position; otherwise, returns false.
*/
if ( ! String.prototype.includes ) {
String.prototype.includes = function( search, start ) {
if ( typeof start !== 'number' ) {
start = 0;
}
if ( start + search.length > this.length ) {
return false;
}
return this.indexOf( search, start ) !== -1;
};
}
require( './modules/helpers' );
require( './modules/admin' );
require( './modules/admin-common' );
require( './modules/bulk-smush' );
require( './common/media-library-scanner' );
require( './modules/media-library-scanner-on-bulk-smush' );
require( './modules/media-library-scanner-on-dashboard' );
require( './modules/onboarding' );
require( './modules/onboarding-free' );
require( './modules/directory-smush' );
require( './smush/lazy-load' );
require( './modules/bulk-restore' );
require( './smush/settings' );
require( './smush/product-analytics' );
/**
* Notice scripts.
*
* Notices are used in the following functions:
*
* @used-by \Smush\Core\Modules\Smush::smush_updated()
* @used-by \Smush\Core\Integrations\S3::3_support_required_notice()
* @used-by \Smush\App\Abstract_Page::installation_notice()
*
* TODO: should this be moved out in a separate file like common.scss?
*/
require( './modules/notice' );
@@ -0,0 +1,220 @@
/* global WP_Smush */
export const UpsellManger = ( () => {
return {
maybeShowCDNActivationNotice() {
if ( ! wp_smush_msgs.smush_cdn_activation_notice ) {
return;
}
WP_Smush.helpers.renderActivationCDNNotice( wp_smush_msgs.smush_cdn_activation_notice );
},
maybeShowCDNUpsellForPreSiteOnStart() {
const upsellCdn = document.querySelector( '.wp-smush-upsell-cdn' );
if ( upsellCdn ) {
upsellCdn.classList.remove( 'sui-hidden' );
}
},
maybeShowCDNUpsellForPreSiteOnCompleted() {
const upsellCdn = document.querySelector( '.wp-smush-upsell-cdn' );
if ( upsellCdn ) {
upsellCdn.classList.remove( 'sui-hidden' );
}
}
};
} )();
export const GlobalStats = ( () => {
const $ = document.querySelector.bind( document );
const summarySmush = $( '.sui-summary-smush-metabox' );
if ( ! summarySmush ) {
return {};
}
// Cache initial stats.
let boStats = window.wp_smushit_data.bo_stats;
let globalStats = {
count_images: 0,
count_total: 0,
count_resize: 0,
count_skipped: 0,
count_smushed: 0,
savings_bytes: 0,
savings_resize: 0,
size_after: 0,
size_before: 0,
savings_percent: 0,
percent_grade: 'sui-grade-dismissed',
percent_metric: 0,
percent_optimized: 0,
remaining_count: 0,
human_bytes: '',
savings_conversion_human: '',
savings_conversion: 0,
};
const imageScore = $( '#smush-image-score' );
const logBulk = $( '.smush-final-log .smush-bulk-errors' );
const bulkSmushCountContent = $( '#wp-smush-bulk-content' );
let allErrors = {};
const generateGlobalStatsFromSmushData = ( smushScriptData ) => {
window.wp_smushit_data = Object.assign( window.wp_smushit_data, smushScriptData || {} );
globalStats = Object.keys( globalStats ).reduce( function( newStats, key ) {
if ( key in window.wp_smushit_data ) {
newStats[ key ] = window.wp_smushit_data[ key ];
}
return newStats;
}, {} );
}
generateGlobalStatsFromSmushData( window.wp_smushit_data );
return {
isChangedStats( newBoStats ) {
const primaryKeys = [ 'total_items', 'processed_items', 'failed_items', 'is_cancelled', 'is_completed', 'is_dead' ];
return primaryKeys.some( ( key ) => {
return newBoStats[ key ] !== boStats[ key ];
} );
},
setBoStats( newBoStats ) {
boStats = Object.assign( boStats, newBoStats || {} );
return this;
},
getBoStats() {
return boStats;
},
setGlobalStats( newGlobalStats ) {
globalStats = Object.assign( globalStats, newGlobalStats || {} );
return this;
},
getGlobalStats() {
return globalStats;
},
/**
* Circle progress bar.
*/
renderScoreProgress() {
imageScore.className = imageScore.className.replace( /(^|\s)sui-grade-\S+/g, '' );
imageScore.classList.add( globalStats.percent_grade );
imageScore.dataset.score = globalStats.percent_optimized;
imageScore.querySelector( '.sui-circle-score-label' ).innerHTML = globalStats.percent_optimized;
imageScore
.querySelector( 'circle:last-child' )
.setAttribute( 'style', '--metric-array:' + ( 2.63893782902 * globalStats.percent_metric ) + ' ' + ( 263.893782902 - globalStats.percent_metric ) );
},
/**
* Summary detail - center meta box.
*/
renderSummaryDetail() {
this.renderTotalStats();
this.renderResizedStats();
this.renderConversionSavings();
},
renderTotalStats() {
// Total savings.
summarySmush.querySelector( '.sui-summary-large.wp-smush-stats-human' ).innerHTML = globalStats.human_bytes;
// Update the savings percent.
summarySmush.querySelector( '.wp-smush-savings .wp-smush-stats-percent' ).innerHTML = globalStats.savings_percent;
// To total smushed images files.
summarySmush.querySelector( '.wp-smush-count-total .wp-smush-total-optimised' ).innerHTML = globalStats.count_images;
},
renderResizedStats() {
const resizeCountElement = summarySmush.querySelector( '.wp-smush-count-resize-total' );
if ( ! resizeCountElement ) {
return;
}
if ( globalStats.count_resize > 0 ) {
resizeCountElement.classList.remove( 'sui-hidden' );
} else {
resizeCountElement.classList.add( 'sui-hidden' );
}
resizeCountElement.querySelector( '.wp-smush-total-optimised' ).innerHTML = globalStats.count_resize;
},
renderConversionSavings() {
// PNG2JPG Savings.
const conversionSavingsElement = summarySmush.querySelector( '.smush-conversion-savings .wp-smush-stats' );
if ( ! conversionSavingsElement ) {
return;
}
conversionSavingsElement.innerHTML = globalStats.savings_conversion_human;
if ( globalStats.savings_conversion > 0 ) {
conversionSavingsElement.parentElement.classList.remove( 'sui-hidden' );
} else {
conversionSavingsElement.parentElement.classList.add( 'sui-hidden' );
}
},
renderBoxSummary() {
// Circle core progress.
this.renderScoreProgress();
// Summary detail.
this.renderSummaryDetail();
},
setErrors( newErrors ) {
allErrors = newErrors || {};
},
getErrors() {
return allErrors;
},
renderErrors() {
if ( ! Object.keys( allErrors ).length || ! boStats.is_completed ) {
return;
}
const errors = [];
const errorKeys = Object.keys( allErrors );
// Cache error code to avoid double upsell notice.
let showAnimatedUpsell = false;
errorKeys.map( ( image_id, index ) => {
const upsellErrorCode = allErrors[ image_id ].error_code;
if ( index < 5 && 'animated' === upsellErrorCode ) {
showAnimatedUpsell = true;
}
errors.push( WP_Smush.helpers.prepareBulkSmushErrorRow(
allErrors[ image_id ].error_message,
allErrors[ image_id ].file_name,
allErrors[ image_id ].thumbnail,
image_id,
'media',
allErrors[ image_id ].error_code,
) );
}
);
logBulk.innerHTML = errors.join( '' );
logBulk.parentElement.classList.remove( 'sui-hidden' );
logBulk.parentElement.style.display = null;
// Show view all.
if ( errorKeys.length > 1 ) {
$( '.smush-bulk-errors-actions' ).classList.remove( 'sui-hidden' );
}
// Show animated upsell CDN if user disabled CDN and found an animated error in first 5 errors.
if ( showAnimatedUpsell ) {
UpsellManger.maybeShowCDNActivationNotice();
}
},
resetAndHideBulkErrors() {
if ( ! logBulk ) {
return;
}
this.resetErrors();
logBulk.parentElement.classList.add( 'sui-hidden' );
logBulk.innerHTML = '';
},
resetErrors() {
allErrors = {};
},
renderStats() {
// Render Smush box summary.
this.renderBoxSummary();
// Render Errors.
this.renderErrors();
},
maybeUpdateBulkSmushCountContent( newContent ) {
if ( newContent && bulkSmushCountContent ) {
bulkSmushCountContent.innerHTML = newContent;
}
},
updateGlobalStatsFromSmushScriptData( smushScriptData ) {
this.maybeUpdateBulkSmushCountContent( smushScriptData?.content );
generateGlobalStatsFromSmushData( smushScriptData );
return this;
},
};
} )();
@@ -0,0 +1,294 @@
/* global WP_Smush */
/**
* Abstract Media Library Scanner.
*
*/
import Fetcher from '../utils/fetcher';
import { scanProgressBar } from './progressbar';
import { GlobalStats } from './globalStats';
import loopbackTester from '../loopback-tester';
const { __ } = wp.i18n;
export default class MediaLibraryScanner {
constructor() {
this.autoSyncDuration = 1500;
this.progressTimeoutId = 0;
this.scanProgress = scanProgressBar( this.autoSyncDuration );
}
startScan( optimizeOnScanCompleted = false ) {
this.onStart();
const processType = optimizeOnScanCompleted ? 'smush' : 'scan';
loopbackTester.performTest().then( ( res ) => {
const isLoopbackHealthy = res?.loopback;
if ( isLoopbackHealthy ) {
Fetcher.scanMediaLibrary.start( optimizeOnScanCompleted ).then( ( response ) => {
if ( ! response?.success ) {
this.showFailureNotice( response );
this.onStartFailure( response );
return;
}
this.showProgressBar().autoSyncStatus();
} );
} else {
this.showLoopbackErrorModal( processType );
this.onStartFailure( res );
}
} ).catch( ( error ) => {
console.error( 'Error:', error );
this.showLoopbackErrorModal( processType );
this.onStartFailure( error );
} );
}
onStart() {
// Do nothing at the moment.
}
onStartFailure( response ) {
// Do nothing at the moment.
}
showFailureNotice( response ) {
WP_Smush.helpers.showNotice( response, {
showdismiss: true,
autoclose: false,
} );
}
showLoopbackErrorModal( processType ) {
const loopbackErrorModal = document.getElementById( 'smush-loopback-error-dialog' );
if ( ! loopbackErrorModal || ! window.SUI ) {
return;
}
// Cache current process type.
loopbackErrorModal.dataset.processType = processType || 'scan';
WP_Smush.helpers.showModal( loopbackErrorModal.id );
}
showProgressBar() {
this.onShowProgressBar();
this.scanProgress.reset().setOnCancelCallback( this.showStopScanningModal.bind( this ) ).open();
return this;
}
onShowProgressBar() {
// Do nothing at the moment.
}
showStopScanningModal() {
if ( ! window.SUI ) {
return;
}
this.onShowStopScanningModal();
window.SUI.openModal(
'smush-stop-scanning-dialog',
'wpbody-content',
undefined,
false
);
}
onShowStopScanningModal() {
this.registerCancelProcessEvent();
}
registerCancelProcessEvent() {
const stopScanButton = document.querySelector( '.smush-stop-scanning-dialog-button' );
if ( ! stopScanButton ) {
return;
}
stopScanButton.addEventListener( 'click', this.cancelProgress.bind( this ), { once: true } );
}
closeStopScanningModal() {
if ( ! window.SUI ) {
return;
}
const stopScanningElement = document.querySelector( '#smush-stop-scanning-dialog' );
const isModalClosed = ( ! stopScanningElement ) || ! stopScanningElement.classList.contains( 'sui-content-fade-in' );
if ( isModalClosed ) {
return;
}
window.SUI.closeModal( 'smush-stop-scanning-dialog' );
}
closeProgressBar() {
this.onCloseProgressBar();
this.scanProgress.close();
}
onCloseProgressBar() {
// Do nothing at the moment.
}
updateProgress( stats ) {
const totalItems = this.getTotalItems( stats );
const processedItems = this.getProcessedItems( stats );
return this.scanProgress.update( processedItems, totalItems );
}
getProcessedItems( stats ) {
return stats?.processed_items || 0;
}
getTotalItems( stats ) {
return stats?.total_items || 0;
}
cancelProgress() {
this.scanProgress.setCancelButtonOnCancelling();
return Fetcher.scanMediaLibrary.cancel().then( ( response ) => {
if ( ! response?.success ) {
this.onCancelFailure( response );
return;
}
this.onCancelled( response.data );
} );
}
onCancelFailure( response ) {
WP_Smush.helpers.showNotice( response, {
showdismiss: true,
autoclose: false,
} );
this.scanProgress.resetCancelButtonOnFailure();
}
onDead( stats ) {
this.clearProgressTimeout();
this.closeProgressBar();
this.closeStopScanningModal();
this.showRetryScanModal();
}
showRetryScanModal() {
const retryScanModalElement = document.getElementById( 'smush-retry-scan-notice' );
if ( ! window.SUI || ! retryScanModalElement ) {
return;
}
retryScanModalElement.querySelector( '.smush-retry-scan-notice-button' ).addEventListener( 'click', ( e ) => {
window.SUI.closeModal( 'smush-retry-scan-notice' );
const recheckImagesBtn = document.querySelector( '.wp-smush-scan' );
if ( ! recheckImagesBtn ) {
return;
}
e.preventDefault();
recheckImagesBtn.click();
}, { once: true } );
window.SUI.openModal(
'smush-retry-scan-notice',
'wpbody-content',
undefined,
false
);
}
onCompleted( stats ) {
this.onFinish( stats );
}
onCancelled( stats ) {
this.onFinish( stats );
}
onFinish( stats ) {
this.clearProgressTimeout();
const globalStats = stats?.global_stats;
this.updateGlobalStatsAndBulkContent( globalStats );
this.closeProgressBar();
this.closeStopScanningModal();
}
clearProgressTimeout() {
if ( this.progressTimeoutId ) {
clearTimeout( this.progressTimeoutId );
}
}
updateGlobalStatsAndBulkContent( globalStats ) {
if ( ! globalStats ) {
return;
}
GlobalStats.updateGlobalStatsFromSmushScriptData( globalStats );
GlobalStats.renderStats();
}
getStatus() {
return Fetcher.scanMediaLibrary.getScanStatus();
}
autoSyncStatus() {
const startTime = new Date().getTime();
this.getStatus().then( ( response ) => {
if ( ! response?.success ) {
return;
}
const stats = response.data;
if ( stats.is_dead ) {
this.onDead( response.data );
return;
}
this.beforeUpdateStatus( stats );
this.updateProgress( stats ).then( () => {
this.scanProgress.increaseDurationToHaveChangeOnProgress( new Date().getTime() - startTime );
const isCompleted = stats?.is_completed;
if ( isCompleted ) {
this.onCompleted( stats );
return;
}
const isCancelled = stats?.is_cancelled;
if ( isCancelled ) {
this.onCancelled( stats );
return;
}
this.progressTimeoutId = setTimeout( () => this.autoSyncStatus(), this.autoSyncDuration );
} );
} );
}
beforeUpdateStatus() {
// Do nothing at the moment.
}
setInnerText( element, newText ) {
if ( ! element ) {
return;
}
element.dataset.originalText = element.dataset.originalText || element.innerText.trim();
element.innerText = newText;
}
revertInnerText( element ) {
if ( ! element || ! element.dataset.originalText ) {
return;
}
element.innerText = element.dataset.originalText.trim();
}
hideAnElement( element ) {
if ( element ) {
element.classList.add( 'sui-hidden' );
}
}
showAnElement( element ) {
if ( element ) {
element.classList.remove( 'sui-hidden' );
}
}
}
@@ -0,0 +1,349 @@
/* global WP_Smush */
/**
* SmushProgressBar
* TODO: Update progressbar for free version.
*
* @param autoSyncDuration
*/
export const scanProgressBar = ( autoSyncDuration ) => {
const { __, _n } = wp.i18n;
const scanProgressBar = document.querySelector( '.wp-smush-scan-progress-bar-wrapper' );
const percentElement = scanProgressBar.querySelector( '.wp-smush-progress-percent' );
const progressElement = scanProgressBar.querySelector( '.wp-smush-progress-inner' );
const remainingTimeElement = scanProgressBar.querySelector( '.wp-smush-remaining-time' );
const cancelBtn = scanProgressBar.querySelector( '.wp-smush-cancel-scan-progress-btn' );
const holdOnNoticeElement = scanProgressBar.querySelector( '.wp-smush-scan-hold-on-notice' );
let onCancelCallback = () => {};
let intervalProgressAnimation = 0;
// It should be smaller than autoSyncDuration.
const progressTransitionDuration = autoSyncDuration - 300;//1200
scanProgressBar.style.setProperty( '--progress-transition-duration', progressTransitionDuration / 1000 + 's' );
let prevProcessedItems = window.wp_smushit_data?.media_library_scan?.processed_items || 0;
const cacheProcessTimePerItem = [];
let durationToHaveChangeOnProgress = autoSyncDuration;
let timeLimitToShowNotice = 60000;// 60s.
return {
update( processedItems, totalItems ) {
this.updateRemainingTime( processedItems, totalItems );
let width = ( totalItems && Math.floor( processedItems / totalItems * 100 ) ) || 0;
width = Math.min( width, 100 );
let currentWidth = progressElement.style.width;
currentWidth = ( currentWidth && currentWidth.replace( '%', '' ) ) || 0;
progressElement.style.width = width + '%';
return this.animateProgressBar( currentWidth, width );
},
animateProgressBar( currentWidth, width ) {
if ( intervalProgressAnimation ) {
clearInterval( intervalProgressAnimation );
}
return new Promise( ( resolve ) => {
const delayTime = progressTransitionDuration / ( width - currentWidth );
intervalProgressAnimation = setInterval( () => {
// Progress bar label.
percentElement.innerHTML = currentWidth + '%';
currentWidth++;
if ( currentWidth > width ) {
resolve();
clearInterval( intervalProgressAnimation );
}
}, delayTime );
} );
},
updateRemainingTime( processedItems, totalItems ) {
if ( ! remainingTimeElement ) {
return;
}
const processTimePerItem = this.calcProcessTimePerItem( processedItems ) || 500;
const remainingTime = processTimePerItem * ( totalItems - processedItems );
remainingTimeElement.innerText = this.formatTime( remainingTime );
},
calcProcessTimePerItem( processedItems ) {
if ( ! processedItems ) {
return;
}
prevProcessedItems = prevProcessedItems <= processedItems ? prevProcessedItems : 0;
if ( prevProcessedItems != processedItems ) {
const processTimePerItem = Math.floor( durationToHaveChangeOnProgress / ( processedItems - prevProcessedItems ) );
prevProcessedItems = processedItems;
cacheProcessTimePerItem.push( processTimePerItem );
this.resetDurationToHaveChangeOnProgress();
} else {
this.increaseDurationToHaveChangeOnProgress( autoSyncDuration );
}
if ( ! cacheProcessTimePerItem.length ) {
return;
}
return cacheProcessTimePerItem.reduce(
( accumulator, processTime ) => accumulator + processTime, 0
) / cacheProcessTimePerItem.length;
},
increaseDurationToHaveChangeOnProgress( increaseTime ) {
durationToHaveChangeOnProgress += increaseTime;
if ( durationToHaveChangeOnProgress > timeLimitToShowNotice ) {
this.showHoldOnNotice();
}
},
showHoldOnNotice() {
holdOnNoticeElement.classList.remove( 'sui-hidden' );
timeLimitToShowNotice = 100000000;
},
resetHoldOnNoticeVisibility() {
holdOnNoticeElement.classList.add( 'sui-hidden' );
},
resetDurationToHaveChangeOnProgress() {
durationToHaveChangeOnProgress = autoSyncDuration;
},
formatTime( totalMilliSeconds ) {
const totalSeconds = Math.floor( ( totalMilliSeconds + progressTransitionDuration ) / 1000 );
const seconds = totalSeconds % 60;
const minutes = Math.floor( totalSeconds / 60 );
let timeString = '';
if ( minutes ) {
timeString += minutes + ' ' + _n( 'minute', 'minutes', minutes, 'wp-smushit' );
}
timeString += ' ' + seconds + ' ' + _n( 'second', 'seconds', seconds, 'wp-smushit' );
return timeString.trim();
},
reset() {
progressElement.style.width = '0%';
percentElement.innerHTML = '0%';
this.resetCancelButton();
this.resetHoldOnNoticeVisibility();
return this;
},
open() {
cancelBtn.onclick = onCancelCallback;
scanProgressBar.classList.remove( 'sui-hidden' );
},
close() {
scanProgressBar.classList.add( 'sui-hidden' );
this.reset();
},
setOnCancelCallback( callBack ) {
if ( 'function' !== typeof callBack ) {
return;
}
onCancelCallback = callBack;
return this;
},
setCancelButtonLabel( textContent ) {
cancelBtn.textContent = textContent;
return this;
},
setCancelButtonOnCancelling() {
this.setCancelButtonLabel( wp_smush_msgs.cancelling );
this.setOnCancelCallback( () => false );
cancelBtn.setAttribute( 'disabled', true );
},
resetCancelButton() {
this.setOnCancelCallback( () => {} );
this.resetCancelButtonLabel();
cancelBtn.removeAttribute( 'disabled' );
},
resetCancelButtonLabel() {
this.setCancelButtonLabel( __( 'Cancel Scan', 'wp-smushit' ) );
},
resetCancelButtonOnFailure() {
this.resetCancelButtonLabel();
cancelBtn.removeAttribute( 'disabled' );
}
};
};
const SmushProgressBar = () => {
'use strict';
const progressBar = document.querySelector( '.wp-smush-bulk-progress-bar-wrapper' );
if ( ! progressBar ) {
return {
isEmptyObject: true,
};
}
const cancelBtn = progressBar.querySelector( '.wp-smush-cancel-btn' );
const bulkSmushDescription = document.querySelector( '.wp-smush-bulk-wrapper' );
const bulkRunningNotice = progressBar.querySelector( '#wp-smush-running-notice' );
const bulkSmushAllDone = document.querySelector( '.wp-smush-all-done' );
const stopBulkSmushModal = document.getElementById( 'smush-stop-bulk-smush-modal' );
const holdOnNoticeElement = progressBar.querySelector( '.wp-smush-bulk-hold-on-notice' );
let isStateHidden = false;
let onCancelCallback = () => {};
return {
/**
* Update progress bar.
*
* @param {number} processedItems
* @param {number} totalItems
*/
update( processedItems, totalItems ) {
let width = totalItems && Math.floor( processedItems / totalItems * 100 ) || 0;
width = Math.min( width, 100 );
// Progress bar label.
progressBar.querySelector( '.wp-smush-images-percent' ).innerHTML = width + '%';
// Progress bar.
progressBar.querySelector( '.wp-smush-progress-inner' ).style.width = width + '%';
// Update processed/total.
const processStateStats = progressBar.querySelector( '.sui-progress-state-text' );
processStateStats.firstElementChild.innerHTML = processedItems;
processStateStats.lastElementChild.innerHTML = totalItems;
return this;
},
close() {
progressBar.classList.add( 'sui-hidden' );
this.setCancelButtonLabel( window.wp_smush_msgs.cancel )
.setOnCancelCallback( () => {} )
.update( 0, 0 );
this.resetOriginalNotice();
this.closeStopBulkSmushModal();
return this;
},
show() {
// Show progress bar.
progressBar.classList.remove( 'sui-hidden' );
cancelBtn.onclick = this.showStopBulkSmushModal.bind( this );
this.hideBulkSmushDescription();
this.hideBulkSmushAllDone();
this.hideRecheckImagesNotice();
},
showStopBulkSmushModal() {
if ( ! stopBulkSmushModal ) {
return;
}
const stopBulkSmushButton = stopBulkSmushModal.querySelector( '.smush-stop-bulk-smush-button' );
stopBulkSmushButton.addEventListener( 'click', onCancelCallback, { once: true } );
// Displays the modal with the release's higlights if it exists.
const modalId = stopBulkSmushModal.id,
focusAfterClosed = 'wpbody-content',
focusWhenOpen = undefined,
hasOverlayMask = false,
isCloseOnEsc = false,
isAnimated = true;
window.SUI.openModal(
modalId,
focusAfterClosed,
focusWhenOpen,
hasOverlayMask,
isCloseOnEsc,
isAnimated
);
},
closeStopBulkSmushModal() {
if ( ! window.SUI ) {
return;
}
const isModalClosed = ( ! stopBulkSmushModal ) || ! stopBulkSmushModal.classList.contains( 'sui-content-fade-in' );
if ( isModalClosed ) {
return;
}
window.SUI.closeModal( stopBulkSmushModal.id );
},
setCancelButtonLabel( textContent ) {
cancelBtn.textContent = textContent;
return this;
},
showBulkSmushDescription() {
bulkSmushDescription.classList.remove( 'sui-hidden' );
},
hideBulkSmushDescription() {
bulkSmushDescription.classList.add( 'sui-hidden' );
},
showBulkSmushAllDone() {
bulkSmushAllDone.classList.remove( 'sui-hidden' );
},
hideBulkSmushAllDone() {
bulkSmushAllDone.classList.add( 'sui-hidden' );
},
hideState() {
if ( isStateHidden ) {
return this;
}
isStateHidden = true;
progressBar.querySelector( '.sui-progress-state' ).classList.add( 'sui-hidden' );
return this;
},
showState() {
if ( ! isStateHidden ) {
return this;
}
isStateHidden = false;
progressBar.querySelector( '.sui-progress-state' ).classList.remove( 'sui-hidden' );
return this;
},
setNotice( inProcessNotice ) {
const progressMessage = bulkRunningNotice.querySelector( '.sui-notice-message p' );
this.cacheOriginalNotice( progressMessage );
progressMessage.innerHTML = inProcessNotice;
return this;
},
cacheOriginalNotice( progressMessage ) {
if ( bulkRunningNotice.dataset.progressMessage ) {
return;
}
bulkRunningNotice.dataset.progressMessage = progressMessage.innerHTML;
},
resetOriginalNotice() {
if ( ! bulkRunningNotice.dataset.progressMessage ) {
return;
}
const progressMessage = bulkRunningNotice.querySelector( '.sui-notice-message p' );
progressMessage.innerHTML = bulkRunningNotice.dataset.progressMessage;
},
hideBulkProcessingNotice() {
bulkRunningNotice.classList.add( 'sui-hidden' );
return this;
},
showBulkProcessingNotice() {
bulkRunningNotice.classList.remove( 'sui-hidden' );
return this;
},
setCountUnitText( unitText ) {
const progressUnit = progressBar.querySelector( '.sui-progress-state-unit' );
progressUnit.innerHTML = unitText;
},
setOnCancelCallback( callBack ) {
if ( 'function' !== typeof callBack ) {
return;
}
onCancelCallback = callBack;
return this;
},
disableExceedLimitMode() {
progressBar.classList.remove( 'wp-smush-exceed-limit' );
progressBar.querySelector( '#bulk-smush-resume-button' ).classList.add( 'sui-hidden' );
},
hideRecheckImagesNotice() {
const recheckImagesNoticeElement = document.querySelector( '.wp-smush-recheck-images-notice-box' );
if ( recheckImagesNoticeElement ) {
recheckImagesNoticeElement.classList.add( 'sui-hidden' );
}
},
showHoldOnNotice() {
if ( holdOnNoticeElement ) {
holdOnNoticeElement.classList.remove( 'sui-hidden' );
}
},
hideHoldOnNotice() {
if ( holdOnNoticeElement ) {
holdOnNoticeElement.classList.add( 'sui-hidden' );
}
},
};
};
export default new SmushProgressBar();
@@ -0,0 +1,180 @@
import unique from 'unique-selector';
import getXPath from 'get-xpath';
export class SmushLCPDetector {
onLCP(data) {
const element = data?.entries[0]?.element;
const imageUrl = data?.attribution?.url;
if (!element || !imageUrl) {
return;
}
const selector = unique(element);
const xpath = getXPath(element, {ignoreId: true});
this.useRelativeImageURL = this.shouldUseRelativeImageURL( element, imageUrl );
const body = {
url: window.location.href,
data: JSON.stringify({
selector: selector,
selector_xpath: xpath,
selector_id: element?.id,
selector_class: element?.className,
image_url: this.useRelativeImageURL ? this.makeImageURLRelative( imageUrl ) : imageUrl,
background_data: this.getBackgroundDataForElement( element ),
} ),
nonce: smush_detector.nonce,
is_mobile: smush_detector.is_mobile,
data_store: JSON.stringify(smush_detector.data_store),
previous_data_version: smush_detector.previous_data_version,
previous_data_hash: smush_detector.previous_data_hash,
};
const xhr = new XMLHttpRequest();
xhr.open('POST', smush_detector.ajax_url + '?action=smush_handle_lcp_data', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
const urlEncodedData = Object.keys(body)
.map(key => encodeURIComponent(key) + "=" + encodeURIComponent(body[key]))
.join("&");
xhr.send(urlEncodedData);
}
shouldUseRelativeImageURL( element, absoluteImageUrl ) {
if ( ! element?.outerHTML ) {
return false;
}
const outerHTML = element.outerHTML;
const containsAbsoluteUrl = outerHTML.includes( absoluteImageUrl );
if ( containsAbsoluteUrl ) {
return false;
}
const relativeImageUrl = this.makeImageURLRelative( absoluteImageUrl );
const containsRelativeUrl = outerHTML.includes( relativeImageUrl );
return containsRelativeUrl;
}
makeImageURLRelative( imageUrl ) {
try {
const url = new URL( imageUrl, window.location.origin ); // Parse the URL
if ( url.hostname === window.location.hostname ) {
// Only make the URL relative if it belongs to the current host
return url.pathname + url.search; // Keep the path and query string
}
} catch ( e ) {
// If the URL is invalid or relative, return it as-is.
}
return imageUrl; // Return the original URL if it doesn't belong to the host
}
getBackgroundDataForElement(element) {
const computedStyle = window.getComputedStyle(element, null);
const backgroundProps = [
computedStyle.getPropertyValue("background-image"),
getComputedStyle(element, ":after").getPropertyValue("background-image"),
getComputedStyle(element, ":before").getPropertyValue("background-image")
].filter((prop) => prop !== "none");
if (backgroundProps.length === 0) {
return null;
}
return this.getBackgroundDataForPropertyValue(backgroundProps[0]);
}
getBackgroundDataForPropertyValue(fullBackgroundProp) {
let type = "background-image";
if (fullBackgroundProp.includes("image-set(")) {
type = "background-image-set";
}
if (!fullBackgroundProp || fullBackgroundProp === "" || fullBackgroundProp.includes("data:image")) {
return null;
}
// IMPORTANT: the following regex is a copy of the one in the PHP function Parser::get_image_urls. Remember to keep them synced.
const cssBackgroundUrlRegex = /((?:https?:\/|\.+)?\/[^'",\s()]+\.(jpe?g|png|gif|webp|svg|avif)(?:\?[^\s'",?)]+)?)\b/ig;
const matches = [ ...fullBackgroundProp.matchAll( cssBackgroundUrlRegex ) ];
const backgroundSet = matches.map( ( match ) => {
const imageURL = match[ 1 ].trim();
return this.useRelativeImageURL
? this.makeImageURLRelative( imageURL )
: imageURL;
} );
if ( backgroundSet.length <= 0 ) {
return null;
}
return {
type: type,
urls: backgroundSet,
};
}
}
(function () {
let lcpEntry = null;
let finalized = false;
const initialViewportBottom = window.innerHeight;
const pageLoadStartedAtTop = document?.documentElement?.scrollTop === 0;
if (!pageLoadStartedAtTop || !('PerformanceObserver' in window)) {
return;
}
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (isInInitialViewport(entry)) {
lcpEntry = entry; // always keep the latest candidate
}
}
});
try {
po.observe({type: 'largest-contentful-paint', buffered: true});
} catch (e) {
// not supported
}
function finalizeLCP() {
if (finalized) {
return;
}
finalized = true;
if (lcpEntry) {
const detector = new SmushLCPDetector();
detector.onLCP({
entries: [lcpEntry],
attribution: {
url: lcpEntry.url || '',
element: lcpEntry.element || ''
}
});
}
if (po) {
po.disconnect();
}
}
function isInInitialViewport(entry) {
const el = entry && entry.element;
if (!el) {
return true;
}
const rect = el.getBoundingClientRect();
const elementTop = rect.top + window.scrollY;
return elementTop <= initialViewportBottom;
}
// Finalize on first *trusted* user input
['keydown', 'click', 'pointerdown', 'touchstart'].forEach((type) => {
addEventListener(type, (event) => {
if (event.isTrusted) {
finalizeLCP();
}
}, {once: true, capture: true});
});
})();
@@ -0,0 +1,8 @@
import restoreLazySizesConfig from './lazy-load/helper/break-lazysizes';
import lazySizes from './lazy-load/helper/lazysizes';
require( './lazy-load/lazy-load-background-images' );
require( './lazy-load/lazy-load-video' );
lazySizes.init();
restoreLazySizesConfig();
@@ -0,0 +1,637 @@
import { isSmushLazySizesInstance } from './helper/lazysizes';
export const LAZY_BEFORE_SIZES = 'lazybeforesizes';
export const SMUSH_BEFORE_SIZES = 'smush:beforeSizes';
const SMUSH_CDN_DOMAIN = 'smushcdn.com';
const ATTR_DATA_ORIGINAL_SIZES = 'data-original-sizes';
const ATTR_DATA_SRCSET = 'data-srcset';
const ATTR_DATA_SRC = 'data-src';
const SUPPORTED_EXTENSIONS = [ 'gif', 'jpg', 'jpeg', 'png', 'webp' ];
const SRCSET_WIDTH_DESCRIPTOR = 'w';
const SRCSET_DENSITY_DESCRIPTOR = 'x';
/**
* Class representing lazy loading functionality with CDN support.
*/
export class AutoResizing {
/**
* Create a SmushLazyload instance.
*
* @param {Object} [options={}] - Auto resize options for the instance.
* @param {number} [options.precision=0] - Allowed width variation (in pixels) for determining if resizing is necessary.
* @param {boolean} [options.skipAutoWidth=false] - Whether to skip auto width resizing.
*/
constructor( { precision = 0, skipAutoWidth = false } = {} ) {
this.precision = parseInt( precision, 10 );
this.precision = isNaN( this.precision ) ? 0 : this.precision;
this.skipAutoWidth = skipAutoWidth;
this.initEventListeners();
}
/**
* Initialize event listeners.
*/
initEventListeners() {
document.addEventListener( LAZY_BEFORE_SIZES, ( e ) => {
if ( ! isSmushLazySizesInstance( e.detail?.instance ) ) {
return;
}
this.maybeAutoResize( e );
} );
}
/**
* Auto resize for CDN images.
*
* @param {Object} lazyEvent - Event object.
* @return {void}
*/
maybeAutoResize( lazyEvent ) {
const element = lazyEvent.target;
let resizeWidth = lazyEvent.detail?.width;
const isImage = 'IMG' === element?.nodeName;
// Exit early if the element is not an image or resizeWidth is missing.
if ( ! isImage || ! resizeWidth ) {
return;
}
const isInitialRender = lazyEvent.detail?.dataAttr;
// Skip processing if it's not the initial render.
if ( ! isInitialRender ) {
if ( ! this.getOriginalSizesAttr( element ) ) {
lazyEvent.preventDefault();
}
return;
}
// Check if the element is eligible for resizing.
if ( ! this.isElementEligibleForResizing( element ) ) {
return;
}
// Handle reverting to original sizes if necessary.
if ( this.shouldRevertToOriginalSizes( element, resizeWidth ) ) {
if ( this.revertToOriginalSizesIfNeeded( element ) ) {
// Prevent lazySizes from resizing the image.
lazyEvent.preventDefault();
}
return;
}
const customEvent = this.triggerEvent( element, SMUSH_BEFORE_SIZES, {
resizeWidth
} );
if ( customEvent.defaultPrevented ) {
// If the event is prevented, do not proceed with resizing and revert the sizes.
if ( this.revertToOriginalSizesIfNeeded( element ) ) {
// Prevent lazySizes from resizing the image.
lazyEvent.preventDefault();
}
return;
}
resizeWidth = customEvent.detail?.resizeWidth || resizeWidth;
// Resize the image using CDN if applicable.
const src = this.getDataSrc( element );
if ( this.isFromSmushCDN( src ) ) {
this.resizeImageWithCDN( element, resizeWidth );
if ( this.isChildOfPicture( element ) ) {
this.resizeSourceElements( element.parentNode.querySelectorAll( 'source' ), resizeWidth );
}
}
}
/**
* Decide whether Smush should apply auto-resize for this image.
*
* Rules:
* 1. If wrapper is inline/inline-block and wrapper/image already equal resizeWidth, skip (prevents Divi shrink).
* 2. Otherwise, allow.
*
* @param imageElement
* @param resizeWidth
*/
/**
* Decide whether Smush should apply auto-resize for this image.
*
* Rules:
* 1. If wrapper is inline/inline-block and wrapper/image already equal resizeWidth, skip (prevents Divi shrink).
* 2. Otherwise, allow.
*
* @param imageElement
* @param resizeWidth
*/
shouldAutoResize( imageElement, resizeWidth ) {
const wrapper = imageElement.parentNode;
if ( wrapper && this.isInlineElement( wrapper ) ) {
if ( 'PICTURE' === wrapper.nodeName ) {
return false;
}
const wrapperWidth = wrapper.clientWidth;
const imageWidth = imageElement.offsetWidth;
const isWrapperAndImageSameWidth = resizeWidth === wrapperWidth && wrapperWidth === imageWidth;
if ( isWrapperAndImageSameWidth ) {
// BAIL: doing a resize here risks shrinking the inline wrapper
return false;
}
}
return true;
}
isInlineElement( el ) {
if ( ! el || el.nodeType !== 1 ) {
return false;
}
const display = window.getComputedStyle( el ).display;
return display === 'inline' || display === 'inline-block';
}
isChildOfPicture( imageElement ) {
return imageElement && 'PICTURE' === imageElement?.parentNode?.nodeName;
}
resizeSourceElements( sourceElements, resizeWidth ) {
if ( ! sourceElements || ! sourceElements?.length ) {
return;
}
sourceElements.forEach( ( sourceElement ) => this.resizeSourceElement( sourceElement, resizeWidth ) );
}
resizeSourceElement( sourceElement, resizeWidth ) {
const srcset = sourceElement.getAttribute( ATTR_DATA_SRCSET );
if ( ! srcset ) {
return;
}
const sortedSources = this.parseSrcSet( srcset );
if ( ! sortedSources || ! sortedSources.length ) {
return;
}
const isNonResponsive = 1 === sortedSources.length && '' === sortedSources[ 0 ].unit;
if ( isNonResponsive ) {
this.resizeNonResponsiveSource( sourceElement, sortedSources[ 0 ].src, resizeWidth );
return;
}
const baseSourceSrc = this.getBaseSourceSrcForResize( sortedSources, resizeWidth );
if ( ! this.isFromSmushCDN( baseSourceSrc ) ) {
return;
}
this.updateSrcsetForResize( sourceElement, srcset, baseSourceSrc, resizeWidth, sortedSources );
}
resizeNonResponsiveSource(sourceElement, sourceSrc, resizeWidth ) {
if ( ! this.isFromSmushCDN( sourceSrc ) ) {
return;
}
if ( ! this.isSourceActive( sourceElement ) ) {
return;
}
let newSrcset = this.getResizedCDNURL( sourceSrc, resizeWidth );
// Add a new retina source to the srcset if no similar source exists for the retina width.
const scale = this.getPixelRatio();
if ( scale > 1 ) {
const retinaWidth = Math.ceil( resizeWidth * scale );
const retinaCDNURL = this.getResizedCDNURL( sourceSrc, retinaWidth );
const newRetinaSourceString = retinaCDNURL + ' ' + retinaWidth + SRCSET_WIDTH_DESCRIPTOR;
newSrcset += ` ${ resizeWidth }${ SRCSET_WIDTH_DESCRIPTOR }, ${ newRetinaSourceString }`;
}
// Update the element's data-srcset attribute if the srcset has changed.
this.updateElementSrcset( sourceElement, null, newSrcset );
}
isSourceActive( sourceElement ) {
const media = sourceElement.getAttribute( 'media' );
if ( media && ! window?.matchMedia( media )?.matches ) {
return false;
}
return true;
}
getBaseSourceSrcForResize( sortedSources, resizeWidth ) {
const largestSource = sortedSources[ 0 ];
if ( SRCSET_WIDTH_DESCRIPTOR !== largestSource.unit ) {
return null;
}
if (
! this.isThumbnail( largestSource.src ) ||
largestSource.value >= resizeWidth
) {
return largestSource.src;
}
return null;
}
isElementEligibleForResizing( element ) {
const existOriginalSizes = this.getOriginalSizesAttr( element );
const existSrc = this.getDataSrc( element );
const existSrcSet = this.getDataSrcSet( element );
/**
* lazybeforesizes only fires for images with data-sizes="auto",
* so skip checking it.
*/
return Boolean( existOriginalSizes && existSrc && existSrcSet );
}
shouldRevertToOriginalSizes( element, resizeWidth ) {
const imageWidth = this.getElementWidth( element );
// Skip resizing if width is 'auto' and skipping is enabled.
if ( imageWidth === 'auto' ) {
return this.shouldSkipAutoWidth();
}
const originalSizes = this.getOriginalSizesAttr( element );
const maxWidthFromSizes = this.getMaxWidthFromSizes( originalSizes );
if ( maxWidthFromSizes && resizeWidth > maxWidthFromSizes && ! this.isChildOfPicture( element ) ) {
return true;
}
return ! this.shouldAutoResize( element, resizeWidth );
}
triggerEvent( elem, name, detail = {}, bubbles = true, cancelable = true ) {
const event = new CustomEvent( name, {
detail,
bubbles,
cancelable,
} );
elem.dispatchEvent( event );
return event;
}
/**
* Determines if auto width resizing should be skipped.
*
* @return {boolean} - True if auto width resizing should be skipped, false otherwise.
*/
shouldSkipAutoWidth() {
return this.skipAutoWidth;
}
/**
* Resize the image using the CDN to generate appropriate sizes for the target width.
*
* @param {HTMLElement} element - The image element to resize.
* @param {number} resizeWidth - The target width for the image.
*/
resizeImageWithCDN( element, resizeWidth ) {
const srcset = this.getDataSrcSet( element );
const src = this.getDataSrc( element );
// Exit early if the srcset or src is missing.
if ( ! srcset || ! src ) {
return;
}
// Parse the srcset once and reuse the parsed sources.
const sortedSources = this.parseSrcSet( srcset );
//the src attribute can be a thumbnail, so we need to get the larger image url to resize from it.
const baseImageSrc = this.getBaseImageSrcForResize( src, sortedSources, resizeWidth );
this.updateSrcsetForResize( element, srcset, baseImageSrc, resizeWidth, sortedSources );
}
updateSrcsetForResize( element, srcset, baseImageSrc, resizeWidth, sources ) {
// Update the srcset with the target width.
let newSrcset = this.updateSrcsetWithTargetWidth( srcset, baseImageSrc, resizeWidth, sources );
// Update the srcset with retina-specific widths if applicable.
newSrcset = this.updateSrcsetWithRetinaWidth( newSrcset, baseImageSrc, resizeWidth, sources );
// Update the element's data-srcset attribute if the srcset has changed.
this.updateElementSrcset( element, srcset, newSrcset );
}
getBaseImageSrcForResize( src, sortedSources, resizeWidth ) {
if ( ! this.isThumbnail( src ) ) {
return src;
}
// Find the largest source that is larger than resizing width.
const largerSource = sortedSources.find( ( source ) => {
return source.value >= resizeWidth;
} );
return largerSource ? largerSource.src : src;
}
isThumbnail( src ) {
// Find the largest source that is larger than the current src.
const regex = new RegExp( `(-\\d+x\\d+)\\.(${ SUPPORTED_EXTENSIONS.join( '|' ) })(?:\\?.+)?$`, 'i' );
return regex.test( src );
}
/**
* Update the srcset with the target width if no similar source exists.
*
* @param {string} srcset - The current srcset string.
* @param {string} src - The original source URL of the image.
* @param {number} resizeWidth - The target width for the image.
* @param {Array} sources - The parsed sources from the srcset.
* @return {string} The updated srcset string.
*/
updateSrcsetWithTargetWidth( srcset, src, resizeWidth, sources ) {
// Add a new source to the srcset if no similar source exists for the target width.
if ( ! this.findSimilarSource( sources, resizeWidth ) ) {
const resizedCDNURL = this.getResizedCDNURL( src, resizeWidth );
return srcset + ', ' + resizedCDNURL + ' ' + resizeWidth + SRCSET_WIDTH_DESCRIPTOR;
}
return srcset;
}
/**
* Update the srcset with retina-specific widths if applicable.
*
* @param {string} srcset - The current srcset string.
* @param {string} src - The original source URL of the image.
* @param {number} resizeWidth - The target width for the image.
* @param {Array} sources - The parsed sources from the srcset.
* @return {string} The updated srcset string.
*/
updateSrcsetWithRetinaWidth( srcset, src, resizeWidth, sources ) {
const scale = this.getPixelRatio();
if ( scale <= 1 ) {
return srcset;
}
const retinaWidth = Math.ceil( resizeWidth * scale );
const hasRetinaSource = this.findSimilarSource( sources, scale, SRCSET_DENSITY_DESCRIPTOR ) ||
this.findSimilarSource( sources, retinaWidth, SRCSET_WIDTH_DESCRIPTOR );
if ( hasRetinaSource ) {
return srcset;
}
// Add a new retina source to the srcset if no similar source exists for the retina width.
const retinaCDNURL = this.getResizedCDNURL( src, retinaWidth );
const newRetinaSourceString = retinaCDNURL + ' ' + retinaWidth + SRCSET_WIDTH_DESCRIPTOR;
return srcset + ', ' + newRetinaSourceString;
}
/**
* Update the element's data-srcset attribute if the srcset has changed.
*
* @param {HTMLElement} element - The image element to update.
* @param {string} originalSrcset - The original srcset string.
* @param {string} newSrcset - The updated srcset string.
*/
updateElementSrcset( element, originalSrcset, newSrcset ) {
if ( newSrcset !== originalSrcset ) {
element.setAttribute( 'data-srcset', newSrcset );
}
}
/**
* Get the device pixel ratio.
*
* @return {number} The device pixel ratio. Default is 1 if the property is not available.
*/
getPixelRatio() {
return window.devicePixelRatio || 1;
}
/**
* Finds and returns the first source object that has a similar width to the target width.
*
* @param {Array} sources - An array of source objects to search through.
* @param {number} resizeWidth - The target width to match against the source widths.
* @param {string} [unit='w'] - The unit of measurement for the width (default is 'w').
* @param {number} [precision=this.precision] - The allowed width variation (in pixels) used to determine if a source width matches the target width during resizing.
* @return {Object|null} - The first source object that matches the criteria, or null if no match is found.
*/
findSimilarSource( sources, resizeWidth, unit = SRCSET_WIDTH_DESCRIPTOR, precision = this.precision ) {
return sources.find( ( source ) => {
return unit === source.unit && source.value >= resizeWidth &&
this.isFuzzyMatch( source.value, resizeWidth, precision );
} );
}
/**
* Get the resized image CDN URL.
*
* @param {string} src - The original source URL of the image.
* @param {number} resizeWidth - The target width for the resized image.
* @return {string|undefined} The resized image CDN URL, or undefined if resizing is not applicable.
*/
getResizedCDNURL( src, resizeWidth ) {
const url = this.parseURL( src );
if ( ! url ) {
return;
}
const searchParams = new URLSearchParams( url.search );
searchParams.set( 'size', `${ resizeWidth }x0` );
// Get the base URL (without search parameters).
const baseUrl = url.origin + url.pathname;
return `${ baseUrl }?${ searchParams.toString() }`;
}
/**
* Parse the URL from the source string.
*
* @param {string} src - The source URL string.
* @return {URL|null} The parsed URL object, or null if parsing fails.
*/
parseURL( src ) {
try {
return new URL( src );
} catch ( error ) {
return null;
}
}
/**
* Extract width, unit, and src from srcset.
*
* @param {string} srcset - The srcset string.
* @return {Array} An array of objects source info.
*/
parseSrcSet( srcset ) {
const sources = this.extractSourcesFromSrcSet( srcset );
return this.sortSources( sources );
}
extractSourcesFromSrcSet( srcset ) {
return srcset.split( ',' ).map( ( item ) => {
const [ src, descriptor ] = item.trim().split( /\s+/ );
let value = 0;
let unit = '';
if ( descriptor ) {
if ( descriptor.endsWith( SRCSET_WIDTH_DESCRIPTOR ) ) {
value = parseInt( descriptor, 10 );
unit = SRCSET_WIDTH_DESCRIPTOR;
} else if ( descriptor.endsWith( SRCSET_DENSITY_DESCRIPTOR ) ) {
value = parseFloat( descriptor );
unit = SRCSET_DENSITY_DESCRIPTOR;
}
}
return {
markup: item,
src,
value,
unit,
};
} );
}
sortSources( sources ) {
sources.sort( ( a, b ) => {
if ( a.value === b.value ) {
return 0;
}
return a.value > b.value ? -1 : 1;
} );
return sources;
}
/**
* Revert to the original sizes attribute.
*
* @param {Object} element - Image element.
* @return {boolean} True if the original sizes were reverted, false otherwise.
*/
revertToOriginalSizesIfNeeded( element ) {
const originalSizes = this.getOriginalSizesAttr( element );
if ( originalSizes ) {
element.setAttribute( 'sizes', originalSizes );
element.removeAttribute( ATTR_DATA_ORIGINAL_SIZES );
return true;
}
return false;
}
/**
* Get the image width.
*
* @param {Object} element - Image element.
* @return {string|number} The image width.
*/
getElementWidth( element ) {
/**
* Check if the element has an inline width set to 'auto'.
* Note: For external CSS, we couldn't cover it due to getComputedStyle just returning the parsed value.
*/
const inlineWidth = element.style?.width;
if ( inlineWidth && 'auto' === inlineWidth.trim() ) {
return 'auto';
}
const widthStr = window.getComputedStyle( element ).width;
const width = parseInt( widthStr, 10 );
return isNaN( width ) ? widthStr : width;
}
/**
* Get the content width from the original sizes attribute.
*
* @param {string} originalSizes - The original sizes attribute.
* @return {number} The content width.
*/
getMaxWidthFromSizes( originalSizes ) {
const regex = /\(max-width:\s*(\d+)px\)\s*100vw,\s*\1px/;
const match = originalSizes.match( regex );
return match ? parseInt( match[ 1 ], 10 ) : 0;
}
/**
* Get the original sizes attribute.
*
* @param {Object} element - Image element.
* @return {string} The original sizes attribute.
*/
getOriginalSizesAttr( element ) {
return element.getAttribute( ATTR_DATA_ORIGINAL_SIZES );
}
/**
* Get the srcset attribute.
*
* @param {Object} element - Image element.
* @return {string} The srcset attribute.
*/
getDataSrcSet( element ) {
return element.getAttribute( ATTR_DATA_SRCSET );
}
/**
* Get the src attribute.
*
* @param {Object} element - Image element.
* @return {string} The src attribute.
*/
getDataSrc( element ) {
return element.getAttribute( ATTR_DATA_SRC );
}
/**
* Check if the source is from the CDN.
*
* @param {string} src - The source URL.
* @return {boolean} True if the source is from the CDN, false otherwise.
*/
isFromSmushCDN( src ) {
return src && src.includes( SMUSH_CDN_DOMAIN );
}
/**
* Perform a fuzzy match between two numbers.
*
* @param {number} number1 - The first number.
* @param {number} number2 - The second number.
* @param {number} [precision=1] - The allowed variation. Default is 1.
* @return {boolean} True if the numbers are close enough, false otherwise.
*/
isFuzzyMatch( number1, number2, precision = 1 ) {
return Math.abs( number1 - number2 ) <= precision;
}
}
( () => {
'use strict';
const isAutoResizingEnabled = window.smushLazyLoadOptions?.autoResizingEnabled;
if ( ! isAutoResizingEnabled ) {
return;
}
let autoResizeOptions = window.smushLazyLoadOptions?.autoResizeOptions || {};
autoResizeOptions = Object.assign(
{
precision: 5, //5px.
skipAutoWidth: true, // Whether to skip the image has 'auto' width.
},
autoResizeOptions
);
new AutoResizing( autoResizeOptions );
} )();
@@ -0,0 +1,17 @@
/**
* @see https://github.com/aFarkas/lazysizes/issues/643#issuecomment-486168297
* Or https://github.com/aFarkas/lazysizes/issues/647#issuecomment-487724519
*/
const originalLazySizesConfig = window.lazySizesConfig || null;
if ( originalLazySizesConfig ) {
delete window.lazySizesConfig;
}
export default () => {
// Restore the original lazySizesConfig if it was set before.
if ( originalLazySizesConfig ) {
window.lazySizesConfig = originalLazySizesConfig;
} else if ( 'lazySizesConfig' in window ) {
delete window.lazySizesConfig;
}
};
@@ -0,0 +1,15 @@
import lazySizes from 'lazysizes';
/*
* TODO: Change the lazyClass name to be more specific to avoid conflicts with other plugins
* in the case that they are using the same default class name.
* @see https://github.com/aFarkas/lazysizes/issues/643#issuecomment-486168297
* or https://github.com/aFarkas/lazysizes/issues/647#issuecomment-487724519
*/
export const isSmushLazySizesInstance = ( instance ) => {
return instance === lazySizes ||
( JSON.stringify( instance?.cfg || {} ) === JSON.stringify( lazySizes?.cfg || {} ) );
};
export default lazySizes;
@@ -0,0 +1,34 @@
import { isSmushLazySizesInstance } from './helper/lazysizes';
( () => {
'use strict';
// Lazyload for background images.
const lazyloadBackground = ( element ) => {
const backgroundValue = element.getAttribute( 'data-bg-image' ) || element.getAttribute( 'data-bg' );
const cssProperty = element.hasAttribute( 'data-bg-image' ) ? 'background-image' : 'background';
if ( backgroundValue ) {
const currentStyle = element.getAttribute( 'style' ) || '';
const newBackgroundCSS = `${ cssProperty }: ${ backgroundValue };`;
const backgroundRegex = new RegExp( `${ cssProperty }\\s*:\\s*[^;]+;?` );
let updatedStyle;
if ( backgroundRegex.test( currentStyle ) ) {
updatedStyle = currentStyle.replace( backgroundRegex, newBackgroundCSS );
} else {
updatedStyle = currentStyle.length > 0 ? currentStyle.replace( /;$/g, '' ) + ';' + newBackgroundCSS : newBackgroundCSS;
}
element.setAttribute( 'style', updatedStyle.trim() );
}
};
document.addEventListener( 'lazybeforeunveil', function( e ) {
if ( ! isSmushLazySizesInstance( e?.detail?.instance ) ) {
return;
}
// Lazy background image.
lazyloadBackground( e.target );
} );
} )();
@@ -0,0 +1,261 @@
import { isSmushLazySizesInstance } from './helper/lazysizes';
( () => {
'use strict';
// Constants
const VIDEO_WRAPPER_CLASS = 'smush-lazyload-video';
const LAZY_LOADED_CLASS = 'smush-lazyloaded-video';
const AUTO_PLAY_CLASS = 'smush-lazyload-autoplay';
const PLAY_BUTTON_CLASS = 'smush-play-btn';
const DEFAULT_ALLOW_ATTR = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
const USER_INTERACTION_EVENT = 'ontouchstart' in window ? 'touchstart' : 'pointerdown';
const FALLBACK_VIDEO_RENDER_DELAY = Number( window?.smush_video_render_delay ) || 0;
/**
* LazyLoadVideo Class
* Handles lazy loading and autoplay functionality for videos.
*/
class LazyLoadVideo {
constructor() {
this.shouldDelayVideoRenderingForMobile = this.supportsIntersectionObserver();
this.queuedVideoElements = [];
this.isMobileOrSafari = null;
this.init();
}
/**
* Initialize event listeners and fallback mechanisms.
*/
init() {
document.addEventListener( 'lazybeforeunveil', ( e ) => this.handleVideoLazyLoad( e ) );
document.addEventListener(
USER_INTERACTION_EVENT,
() => this.enableVideoRenderingForMobile(),
{ once: true, passive: true }
);
// Unified fallback for delayed video rendering.
const maybeTriggerVideoRenderingFallbackForMobile = () => {
if ( FALLBACK_VIDEO_RENDER_DELAY <= 0 ) {
const hasAutoPlayVideo = document.querySelector( `.${ VIDEO_WRAPPER_CLASS }.${ AUTO_PLAY_CLASS }` );
if ( hasAutoPlayVideo ) {
this.enableVideoRenderingForMobile();
}
return;
}
setTimeout( () => this.enableVideoRenderingForMobile(), FALLBACK_VIDEO_RENDER_DELAY );
};
document.addEventListener( 'DOMContentLoaded', maybeTriggerVideoRenderingFallbackForMobile );
}
/**
* Handles lazy loading of video elements.
*
* @param {Event} e - The lazybeforeunveil event.
*/
handleVideoLazyLoad( e ) {
const videoWrapper = e.target;
if (
! isSmushLazySizesInstance( e?.detail?.instance ) ||
! videoWrapper.classList.contains( VIDEO_WRAPPER_CLASS )
) {
return;
}
this.handleButtonPlay( videoWrapper );
this.maybePrepareVideoForPlay( videoWrapper );
}
/**
* Handles the play button click event.
*
* @param {HTMLElement} videoWrapper - The video wrapper element.
*/
handleButtonPlay( videoWrapper ) {
const playButton = videoWrapper.querySelector( `.${ PLAY_BUTTON_CLASS }` );
if ( playButton ) {
const playHandler = () => this.loadIframeVideoWithAutoPlay( videoWrapper );
playButton.addEventListener( 'click', playHandler );
playButton.addEventListener( 'keydown', ( e ) => {
if ( e.key === 'Enter' || e.key === ' ' ) {
e.preventDefault();
playHandler();
}
} );
} else {
this.loadIframeVideo( videoWrapper );
window.console?.warning( 'Missing play button [.smush-play-btn] for video element:', videoWrapper );
}
}
/**
* Prepares the video for play based on autoplay and device conditions.
*
* @param {HTMLElement} videoWrapper - The video wrapper element.
*/
maybePrepareVideoForPlay( videoWrapper ) {
const shouldAutoPlay = videoWrapper.classList.contains( AUTO_PLAY_CLASS );
if ( this.shouldPrepareIframeForPlay() ) {
this.maybePrepareVideoForMobileAndSafari( videoWrapper, shouldAutoPlay );
} else if ( shouldAutoPlay ) {
this.loadIframeVideoWithAutoPlay( videoWrapper );
}
}
/**
* Enables video rendering for mobile devices.
*/
enableVideoRenderingForMobile() {
if ( ! this.shouldDelayVideoRenderingForMobile ) {
return;
}
this.shouldDelayVideoRenderingForMobile = false;
this.maybeObserveQueuedVideoElements();
}
/**
* Checks if the browser supports IntersectionObserver.
*
* @return {boolean} True if supported, false otherwise.
*/
supportsIntersectionObserver() {
return 'IntersectionObserver' in window;
}
/**
* Observes queued video elements for lazy loading.
*/
maybeObserveQueuedVideoElements() {
if ( this.queuedVideoElements.length ) {
this.observeQueuedVideoElements();
}
}
/**
* Observes video elements using IntersectionObserver.
*/
observeQueuedVideoElements() {
const observer = new IntersectionObserver(
( entries ) => {
entries.forEach( ( entry ) => {
if ( entry.isIntersecting ) {
const videoWrapper = entry.target;
this.loadIframeVideo( videoWrapper );
observer.unobserve( videoWrapper );
}
} );
},
{
rootMargin: '0px 0px 200px 0px',
threshold: 0.1,
}
);
this.queuedVideoElements.forEach( ( videoWrapper ) => {
observer.observe( videoWrapper );
} );
}
/**
* Prepares video for mobile and Safari browsers.
*
* @param {HTMLElement} videoWrapper - The video wrapper element.
* @param {boolean} shouldAutoPlay - Whether the video should autoplay.
*/
maybePrepareVideoForMobileAndSafari( videoWrapper, shouldAutoPlay ) {
if ( this.shouldDelayVideoRenderingForMobile ) {
this.queuedVideoElements.push( videoWrapper );
return;
}
this.loadIframeVideo( videoWrapper, shouldAutoPlay );
}
/**
* Checks if the iframe should be prepared for play.
*
* @return {boolean} True if preparation is needed, false otherwise.
*/
shouldPrepareIframeForPlay() {
if ( this.isMobileOrSafari === null ) {
this.isMobileOrSafari = this.checkIfMobileOrSafari();
}
return this.isMobileOrSafari;
}
/**
* Checks if the device is mobile or Safari browser.
*
* @return {boolean} True if mobile or Safari, false otherwise.
*/
checkIfMobileOrSafari() {
const userAgent = navigator.userAgent;
return userAgent.includes( 'Mobi' ) || ( userAgent.includes( 'Safari' ) && ! userAgent.includes( 'Chrome' ) );
}
/**
* Loads the iframe video.
*
* @param {HTMLElement} videoWrapper - The video wrapper element.
* @param {boolean} [autoPlay=false] - Whether to autoplay the video.
*/
loadIframeVideo( videoWrapper, autoPlay = false ) {
if ( videoWrapper.classList.contains( LAZY_LOADED_CLASS ) ) {
return;
}
videoWrapper.classList.add( LAZY_LOADED_CLASS, 'loading' );
const iframe = videoWrapper.querySelector( 'iframe' );
if ( ! iframe ) {
window.console?.error( 'Missing iframe element in video wrapper:', videoWrapper );
return;
}
let videoUrl = iframe.dataset?.src;
if ( ! videoUrl ) {
window.console?.error( 'Missing data-src attribute for iframe:', iframe );
return;
}
if ( autoPlay ) {
const url = new URL( videoUrl );
url.searchParams.set( 'autoplay', '1' );
url.searchParams.set( 'playsinline', '1' );
videoUrl = url.toString();
}
let allowAttribute = iframe.getAttribute( 'allow' ) || DEFAULT_ALLOW_ATTR;
if ( ! allowAttribute.includes( 'autoplay' ) ) {
allowAttribute += '; autoplay';
}
iframe.setAttribute( 'allow', allowAttribute );
iframe.setAttribute( 'allowFullscreen', 'true' );
iframe.setAttribute( 'src', videoUrl );
videoWrapper.classList.remove( 'loading' );
}
/**
* Loads the iframe video with autoplay enabled.
*
* @param {HTMLElement} videoWrapper - The video wrapper element.
*/
loadIframeVideoWithAutoPlay( videoWrapper ) {
this.loadIframeVideo( videoWrapper, true );
}
}
// Initialize LazyLoadVideo
new LazyLoadVideo();
} )();
@@ -0,0 +1,76 @@
(function () {
'use strict';
// Source: https://developers.google.com/speed/webp/faq#in_your_own_javascript.
function check_feature(feature, callback) {
const testImages = {
webp: "data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==",
avif: "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A="
};
const img = new Image();
img.onload = function () {
const result = (img.width > 0) && (img.height > 0);
callback(result);
};
img.onerror = function () {
callback(false);
};
img.src = testImages[feature];
}
function make_callback(fallbackAttributeName, dataValueName, extension) {
return function (isNextGenSupported) {
document.documentElement.classList.add(isNextGenSupported ? extension : 'no-' + extension);
if (isNextGenSupported) {
return;
}
const originalGetAttribute = Object.getOwnPropertyDescriptor(Element.prototype, 'getAttribute');
// Redefine the getAttribute function with a custom implementation
Object.defineProperty(Element.prototype, 'getAttribute', {
value: function (attributeName) {
if (!this.dataset.hasOwnProperty(dataValueName)) {
return originalGetAttribute.value.call(this, attributeName);
}
const fallbackObject = JSON.parse(this.dataset[dataValueName]);
if (attributeName in fallbackObject) {
return fallbackObject[attributeName];
}
return originalGetAttribute.value.call(this, attributeName);
}
});
const elementsWithFallback = document.querySelectorAll('[' + fallbackAttributeName + ']:not(.lazyload)');
if (elementsWithFallback.length) {
// Update background image, src, srcset.
const imageDisplayAttrs = ['src', 'srcset'];
elementsWithFallback.forEach((element) => {
const fallbackObject = JSON.parse(element.dataset[dataValueName]);
imageDisplayAttrs.forEach(function (attrName) {
if (attrName in fallbackObject) {
element.setAttribute(attrName, fallbackObject[attrName]);
}
});
// Update background image.
if ('bg' in fallbackObject) {
element.style.background = fallbackObject.bg;
}
if ('bg-image' in fallbackObject) {
element.style.backgroundImage = fallbackObject['bg-image'];
}
});
}
};
}
if (wp_smushit_nextgen_data?.mode === 'avif') {
check_feature('avif', make_callback('data-smush-avif-fallback', 'smushAvifFallback', 'avif'));
} else {
check_feature('webp', make_callback('data-smush-webp-fallback', 'smushWebpFallback', 'webp'));
}
})();
@@ -0,0 +1,437 @@
import '../../scss/resize-detection.scss';
/**
* Image resize detection (IRD).
*
* Show all wrongly scaled images with a highlighted border and resize box.
*
* Made in pure JS.
* DO NOT ADD JQUERY SUPPORT!!!
*
* @since 2.9
*/
( function() {
'use strict';
const SmushIRS = {
bar: document.getElementById( 'smush-image-bar' ),
toggle: document.getElementById( 'smush-image-bar-toggle' ),
images: {
bigger: [],
smaller: [],
},
strings: window.wp_smush_resize_vars,
/**
* Init scripts.
*/
init() {
/**
* Make sure these are set, before we proceed.
*/
if ( ! this.bar ) {
this.bar = document.getElementById( 'smush-image-bar' );
}
if ( ! this.toggle ) {
this.toggle = document.getElementById(
'smush-image-bar-toggle'
);
}
this.process();
// Register the event handler after everything is done.
this.toggle.addEventListener(
'click',
this.handleToggleClick.bind( this )
);
this.initImageVisibilityObserver();
},
/**
* Do image processing.
*/
process() {
const icon = this.toggle.querySelector( 'i' );
icon.classList.add( 'sui-icon-loader' );
icon.classList.remove( 'sui-icon-info' );
this.detectImages();
if ( ! this.images.bigger.length && ! this.images.smaller.length ) {
this.toggle.classList.add( 'smush-toggle-success' );
document.getElementById(
'smush-image-bar-notice'
).style.display = 'block';
document.getElementById(
'smush-image-bar-notice-desc'
).style.display = 'none';
} else {
this.toggle.classList.remove( 'smush-toggle-success' );
document.getElementById(
'smush-image-bar-notice'
).style.display = 'none';
document.getElementById(
'smush-image-bar-notice-desc'
).style.display = 'block';
this.generateMarkup( 'bigger' );
this.generateMarkup( 'smaller' );
}
this.toggleDivs();
icon.classList.remove( 'sui-icon-loader' );
icon.classList.add( 'sui-icon-info' );
},
/**
* Various checks to see if the image should be processed.
*
* @param {Object} image
* @return {boolean} Should skip image or not.
*/
shouldSkipImage( image ) {
if ( this.isIgnoredImage( image ) ) {
return true;
}
// Skip 1x1px and 0x0px images.
if (
image.clientWidth === image.clientHeight &&
( 1 === image.clientWidth || 0 === image.clientWidth )
) {
return true;
}
// Skip 1x1px and 0x0px placeholders.
if ( this.isPlaceholder( image ) ) {
return true;
}
// If width attribute is not set, do not continue.
return null === image.clientWidth || null === image.clientHeight;
},
isIgnoredImage( image ) {
// Skip avatars.
if ( image.classList.contains( 'avatar' ) ) {
return true;
}
// Skip images from Smush CDN with auto-resize feature.
return 'string' === typeof image.getAttribute( 'no-resize-detection' );
},
isPlaceholder( image ) {
return image.naturalWidth === image.naturalHeight && image.naturalWidth < 32;
},
/**
* Get tooltip text.
*
* @param {Object} props
* @return {string} Tooltip.
*/
getTooltipText( props ) {
let tooltipText = '';
if ( props.bigger_width || props.bigger_height ) {
/** @param {string} strings.large_image */
tooltipText = this.strings.large_image;
} else if ( props.smaller_width || props.smaller_height ) {
/** @param {string} strings.small_image */
tooltipText = this.strings.small_image;
}
return tooltipText
.replace( 'width', props.real_width )
.replace( 'height', props.real_height );
},
/**
* Generate markup.
*
* @param {string} type Accepts: 'bigger' or 'smaller'.
*/
generateMarkup( type ) {
this.images[ type ].forEach( ( image, index ) => {
const item = document.createElement( 'div' ),
tooltip = this.getTooltipText( image.props );
item.setAttribute(
'class',
'smush-resize-box smush-tooltip smush-tooltip-constrained'
);
item.setAttribute( 'data-tooltip', tooltip );
item.setAttribute( 'data-image', image.class );
item.addEventListener( 'click', ( e ) =>
this.highlightImage( e )
);
item.innerHTML = `
<div class="smush-image-info">
<span>${ index + 1 }</span>
<span class="smush-tag">${ image.props.computed_width } x ${ image.props.computed_height }px</span>
<i class="smush-front-icons smush-front-icon-arrows-in" aria-hidden="true">&nbsp;</i>
<span class="smush-tag smush-tag-success">${ image.props.real_width } × ${ image.props.real_height }px</span>
</div>
<div class="smush-image-description">${ tooltip }</div>
`;
document
.getElementById( 'smush-image-bar-items-' + type )
.appendChild( item );
} );
},
/**
* Show/hide sections based on images.
*/
toggleDivs() {
const types = [ 'bigger', 'smaller' ];
types.forEach( ( type ) => {
const div = document.getElementById(
'smush-image-bar-items-' + type
);
if ( 0 === this.images[ type ].length ) {
div.style.display = 'none';
} else {
div.style.display = 'block';
}
} );
},
/**
* Scroll the selected image into view and highlight it.
*
* @param {Object} e
*/
highlightImage( e ) {
this.removeSelection();
const el = document.getElementsByClassName(
e.currentTarget.dataset.image
);
if ( 'undefined' !== typeof el[ 0 ] ) {
// Display description box.
e.currentTarget.classList.toggle( 'show-description' );
// Scroll and flash image.
el[ 0 ].scrollIntoView( {
behavior: 'smooth',
block: 'center',
inline: 'nearest',
} );
el[ 0 ].style.opacity = '0.5';
setTimeout( () => {
el[ 0 ].style.opacity = '1';
}, 1000 );
}
},
/**
* Handle click on the toggle item.
*/
handleToggleClick() {
this.bar.classList.toggle( 'closed' );
this.toggle.classList.toggle( 'closed' );
this.removeSelection();
},
/**
* Remove selected items.
*/
removeSelection() {
const items = document.getElementsByClassName( 'show-description' );
if ( items.length > 0 ) {
Array.from( items ).forEach( ( div ) =>
div.classList.remove( 'show-description' )
);
}
},
/**
* Function to highlight all scaled images.
*
* Add yellow border and then show one small box to
* resize the images as per the required size, on fly.
*/
detectImages() {
const images = document.getElementsByTagName( 'img' );
for ( const image of images ) {
if ( this.shouldSkipImage( image ) ) {
continue;
}
// Get defined width and height.
const props = {
real_width: image.clientWidth,
real_height: image.clientHeight,
computed_width: image.naturalWidth,
computed_height: image.naturalHeight,
bigger_width: image.clientWidth * 1.5 < image.naturalWidth,
bigger_height:
image.clientHeight * 1.5 < image.naturalHeight,
smaller_width: image.clientWidth > image.naturalWidth,
smaller_height: image.clientHeight > image.naturalHeight,
};
// In case image is in correct size, do not continue.
if (
! props.bigger_width &&
! props.bigger_height &&
! props.smaller_width &&
! props.smaller_height
) {
continue;
}
const imgType =
props.bigger_width || props.bigger_height
? 'bigger'
: 'smaller',
imageClass =
`smush-image-${ imgType }-${ this.images[ imgType ].length + 1 }`;
// Fill the images arrays.
this.images[ imgType ].push( {
src: image,
props,
class: imageClass,
} );
/**
* Add class to original image.
* Can't add two classes in single add(), because no support in IE11.
* image.classList.add('smush-detected-img', imageClass);
*/
image.classList.add( 'smush-detected-img' );
image.classList.add( imageClass );
}
}, // End detectImages()
/**
* Allows refreshing the list. A good way is to refresh on lazyload actions.
*
* @since 3.6.0
*/
refresh() {
// Clear out classes on DOM.
for ( let id in this.images.bigger ) {
if ( this.images.bigger.hasOwnProperty( id ) ) {
this.images.bigger[ id ].src.classList.remove(
'smush-detected-img'
);
this.images.bigger[ id ].src.classList.remove(
'smush-image-bigger-' + ( ++id )
);
}
}
for ( let id in this.images.smaller ) {
if ( this.images.smaller.hasOwnProperty( id ) ) {
this.images.smaller[ id ].src.classList.remove(
'smush-detected-img'
);
this.images.smaller[ id ].src.classList.remove(
'smush-image-smaller-' + ( ++id )
);
}
}
this.images = {
bigger: [],
smaller: [],
};
// This might be overkill - there will probably never be a situation when there are less images than on
// initial page load.
const elements = document.getElementsByClassName(
'smush-resize-box'
);
while ( elements.length > 0 ) {
elements[ 0 ].remove();
}
this.process();
},
/**
* This will monitor all images on the page and detect their sizes once theyve loaded.
*
* @since 3.18.0
*/
initImageVisibilityObserver() {
let imagesSelector = 'img:not(.smush-detected-img)';
const smushLazyloadEnabled = typeof window.lazySizes !== 'undefined';
if ( smushLazyloadEnabled ) {
const lazyLoadClassName = window.lazySizes?.cfg?.lazyClass || 'lazyload';
imagesSelector += `:not(.${ lazyLoadClassName } )`;
}
const lazyImages = document.querySelectorAll( imagesSelector );
if ( ! lazyImages.length ) {
return;
}
const config = {
threshold: 0.1
};
let refreshTimeout = null;
const debouncedRefresh = () => {
if ( refreshTimeout ) {
clearTimeout( refreshTimeout );
}
refreshTimeout = setTimeout( () => {
this.refresh();
refreshTimeout = null;
}, 500 );
};
const imageObserver = new IntersectionObserver( ( entries ) => {
entries.forEach( ( entry ) => {
if ( entry.isIntersecting ) {
const img = entry.target;
if ( ! img.classList.contains( 'smush-detected-img' ) ) {
if ( img.complete && ! this.isPlaceholder( img ) ) {
debouncedRefresh();
} else {
img.addEventListener( 'load', () => {
debouncedRefresh();
}, { once: true } );
}
imageObserver.unobserve( img );
}
}
} );
}, config );
lazyImages.forEach( ( img ) => {
if ( this.isIgnoredImage( img ) ) {
return;
}
imageObserver.observe( img );
} );
},
}; // End WP_Smush_IRS
/**
* After page load, initialize toggle event.
*/
window.addEventListener( 'DOMContentLoaded', () => SmushIRS.init() );
window.addEventListener( 'lazyloaded', ( event ) => {
if ( 'IMG' !== event?.target?.tagName ) {
return;
}
SmushIRS.refresh();
} );
}() );
@@ -0,0 +1,62 @@
import tracker from './utils/tracker';
export default class GlobalTracking {
init() {
this.trackSubmenuProUpsell();
this.trackPluginListProUpsell();
this.trackDashboardWidgetProUpsell();
}
trackSubmenuProUpsell() {
const submenuUpgradeLink = document.querySelector( '#toplevel_page_smush a[href*="utm_campaign=smush_submenu_upsell' );
if ( submenuUpgradeLink ) {
submenuUpgradeLink.addEventListener( 'click', ( event ) => {
this.trackGeneralProUpsell( 'submenu', event?.target?.href );
} );
}
}
trackPluginListProUpsell() {
const pluginlistUpgradeLink = document.getElementById( 'smush-pluginlist-upgrade-link' );
if ( pluginlistUpgradeLink ) {
pluginlistUpgradeLink.addEventListener( 'click', ( event ) => {
this.trackGeneralProUpsell( 'plugins_list', event?.target?.href );
} );
}
}
trackDashboardWidgetProUpsell() {
const upsellBox = document.getElementById( 'smush-box-dashboard-upsell-upsell' );
if ( ! upsellBox ) {
return;
}
const dashboardProUpsellLink = upsellBox.querySelector( 'a[href*=smush-dashboard-upsell]' );
if ( dashboardProUpsellLink ) {
dashboardProUpsellLink.addEventListener( 'click', ( event ) => {
this.trackGeneralProUpsell( 'dash_widget', event?.target?.href );
} );
}
}
trackSetupWizardProUpsell( utmLink, proInterests ) {
this.trackGeneralProUpsell( 'wizard', utmLink, proInterests );
}
trackGeneralProUpsell( localtion, utmLink, proInterests = 'na' ) {
this.trackProUpsell( {
Feature: 'pro_general',
Location: localtion,
'UTM Link': utmLink,
'Pro Interests': proInterests,
} );
}
trackProUpsell( properties ) {
properties = Object.assign( {
'User Action': 'cta_clicked',
}, properties );
tracker.track( 'smush_pro_upsell', properties );
}
}
@@ -0,0 +1,119 @@
import '../scss/common.scss';
import DeactivationSurvey from './modules/deactivation-survey';
import GlobalTracking from './global-tracking';
/* global ajaxurl */
document.addEventListener( 'DOMContentLoaded', function() {
// Deactivation survey modal.
( new DeactivationSurvey() ).init();
// Global Trackings.
( new GlobalTracking() ).init();
// Dismiss notices.
const dismissNoticeButton = document.querySelectorAll(
'.smush-dismissible-notice .smush-dismiss-notice-button'
);
dismissNoticeButton.forEach((button) => {
button.addEventListener('click', handleDismissNotice);
});
function handleDismissNotice(event) {
event.preventDefault();
const button = event.target;
const notice = button.closest('.smush-dismissible-notice');
const key = notice.getAttribute('data-key');
dismissNotice( key, notice );
}
function dismissNotice( key, notice ) {
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
ajaxurl + '?action=smush_dismiss_notice&key=' + key + '&_ajax_nonce=' + smush_global.nonce,
true
);
xhr.onload = () => {
if (notice) {
// Trigger WordPress notice dismissal.
const noticeDismissButton = notice.querySelector('button.notice-dismiss');
if (noticeDismissButton) {
noticeDismissButton.dispatchEvent(new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
}));
} else {
notice.style.display = 'none';
}
}
};
xhr.send();
}
const dismissCacheNoticeButton = document.querySelector( '#wp-smush-cache-notice .smush-dismiss-notice-button' );
if ( dismissCacheNoticeButton ) {
dismissCacheNoticeButton.addEventListener( 'click', function() {
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
ajaxurl + '?action=smush_dismiss_cache_notice&_ajax_nonce=' + smush_global.nonce,
true
);
xhr.onload = () => {
window.SUI.closeNotice( 'wp-smush-cache-notice' );
};
xhr.send();
} );
}
// Show header notices.
const handleHeaderNotice = () => {
const headerNotice = document.querySelector('.wp-smush-dismissible-header-notice');
if ( ! headerNotice || ! headerNotice.id ) {
return;
}
const { dismissKey, message } = headerNotice.dataset;
if ( ! message ) {
return;
}
headerNotice.onclick = (e) => {
const classList = e.target.classList;
const isCloseAndDismissLink = classList && classList.contains( 'smush-close-and-dismiss-notice' );
const shouldDismissNotice = classList && ( isCloseAndDismissLink || classList.contains('sui-icon-check') || classList.contains('sui-button-icon') );
if ( ! shouldDismissNotice ) {
return;
}
if ( dismissKey ) {
dismissNotice( dismissKey );
}
if ( isCloseAndDismissLink ) {
window.SUI.closeNotice( headerNotice.id );
}
}
const noticeOptions = {
type: 'warning',
icon: 'info',
dismiss: {
show: true,
label: wp_smush_msgs.noticeDismiss,
tooltip: wp_smush_msgs.noticeDismissTooltip,
},
};
window.SUI.openNotice(
headerNotice.id,
message,
noticeOptions
);
}
handleHeaderNotice();
});
@@ -0,0 +1,50 @@
import Fetcher from './utils/fetcher';
class LoopbackTester {
delayTimeOnFailure = 5000;
performTest() {
return new Promise( ( resolve, reject ) => {
this.startTest().then( ( res ) => {
if ( res?.success ) {
this.getResult(
resolve,
() => {
setTimeout( () => {
this.getResult( resolve, reject, reject );
}, this.delayTimeOnFailure );
},
reject
);
} else {
reject( res );
}
} ).catch( ( error ) => {
reject( error );
} );
} );
}
startTest() {
return Fetcher.background.backgroundHealthyCheck();
}
getResult( successCallback, failedCallback, errorCallback ) {
return this.fetchResult().then( ( status ) => {
let data = status?.data;
if (status?.success && data?.loopback) {
successCallback(data);
} else {
failedCallback(status);
}
} ).catch( ( error ) => {
errorCallback( error );
} );
}
fetchResult() {
return Fetcher.background.backgroundHealthyStatus();
}
}
export default ( new LoopbackTester() );
@@ -0,0 +1,22 @@
jQuery(function ($) {
'use strict';
/**
* Handle the Smush Stats link click
*/
$( 'body' ).on( 'click', 'a.smush-stats-details', function ( e ) {
// If disabled
if ( $( this ).prop( 'disabled' ) ) {
return false;
}
e.preventDefault();
const $link = $( this );
const $wrapper = $link.parents().eq( 1 ).find( '.smush-stats-wrapper' );
// Toggle expanded state
$link.toggleClass( 'smush-stats-expanded' );
$wrapper.slideToggle();
} );
});
@@ -0,0 +1,472 @@
/* global WP_Smush */
/**
* Bulk Smush Background Optimization.
*
* @since 3.12.0
*/
import Fetcher from '../utils/fetcher';
import tracker from '../utils/tracker';
import SmushProgress from '../common/progressbar';
import {GlobalStats, UpsellManger} from "../common/globalStats";
import loopbackTester from '../loopback-tester';
(function () {
'use strict';
if (!window.wp_smush_msgs) {
return;
}
const $ = document.querySelector.bind(document);
const NO_FREE_LIMITED = 50;
/**
* Handle Background Process.
* @returns
*/
const BackgroundProcess = () => {
return {
handle(action) {
return Fetcher.background[action]();
},
initState() {
return Fetcher.background.initState();
},
}
}
/**
* Background Optimization.
*/
const BackgroundSmush = (() => {
const startBtn = window.wp_smushit_data && window.wp_smushit_data.bo_stats && $('.wp-smush-bo-start');
if (!startBtn) {
return;
}
const BO = new BackgroundProcess();
const bulkWrapper = $('.bulk-smush-wrapper');
const reScanImagesButton = $('.wp-smush-scan');
let intervalHandle = 0;
let cancellationInProgress = false;
return {
hookStatusChecks() {
if (intervalHandle) {
// Already done
return;
}
let count = 0;
let statusSyncInProgress = false;
let statSyncInProgress = false;
intervalHandle = setInterval(() => {
if (statusSyncInProgress) {
return;
}
statusSyncInProgress = true;
count++;
const statusSynced = this.syncBackgroundStatus();
if (count % 3 === 0) {
// Do a full update every nth time
statusSynced.then(() => {
if (!statSyncInProgress) {
this.syncStats().then(() => {
statSyncInProgress = false;
});
statSyncInProgress = true;
}
});
}
statusSynced.finally(() => {
statusSyncInProgress = false;
});
}, 3 * 1000);
},
resetBOStatsOnStart() {
GlobalStats.setBoStats( {
is_cancelled: false,
is_completed: false,
processed_items: 0,
failed_items: 0,
} );
},
start() {
// Reset boStats.
this.resetBOStatsOnStart();
// Disable UI.
this.onStart();
loopbackTester.performTest().then( ( res ) => {
const isLoopbackHealthy = res?.loopback;
if ( isLoopbackHealthy ) {
// Start BO.
this.startBulkSmush();
} else {
this.showLoopbackErrorModal();
this.onStartFailure();
}
} ).catch( ( error ) => {
console.error( 'Error:', error );
this.showLoopbackErrorModal();
this.onStartFailure();
} );
},
startBulkSmush() {
BO.handle('start').then((res) => {
if (res.success) {
// Update stats.
const updatedStats = this.updateStats(res.data, false);
// Show progress bar.
this.showProgressBar();
this.hookStatusChecks();
if ( updatedStats ) {
// Render stats.
GlobalStats.renderStats();
}
} else {
this.showFailureNotice( res );
this.onStartFailure( res );
}
});
},
showFailureNotice( res ) {
WP_Smush.helpers.showNotice( res, {
'showdismiss': true,
'autoclose' : false,
} );
},
onStartFailure( res ) {
this.cancelBulk();
},
showLoopbackErrorModal() {
const loopbackErrorModal = document.getElementById( 'smush-loopback-error-dialog' );
if ( ! loopbackErrorModal || ! window.SUI ) {
return;
}
// Cache current process type.
loopbackErrorModal.dataset.processType = 'smush';
WP_Smush.helpers.showModal( loopbackErrorModal.id );
},
/**
* Initial state when load the Bulk Smush page while BO is running.
*/
initState() {
if (!GlobalStats.getBoStats().in_processing) {
return;
}
// Disable UI.
this.onStart();
// Start BO.
BO.initState().then((res) => {
if (res.success) {
// Update stats.
this.updateStats(res.data, false);
// Show progress bar.
this.showProgressBar();
this.hookStatusChecks();
// Maybe update errors.
if ( res.data.errors && ! Object.keys( GlobalStats.getErrors() ).length ) {
GlobalStats.setErrors( res.data.errors );
}
// Render stats.
GlobalStats.renderStats();
} else {
WP_Smush.helpers.showNotice( res );
}
});
},
/**
* Cancel.
*/
cancel() {
cancellationInProgress = true;
this.setCancelButtonStateToStarted();
BO.handle('cancel').then((res) => {
if (res.success) {
this.cancelBulk();
} else {
WP_Smush.helpers.showNotice( res );
}
});
},
hideProgressBar() {
// Hide progress bar.
SmushProgress.close().update(0, GlobalStats.getBoStats().total_items);
},
showProgressBar() {
// Reset progress bar.
SmushProgress.update(GlobalStats.getBoStats().processed_items, GlobalStats.getBoStats().total_items);
// Show progress bar.
SmushProgress.show();
},
/**
* Update stats.
* @param {Object} newStats Included increment stats and new BO stats.
* @param updateGlobal
*/
updateStats(newStats, updateGlobal) {
// Make sure we have processed_stats/errors property (not added by default when start).
newStats.global_stats = newStats.global_stats || {};
newStats.errors = newStats.errors || {};
const {
global_stats,
errors,
...newBoStats
} = newStats;
if ( ! GlobalStats.isChangedStats( newBoStats ) ) {
return false;
}
// Update BO stats.
GlobalStats.setBoStats(newBoStats);
if (updateGlobal) {
// Update global stats.
GlobalStats.setGlobalStats(global_stats);
}
// Update Errors.
GlobalStats.setErrors( errors );
return true;
},
cancelBulk() {
// Sync Stats.
this.syncStats(() => {
if (100 === GlobalStats.getGlobalStats().percent_optimized) {
// If the last item was getting processed when the user cancelled then the process might have completed
GlobalStats.setBoStats( {
is_completed: true
} );
this.onCompletedBulk();
} else {
// Update status of boStats.
GlobalStats.setBoStats( {
is_cancelled: true
} );
// Reset and hide progress bar.
this.onFinish();
// Bulk is cancelled, show bulk desc.
SmushProgress.showBulkSmushDescription();
}
cancellationInProgress = false;
});
},
showCompletedMessage() {
// Render completed message.
// Show completed message.
const processedWrapper = bulkWrapper.querySelector('.wp-smush-all-done');
if ( GlobalStats.getBoStats().failed_items ) {
let completeMessage = wp_smush_msgs.all_failed;
if ( ! this.isFailedAllItems() ) {
completeMessage = wp_smush_msgs.error_in_bulk.replace( '{{smushed}}', GlobalStats.getBoStats().total_items - GlobalStats.getBoStats().failed_items )
.replace('{{total}}', GlobalStats.getBoStats().total_items )
.replace('{{errors}}', GlobalStats.getBoStats().failed_items );
}
processedWrapper.querySelector('p').innerHTML = completeMessage;
processedWrapper.classList.remove('sui-notice-success', 'sui-notice-warning');
const noticeType = this.getNoticeType();
const noticeIcon = 'warning' === noticeType ? 'info' : 'check-tick';
const iconElement = processedWrapper.querySelector('.sui-notice-icon');
processedWrapper.classList.add( 'sui-notice-' + noticeType );
iconElement.classList.remove('sui-icon-check-tick', 'sui-icon-info');
iconElement.classList.add( 'sui-icon-' + noticeIcon );
} else {
processedWrapper.querySelector('p').innerHTML = wp_smush_msgs.all_smushed;
}
processedWrapper.classList.remove('sui-hidden');
},
isFailedAllItems() {
return GlobalStats.getBoStats().failed_items === GlobalStats.getBoStats().total_items;
},
getNoticeType() {
return this.isFailedAllItems() ? 'warning' : 'success';
},
onCompletedBulk() {
// Reset and hide progress bar.
this.onFinish();
// Bulk is completed, hide bulk desc.
SmushProgress.hideBulkSmushDescription();
// Show completed message.
this.showCompletedMessage();
// Reset the progress when we finish so the next smushing starts from zero.
SmushProgress.update(0, GlobalStats.getBoStats().total_items);
},
completeBulk() {
// Sync Stats.
this.syncStats(() => this.onCompletedBulk());
},
syncStats(onComplete = () => false) {
return BO.handle('getStats').then((res) => {
if ( res.success ) {
const responseErrors = res.data.errors || {};
this.updateStats( { global_stats: res.data, errors: responseErrors }, true );
GlobalStats.renderStats();
if ( res.data.content ) {
$('#wp-smush-bulk-content').innerHTML = res.data.content;
}
onComplete();
} else {
WP_Smush.helpers.showNotice( res );
}
}).catch( (error) => console.log('error', error));
},
syncBackgroundStatus() {
return BO.handle('getStatus').then((res) => {
if ((res.data || {}).in_process_notice) {
SmushProgress.setNotice( res.data.in_process_notice );
}
if ( (res.data || {}).is_process_stuck ) {
SmushProgress.showHoldOnNotice();
}
if (res.success) {
// Update stats.
if ( this.updateStats(res.data, false) ) {
// Update progress bar.
SmushProgress.update(GlobalStats.getBoStats().processed_items, GlobalStats.getBoStats().total_items);
if (! GlobalStats.getBoStats().is_cancelled && ! GlobalStats.getBoStats().is_completed) {
// Render stats.
GlobalStats.renderStats();
}
}
if (GlobalStats.getBoStats().is_cancelled && !cancellationInProgress) {
// Cancelled on server side
this.cancelBulk();
} else if (GlobalStats.getBoStats().is_completed) {
this.completeBulk();
} else if ( GlobalStats.getBoStats().is_dead ) {
this.onDead();
}
} else {
WP_Smush.helpers.showNotice( res );
}
});
},
onStart() {
// Disable btn.
startBtn.setAttribute('disabled', '');
// Disable re-check images.
reScanImagesButton && reScanImagesButton.setAttribute('disabled', '');
$('.wp-smush-restore').setAttribute('disabled', '');
// Show upsell cdn.
UpsellManger.maybeShowCDNUpsellForPreSiteOnStart();
this.hideBulkSmushFailedNotice();
this.setCancelButtonStateToInitial();
},
hideBulkSmushFailedNotice() {
const bulkSmushFailedNotice = document.querySelector( '.wp-smush-inline-retry-bulk-smush-notice' );
if ( bulkSmushFailedNotice ) {
bulkSmushFailedNotice.parentElement.classList.add( 'sui-hidden' );
}
},
onFinish() {
// Clear interval.
if (intervalHandle) {
clearInterval(intervalHandle);
intervalHandle = 0;
}
// Disable btn.
startBtn.removeAttribute('disabled');
// Reset and hide Progress Bar.
this.hideProgressBar();
// Disable re-check images.
reScanImagesButton && reScanImagesButton.removeAttribute('disabled', '');
$('.wp-smush-restore').removeAttribute('disabled', '');
// Show upsell cdn.
UpsellManger.maybeShowCDNUpsellForPreSiteOnCompleted();
},
onDead() {
this.onFinish();
SmushProgress.showBulkSmushDescription();
this.showRetryBulkSmushModal();
},
showRetryBulkSmushModal() {
const retryModalElement = document.getElementById( 'smush-retry-bulk-smush-notice' );
if ( ! window.SUI || ! retryModalElement ) {
return;
}
retryModalElement.querySelector('.smush-retry-bulk-smush-notice-button').onclick = (e) => {
e.preventDefault();
window.SUI.closeModal( 'smush-retry-bulk-smush-notice' );
this.start();
}
window.SUI.openModal(
'smush-retry-bulk-smush-notice',
'wpbody-content',
undefined,
false
);
},
init() {
if (!startBtn) {
return;
}
// Start BO.
startBtn.onclick = () => {
const requiredScanMedia = startBtn.classList.contains('wp-smush-scan-and-bulk-smush');
if ( requiredScanMedia ) {
return;
}
this.start();
}
const triggerBulkSmushButton = document.querySelector( '.wp-smush-inline-retry-bulk-smush-notice .wp-smush-trigger-bulk-smush' );
if ( triggerBulkSmushButton ) {
triggerBulkSmushButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
startBtn.click();
} );
}
// If BO is running, initial new state.
this.initState();
},
setCancelButtonStateToInitial() {
SmushProgress.setCancelButtonLabel( wp_smush_msgs.cancel );
SmushProgress.setOnCancelCallback( this.cancel.bind(this) );
},
setCancelButtonStateToStarted() {
SmushProgress.setCancelButtonLabel( wp_smush_msgs.cancelling );
SmushProgress.setOnCancelCallback( () => false );
},
}
})();
// Run.
BackgroundSmush && BackgroundSmush.init();
/**
* For globalStats, we will need to update it on reload and after re-checking images,
* and on finish the BO.
* 1. On finish, we handled via BackgroundSmush.syncStats -> BackgroundSmush.updateStats
* 2. On reload or after re-checking images, we need to update globalStats from global variable:
* window.wp_smushit_data
*/
// Update global stats after re-checking images.
document.addEventListener('wpSmushAfterRecheckImages', function(){
GlobalStats.updateGlobalStatsFromSmushScriptData();
});
document.addEventListener('backgroundBulkSmushOnScanCompleted', function(){
if ( ! BackgroundSmush ) {
return;
}
GlobalStats.setBoStats({
in_processing: true,
});
BackgroundSmush.initState();
});
})();
@@ -0,0 +1,303 @@
/* global WP_Smush */
/* global ajaxurl */
/* global _ */
import tracker from "../utils/tracker";
/**
* Bulk restore JavaScript code.
*
* @since 3.2.2
*/
(function () {
'use strict';
/**
* Bulk restore modal.
*
* @since 3.2.2
*/
WP_Smush.restore = {
modal: document.getElementById('smush-restore-images-dialog'),
contentContainer: document.getElementById('smush-bulk-restore-content'),
settings: {
slide: 'start', // start, progress or finish.
success: 0,
errors: [],
},
items: [], // total items, 1 item = 1 step.
success: [], // successful items restored.
errors: [], // failed items.
currentStep: 0,
totalSteps: 0,
/**
* Init module.
*/
init() {
if (!this.modal) {
return;
}
this.settings = {
slide: 'start',
success: 0,
errors: [],
};
this.resetModalWidth();
this.renderTemplate();
// Show the modal.
window.SUI.openModal(
'smush-restore-images-dialog',
'wpbody-content',
undefined,
false
);
},
/**
* Update the template, register new listeners.
*/
renderTemplate() {
const template = WP_Smush.onboarding.template('smush-bulk-restore');
const content = template(this.settings);
if (content) {
this.contentContainer.innerHTML = content;
}
this.bindSubmit();
},
/**
* Reset modal width.
*
* @since 3.6.0
*/
resetModalWidth() {
this.modal.style.maxWidth = '460px';
this.modal.querySelector('.sui-box').style.maxWidth = '460px';
},
/**
* Catch "Finish setup wizard" button click.
*/
bindSubmit() {
const confirmButton = this.modal.querySelector(
'button[id="smush-bulk-restore-button"]'
);
const self = this;
if (confirmButton) {
confirmButton.addEventListener('click', function (e) {
e.preventDefault();
self.resetModalWidth();
self.settings = { slide: 'progress' };
self.errors = [];
self.success = [];
self.renderTemplate();
self.initScan();
});
}
},
/**
* Cancel the bulk restore.
*/
cancel() {
if (
'start' === this.settings.slide ||
'finish' === this.settings.slide
) {
// Hide the modal.
window.SUI.closeModal();
} else {
this.updateProgressBar(true);
window.location.reload();
}
},
/**
* Update progress bar during directory smush.
*
* @param {boolean} cancel Cancel status.
*/
updateProgressBar(cancel = false) {
let progress = 0;
if (0 < this.currentStep) {
progress = Math.min(
Math.round((this.currentStep * 100) / this.totalSteps),
99
);
}
if (progress > 100) {
progress = 100;
}
// Update progress bar
this.modal.querySelector('.sui-progress-text span').innerHTML =
progress + '%';
this.modal.querySelector('.sui-progress-bar span').style.width =
progress + '%';
const statusDiv = this.modal.querySelector(
'.sui-progress-state-text'
);
if (progress >= 90) {
statusDiv.innerHTML = 'Finalizing...';
} else if (cancel) {
statusDiv.innerHTML = 'Cancelling...';
} else {
statusDiv.innerHTML =
this.currentStep +
'/' +
this.totalSteps +
' ' +
'images restored';
}
},
/**
* First step in bulk restore - get the bulk attachment count.
*/
initScan() {
const self = this;
const _nonce = document.getElementById('_wpnonce');
const xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl + '?action=get_image_count', true);
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if (200 === xhr.status) {
const res = JSON.parse(xhr.response);
if ('undefined' !== typeof res.data.items) {
self.items = res.data.items;
self.totalSteps = res.data.items.length;
self.step();
}
} else {
window.console.log(
'Request failed. Returned status of ' + xhr.status
);
}
};
xhr.send('_ajax_nonce=' + _nonce.value);
},
/**
* Execute a scan step recursively
*/
step() {
const self = this;
const _nonce = document.getElementById('_wpnonce');
if (0 < this.items.length) {
const item = this.items.pop();
const xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl + '?action=restore_step', true);
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
this.currentStep++;
if (200 === xhr.status) {
const res = JSON.parse(xhr.response);
const data = ((res || {}).data || {});
if (data.success) {
self.success.push(item);
} else {
self.errors.push({
id: item,
src: data.src || "Error",
thumb: data.thumb,
link: data.link,
error_code: data.error_code || '',
});
}
}
self.updateProgressBar();
self.step();
};
xhr.send('item=' + item + '&_ajax_nonce=' + _nonce.value);
} else {
this.onFinish();
}
},
onFinish() {
const missingBackupCount = this.errors.filter(
(e) => e.error_code === 'missing_backup'
).length;
const errorCopyCount = this.errors.filter(
(e) => e.error_code === 'copy_failed'
).length;
// Finish.
this.settings = {
slide: 'finish',
success: this.success.length,
errors: this.errors,
errorsCount: this.errors.length,
missingBackupCount: missingBackupCount,
errorCopyCount: errorCopyCount,
total: this.totalSteps,
};
this.renderTemplate();
if (0 < this.errors.length) {
this.modal.style.maxWidth = '660px';
this.modal.querySelector('.sui-box').style.maxWidth =
'660px';
}
this.trackBulkRestoredEvent();
},
trackBulkRestoredEvent() {
tracker.track(
'Bulk Restore Triggered',
{
Type: 'All',
'Total images restored': this.settings.success,
'Total images': this.settings.total,
'Backup not found': this.settings.missingBackupCount
}
);
}
};
/**
* Template function (underscores based).
*
* @type {Function}
*/
WP_Smush.restore.template = _.memoize((id) => {
let compiled;
const options = {
evaluate: /<#([\s\S]+?)#>/g,
interpolate: /{{{([\s\S]+?)}}}/g,
escape: /{{([^}]+?)}}(?!})/g,
variable: 'data',
};
return (data) => {
_.templateSettings = options;
compiled =
compiled || _.template(document.getElementById(id).innerHTML);
return compiled(data);
};
});
})();
@@ -0,0 +1,202 @@
/* global WP_Smush */
/* global ajaxurl */
/**
* Bulk Smush functionality.
*
* @since 2.9.0 Moved from admin.js
*/
import Smush from '../smush/smush';
import Fetcher from '../utils/fetcher';
import SmushProcess from '../common/progressbar';
( function( $ ) {
'use strict';
class WP_Smush_Bulk {
#bulkSmushObj;
constructor() {
this.onClickBulkSmushNow();
this.onClickIgnoreImage();
this.onClickIgnoreAllImages();
this.onScanCompleted();
this.resumeBulkSmushHandler();
}
onClickBulkSmushNow() {
/**
* Handle the Bulk Smush/Bulk re-Smush button click.
*/
const self = this;
$( '.wp-smush-all' ).on( 'click', function( e ) {
const bulkSmushButton = $( this );
if ( bulkSmushButton.hasClass( 'wp-smush-scan-and-bulk-smush' ) ) {
return;
}
e.preventDefault();
self.ajaxBulkSmushStart( bulkSmushButton );
} );
}
resumeBulkSmushHandler() {
const resumeButton = document.querySelector( '.wp-smush-resume-bulk-smush' );
if ( ! resumeButton ) {
return;
}
resumeButton.addEventListener( 'click', ( e ) => {
if ( ! this.#bulkSmushObj ) {
return;
}
e.preventDefault();
const isUserClick = e.clientX > 0 && e.clientY > 0 && e.isTrusted;
if ( ! isUserClick ) {
return;
}
WP_Smush_Bulk.#resumeBulkSmush( this.#bulkSmushObj );
} );
}
ajaxBulkSmushStart( bulkSmushButton ) {
bulkSmushButton = bulkSmushButton || $( '#wp-smush-bulk-content .wp-smush-all' );
// Check for IDs, if there is none (unsmushed or lossless), don't call Smush function.
/** @param {Array} wp_smushit_data.unsmushed */
if (
'undefined' === typeof window.wp_smushit_data ||
( 0 === window.wp_smushit_data.unsmushed.length &&
0 === window.wp_smushit_data.resmush.length )
) {
return false;
}
// Disable re-Smush and scan button.
// TODO: refine what is disabled.
$(
'.wp-resmush.wp-smush-action, .wp-smush-scan, .wp-smush-all:not(.sui-progress-close), a.wp-smush-lossy-enable, button.wp-smush-resize-enable, button#save-settings-button'
).prop( 'disabled', true );
this.#bulkSmushObj = new Smush( bulkSmushButton, true );
SmushProcess.setOnCancelCallback( () => {
this.#bulkSmushObj.cancelAjax();
} ).update( 0, this.#bulkSmushObj.ids.length ).show();
// Show upsell cdn.
this.maybeShowCDNUpsellForPreSiteOnStart();
// Run bulk Smush.
this.#bulkSmushObj.run();
}
static #resumeBulkSmush( bulkSmushObj ) {
SmushProcess.disableExceedLimitMode();
SmushProcess.hideBulkSmushDescription();
bulkSmushObj.onStart();
bulkSmushObj.callAjax();
}
onClickIgnoreImage() {
/**
* Ignore file from bulk Smush.
*
* @since 2.9.0
*/
$( 'body' ).on( 'click', '.smush-ignore-image', function( e ) {
e.preventDefault();
const self = $( this );
self.prop( 'disabled', true );
self.attr( 'data-tooltip' );
self.removeClass( 'sui-tooltip' );
$.post( ajaxurl, {
action: 'ignore_bulk_image',
id: self.attr( 'data-id' ),
_ajax_nonce: wp_smush_msgs.nonce,
} ).done( ( response ) => {
if ( self.is( 'a' ) && response.success && 'undefined' !== typeof response.data.html ) {
if ( e.target.closest( '.smush-status-links' ) ) {
self.closest( '.smush-status-links' ).parent().html( response.data.html );
} else if ( e.target.closest( '.smush-bulk-error-row' ) ) {
self.addClass( 'disabled' );
e.target.closest( '.smush-bulk-error-row' ).style.opacity = 0.5;
}
}
} );
} );
}
onClickIgnoreAllImages() {
/**
* Ignore file from bulk Smush.
*
* @since 3.12.0
*/
const ignoreAll = document.querySelector( '.wp_smush_ignore_all_failed_items' );
if ( ignoreAll ) {
ignoreAll.onclick = ( e ) => {
e.preventDefault();
e.target.setAttribute( 'disabled', '' );
e.target.style.cursor = 'progress';
const type = e.target.dataset.type || null;
e.target.classList.remove( 'sui-tooltip' );
Fetcher.smush.ignoreAll( type ).then( ( res ) => {
if ( res.success ) {
window.location.reload();
} else {
e.target.style.cursor = 'pointer';
e.target.removeAttribute( 'disabled' );
WP_Smush.helpers.showNotice( res );
}
} );
};
}
}
onScanCompleted() {
document.addEventListener( 'ajaxBulkSmushOnScanCompleted', ( e ) => {
this.ajaxBulkSmushStart();
} );
}
maybeShowCDNUpsellForPreSiteOnStart() {
// Show upsell cdn.
const upsell_cdn = document.querySelector( '.wp-smush-upsell-cdn' );
if ( upsell_cdn ) {
upsell_cdn.classList.remove( 'sui-hidden' );
}
}
isBulkSmushInProgress() {
return this.#bulkSmushObj?.ids?.length > 0 && this.#bulkSmushObj.button?.hasClass( 'wp-smush-started' );
}
getTotalEnqueuedImages() {
return this.#bulkSmushObj?.total || 0;
}
getCompletionPercentage() {
const bulkSmushObj = this.#bulkSmushObj;
if ( ! bulkSmushObj ) {
return 0;
}
const totalEnqueuedImages = this.getTotalEnqueuedImages();
const smushed = Number( bulkSmushObj.smushed ) || 0;
const errors = Array.isArray( bulkSmushObj.errors ) ? bulkSmushObj.errors.length : 0;
const processedImages = smushed + errors;
if ( totalEnqueuedImages > 0 ) {
return Math.ceil( ( processedImages * 100 ) / totalEnqueuedImages );
}
return 0;
}
}
WP_Smush.bulk = new WP_Smush_Bulk();
}( jQuery ) );
@@ -0,0 +1,210 @@
// Deactivation survey.
import Fetcher from '../utils/fetcher';
import tracker from '../utils/tracker';
export default class DeactivationSurvey {
constructor() {
this.reason = 'not_set';
this.requestedAssistance = 'na';
this.modalAction = 'close';
this.modalId = 'wp-smush-deactivation-survey-modal';
this.modal = document.getElementById( this.modalId );
}
init() {
if ( ! this.modal ) {
return;
}
this.handleSurveyModal();
}
handleSurveyModal() {
this.deactivatePluginLink = document.querySelector( 'a[id^="deactivate-smush"]' ) || document.querySelector( 'a[id^="deactivate-wp-smushit"]' );
if ( ! this.deactivatePluginLink ) {
return;
}
this.deactivatePluginLink.addEventListener( 'click', ( e ) => {
e.preventDefault();
// Show modal.
this.showModal();
// Handle survey form.
this.handleSurveyForm();
} );
}
handleSurveyForm() {
this.registerRequestAssistanceLinkClickEvent();
this.handleRadioChange();
this.handleSkipDeactivation();
this.handleSubmitForm();
this.handleTrackDeactivate();
}
registerRequestAssistanceLinkClickEvent() {
const requestAssistanceLink = this.modal.querySelector( '#smush-request-assistance-link' );
if ( requestAssistanceLink ) {
this.requestedAssistance = 'no';
requestAssistanceLink.addEventListener( 'click', () => {
this.requestedAssistance = 'yes';
} );
}
}
handleRadioChange() {
const that = this;
this.userMessageField = document.getElementById( 'smush-deactivation-user-message-field' );
if ( ! this.userMessageField ) {
return;
}
this.modal.querySelectorAll( 'input[type="radio"]' ).forEach( ( inputRadio ) => {
inputRadio.addEventListener( 'change', function() {
that.reason = this.value;
that.toggleUserMessageField( this.parentElement );
} );
} );
}
handleSkipDeactivation() {
const skipButton = this.modal.querySelector( '.smush-skip-deactivate-button' );
if ( ! skipButton ) {
return;
}
skipButton.addEventListener( 'click', ( e ) => {
e.target.classList.add( 'sui-button-onload' );
this.modalAction = 'skip';
// Close modal and track on closed event.
this.closeModal();
// Deactivate the plugin when tracking is disabled; otherwise, handle it after tracking.
// @see this.trackDeactivate().
if ( ! tracker.allowToTrack() ) {
this.redirectToDeactivateLink();
}
}, { once: true } );
}
handleSubmitForm() {
const submitButton = this.modal.querySelector( '.smush-submit-deactivate-button' );
if ( ! submitButton ) {
return;
}
submitButton.addEventListener( 'click', ( e ) => {
e.target.classList.add( 'sui-button-onload' );
this.modalAction = 'submit';
// Close modal and track on closed event.
this.closeModal();
// Plugin deactivation has been handled after tracking.
// @see this.trackDeactivate().
}, { once: true } );
}
toggleUserMessageField( labelField ) {
if ( ! this.userMessageField ) {
return;
}
// Remove current user message field.
this.userMessageField.remove();
const placeholder = labelField.dataset?.placeholder;
if ( ! placeholder ) {
return;
}
// Update placeholder.
const textarea = this.userMessageField.querySelector( 'textarea' );
textarea.placeholder = placeholder;
// Append user message field.
labelField.after( this.userMessageField );
this.userMessageField.classList.remove( 'sui-hidden' );
// Focus on textarea.
textarea.focus();
}
getDeactivateLink() {
return this.deactivatePluginLink.href;
}
showModal() {
const focusAfterClosed = 'wpbody-content',
focusWhenOpen = undefined,
hasOverlayMask = true,
isCloseOnEsc = false,
isAnimated = true;
window.SUI?.openModal(
this.modalId,
focusAfterClosed,
focusWhenOpen,
hasOverlayMask,
isCloseOnEsc,
isAnimated
);
}
closeModal() {
window.SUI?.closeModal( true );
}
handleTrackDeactivate() {
this.modal.addEventListener( 'afterClose', () => this.trackDeactivate(), { once: true } );
}
trackDeactivate() {
if ( ! this.shouldTrack() ) {
return;
}
const event = 'Deactivation Survey';
const textarea = this.userMessageField.querySelector( 'textarea' );
const message = textarea.value;
const properties = {
Reason: this.reason,
Message: message,
'Modal Action': this.modalAction,
'Requested Assistance': this.requestedAssistance,
'Tracking Status': tracker.allowToTrack() ? 'opted_in' : 'opted_out',
};
Fetcher.common.request( {
action: 'smush_track_deactivate',
event,
properties,
} ).finally( () => {
if ( this.shouldDeactivatePlugin() ) {
this.redirectToDeactivateLink();
}
} );
}
shouldTrack() {
return tracker.allowToTrack() || this.isSubmitAction();
}
isSubmitAction() {
return 'submit' === this.modalAction;
}
shouldDeactivatePlugin() {
const skipAndDeactivate = 'skip' === this.modalAction;
return skipAndDeactivate || this.isSubmitAction();
}
redirectToDeactivateLink() {
const deactivateLink = this.getDeactivateLink();
window.location.href = deactivateLink;
}
}
@@ -0,0 +1,284 @@
/* global WP_Smush */
/* global ajaxurl */
/**
* Directory Smush module JavaScript code.
*
* @since 2.8.1 Separated from admin.js into dedicated file.
*/
import { createTree } from 'jquery.fancytree';
import Scanner from '../smush/directory-scanner';
( function( $ ) {
'use strict';
WP_Smush.directory = {
selected: [],
tree: [],
wp_smush_msgs: [],
triggered: false,
init() {
const self = this,
progressDialog = $( '#wp-smush-progress-dialog' );
let totalSteps = 0,
currentScanStep = 0;
// Make sure directory smush vars are set.
if ( typeof window.wp_smushit_data.dir_smush !== 'undefined' ) {
totalSteps = window.wp_smushit_data.dir_smush.totalSteps;
currentScanStep =
window.wp_smushit_data.dir_smush.currentScanStep;
}
// Init image scanner.
this.scanner = new Scanner( totalSteps, currentScanStep );
/**
* Smush translation strings.
*
* @param {Array} wp_smush_msgs
*/
this.wp_smush_msgs = window.wp_smush_msgs || {};
/**
* Open the "Select Smush directory" modal.
*/
$( 'button.wp-smush-browse, a#smush-directory-open-modal' ).on(
'click',
function( e ) {
e.preventDefault();
if ( $( e.currentTarget ).hasClass( 'wp-smush-browse' ) ) {
// Hide all the notices.
$( 'div.wp-smush-scan-result div.wp-smush-notice' ).hide();
// Remove notice.
$( 'div.wp-smush-info' ).remove();
}
window.SUI.openModal(
'wp-smush-list-dialog',
e.currentTarget,
$(
'#wp-smush-list-dialog .sui-box-header [data-modal-close]'
)[0],
true
);
//Display File tree for Directory Smush
self.initFileTree();
}
);
/**
* Smush images: Smush in Choose Directory modal clicked
*/
$( '#wp-smush-select-dir' ).on( 'click', function( e ) {
e.preventDefault();
$( 'div.wp-smush-list-dialog div.sui-box-body' ).css( {
opacity: '0.8',
} );
$( 'div.wp-smush-list-dialog div.sui-box-body a' ).off(
'click'
);
const button = $( this );
// Display the spinner.
button.addClass('sui-button-onload');
const selectedFolders = self.tree.getSelectedNodes();
const paths = [];
selectedFolders.forEach( function( folder ) {
paths.push( folder.key );
} );
// Send a ajax request to get a list of all the image files
const param = {
action: 'image_list',
smush_path: paths,
image_list_nonce: $(
'input[name="image_list_nonce"]'
).val(),
};
$.post( ajaxurl, param, function( response ) {
if ( response.success ) {
// Close the modal.
window.SUI.closeModal();
self.scanner = new Scanner( response.data, 0 );
self.showProgressDialog( response.data );
self.scanner.scan();
} else {
// Remove the spinner.
button.removeClass('sui-button-onload');
window.SUI.openNotice(
'wp-smush-ajax-notice',
response.data.message,
{ type: 'warning' }
);
}
} );
} );
/**
* Cancel scan.
*/
progressDialog.on(
'click',
'#cancel-directory-smush, #dialog-close-div, .wp-smush-cancel-dir',
function( e ) {
e.preventDefault();
// Display the spinner
$( '.wp-smush-cancel-dir' ).addClass( 'sui-button-onload' );
self.scanner
.cancel()
.done(
() => {
const directorySmushUrl = `${ self.wp_smush_msgs.bulk_smush_url }&smush__directory-scan=done#directory_smush-settings-row`;
if ( window.location.href === directorySmushUrl ) {
window.location.reload();
} else {
window.location.href = directorySmushUrl;
}
}
);
}
);
/**
* Continue scan.
*/
progressDialog.on(
'click',
'.sui-icon-play, .wp-smush-resume-scan',
function( e ) {
e.preventDefault();
self.scanner.resume();
}
);
/**
* Check to see if we should open the directory module.
* Used to redirect from dashboard page.
*
* @since 3.8.6
*/
const queryString = window.location.search;
const urlParams = new URLSearchParams( queryString );
if ( urlParams.has( 'smush__directory-start' ) && ! this.triggered ) {
this.triggered = true;
$( 'button.wp-smush-browse' ).trigger( 'click' );
}
},
/**
* Init fileTree.
*/
initFileTree() {
const self = this,
smushButton = $( 'button#wp-smush-select-dir' ),
ajaxSettings = {
type: 'GET',
url: ajaxurl,
data: {
action: 'smush_get_directory_list',
list_nonce: $( 'input[name="list_nonce"]' ).val(),
},
cache: false,
};
// Object already defined.
if ( Object.entries( self.tree ).length > 0 ) {
return;
}
self.tree = createTree( '.wp-smush-list-dialog .content', {
autoCollapse: true, // Automatically collapse all siblings, when a node is expanded
clickFolderMode: 3, // 1:activate, 2:expand, 3:activate and expand, 4:activate (dblclick expands)
checkbox: true, // Show checkboxes
debugLevel: 0, // 0:quiet, 1:errors, 2:warnings, 3:infos, 4:debug
selectMode: 3, // 1:single, 2:multi, 3:multi-hier
tabindex: '0', // Whole tree behaves as one single control
keyboard: true, // Support keyboard navigation
quicksearch: true, // Navigate to next node by typing the first letters
source: ajaxSettings,
lazyLoad: ( event, data ) => {
data.result = new Promise( function( resolve, reject ) {
ajaxSettings.data.dir = data.node.key;
$.ajax( ajaxSettings )
.done( ( response ) => resolve( response ) )
.fail( reject );
} );
},
loadChildren: ( event, data ) =>
data.node.fixSelection3AfterClick(), // Apply parent's state to new child nodes:
select: () =>
smushButton.prop(
'disabled',
! +self.tree.getSelectedNodes().length
),
init: () => smushButton.prop( 'disabled', true ),
} );
},
/**
* Show progress dialog.
*
* @param {number} items Number of items in the scan.
*/
showProgressDialog( items ) {
// Update items status and show the progress dialog..
$( '.wp-smush-progress-dialog .sui-progress-state-text' ).html(
'0/' + items + ' ' + self.wp_smush_msgs.progress_smushed
);
window.SUI.openModal(
'wp-smush-progress-dialog',
'dialog-close-div',
undefined,
false
);
},
/**
* Update progress bar during directory smush.
*
* @param {number} progress Current progress in percent.
* @param {boolean} cancel Cancel status.
*/
updateProgressBar( progress, cancel = false ) {
if ( progress > 100 ) {
progress = 100;
}
// Update progress bar
$( '.sui-progress-block .sui-progress-text span' ).text(
progress + '%'
);
$( '.sui-progress-block .sui-progress-bar span' ).width(
progress + '%'
);
if ( progress >= 90 ) {
$( '.sui-progress-state .sui-progress-state-text' ).text(
'Finalizing...'
);
}
if ( cancel ) {
$( '.sui-progress-state .sui-progress-state-text' ).text(
'Cancelling...'
);
}
},
};
WP_Smush.directory.init();
}( jQuery ) );
@@ -0,0 +1,105 @@
// Disconnect Site.
/* global WP_Smush */
import Fetcher from '../utils/fetcher';
import tracker from '../utils/tracker';
class DisconnectSite {
constructor() {
document.addEventListener( 'DOMContentLoaded', () => {
this.modalAction = 'cancel';
this.modalId = 'smush-disconnect-site-modal';
this.modal = document.getElementById( this.modalId );
this._onAfterClose = this.trackDisconnectSite.bind( this );
if ( this.modal ) {
this.modal.addEventListener( 'afterClose', this._onAfterClose, { once: true } );
document.addEventListener( 'onSavedSmushSettings', ( e ) => {
const usage = document.getElementById( 'usage' );
if ( usage ) {
tracker.setAllowToTrack( usage.checked );
}
} );
}
} );
}
trackDisconnectSite() {
const textarea = document.getElementById( 'smush-disconnect-site-message' );
const message = textarea.value.trim();
const skipMessage = message.length === 0;
if ( skipMessage && this.isSubmitAction() ) {
this.setModalAction( 'skip' );
}
if ( ! this.shouldTrack() ) {
return Promise.resolve();
}
const event = 'Disconnect Site';
const properties = {
'User Message': message,
'Modal Action': this.modalAction,
'Tracking Status': tracker.allowToTrack() ? 'opted_in' : 'opted_out',
};
return tracker.setAllowToTrack( true ).track( event, properties );
}
shouldTrack() {
return tracker.allowToTrack() || this.isSubmitAction();
}
isSubmitAction() {
return 'submit' === this.modalAction;
}
setModalAction( action ) {
this.modalAction = action;
return this;
}
closeModal() {
if ( ! this.modal ) {
return Promise.resolve();
}
return new Promise( ( resolve ) => {
// Ensure only one event listener is active.
this.modal.removeEventListener( 'afterClose', this._onAfterClose );
this.modal.addEventListener( 'afterClose', async () => {
await this.trackDisconnectSite();
resolve();
}, { once: true } );
window.SUI?.closeModal( true );
} );
}
disconnect( btn ) {
if ( btn ) {
btn.classList.add( 'sui-button-onload-text' );
}
return Fetcher.settings.disconnectSite().then( async ( res ) => {
if ( res.success ) {
await this.setModalAction( 'submit' ).closeModal();
window.location.search = window.location.search + `&smush-notice=site-disconnected`;
} else {
WP_Smush.helpers.showNotice( res );
}
} ).catch( ( error ) => {
WP_Smush.helpers.showNotice( error );
} ).finally( () => {
if ( btn ) {
btn.classList.remove( 'sui-button-onload-text' );
}
} );
}
}
const disconnectSite = new DisconnectSite();
export default disconnectSite;
@@ -0,0 +1,328 @@
/* global WP_Smush */
/* global ajaxurl */
/* global wp_smush_msgs */
/**
* Helpers functions.
*
* @since 2.9.0 Moved from admin.js
*/
( function() {
'use strict';
WP_Smush.helpers = {
init: () => {},
cacheUpsellErrorCodes: [],
/**
* Convert bytes to human-readable form.
*
* @param {number} a Bytes
* @param {number} b Number of digits
* @return {*} Formatted Bytes
*/
formatBytes: ( a, b ) => {
const thresh = 1024,
units = [ 'KB', 'MB', 'GB', 'TB', 'PB' ];
if ( Math.abs( a ) < thresh ) {
return a + ' B';
}
let u = -1;
do {
a /= thresh;
++u;
} while ( Math.abs( a ) >= thresh && u < units.length - 1 );
return a.toFixed( b ) + ' ' + units[ u ];
},
/**
* Get size from a string.
*
* @param {string} formattedSize Formatter string
* @return {*} Formatted Bytes
*/
getSizeFromString: ( formattedSize ) => {
return formattedSize.replace( /[a-zA-Z]/g, '' ).trim();
},
/**
* Get type from formatted string.
*
* @param {string} formattedSize Formatted string
* @return {*} Formatted Bytes
*/
getFormatFromString: ( formattedSize ) => {
return formattedSize.replace( /[0-9.]/g, '' ).trim();
},
/**
* Stackoverflow: http://stackoverflow.com/questions/1726630/formatting-a-number-with-exactly-two-decimals-in-javascript
*
* @param {number} num
* @param {number} decimals
* @return {number} Number
*/
precise_round: ( num, decimals ) => {
const sign = num >= 0 ? 1 : -1;
// Keep the percentage below 100.
num = num > 100 ? 100 : num;
return (
Math.round( num * Math.pow( 10, decimals ) + sign * 0.001 ) /
Math.pow( 10, decimals )
);
},
/**
* Displays a floating error message using the #wp-smush-ajax-notice container.
*
* @since 3.8.0
*
* @param {string} message
*/
showErrorNotice: ( message ) => {
if ( 'undefined' === typeof message ) {
return;
}
const noticeMessage = `<p>${ message }</p>`,
noticeOptions = {
type: 'error',
icon: 'info',
};
SUI.openNotice( 'wp-smush-ajax-notice', noticeMessage, noticeOptions );
const loadingButton = document.querySelector( '.sui-button-onload' );
if ( loadingButton ) {
loadingButton.classList.remove( 'sui-button-onload' );
}
},
/**
* Reset settings.
*
* @since 3.2.0
*/
resetSettings: () => {
const _nonce = document.getElementById( 'wp_smush_reset' );
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=reset_settings', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
const res = JSON.parse( xhr.response );
if ( 'undefined' !== typeof res.success && res.success ) {
window.location.href = wp_smush_msgs.smush_url;
}
} else {
window.console.log(
'Request failed. Returned status of ' + xhr.status
);
}
};
xhr.send( '_ajax_nonce=' + _nonce.value );
},
/**
* Prepare error row. Will only allow to hide errors for WP media attachments (not nextgen).
*
* @since 1.9.0
* @since 3.12.0 Moved from Smush.
*
* @param {string} errorMsg Error message.
* @param {string} fileName File name.
* @param {string} thumbnail Thumbnail for image (if available).
* @param {number} id Image ID.
* @param {string} type Smush type: media or netxgen.
* @param {string} errorCode Error code.
*
* @return {string} Row with error.
*/
prepareBulkSmushErrorRow: (errorMsg, fileName, thumbnail, id, type, errorCode) => {
const thumbDiv =
thumbnail && 'undefined' !== typeof thumbnail ?
`<img class="attachment-thumbnail" src="${thumbnail}" />` :
'<i class="sui-icon-photo-picture" aria-hidden="true"></i>';
const editLink = window.wp_smush_msgs.edit_link.replace('{{id}}', id);
fileName =
'undefined' === fileName || 'undefined' === typeof fileName ?
'undefined' :
fileName;
let tableDiv =
`<div class="smush-bulk-error-row" data-error-code="${errorCode}">
<div class="smush-bulk-image-data">
<div class="smush-bulk-image-title">
${ thumbDiv }
<span class="smush-image-name">
<a href="${editLink}">${fileName}</a>
</span>
</div>
<div class="smush-image-error">
${errorMsg}
</div>
</div>`;
if ('media' === type) {
tableDiv +=
`<div class="smush-bulk-image-actions">
<a href="javascript:void(0)" class="sui-tooltip sui-tooltip-constrained sui-tooltip-left smush-ignore-image" data-tooltip="${window.wp_smush_msgs.error_ignore}" data-id="${id}">
${window.wp_smush_msgs.btn_ignore}
</a>
<a class="smush-link-detail" href="${editLink}">
${window.wp_smush_msgs.view_detail}
</a>
</div>`;
}
tableDiv += '</div>';
tableDiv += WP_Smush.helpers.upsellWithError( errorCode );
return tableDiv;
},
cacheUpsellErrorCode( errorCode ) {
this.cacheUpsellErrorCodes.push( errorCode );
},
/**
* Get upsell base on error code.
* @param {string} errorCode Error code.
* @returns {string}
*/
upsellWithError(errorCode) {
if (
!errorCode
|| !window.wp_smush_msgs['error_' + errorCode]
|| this.isUpsellRendered( errorCode )
) {
return '';
}
this.cacheRenderedUpsell( errorCode );
return '<div class="smush-bulk-error-row smush-error-upsell">' +
'<div class="smush-bulk-image-title">' +
'<span class="smush-image-error">' +
window.wp_smush_msgs['error_' + errorCode] +
'</span>' +
'</div></div>';
},
// Do not use arrow function to use `this`.
isUpsellRendered( errorCode ) {
return this.cacheUpsellErrorCodes.includes( errorCode );
},
// Do not use arrow function to use `this`.
cacheRenderedUpsell( errorCode ) {
this.cacheUpsellErrorCodes.push( errorCode );
},
/**
* Get error message from Ajax response or Error.
* @param {Object} resp
*/
getErrorMessage: ( resp ) => {
return resp.message || resp.data && resp.data.message ||
resp.responseJSON && resp.responseJSON.data && resp.responseJSON.data.message ||
window.wp_smush_msgs.generic_ajax_error ||
resp.status && 'Request failed. Returned status of ' + resp.status
},
/**
* Displays a floating message from response,
* using the #wp-smush-ajax-notice container.
*
* @param {Object|string} notice
* @param {Object} noticeOptions
*/
showNotice: function( notice, noticeOptions ) {
let message;
if ( 'object' === typeof notice ) {
message = this.getErrorMessage( notice );
} else {
message = notice;
}
if ( ! message ) {
return;
}
noticeOptions = noticeOptions || {};
noticeOptions = Object.assign({
showdismiss: false,
autoclose: true,
},noticeOptions);
noticeOptions = {
type: noticeOptions.type || 'error',
icon: noticeOptions.icon || ( 'success' === noticeOptions.type ? 'check-tick' : 'info' ),
dismiss: {
show: noticeOptions.showdismiss,
label: window.wp_smush_msgs.noticeDismiss,
tooltip: window.wp_smush_msgs.noticeDismissTooltip,
},
autoclose: {
show: noticeOptions.autoclose
}
};
const noticeMessage = `<p>${ message }</p>`;
SUI.openNotice( 'wp-smush-ajax-notice', noticeMessage, noticeOptions );
return Promise.resolve( '#wp-smush-ajax-notice' );
},
closeNotice() {
window.SUI.closeNotice( 'wp-smush-ajax-notice' );
},
renderActivationCDNNotice: function( noticeMessage ) {
const animatedNotice = document.getElementById('wp-smush-animated-upsell-notice');
if ( animatedNotice ) {
return;
}
const upsellHtml = `<div class="sui-notice sui-notice-info sui-margin-top" id="wp-smush-animated-upsell-notice">
<div class="sui-notice-content">
<div class="sui-notice-message">
<i class="sui-notice-icon sui-icon-info" aria-hidden="true"></i>
<p>${noticeMessage}</p>
</div>
</div>
</div>`;
document.querySelector( '#smush-box-bulk .wp-smush-bulk-wrapper' ).outerHTML += upsellHtml;
},
redirectToPage( page ) {
page = `page=smush-${page}`;
if ( window.location.href.includes( page ) ) {
window.location.reload();
} else {
window.location.search = page;
}
},
showModal( modalId, options = {} ) {
if ( ! window.SUI ) {
return;
}
options = Object.assign( {
focusAfterClosed: 'wpbody-content',
focusWhenOpen: undefined,
hasOverlayMask: false,
isCloseOnEsc: false,
isAnimated: true,
}, options );
window.SUI.openModal(
modalId,
options.focusAfterClosed,
options.focusWhenOpen,
options.hasOverlayMask,
options.isCloseOnEsc,
options.isAnimated
);
}
};
WP_Smush.helpers.init();
}() );
@@ -0,0 +1,440 @@
/* global WP_Smush */
/**
* Scan Media Library.
*
*/
import SmushProgress from '../common/progressbar';
import MediaLibraryScanner from '../common/media-library-scanner';
import { GlobalStats } from '../common/globalStats';
( function() {
'use strict';
if ( ! window.wp_smush_msgs ) {
return;
}
const $ = document.querySelector.bind( document );
const existScanProgressBar = $( '.wp-smush-scan-progress-bar-wrapper' );
if ( ! existScanProgressBar ) {
return;
}
const recheckImagesBtn = $( '.wp-smush-scan' );
if ( ! recheckImagesBtn ) {
return;
}
const bulkSmushButton = $( '.wp-smush-bo-start' ) || $( '.wp-smush-bulk-wrapper .wp-smush-all' );
const { __ } = wp.i18n;
class MediaLibraryScannerOnBulkSmush extends MediaLibraryScanner {
constructor() {
super();
this.runBulkSmushOnComplete = false;
this.restoreButton = $( '.wp-smush-restore' );
this.autoBulkSmushNotification = $( '.wp-smush-auto-bulk-smush-notification' );
}
startScanThenBulkSmushOnComplete() {
this.runBulkSmushOnComplete = true;
this.startScan( true );
}
onStart() {
this.hideRecheckNotice();
this.hideFailedBulkSmushNotice();
this.disableRelatedButtons();
this.setRecheckImagesButtonOnLoad();
this.toggleBulkSmushBoxContent();
return this;
}
onStartFailure( response ) {
super.onStartFailure( response );
this.revertRelatedButtons();
}
onCloseProgressBar() {
this.maybeHideAutoBulkSmushNotification();
}
disableRelatedButtons() {
this.restoreButton.setAttribute( 'disabled', true );
if ( bulkSmushButton ) {
bulkSmushButton.setAttribute( 'disabled', true );
this.setInnerText( bulkSmushButton, __( 'Waiting for Re-check to finish', 'wp-smushit' ) );
}
}
revertRelatedButtons() {
if ( bulkSmushButton ) {
bulkSmushButton.removeAttribute( 'disabled' );
this.revertInnerText( bulkSmushButton );
}
this.restoreButton.removeAttribute( 'disabled' );
this.revertRecheckImagesButton();
return this;
}
setRecheckImagesButtonOnLoad() {
// recheckImagesBtn.classList.add( 'sui-button-onload' );
this.disableRecheckImagesButton();
this.setInnerText( recheckImagesBtn.querySelector( '.wp-smush-inner-text' ), __( 'Checking Images', 'wp-smushit' ) );
}
disableRecheckImagesButton() {
recheckImagesBtn.setAttribute( 'disabled', true );
}
revertRecheckImagesButton() {
// recheckImagesBtn.classList.remove( 'sui-button-onload' );
recheckImagesBtn.removeAttribute( 'disabled' );
this.revertInnerText( recheckImagesBtn.querySelector( '.wp-smush-inner-text' ) );
}
beforeUpdateStatus( stats ) {
this.runBulkSmushOnComplete = stats?.optimize_on_scan_completed;
this.maybeShowAutoBulkSmushNotification();
}
onDead( stats ) {
super.onDead( stats );
this.revertRelatedButtons();
this.setRequiredScanForBulkSmushButton();
}
onFinish( stats ) {
const globalStats = stats.global_stats;
super.onFinish( stats );
this.revertRelatedButtons();
this.toggleBulkSmushDescription( globalStats );
if ( globalStats.is_outdated ) {
this.setRequiredScanForBulkSmushButton();
} else {
this.removeScanEventFromBulkSmushButton();
}
this.revertRecheckWarning();
}
onCompleted( stats ) {
const requiredReloadPage = ! bulkSmushButton;
if ( requiredReloadPage ) {
window.location.reload();
return;
}
this.onFinish( stats );
const globalStats = stats.global_stats;
const allImagesSmushed = globalStats.remaining_count < 1;
if ( allImagesSmushed ) {
return;
}
if ( ! this.runBulkSmushOnComplete ) {
this.showRecheckNoticeSuccess();
return;
}
this.runBulkSmushOnComplete = false;
this.triggerBulkSmushEvent( stats );
}
showNotice( stats ) {
if ( ! stats.notice ) {
return;
}
let type = 'success';
if ( 'undefined' !== typeof stats.noticeType ) {
type = stats.noticeType;
}
window.SUI.openNotice(
'wp-smush-ajax-notice',
'<p>' + stats.notice + '</p>',
{ type, icon: 'check-tick' }
);
}
showRecheckNoticeSuccess() {
const recheckNotice = $( '.wp-smush-recheck-images-notice-box' );
if ( ! recheckNotice ) {
return;
}
this.hideFailedBulkSmushNotice();
this.showAnElement( recheckNotice );
this.hideAnElement( recheckNotice.querySelector( '.wp-smush-recheck-images-notice-warning' ) );
this.showAnElement( recheckNotice.querySelector( '.wp-smush-recheck-images-notice-success' ) );
}
showRecheckNoticeWarning() {
const recheckNotice = $( '.wp-smush-recheck-images-notice-box' );
if ( ! recheckNotice ) {
return;
}
this.hideFailedBulkSmushNotice();
this.showAnElement( recheckNotice );
this.hideAnElement( recheckNotice.querySelector( '.wp-smush-recheck-images-notice-success' ) );
this.showAnElement( recheckNotice.querySelector( '.wp-smush-recheck-images-notice-warning' ) );
}
hideRecheckNotice() {
this.hideAnElement( $( '.wp-smush-recheck-images-notice-box' ) );
}
hideFailedBulkSmushNotice() {
this.hideAnElement( $( '#smush-box-inline-retry-bulk-smush-notice' ) );
}
showProgressErrorNoticeOnRecheckNotice() {
const recheckWarningElement = $( '.wp-smush-recheck-images-notice-box .wp-smush-recheck-images-notice-warning' );
if ( ! recheckWarningElement ) {
return;
}
recheckWarningElement.classList.add( 'sui-notice-error' );
recheckWarningElement.classList.remove( 'sui-notice-warning' );
this.showRecheckNoticeWarning();
}
revertRecheckWarning() {
const recheckWarningElement = $( '.wp-smush-recheck-images-notice-box .wp-smush-recheck-images-notice-warning' );
if ( ! recheckWarningElement ) {
return;
}
recheckWarningElement.classList.add( 'sui-notice-warning' );
recheckWarningElement.classList.remove( 'sui-notice-error' );
this.revertInnerText( recheckWarningElement.querySelector( 'span' ) );
}
triggerBulkSmushEvent( stats ) {
this.disableRecheckImagesButton();
if ( stats.enabled_background_process ) {
this.triggerBackgroundBulkSmushEvent( stats.global_stats );
} else {
this.triggerAjaxBulkSmushEvent( stats.global_stats );
}
}
toggleBulkSmushDescription( globalStats ) {
if ( SmushProgress.isEmptyObject ) {
return;
}
if ( globalStats.remaining_count < 1 ) {
SmushProgress.hideBulkSmushDescription();
SmushProgress.showBulkSmushAllDone();
} else {
SmushProgress.showBulkSmushDescription();
SmushProgress.hideBulkSmushAllDone();
}
}
setRequiredScanForBulkSmushButton() {
bulkSmushButton && bulkSmushButton.classList.add( 'wp-smush-scan-and-bulk-smush' );
}
removeScanEventFromBulkSmushButton() {
bulkSmushButton && bulkSmushButton.classList.remove( 'wp-smush-scan-and-bulk-smush' );
}
triggerBackgroundBulkSmushEvent( globalStats ) {
document.dispatchEvent(
new CustomEvent( 'backgroundBulkSmushOnScanCompleted', {
detail: globalStats
} )
);
}
triggerAjaxBulkSmushEvent( globalStats ) {
document.dispatchEvent(
new CustomEvent( 'ajaxBulkSmushOnScanCompleted', {
detail: globalStats
} )
);
}
onCancelled( stats ) {
this.onFinish( stats );
this.runBulkSmushOnComplete = false;
this.setRequiredScanForBulkSmushButton();
}
maybeShowAutoBulkSmushNotification() {
if (
! this.runBulkSmushOnComplete
) {
return;
}
this.showAnElement( this.autoBulkSmushNotification );
}
maybeHideAutoBulkSmushNotification() {
if (
! this.runBulkSmushOnComplete
) {
return;
}
this.hideAnElement( this.autoBulkSmushNotification );
}
toggleBulkSmushBoxContent() {
GlobalStats.resetAndHideBulkErrors();
this.toggleBulkSmushDescription( GlobalStats.getGlobalStats() );
}
}
const mediaLibScanner = new MediaLibraryScannerOnBulkSmush();
/**
* Event Listeners.
*/
// Background Scan Media Library.
const registerScanMediaLibraryEvent = () => {
if ( ! recheckImagesBtn ) {
return;
}
const canScanInBackground = recheckImagesBtn.classList.contains( 'wp-smush-background-scan' );
if ( ! canScanInBackground ) {
return;
}
recheckImagesBtn.addEventListener( 'click', () => mediaLibScanner.startScan() );
//Check scan is running.
if ( window.wp_smushit_data.media_library_scan?.in_processing ) {
mediaLibScanner.onStart().showProgressBar().autoSyncStatus();
}
};
const maybeAutoStartMediaLibraryScanOnRedirect = () => {
if ( ! recheckImagesBtn || ! window.location.search.includes( 'smush-action=start-scan-media' ) ) {
return;
}
recheckImagesBtn.click();
const removeScanActionFromURLAddress = () => {
const cleanedURL = window.location.href.replace( '&smush-action=start-scan-media', '' );
window.history.pushState( null, null, cleanedURL );
};
removeScanActionFromURLAddress();
};
/**
* Recheck Images Notice events.
*/
const registerEventsRelatedInlineNoticeOnBulkSmushPage = () => {
const recheckImagesNotice = $( '.wp-smush-recheck-images-notice-box' );
if ( ! recheckImagesNotice || ! recheckImagesBtn ) {
return;
}
const triggerBackgroundScanImagesLink = recheckImagesNotice.querySelector( '.wp-smush-trigger-background-scan' );
if ( triggerBackgroundScanImagesLink ) {
triggerBackgroundScanImagesLink.onclick = ( e ) => {
e.preventDefault();
recheckImagesBtn.click();
};
if ( ! window.location.search.includes( 'smush-action=start-scan-media' ) ) {
if ( window.wp_smushit_data.media_library_scan?.is_dead ) {
mediaLibScanner.showProgressErrorNoticeOnRecheckNotice();
} else if( window.wp_smushit_data.is_outdated && ! window.wp_smushit_data?.bo_stats?.is_dead ) {
mediaLibScanner.showRecheckNoticeWarning();
}
}
}
const triggerBulkSmush = recheckImagesNotice.querySelector( '.wp-smush-trigger-bulk-smush' );
if ( triggerBulkSmush && bulkSmushButton ) {
triggerBulkSmush.onclick = ( e ) => {
e.preventDefault();
recheckImagesNotice.classList.add( 'sui-hidden' );
bulkSmushButton.click();
};
}
const dismissNotices = recheckImagesNotice.querySelectorAll( 'button.sui-button-icon' );
if ( dismissNotices ) {
dismissNotices.forEach( ( dismissNotice ) => {
dismissNotice.onclick = ( e ) => {
dismissNotice.closest( '.sui-recheck-images-notice' ).classList.add( 'sui-hidden' );
};
} );
}
document.addEventListener( 'onSavedSmushSettings', function( e ) {
if ( ! e?.detail?.is_outdated_stats ) {
return;
}
mediaLibScanner.setRequiredScanForBulkSmushButton();
const bulkSmushFailedNotice = document.querySelector( '#smush-box-inline-retry-bulk-smush-notice' );
const isShowingBulkSmushFailedNotice = bulkSmushFailedNotice && ! bulkSmushFailedNotice.classList.contains( 'sui-hidden' );
if ( isShowingBulkSmushFailedNotice ) {
return;
}
recheckImagesNotice.classList.remove( 'sui-hidden' );
recheckImagesNotice.querySelector( '.wp-smush-recheck-images-notice-success' ).classList.add( 'sui-hidden' );
recheckImagesNotice.querySelector( '.wp-smush-recheck-images-notice-warning' ).classList.remove( 'sui-hidden' );
} );
};
// Scan and Bulk Smush.
const registerScanAndBulkSmushEvent = () => {
if ( ! bulkSmushButton ) {
return;
}
const handleScanAndBulkSmush = ( e ) => {
const shouldRunScan = bulkSmushButton.classList.contains( 'wp-smush-scan-and-bulk-smush' );
if ( ! shouldRunScan ) {
return;
}
e.preventDefault();
mediaLibScanner.startScanThenBulkSmushOnComplete();
};
bulkSmushButton.addEventListener( 'click', handleScanAndBulkSmush );
};
const autoStartBulkSmushOnRedirect = () => {
if ( ! bulkSmushButton || ! window.location.search.includes( 'smush-action=start-bulk-' ) ) {
return;
}
bulkSmushButton.click();
const removeSmushActionFromURLAddress = () => {
const cleanedURL = window.location.href.replace( /&smush-action=start-bulk-(smush|webp-conversion)/i, '' );
window.history.pushState( null, null, cleanedURL );
};
removeSmushActionFromURLAddress();
};
const registerRetryProcessFromLoopbackErrorModal = () => {
const loopbackErrorModal = document.getElementById( 'smush-loopback-error-dialog' );
if ( ! loopbackErrorModal ) {
return;
}
const retryButton = loopbackErrorModal.querySelector( '.smush-retry-process-button' );
if ( retryButton ) {
retryButton.addEventListener( 'click', () => {
const processType = loopbackErrorModal.dataset?.processType || 'scan';
if ( 'scan' === processType ) {
recheckImagesBtn.click();
} else {
bulkSmushButton.click();
}
} );
}
};
registerScanMediaLibraryEvent();
registerScanAndBulkSmushEvent();
registerEventsRelatedInlineNoticeOnBulkSmushPage();
maybeAutoStartMediaLibraryScanOnRedirect();
autoStartBulkSmushOnRedirect();
registerRetryProcessFromLoopbackErrorModal();
}() );
@@ -0,0 +1,63 @@
/* global WP_Smush */
/**
* Scan Media Library.
*
*/
import MediaLibraryScanner from '../common/media-library-scanner';
( function() {
'use strict';
if ( ! window.wp_smush_msgs ) {
return;
}
const $ = document.querySelector.bind( document );
const existScanProgressBar = $( '.wp-smush-scan-progress-bar-wrapper' );
if ( ! existScanProgressBar ) {
return;
}
const recheckImagesBtn = $( '.wp-smush-scan' );
if ( recheckImagesBtn ) {
return;
}
//Check scan is running.
const is_scan_running = window.wp_smushit_data.media_library_scan?.in_processing;
if ( ! is_scan_running ) {
return;
}
const { __ } = wp.i18n;
class mediaLibraryScannerOnDashboard extends MediaLibraryScanner {
constructor() {
super();
this.bulkSmushLink = $( '.wp-smush-bulk-smush-link' );
}
onShowProgressBar() {
this.disableBulkSmushLink();
}
onCloseProgressBar() {
this.revertBulkSmushLink();
}
disableBulkSmushLink() {
if ( ! this.bulkSmushLink ) {
return;
}
this.bulkSmushLink.setAttribute( 'disabled', true );
this.setInnerText( this.bulkSmushLink, __( 'Waiting for Re-check to finish', 'wp-smushit' ) );
}
revertBulkSmushLink() {
if ( ! this.bulkSmushLink ) {
return;
}
this.bulkSmushLink.removeAttribute( 'disabled' );
this.revertInnerText( this.bulkSmushLink );
}
}
( new mediaLibraryScannerOnDashboard() ).showProgressBar().autoSyncStatus();
}() );
@@ -0,0 +1,52 @@
import Smush from '../smush/smush';
import SmushProcess from '../common/progressbar';
(function($) {
$(function() {
/** Handle NextGen Gallery smush button click **/
$('body').on('click', '.wp-smush-nextgen-send', function (e) {
// prevent the default action
e.preventDefault();
new Smush($(this), false, 'nextgen');
});
/** Handle NextGen Gallery Bulk smush button click **/
$('body').on('click', '.wp-smush-nextgen-bulk', function (e) {
// prevent the default action
e.preventDefault();
// Remove existing Re-Smush notices.
// TODO: REMOVE re-smush-notice since no longer used.
$('.wp-smush-resmush-notice').remove();
//Check for ids, if there is none (Unsmushed or lossless), don't call smush function
if (
'undefined' === typeof wp_smushit_data ||
(wp_smushit_data.unsmushed.length === 0 &&
wp_smushit_data.resmush.length === 0)
) {
return false;
}
const bulkSmush = new Smush( $(this), true, 'nextgen' );
SmushProcess.setOnCancelCallback( () => {
bulkSmush.cancelAjax();
}).update( 0, bulkSmush.ids.length ).show();
jQuery('.wp-smush-all, .wp-smush-scan').prop('disabled', true);
$('.wp-smush-notice.wp-smush-remaining').hide();
// Run bulk Smush.
bulkSmush.run();
})
.on('click', '.wp-smush-trigger-nextgen-bulk', function(e){
e.preventDefault();
const bulkSmushButton = $('.wp-smush-nextgen-bulk');
if ( bulkSmushButton.length ) {
bulkSmushButton.trigger('click');
SUI.closeNotice( 'wp-smush-ajax-notice' );
}
});
});
}(window.jQuery));
@@ -0,0 +1,113 @@
/* global ajaxurl */
/* global wp_smush_msgs */
( function( $ ) {
'use strict';
const s3alert = $( '#wp-smush-s3support-alert' );
/**
* S3 support alert.
*
* @since 3.6.2 Moved from class-s3.php
*/
if ( s3alert.length ) {
const noticeOptions = {
type: 'warning',
icon: 'info',
dismiss: {
show: true,
label: wp_smush_msgs.noticeDismiss,
tooltip: wp_smush_msgs.noticeDismissTooltip,
},
};
window.SUI.openNotice(
'wp-smush-s3support-alert',
s3alert.data( 'message' ),
noticeOptions
);
}
// Dismiss S3 support alert.
s3alert.on( 'click', 'button', () => {
$.post( ajaxurl,
{
action: 'dismiss_s3support_alert',
_ajax_nonce: window.wp_smush_msgs.nonce,
}
);
} );
// Remove API message.
$( '#wp-smush-api-message button.sui-button-icon' ).on( 'click', function( e ) {
e.preventDefault();
const notice = $( '#wp-smush-api-message' );
notice.slideUp( 'slow', function() {
notice.remove();
} );
$.post( ajaxurl,
{
action: 'hide_api_message',
_ajax_nonce: window.wp_smush_msgs.nonce,
}
);
} );
// Hide the notice after a CTA button was clicked
function removeNotice( e ) {
const $notice = $( e.currentTarget ).closest( '.smush-notice' );
$notice.fadeTo( 100, 0, () =>
$notice.slideUp( 100, () => $notice.remove() )
);
}
// Only used for the Dashboard notification for now.
$( '.smush-notice .smush-notice-act' ).on( 'click', ( e ) => {
removeNotice( e );
} );
// Dismiss the update notice.
$( '.wp-smush-update-info' ).on( 'click', '.notice-dismiss', ( e ) => {
e.preventDefault();
removeNotice( e );
$.post( ajaxurl,
{
action: 'dismiss_update_info',
_ajax_nonce: window.wp_smush_msgs.nonce,
}
);
} );
function insertHubConnectNotice() {
const mediaHubConnectNotice = $( '#smush-hub-connect-media-notice' );
if ( ! mediaHubConnectNotice.length ) {
return;
}
const headerEnd = $( '.wrap > .wp-header-end' );
if ( headerEnd.length ) {
headerEnd.after( mediaHubConnectNotice.show() );
}
}
insertHubConnectNotice();
$( '#smush-media-notification-skip' ).on( 'click', function ( e ) {
e.preventDefault();
$.post( ajaxurl, {
action: 'dismiss_media_hub_connect_notice',
_ajax_nonce: window.wp_smush_msgs.nonce,
} )
.done( function ( response ) {
if ( response && response.success ) {
$( '#smush-hub-connect-media-notice' ).remove();
}
} )
.fail( function () {
console.error( 'Failed to dismiss the media hub connect notice.' );
} );
} );
}( jQuery ) );
@@ -0,0 +1,558 @@
/* global WP_Smush */
/* global ajaxurl */
import tracker from '../utils/tracker';
import GlobalTracking from '../global-tracking';
/**
* Modals JavaScript code.
*/
( function() {
'use strict';
const onBoardingTemplateID = 'smush-onboarding-free';
if ( ! document.getElementById( onBoardingTemplateID ) ) {
return;
}
/**
* Onboarding modal.
*
* @since 3.1
*/
WP_Smush.onboarding = {
membership: 'free', // Assume free by default.
onboardingModal: document.getElementById( 'smush-onboarding-dialog' ),
settings: {
first: true,
last: false,
slide: 'start',
fields: {},
},
fields: {},
contentContainer: document.getElementById( 'smush-onboarding-content' ),
onboardingSlides: [],
touchX: null,
touchY: null,
recheckImagesLink: '',
upsellUTMClicked: false,
registerEventListeners() {
document.addEventListener(
'smush-onboarding:rendered-slide-scan_completed',
this.progressBarAnimation.bind( this )
);
// Skip setup.
this.skipButton = this.onboardingModal.querySelector(
'.smush-onboarding-skip-link'
);
if ( this.skipButton ) {
this.skipButton.addEventListener( 'click', this.skipSetup.bind( this ) );
}
},
/**
* Init module.
*/
init() {
if ( ! this.onboardingModal ) {
return;
}
this.onboardingSlides = window.onBoardingData?.slideKeys || [];
this.fields = window.onBoardingData?.slideFields || {};
this.settings.slide = this.onboardingSlides.length ? this.onboardingSlides[ 0 ] : 'start';
const dialog = document.getElementById( onBoardingTemplateID );
this.membership = dialog.dataset.type;
this.recheckImagesLink = dialog.dataset.ctaUrl;
if ( 'false' === dialog.dataset.tracking ) {
this.onboardingSlides.pop();
}
this.registerEventListeners();
this.renderTemplate();
// Show the modal.
window.SUI.openModal(
'smush-onboarding-dialog',
'wpcontent',
undefined,
false
);
},
/**
* Get swipe coordinates.
*
* @param {Object} e
*/
handleTouchStart( e ) {
const firstTouch = e.touches[ 0 ];
this.touchX = firstTouch.clientX;
this.touchY = firstTouch.clientY;
},
/**
* Process swipe left/right.
*
* @param {Object} e
*/
handleTouchMove( e ) {
if ( ! this.touchX || ! this.touchY ) {
return;
}
const xUp = e.touches[ 0 ].clientX,
yUp = e.touches[ 0 ].clientY,
xDiff = this.touchX - xUp,
yDiff = this.touchY - yUp;
if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {
if ( xDiff > 0 ) {
if ( false === WP_Smush.onboarding.settings.last ) {
WP_Smush.onboarding.next( null, 'next' );
}
} else if ( false === WP_Smush.onboarding.settings.first ) {
WP_Smush.onboarding.next( null, 'prev' );
}
}
this.touchX = null;
this.touchY = null;
},
progressBarAnimation( event ) {
const progressBar = this.onboardingModal.querySelector( '.sui-progress-bar span' );
const progressText = this.onboardingModal.querySelector( '.sui-progress-text' );
let start = 0;
const duration = 3000; // 3 seconds
const step = 10; // ms
if ( ! progressBar || ! progressText ) {
return;
}
const interval = setInterval( () => {
start += step;
const percent = Math.min( Math.round( ( start / duration ) * 100 ), 100 );
progressBar.style.width = percent + '%';
progressText.textContent = percent + '%';
if ( percent >= 100 ) {
clearInterval( interval );
const loader = this.onboardingModal.querySelector( '.sui-icon-loader' );
if ( loader ) {
loader.classList.remove( 'sui-icon-loader', 'sui-loading' );
loader.style.color = '#1ABC9C';
loader.classList.add( 'sui-icon-check-tick' );
}
const scanStats = this.onboardingModal.querySelector( '.scan-stats' );
if ( scanStats ) {
scanStats.classList.remove( 'sui-hidden' );
}
}
}, step );
},
/**
* Update the template, register new listeners.
*
* @param {string} directionClass Accepts: fadeInRight, fadeInLeft, none.
*
* Todo: Maybe redirect to finish scan slide when users go to in progressing slide.
*/
renderTemplate( directionClass = 'none' ) {
// Grab the selected value.
this.updateCheckboxStates();
const template = WP_Smush.onboarding.template( onBoardingTemplateID );
const settings = this.settings;
settings.fields = this.fields;
const content = template( settings );
if ( content ) {
this.contentContainer.innerHTML = content;
if ( 'none' === directionClass ) {
this.contentContainer.classList.add( 'loaded' );
} else {
this.contentContainer.classList.remove( 'loaded' );
this.contentContainer.classList.add( directionClass );
setTimeout( () => {
this.contentContainer.classList.add( 'loaded' );
this.contentContainer.classList.remove(
directionClass
);
}, 600 );
}
}
this.onboardingModal.addEventListener(
'touchstart',
this.handleTouchStart,
false
);
this.onboardingModal.addEventListener(
'touchmove',
this.handleTouchMove,
false
);
this.bindSubmit();
this.toggleSkipButton();
this.maybeHandleUpsellUTMClick();
const slideRendered = new CustomEvent(
`smush-onboarding:rendered-slide-${ this.settings.slide }`,
{
detail: {
settings: this.settings,
},
}
);
document.dispatchEvent( slideRendered );
},
updateCheckboxStates() {
const inputs = this.onboardingModal.querySelectorAll(
'input[type="checkbox"]'
);
if ( inputs ) {
inputs.forEach( ( checkbox ) => {
this.fields[ checkbox.id ] = checkbox.checked;
} );
}
},
toggleSkipButton() {
if ( ! this.skipButton ) {
return;
}
if ( this.settings.last ) {
this.skipButton.classList.add( 'sui-hidden' );
} else {
this.skipButton.classList.remove( 'sui-hidden' );
}
},
/**
* Catch "Finish setup wizard" button click.
*/
bindSubmit() {
const submitButton = this.onboardingModal.querySelector(
'button[type="submit"]'
);
const self = this;
if ( submitButton ) {
submitButton.addEventListener( 'click', async function( e ) {
e.preventDefault();
submitButton.classList.add( 'sui-button-onload-text' );
// Because we are not rendering the template, we need to update the last element value.
self.updateCheckboxStates();
try {
await self.trackFinishSetupWizard();
} catch ( err ) {}
const _nonce = document.getElementById(
'smush_quick_setup_nonce'
);
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=smush_free_setup', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
self.onFinishingSetup();
} else {
window.console.log(
'Request failed. Returned status of ' +
xhr.status
);
}
};
xhr.send(
'smush_settings=' +
JSON.stringify( self.fields ) +
'&_ajax_nonce=' +
_nonce.value
);
} );
}
},
onFinishingSetup() {
this.onFinish();
if ( window.onBoardingData?.isSiteConnected ) {
this.redirectAndStartBulkSmush();
} else {
this.redirectToConnectSite();
}
},
redirectAndStartBulkSmush() {
if ( window.onBoardingData?.startBulkSmushURL ) {
window.location.href = window.onBoardingData?.startBulkSmushURL;
}
},
onFinish() {
window.SUI.closeModal();
},
redirectToConnectSite() {
if ( ! window.onBoardingData?.connectSiteUrl ) {
return;
}
window.location.href = window.onBoardingData?.connectSiteUrl;
},
/**
* Handle navigation.
*
* @param {Object} e
* @param {null|string} whereTo
*/
next( e, whereTo = null ) {
const index = this.onboardingSlides.indexOf( this.settings.slide );
let newIndex = 0;
if ( ! whereTo ) {
newIndex =
null !== e && e.classList.contains( 'next' )
? index + 1
: index - 1;
} else {
newIndex = 'next' === whereTo ? index + 1 : index - 1;
}
const directionClass =
null !== e && e.classList.contains( 'next' )
? 'fadeInRight'
: 'fadeInLeft';
this.settings = {
first: 0 === newIndex,
last: newIndex + 1 === this.onboardingSlides.length, // length !== index
slide: this.onboardingSlides[ newIndex ],
fields: this.fields,
};
this.renderTemplate( directionClass );
},
/**
* Handle circle navigation.
*
* @param {string} target
*/
goTo( target ) {
const newIndex = this.onboardingSlides.indexOf( target );
this.settings = {
first: 0 === newIndex,
last: newIndex + 1 === this.onboardingSlides.length, // length !== index
slide: target,
fields: this.fields,
};
this.renderTemplate();
},
/**
* Skip onboarding experience.
*/
async skipSetup() {
const _nonce = document.getElementById( 'smush_quick_setup_nonce' );
this.updateCheckboxStates();
try {
await this.trackSkipSetupWizard();
} catch ( err ) {}
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
ajaxurl + '?action=skip_smush_setup&_ajax_nonce=' + _nonce.value
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
this.onSkipSetup();
} else {
window.console.log(
'Request failed. Returned status of ' + xhr.status
);
}
};
xhr.send();
},
onSkipSetup() {
this.onFinish();
this.redirectBulkSmushPage();
},
redirectBulkSmushPage() {
const bulkSmushPage = window.wp_smush_msgs?.bulk_smush_url;
if ( bulkSmushPage ) {
window.location.href = bulkSmushPage;
}
},
/**
* Hide new features modal.
*
* @param e
* @param button
* @since 3.7.0
* @since 3.12.2 Add a new parameter redirectUrl
*/
hideUpgradeModal: ( e, button ) => {
const isRedirectRequired = '_blank' !== button?.target;
if ( isRedirectRequired ) {
e.preventDefault();
}
button.classList.add( 'wp-smush-link-in-progress' );
const redirectUrl = button?.href;
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=hide_new_features&_ajax_nonce=' + window.wp_smush_msgs.nonce );
xhr.onload = () => {
window.SUI.closeModal();
button.classList.remove( 'wp-smush-link-in-progress' );
const actionName = redirectUrl ? 'cta_clicked' : 'closed';
tracker.track( 'update_modal_displayed', {
Action: actionName,
} );
if ( 200 === xhr.status ) {
if ( redirectUrl && isRedirectRequired ) {
window.location.href = redirectUrl;
}
} else {
window.console.log(
'Request failed. Returned status of ' + xhr.status
);
}
};
xhr.send();
},
maybeHandleUpsellUTMClick() {
const isConfigureSlide = 'configure' === this.settings?.slide;
if ( ! isConfigureSlide ) {
return;
}
const upsellLink = this.onboardingModal.querySelector( '.smush-btn-pro-upsell' );
if ( upsellLink ) {
upsellLink.addEventListener( 'click', () => {
this.upsellUTMClicked = true;
}, { once: true } );
}
this.trackProUpsellOnClick( upsellLink );
},
trackFinishSetupWizard() {
return this.trackSetupWizard( window.onBoardingData?.isSiteConnected ? 'complete_wizard' : 'connect' );
},
trackSkipSetupWizard() {
return this.trackSetupWizard( 'quit' );
},
trackSetupWizard( action ) {
const isWizardQuit = 'quit' === action;
const properties = {
Action: action,
'Quit Step': this.getQuitStep( isWizardQuit ),
'Settings Enabled': this.getEnabledSettings( isWizardQuit ),
'Wizard Upsell': this.upsellUTMClicked ? 'clicked_utm' : 'na',
};
const allowToTrack = this.fields?.usage;
return tracker.setAllowToTrack( allowToTrack ).track( 'Setup Wizard New', properties );
},
getQuitStep( isWizardQuit ) {
return isWizardQuit ? ( this.settings.slide || 'na' ) : 'na';
},
getEnabledSettings( isWizardQuit ) {
if ( isWizardQuit ) {
return 'na';
}
const fieldMapsForTracking = this.getFieldMapsForTracking();
const enabledSettings = [];
Object.entries( this.fields ).forEach( ( [ setting, enabled ] ) => {
if ( enabled ) {
const featureName = setting in fieldMapsForTracking ? fieldMapsForTracking[ setting ] : setting;
enabledSettings.push( featureName );
}
} );
return enabledSettings;
},
getProInterests() {
if ( 'pro' === this.membership || ! this.upsellUTMClicked.length ) {
return 'na';
}
return this.upsellUTMClicked;
},
getFieldMapsForTracking() {
return {
usage: 'tracking',
auto: 'auto_smush',
lossy: 'super_smush',
strip_exif: 'strip_exif',
compress_backup: 'compress_backup',
lazy_load: 'lazy_load',
};
},
trackProUpsellOnClick( upsellLink ) {
if ( ! upsellLink ) {
return;
}
upsellLink.addEventListener( 'click', ( event ) => {
const allowToTrack = this.fields?.usage;
tracker.setAllowToTrack( allowToTrack );
( new GlobalTracking() ).trackSetupWizardProUpsell( event?.target?.href, 'na' );
} );
}
};
/**
* Template function (underscores based).
*
* @type {Function}
*/
WP_Smush.onboarding.template = _.memoize( ( id ) => {
let compiled;
const options = {
evaluate: /<#([\s\S]+?)#>/g,
interpolate: /{{{([\s\S]+?)}}}/g,
escape: /{{([^}]+?)}}(?!})/g,
variable: 'data',
};
return ( data ) => {
_.templateSettings = options;
compiled =
compiled ||
_.template( document.getElementById( id ).innerHTML );
return compiled( data );
};
} );
window.addEventListener( 'load', () => WP_Smush.onboarding.init() );
}() );
@@ -0,0 +1,542 @@
/* global WP_Smush */
/* global ajaxurl */
import tracker from "../utils/tracker";
import GlobalTracking from '../global-tracking';
/**
* Modals JavaScript code.
*/
( function() {
'use strict';
// Keep loading onboarding script for other dependencies.
if ( document.getElementById( 'smush-onboarding-free' ) ) {
return;
}
/**
* Onboarding modal.
*
* @since 3.1
*/
WP_Smush.onboarding = {
membership: 'free', // Assume free by default.
onboardingModal: document.getElementById( 'smush-onboarding-dialog' ),
first_slide: 'usage',
settings: {
first: true,
last: false,
slide: 'usage',
value: false,
},
selection: {
usage: false,
auto: true,
lossy: true,
strip_exif: true,
original: true,
preload_images: true,
lazy_load: true,
},
contentContainer: document.getElementById( 'smush-onboarding-content' ),
onboardingSlides: [
'usage',
'auto',
'lossy',
'strip_exif',
'original',
'preload_images',
'lazy_load',
],
touchX: null,
touchY: null,
recheckImagesLink: '',
proFeaturesClicked: [],
/**
* Init module.
*/
init() {
if ( ! this.onboardingModal ) {
return;
}
const dialog = document.getElementById( 'smush-onboarding' );
this.membership = dialog.dataset.type;
this.recheckImagesLink = dialog.dataset.ctaUrl;
if ( 'pro' !== this.membership ) {
this.onboardingSlides = [
'usage',
'auto',
'lossy',
'strip_exif',
'original',
'lazy_load',
'pro_upsell',
];
}
if ( 'false' === dialog.dataset.tracking ) {
this.onboardingSlides.pop();
}
this.renderTemplate();
// Skip setup.
this.skipButton = this.onboardingModal.querySelector(
'.smush-onboarding-skip-link'
);
if ( this.skipButton ) {
this.skipButton.addEventListener( 'click', this.skipSetup.bind( this ) );
}
// Show the modal.
window.SUI.openModal(
'smush-onboarding-dialog',
'wpcontent',
undefined,
false
);
},
/**
* Get swipe coordinates.
*
* @param {Object} e
*/
handleTouchStart( e ) {
const firstTouch = e.touches[ 0 ];
this.touchX = firstTouch.clientX;
this.touchY = firstTouch.clientY;
},
/**
* Process swipe left/right.
*
* @param {Object} e
*/
handleTouchMove( e ) {
if ( ! this.touchX || ! this.touchY ) {
return;
}
const xUp = e.touches[ 0 ].clientX,
yUp = e.touches[ 0 ].clientY,
xDiff = this.touchX - xUp,
yDiff = this.touchY - yUp;
if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {
if ( xDiff > 0 ) {
if ( false === WP_Smush.onboarding.settings.last ) {
WP_Smush.onboarding.next( null, 'next' );
}
} else if ( false === WP_Smush.onboarding.settings.first ) {
WP_Smush.onboarding.next( null, 'prev' );
}
}
this.touchX = null;
this.touchY = null;
},
/**
* Update the template, register new listeners.
*
* @param {string} directionClass Accepts: fadeInRight, fadeInLeft, none.
*/
renderTemplate( directionClass = 'none' ) {
this.updateCheckboxStates();
const template = WP_Smush.onboarding.template( 'smush-onboarding' );
const content = template( this.settings );
if ( content ) {
this.contentContainer.innerHTML = content;
if ( 'none' === directionClass ) {
this.contentContainer.classList.add( 'loaded' );
} else {
this.contentContainer.classList.remove( 'loaded' );
this.contentContainer.classList.add( directionClass );
setTimeout( () => {
this.contentContainer.classList.add( 'loaded' );
this.contentContainer.classList.remove(
directionClass
);
}, 600 );
}
}
this.onboardingModal.addEventListener(
'touchstart',
this.handleTouchStart,
false
);
this.onboardingModal.addEventListener(
'touchmove',
this.handleTouchMove,
false
);
this.bindSubmit();
this.toggleSkipButton();
this.maybeHandleProFeatureClick();
},
updateCheckboxStates() {
// Grab the selected value.
const input = this.onboardingModal.querySelector(
'input[type="checkbox"]'
);
if ( input ) {
this.selection[ input.id ] = input.checked;
}
},
toggleSkipButton() {
if ( ! this.skipButton ) {
return;
}
if ( this.settings.last ) {
this.skipButton.classList.add( 'sui-hidden' );
} else {
this.skipButton.classList.remove( 'sui-hidden' );
}
},
/**
* Catch "Finish setup wizard" button click.
*/
bindSubmit() {
const submitButton = this.onboardingModal.querySelector(
'button[type="submit"]'
);
const self = this;
if ( submitButton ) {
submitButton.addEventListener( 'click', async function( e ) {
e.preventDefault();
submitButton.classList.add( 'sui-button-onload-text' );
// Because we are not rendering the template, we need to update the last element value.
self.updateCheckboxStates();
try {
await self.trackFinishSetupWizard();
} catch ( err ) {
// Do nothing..
}
const _nonce = document.getElementById(
'smush_quick_setup_nonce'
);
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=smush_setup', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
self.onFinishingSetup();
} else {
window.console.log(
'Request failed. Returned status of ' +
xhr.status
);
}
};
xhr.send(
'smush_settings=' +
JSON.stringify( self.selection ) +
'&_ajax_nonce=' +
_nonce.value
);
} );
}
},
onFinishingSetup() {
this.onFinish();
this.startRecheckImages();
},
onFinish() {
window.SUI.closeModal();
},
startRecheckImages() {
if ( ! this.recheckImagesLink ) {
return;
}
window.location.href = this.recheckImagesLink;
},
/**
* Handle navigation.
*
* @param {Object} e
* @param {null|string} whereTo
*/
next( e, whereTo = null ) {
const index = this.onboardingSlides.indexOf( this.settings.slide );
let newIndex = 0;
if ( ! whereTo ) {
newIndex =
null !== e && e.classList.contains( 'next' )
? index + 1
: index - 1;
} else {
newIndex = 'next' === whereTo ? index + 1 : index - 1;
}
const directionClass =
null !== e && e.classList.contains( 'next' )
? 'fadeInRight'
: 'fadeInLeft';
this.settings = {
first: 0 === newIndex,
last: newIndex + 1 === this.onboardingSlides.length, // length !== index
slide: this.onboardingSlides[ newIndex ],
value: this.selection[ this.onboardingSlides[ newIndex ] ],
};
this.renderTemplate( directionClass );
},
/**
* Handle circle navigation.
*
* @param {string} target
*/
goTo( target ) {
const newIndex = this.onboardingSlides.indexOf( target );
this.settings = {
first: 0 === newIndex,
last: newIndex + 1 === this.onboardingSlides.length, // length !== index
slide: target,
value: this.selection[ target ],
};
this.renderTemplate();
},
/**
* Skip onboarding experience.
*/
async skipSetup() {
const _nonce = document.getElementById( 'smush_quick_setup_nonce' );
this.updateCheckboxStates();
// Track skip setup wizard.
try {
await this.trackSkipSetupWizard();
} catch ( err ) {
// Do nothing..
}
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
ajaxurl + '?action=skip_smush_setup&_ajax_nonce=' + _nonce.value
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
this.onSkipSetup();
} else {
window.console.log(
'Request failed. Returned status of ' + xhr.status
);
}
};
xhr.send();
},
onSkipSetup() {
this.onFinish();
},
/**
* Hide new features modal.
* @since 3.7.0
* @since 3.12.2 Add a new parameter redirectUrl
*/
hideUpgradeModal: ( e, button ) => {
const isRedirectRequired = '_blank' !== button?.target;
if ( isRedirectRequired ) {
e.preventDefault();
}
button.classList.add( 'wp-smush-link-in-progress' );
const redirectUrl = button?.href;
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=hide_new_features&_ajax_nonce=' + window.wp_smush_msgs.nonce );
xhr.onload = () => {
window.SUI.closeModal();
button.classList.remove( 'wp-smush-link-in-progress' );
const actionName = redirectUrl ? 'cta_clicked' : 'closed';
tracker.track( 'update_modal_displayed', {
Action: actionName,
} );
if ( 200 === xhr.status ) {
if ( redirectUrl && isRedirectRequired ) {
window.location.href = redirectUrl;
}
} else {
window.console.log(
'Request failed. Returned status of ' + xhr.status
);
}
};
xhr.send();
},
maybeHandleProFeatureClick() {
const isProUpsellSlide = 'pro_upsell' === this.settings?.slide;
if ( ! isProUpsellSlide ) {
return;
}
this.upsellButton = this.onboardingModal.querySelector( '.smush-btn-pro-upsell' );
const proFeatureToggleContainer = this.onboardingModal.querySelector( '.sui-field-list' );
if ( proFeatureToggleContainer ) {
proFeatureToggleContainer.addEventListener( 'click', ( event ) => {
const proFeatureClicked = event.target.matches( 'label' ) || event.target.closest( '.sui-toggle' );
if ( proFeatureClicked ) {
const featureName = event.target.closest( '.sui-field-list-item' ).querySelector( 'input[type="checkbox"]' )?.name;
this.handleProFeatureClicked( featureName );
}
});
}
this.maybeTrackProUpsell();
},
handleProFeatureClicked( featureName ) {
this.cacheProFeatureClick( featureName );
this.highlightUpsellButton();
},
highlightUpsellButton() {
if ( ! this.upsellButton ) {
return;
}
this.upsellButton.classList.remove( 'smush-btn-ripple' );
void this.upsellButton.offsetWidth; // Trigger a reflow.
this.upsellButton.classList.add( 'smush-btn-ripple' );
},
cacheProFeatureClick( proFeature ) {
if ( ! this.proFeaturesClicked.includes( proFeature ) ) {
this.proFeaturesClicked.push( proFeature );
}
},
trackFinishSetupWizard() {
return this.trackSetupWizard( 'finish' );
},
trackSkipSetupWizard() {
return this.trackSetupWizard( 'quit' );
},
trackSetupWizard( action ) {
const quitWizard = 'quit' === action;
const properties = {
Action: quitWizard ? 'quit' : 'finish',
'Quit Step': this.getQuitStep( quitWizard ),
'Settings Enabled': this.getEnabledSettings( quitWizard ),
'Pro Interests': this.getProInterests(),
};
const allowToTrack = this.selection?.usage;
return tracker.setAllowToTrack( allowToTrack ).track( 'Setup Wizard', properties );
},
getQuitStep( quitWizard ) {
if ( ! quitWizard ) {
return 'na';
}
const fieldMapsForTracking = this.getFieldMapsForTracking();
const setting = this.settings.slide;
return setting in fieldMapsForTracking ? fieldMapsForTracking[ setting ] : 'na';
},
getEnabledSettings( quitWizard ) {
if ( quitWizard ) {
return 'na';
}
const fieldMapsForTracking = this.getFieldMapsForTracking();
const enabledSettings = [];
Object.entries( this.selection ).forEach( ( [ setting, enabled ] ) => {
if ( enabled ) {
const featureName = setting in fieldMapsForTracking ? fieldMapsForTracking[ setting ] : setting;
enabledSettings.push( featureName );
}
} );
return enabledSettings;
},
getProInterests() {
if ( 'pro' === this.membership || ! this.proFeaturesClicked.length ) {
return 'na';
}
return this.proFeaturesClicked;
},
getFieldMapsForTracking() {
return {
usage: 'tracking',
auto: 'auto_smush',
lossy: 'free' === this.membership ? 'super_smush' : 'ultra_smush',
strip_exif: 'strip_exif',
original: 'full_size',
lazy_load: 'lazy_load',
pro_upsell: 'upgrade',
preload_images: 'preload_images',
};
},
maybeTrackProUpsell() {
if ( ! this.upsellButton ) {
return;
}
this.upsellButton.addEventListener( 'click', ( event ) => {
const allowToTrack = this.selection?.usage;
tracker.setAllowToTrack( allowToTrack );
( new GlobalTracking() ).trackSetupWizardProUpsell( event?.target?.href, this.getProInterests() );
} );
}
};
/**
* Template function (underscores based).
*
* @type {Function}
*/
WP_Smush.onboarding.template = _.memoize( ( id ) => {
let compiled;
const options = {
evaluate: /<#([\s\S]+?)#>/g,
interpolate: /{{{([\s\S]+?)}}}/g,
escape: /{{([^}]+?)}}(?!})/g,
variable: 'data',
};
return ( data ) => {
_.templateSettings = options;
compiled =
compiled ||
_.template( document.getElementById( id ).innerHTML );
data.first_slide = WP_Smush.onboarding.first_slide;
return compiled( data );
};
} );
window.addEventListener( 'load', () => WP_Smush.onboarding.init() );
}() );
@@ -0,0 +1,105 @@
// Review Prompts
import Tracker from '../utils/tracker';
import Fetcher from '../utils/fetcher';
class ReviewPrompts {
#dismissNoticeKey = 'review-prompts';
constructor() {
document.addEventListener( 'DOMContentLoaded', this.init.bind( this ) );
}
init() {
this.reviewPromptsElement = document.getElementById( 'smush-review-prompts-notice' );
if ( ! this.reviewPromptsElement ) {
return;
}
this.remindLater = this.reviewPromptsElement.querySelector( '#smush-review-prompts-remind-later' );
this.alreadyDid = this.reviewPromptsElement.querySelector( '#smush-review-prompts-already-did' );
this.rateLink = this.reviewPromptsElement.querySelector( '.button-primary' );
this.bindEvents();
}
bindEvents() {
if ( this.alreadyDid ) {
this.alreadyDid.addEventListener( 'click', ( e ) => {
e.preventDefault();
this.handleDismissNotice();
this.trackRateNoticeEvent( 'dismiss' );
} );
}
if ( this.remindLater ) {
this.remindLater.addEventListener( 'click', ( e ) => {
e.preventDefault();
this.handleRemindLater();
this.trackRateNoticeEvent( 'remind_later' );
} );
}
if ( this.rateLink ) {
this.rateLink.addEventListener( 'click', () => {
this.handleDismissNotice();
this.trackRateNoticeEvent( 'rate' );
} );
}
}
handleDismissNotice() {
this.alreadyDid.classList.add( 'wp-smush-link-in-progress' );
return Fetcher.common.dismissNotice( this.#dismissNoticeKey ).then( ( res ) => {
if ( res.success ) {
this.reviewPromptsElement.style.display = 'none';
} else {
window.WP_Smush?.helpers.showNotice( res );
}
} ).catch( ( error ) => {
window.WP_Smush?.helpers.showNotice( error );
} ).finally( () => {
this.alreadyDid.classList.remove( 'wp-smush-link-in-progress' );
} );
}
handleRemindLater() {
this.alreadyDid.classList.add( 'wp-smush-link-in-progress' );
return Fetcher.common.remindReviewPrompt().then( ( res ) => {
if ( res.success ) {
this.reviewPromptsElement.style.display = 'none';
} else {
window.WP_Smush?.helpers.showNotice( res );
}
} ).catch( ( error ) => {
window.WP_Smush?.helpers.showNotice( error );
} ).finally( () => {
this.alreadyDid.classList.remove( 'wp-smush-link-in-progress' );
} );
}
trackRateNoticeEvent( userAction ) {
if ( userAction ) {
const url = new URL( window.location.href );
const page = url.searchParams.get( 'page' );
const locationMaps = {
smush: 'Dashboard',
'smush-bulk': 'Bulk Smush',
'smush-lazy-preload': 'Lazy Load',
'smush-cdn': 'CDN',
'smush-next-gen': 'Next-Gen Formats',
'smush-integrations': 'Integrations',
'smush-settings': 'Settings',
'smush-cross-sell': 'More free Plugins',
};
const location = locationMaps[ page ] || 'WordPress admin';
Tracker.track( 'Rating Notice', {
Action: userAction,
'Notice type': this.reviewPromptsElement.dataset.noticeType,
Location: location
} );
}
}
}
new ReviewPrompts();
@@ -0,0 +1,14 @@
/**
* Shared UI JS libraries. Use only what we need to keep the vendor file size smaller.
*
* @package
*/
require( '@wpmudev/shared-ui/dist/js/_src/code-snippet' );
require( '@wpmudev/shared-ui/dist/js/_src/modal-dialog' );
require( '@wpmudev/shared-ui/dist/js/_src/notifications' );
require( '@wpmudev/shared-ui/dist/js/_src/select2.full' );
require( '@wpmudev/shared-ui/dist/js/_src/select2' );
require( '@wpmudev/shared-ui/dist/js/_src/tabs' );
require( '@wpmudev/shared-ui/dist/js/_src/upload' ); // Used on lazy load page (since 3.2.2).
require( '@wpmudev/shared-ui/dist/js/_src/reviews' );
require( '@wpmudev/shared-ui/dist/js/_src/accordion' );
@@ -0,0 +1,15 @@
import NextGen from './next-gen';
/**
* Avif Class.
*/
class Avif extends NextGen {
constructor() {
super( 'avif' );
}
}
( function() {
'use strict';
new Avif();
}() );
@@ -0,0 +1,147 @@
/**
* BLOCK: extend image block
*/
const { createHigherOrderComponent } = wp.compose,
{ Fragment } = wp.element,
{ InspectorControls } = wp.blockEditor,
{ PanelBody } = wp.components;
/**
* Transform bytes to human readable format.
*
* @param {number} bytes
* @return {string} Readable size string.
*/
function humanFileSize( bytes ) {
const thresh = 1024,
units = [ 'kB', 'MB', 'GB', 'TB' ];
if ( Math.abs( bytes ) < thresh ) {
return bytes + ' B';
}
let u = -1;
do {
bytes /= thresh;
++u;
} while ( Math.abs( bytes ) >= thresh && u < units.length - 1 );
return bytes.toFixed( 1 ) + ' ' + units[ u ];
}
/**
* Generate Smush stats table.
*
* @param {number} id
* @param {Object} stats
* @return {*} Smush stats.
*/
export function smushStats( id, stats ) {
if ( 'undefined' === typeof stats ) {
return window.smush_vars.strings.gb.select_image;
} else if ( 'string' === typeof stats ) {
return stats;
}
return (
<div
id="smush-stats"
className="sui-smush-media smush-stats-wrapper hidden"
style={ { display: 'block' } }
>
<table className="wp-smush-stats-holder">
<thead>
<tr>
<th className="smush-stats-header">
{ window.smush_vars.strings.gb.size }
</th>
<th className="smush-stats-header">
{ window.smush_vars.strings.gb.savings }
</th>
</tr>
</thead>
<tbody>
{ Object.keys( stats.sizes )
.filter( ( item ) => 0 < stats.sizes[ item ].percent )
.map( ( item, i ) => (
<tr key={ i }>
<td>{ item.toUpperCase() }</td>
<td>
{ humanFileSize(
stats.sizes[ item ].bytes
) }{ ' ' }
( { stats.sizes[ item ].percent }% )
</td>
</tr>
) ) }
</tbody>
</table>
</div>
);
}
/**
* Fetch image data. If image is Smushing, update in 3 seconds.
*
* TODO: this could be optimized not to query so much.
*
* @param {Object} props
*/
export function fetchProps( props ) {
const image = new wp.api.models.Media( { id: props.attributes.id } ),
smushData = props.attributes.smush;
image.fetch( { attribute: 'smush' } ).done( function( img ) {
if ( 'string' === typeof img.smush ) {
props.setAttributes( { smush: img.smush } );
//setTimeout( () => fetch( props ), 3000 );
} else if (
'undefined' !== typeof img.smush &&
( 'undefined' === typeof smushData ||
JSON.stringify( smushData ) !== JSON.stringify( img.smush ) )
) {
props.setAttributes( { smush: img.smush } );
}
} );
}
/**
* Modify the blocks edit component.
* Receives the original block BlockEdit component and returns a new wrapped component.
*/
const smushStatsControl = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
// If not image block or not selected, return unmodified block.
if (
'core/image' !== props.name ||
! props.isSelected ||
'undefined' === typeof props.attributes.id
) {
return (
<Fragment>
<BlockEdit { ...props } />
</Fragment>
);
}
const smushData = props.attributes.smush;
fetchProps( props );
return (
<Fragment>
<BlockEdit { ...props } />
<InspectorControls>
<PanelBody title={ window.smush_vars.strings.gb.stats }>
{ smushStats( props.attributes.id, smushData ) }
</PanelBody>
</InspectorControls>
</Fragment>
);
};
}, 'withInspectorControl' );
wp.hooks.addFilter(
'editor.BlockEdit',
'wp-smush/smush-data-control',
smushStatsControl
);
@@ -0,0 +1,139 @@
/* global WP_Smush */
/* global ajaxurl */
/**
* CDN functionality.
*
* @since 3.0
*/
( function() {
'use strict';
WP_Smush.CDN = {
cdnEnableButton: document.getElementById( 'smush-enable-cdn' ),
cdnDisableButton: document.getElementById( 'smush-cancel-cdn' ),
cdnStatsBox: document.querySelector( '.smush-cdn-stats' ),
init() {
/**
* Handle "Get Started" button click on disabled CDN page.
*/
if ( this.cdnEnableButton ) {
this.cdnEnableButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
e.currentTarget.classList.add( 'sui-button-onload' );
this.toggle_cdn( true );
} );
}
/**
* Handle "Deactivate' button click on CDN page.
*/
if ( this.cdnDisableButton ) {
this.cdnDisableButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
e.currentTarget.classList.add( 'sui-button-onload' );
this.toggle_cdn( false );
} );
}
this.updateStatsBox();
},
/**
* Toggle CDN.
*
* @since 3.0
*
* @param {boolean} enable
*/
toggle_cdn( enable ) {
const nonceField = document.getElementsByName(
'wp_smush_options_nonce'
);
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=smush_toggle_cdn', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
const res = JSON.parse( xhr.response );
if ( 'undefined' !== typeof res.success && res.success ) {
WP_Smush.helpers.redirectToPage( 'cdn' );
} else if ( 'undefined' !== typeof res.data.message ) {
WP_Smush.helpers.showErrorNotice( res.data.message );
}
} else {
WP_Smush.helpers.showErrorNotice( 'Request failed. Returned status of ' + xhr.status );
}
};
xhr.send(
'param=' + enable + '&_ajax_nonce=' + nonceField[ 0 ].value
);
},
/**
* Update the CDN stats box in summary meta box. Only fetch new data when on CDN page.
*
* @since 3.0
*/
updateStatsBox() {
if (
'undefined' === typeof this.cdnStatsBox ||
! this.cdnStatsBox
) {
return;
}
// Only fetch the new stats, when user is on CDN page.
if ( ! window.location.search.includes( 'page=smush-cdn' ) ) {
return;
}
this.toggleElements();
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=get_cdn_stats', true );
xhr.onload = () => {
const endpoint_missing = xhr.status === 400 && xhr.response === '0';
if (200 === xhr.status) {
const res = JSON.parse(xhr.response);
if ('undefined' !== typeof res.success && res.success) {
this.toggleElements();
} else if ('undefined' !== typeof res.data.message) {
WP_Smush.helpers.showErrorNotice(res.data.message);
}
} else if (!endpoint_missing) {
WP_Smush.helpers.showErrorNotice('Request failed. Returned status of ' + xhr.status);
}
};
xhr.send();
},
/**
* Show/hide elements during status update in the updateStatsBox()
*
* @since 3.1 Moved out from updateStatsBox()
*/
toggleElements() {
const spinner = this.cdnStatsBox.querySelector(
'.sui-icon-loader'
);
const elements = this.cdnStatsBox.querySelectorAll(
'.wp-smush-stats > :not(.sui-icon-loader)'
);
for ( let i = 0; i < elements.length; i++ ) {
elements[ i ].classList.toggle( 'sui-hidden' );
}
spinner.classList.toggle( 'sui-hidden' );
},
};
WP_Smush.CDN.init();
} )();
@@ -0,0 +1,212 @@
/* global WP_Smush */
/* global ajaxurl */
/**
* Directory scanner module that will Smush images in the Directory Smush modal.
*
* @since 2.8.1
*
* @param {string|number} totalSteps
* @param {string|number} currentStep
* @return {Object} Scan object.
* @class
*/
const DirectoryScanner = ( totalSteps, currentStep ) => {
totalSteps = parseInt( totalSteps );
currentStep = parseInt( currentStep );
let cancelling = false,
failedItems = 0,
skippedItems = 0;
const obj = {
scan() {
const remainingSteps = totalSteps - currentStep;
if ( currentStep !== 0 ) {
// Scan started on a previous page load.
step( remainingSteps ).fail( this.showScanError );
} else {
jQuery
.post( ajaxurl, {
action: 'directory_smush_start',
_ajax_nonce: window.wp_smush_msgs.nonce
}, () =>
step( remainingSteps ).fail( this.showScanError )
)
.fail( this.showScanError );
}
},
cancel() {
cancelling = true;
return jQuery.post( ajaxurl, {
action: 'directory_smush_cancel',
_ajax_nonce: window.wp_smush_msgs.nonce
} );
},
getProgress() {
if ( cancelling ) {
return 0;
}
// O M G ... Logic at it's finest!
const remainingSteps = totalSteps - currentStep;
return Math.min(
Math.round(
( parseInt( totalSteps - remainingSteps ) * 100 ) /
totalSteps
),
99
);
},
onFinishStep( progress ) {
jQuery( '.wp-smush-progress-dialog .sui-progress-state-text' ).html(
currentStep -
failedItems +
'/' +
totalSteps +
' ' +
window.wp_smush_msgs.progress_smushed
);
WP_Smush.directory.updateProgressBar( progress );
},
onFinish() {
WP_Smush.directory.updateProgressBar( 100 );
const directorySmushUrl = `${ self.wp_smush_msgs.bulk_smush_url }&smush__directory-scan=done#directory_smush-settings-row`;
if ( window.location.href === directorySmushUrl ) {
window.location.reload();
} else {
window.location.href = directorySmushUrl;
}
},
/**
* Displays an error when the scan request fails.
*
* @param {Object} res XHR object.
*/
showScanError( res ) {
const dialog = jQuery( '#wp-smush-progress-dialog' );
// Add the error class to show/hide elements in the dialog.
dialog
.removeClass( 'wp-smush-exceed-limit' )
.addClass( 'wp-smush-scan-error' );
// Add the error status and description to the error message.
dialog
.find( '#smush-scan-error' )
.text( `${ res.status } ${ res.statusText }` );
// Show/hide the 403 error specific instructions.
const forbiddenMessage = dialog.find( '.smush-403-error-message' );
if ( 403 !== res.status ) {
forbiddenMessage.addClass( 'sui-hidden' );
} else {
forbiddenMessage.removeClass( 'sui-hidden' );
}
},
limitReached() {
const dialog = jQuery( '#wp-smush-progress-dialog' );
dialog.addClass( 'wp-smush-exceed-limit' );
dialog
.find( '#cancel-directory-smush' )
.attr( 'data-tooltip', window.wp_smush_msgs.bulk_resume );
dialog
.find( '.sui-box-body .sui-icon-close' )
.removeClass( 'sui-icon-close' )
.addClass( 'sui-icon-play' );
dialog
.find( '#cancel-directory-smush' )
.attr( 'id', 'cancel-directory-smush-disabled' );
},
resume() {
const dialog = jQuery( '#wp-smush-progress-dialog' );
const resume = dialog.find( '#cancel-directory-smush-disabled' );
dialog.removeClass( 'wp-smush-exceed-limit' );
dialog
.find( '.sui-box-body .sui-icon-play' )
.removeClass( 'sui-icon-play' )
.addClass( 'sui-icon-close' );
resume.attr( 'data-tooltip', 'Cancel' );
resume.attr( 'id', 'cancel-directory-smush' );
obj.scan();
},
};
/**
* Execute a scan step recursively
*
* Private to avoid overriding
*
* @param {number} remainingSteps
*/
const step = function( remainingSteps ) {
if ( remainingSteps >= 0 ) {
currentStep = totalSteps - remainingSteps;
return jQuery.post(
ajaxurl,
{
action: 'directory_smush_check_step',
_ajax_nonce: window.wp_smush_msgs.nonce,
step: currentStep,
},
( response ) => {
// We're good - continue on.
if (
'undefined' !== typeof response.success &&
response.success
) {
if (
'undefined' !== typeof response.data &&
'undefined' !== typeof response.data.skipped &&
true === response.data.skipped
) {
skippedItems++;
}
currentStep++;
remainingSteps = remainingSteps - 1;
obj.onFinishStep( obj.getProgress() );
step( remainingSteps ).fail( obj.showScanError );
} else if (
'undefined' !== typeof response.data.error &&
'dir_smush_limit_exceeded' === response.data.error
) {
// Limit reached. Stop.
obj.limitReached();
} else {
// Error? never mind, continue, but count them.
failedItems++;
currentStep++;
remainingSteps = remainingSteps - 1;
obj.onFinishStep( obj.getProgress() );
step( remainingSteps ).fail( obj.showScanError );
}
}
);
}
return jQuery.post(
ajaxurl,
{
action: 'directory_smush_finish',
_ajax_nonce: window.wp_smush_msgs.nonce,
items: totalSteps - ( failedItems + skippedItems ),
failed: failedItems,
skipped: skippedItems,
},
( response ) => obj.onFinish( response )
);
};
return obj;
};
export default DirectoryScanner;
@@ -0,0 +1,307 @@
/* global WP_Smush */
/* global ajaxurl */
/**
* Lazy loading functionality.
*
* @since 3.0
*/
( function() {
'use strict';
WP_Smush.Lazyload = {
lazyloadEnableButton: document.getElementById(
'smush-enable-lazyload'
),
lazyloadDisableButton: document.getElementById(
'smush-cancel-lazyload'
),
lazyloadIframeCheckbox: document.getElementById( 'format-iframe' ),
init() {
const self = this;
/**
* Handle "Activate" button click on disabled Lazy load page.
*/
if ( this.lazyloadEnableButton ) {
this.lazyloadEnableButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
e.currentTarget.classList.add( 'sui-button-onload' );
this.toggle_lazy_load( true );
} );
}
/**
* Handle "Deactivate' button click on Lazy load page.
*/
if ( this.lazyloadDisableButton ) {
this.lazyloadDisableButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
e.currentTarget.classList.add( 'sui-button-onload' );
this.toggle_lazy_load( false );
} );
}
/**
* Handle "Remove icon" button click on Lazy load page.
*
* This removes the image from the upload placeholder.
*
* @since 3.2.2
*/
const removeSpinner = document.getElementById(
'smush-remove-spinner'
);
if ( removeSpinner ) {
removeSpinner.addEventListener( 'click', ( e ) => {
e.preventDefault();
this.removeLoaderIcon();
} );
}
const removePlaceholder = document.getElementById(
'smush-remove-placeholder'
);
if ( removePlaceholder ) {
removePlaceholder.addEventListener( 'click', ( e ) => {
e.preventDefault();
this.removeLoaderIcon( 'placeholder' );
} );
}
/**
* Handle "Remove" icon click.
*
* This removes the select icon from the list (not same as above functions).
*
* @since 3.2.2
*/
const items = document.querySelectorAll( '.smush-ll-remove' );
if ( items && 0 < items.length ) {
items.forEach( function( el ) {
el.addEventListener( 'click', ( e ) => {
e.preventDefault();
e.target.closest( 'li' ).style.display = 'none';
self.remove(
e.target.dataset.id,
e.target.dataset.type
);
} );
} );
}
this.handlePredefinedPlaceholders();
this.handleEmbedVideosBoxVisibility();
},
handleEmbedVideosBoxVisibility() {
if ( ! this.lazyloadIframeCheckbox ) {
return;
}
const embedVideosBox = document.querySelector( '.lazyload-embed-videos' );
if ( ! embedVideosBox ) {
return;
}
this.lazyloadIframeCheckbox.addEventListener( 'click', function() {
if ( true === this.checked ) {
embedVideosBox.classList.remove( 'sui-hidden' );
} else {
embedVideosBox.classList.add( 'sui-hidden' );
}
});
},
/**
* Handle background color changes for the two predefined placeholders.
*
* @since 3.7.1
*/
handlePredefinedPlaceholders() {
const pl1 = document.getElementById( 'placeholder-icon-1' );
if ( pl1 ) {
pl1.addEventListener( 'click', () => this.changeColor( '#F3F3F3' ) );
}
const pl2 = document.getElementById( 'placeholder-icon-2' );
if ( pl2 ) {
pl2.addEventListener( 'click', () => this.changeColor( '#333333' ) );
}
},
/**
* Set color.
*
* @since 3.7.1
* @param {string} color
*/
changeColor( color ) {
document.getElementById( 'smush-color-picker' ).value = color;
document.querySelector( '.sui-colorpicker-hex .sui-colorpicker-value > span > span' ).style.backgroundColor = color;
document.querySelector( '.sui-colorpicker-hex .sui-colorpicker-value > input' ).value = color;
},
/**
* Toggle lazy loading.
*
* @since 3.2.0
*
* @param {string} enable
*/
toggle_lazy_load( enable ) {
const nonceField = document.getElementsByName(
'wp_smush_options_nonce'
);
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
ajaxurl + '?action=smush_toggle_lazy_load',
true
);
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if ( 200 === xhr.status ) {
const res = JSON.parse( xhr.response );
if ( 'undefined' !== typeof res.success && res.success ) {
WP_Smush.helpers.redirectToPage( 'lazy-preload' );
} else if ( 'undefined' !== typeof res.data.message ) {
WP_Smush.helpers.showErrorNotice( res.data.message );
document.querySelector( '.sui-button-onload' ).classList.remove( 'sui-button-onload' );
}
} else {
WP_Smush.helpers.showErrorNotice( 'Request failed. Returned status of ' + xhr.status );
document.querySelector( '.sui-button-onload' ).classList.remove( 'sui-button-onload' );
}
};
xhr.send(
'param=' + enable + '&_ajax_nonce=' + nonceField[ 0 ].value
);
},
/**
* Add lazy load spinner icon.
*
* @since 3.2.2
* @param {string} type Accepts: spinner, placeholder.
*/
addLoaderIcon( type = 'spinner' ) {
let frame;
// If the media frame already exists, reopen it.
if ( frame ) {
frame.open();
return;
}
// Create a new media frame
frame = wp.media( {
title: 'Select or upload an icon',
button: {
text: 'Select icon',
},
multiple: false, // Set to true to allow multiple files to be selected
} );
// When an image is selected in the media frame...
frame.on( 'select', function() {
// Get media attachment details from the frame state
const attachment = frame
.state()
.get( 'selection' )
.first()
.toJSON();
// Send the attachment URL to our custom image input field.
const imageIcon = document.getElementById(
'smush-' + type + '-icon-preview'
);
imageIcon.style.backgroundImage =
'url("' + attachment.url + '")';
imageIcon.style.display = 'block';
// Send the attachment id to our hidden input
document
.getElementById( 'smush-' + type + '-icon-file' )
.setAttribute( 'value', attachment.id );
// Hide the add image link
document.getElementById(
'smush-upload-' + type
).style.display = 'none';
// Unhide the remove image link
const removeDiv = document.getElementById(
'smush-remove-' + type
);
removeDiv.querySelector( 'span' ).innerHTML =
attachment.filename;
removeDiv.style.display = 'block';
} );
// Finally, open the modal on click
frame.open();
},
/**
* Remove lazy load spinner icon.
*
* @since 3.2.2
* @param {string} type Accepts: spinner, placeholder.
*/
removeLoaderIcon: ( type = 'spinner' ) => {
// Clear out the preview image
const imageIcon = document.getElementById(
'smush-' + type + '-icon-preview'
);
imageIcon.style.backgroundImage = '';
imageIcon.style.display = 'none';
// Un-hide the add image link
document.getElementById( 'smush-upload-' + type ).style.display =
'block';
// Hide the delete image link
document.getElementById( 'smush-remove-' + type ).style.display =
'none';
// Delete the image id from the hidden input
document
.getElementById( 'smush-' + type + '-icon-file' )
.setAttribute( 'value', '' );
},
/**
* Remove item.
*
* @param {number} id Image ID.
* @param {string} type Accepts: spinner, placeholder.
*/
remove: ( id, type = 'spinner' ) => {
const nonceField = document.getElementsByName(
'wp_smush_options_nonce'
);
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=smush_remove_icon', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.send(
'id=' +
id +
'&type=' +
type +
'&_ajax_nonce=' +
nonceField[ 0 ].value
);
},
};
WP_Smush.Lazyload.init();
} )();
@@ -0,0 +1,180 @@
/* global smush_vars */
/* global _ */
/**
* Adds a Smush Now button and displays stats in Media Attachment Details Screen
*/
(function ($, _) {
'use strict';
// Local reference to the WordPress media namespace.
const smushMedia = wp.media,
sharedTemplate =
"<span class='setting smush-stats' data-setting='smush'>" +
"<span class='name'><%= label %></span>" +
"<span class='value'><%= value %></span>" +
'</span>',
template = _.template(sharedTemplate);
/**
* Create the template.
*
* @param {string} smushHTML
* @return {Object} Template object
*/
const prepareTemplate = function (smushHTML) {
/**
* @param {Array} smush_vars.strings Localization strings.
* @param {Object} smush_vars Object from wp_localize_script()
*/
return template({
label: smush_vars.strings.stats_label,
value: smushHTML,
});
};
if (
'undefined' !== typeof smushMedia.view &&
'undefined' !== typeof smushMedia.view.Attachment.Details.TwoColumn
) {
// Local instance of the Attachment Details TwoColumn used in the edit attachment modal view
const smushMediaTwoColumn =
smushMedia.view.Attachment.Details.TwoColumn;
/**
* Add Smush details to attachment.
*
* A similar view to media.view.Attachment.Details
* for use in the Edit Attachment modal.
*
* @see wp-includes/js/media-grid.js
*/
smushMedia.view.Attachment.Details.TwoColumn = smushMediaTwoColumn.extend(
{
initialize() {
smushMediaTwoColumn.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:smush', this.render);
},
render() {
// Ensure that the main attachment fields are rendered.
smushMedia.view.Attachment.prototype.render.apply(
this,
arguments
);
const smushHTML = this.model.get('smush');
if (typeof smushHTML === 'undefined') {
return this;
}
this.model.fetch();
/**
* Detach the views, append our custom fields, make sure that our data is fully updated
* and re-render the updated view.
*/
this.views.detach();
this.$el
.find('.settings')
.append(prepareTemplate(smushHTML));
this.views.render();
return this;
},
}
);
}
// Local instance of the Attachment Details TwoColumn used in the edit attachment modal view
const smushAttachmentDetails = smushMedia.view.Attachment.Details;
/**
* Add Smush details to attachment.
*/
smushMedia.view.Attachment.Details = smushAttachmentDetails.extend({
initialize() {
smushAttachmentDetails.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:smush', this.render);
},
render() {
// Ensure that the main attachment fields are rendered.
smushMedia.view.Attachment.prototype.render.apply(this, arguments);
const smushHTML = this.model.get('smush');
if (typeof smushHTML === 'undefined') {
return this;
}
this.model.fetch();
/**
* Detach the views, append our custom fields, make sure that our data is fully updated
* and re-render the updated view.
*/
this.views.detach();
this.$el.append(prepareTemplate(smushHTML));
return this;
},
});
/**
* Create a new MediaLibraryTaxonomyFilter we later will instantiate
*
* @since 3.0
*/
const MediaLibraryTaxonomyFilter = wp.media.view.AttachmentFilters.extend({
id: 'media-attachment-smush-filter',
createFilters() {
this.filters = {
all: {
text: smush_vars.strings.filter_all,
props: { stats: 'all' },
priority: 10,
},
unsmushed: {
text: smush_vars.strings.filter_not_processed,
props: { stats: 'unsmushed' },
priority: 20,
},
excluded: {
text: smush_vars.strings.filter_excl,
props: { stats: 'excluded' },
priority: 30,
},
failed: {
text: smush_vars.strings.filter_failed,
props: { stats: 'failed_processing' },
priority: 40,
},
};
},
});
/**
* Extend and override wp.media.view.AttachmentsBrowser to include our new filter.
*
* @since 3.0
*/
const AttachmentsBrowser = wp.media.view.AttachmentsBrowser;
wp.media.view.AttachmentsBrowser = wp.media.view.AttachmentsBrowser.extend({
createToolbar() {
// Make sure to load the original toolbar
AttachmentsBrowser.prototype.createToolbar.call(this);
this.toolbar.set(
'MediaLibraryTaxonomyFilter',
new MediaLibraryTaxonomyFilter({
controller: this.controller,
model: this.collection.props,
priority: -75,
}).render()
);
},
});
})(jQuery, _);
@@ -0,0 +1,283 @@
/* global ajaxurl */
/* global WP_Smush */
/**
* NextGen class.
*/
export default class NextGen {
constructor( nextGenFormat ) {
this.nextGenFormat = nextGenFormat;
this.nonceField = document.getElementsByName( 'wp_smush_options_nonce' );
this.toggleModuleButton = document.getElementById( `smush-toggle-${ nextGenFormat }-button` );
this.deleteAllButton = document.getElementById( `wp-smush-${ nextGenFormat }-delete-all` );
this.registerGlobalEvents();
this.registerEvents();
}
/**
* Register global events.
*/
registerGlobalEvents() {
if ( NextGen.isGlobalEventListenerAdded ) {
return;
}
NextGen.isGlobalEventListenerAdded = true;
document.addEventListener( 'onSavedSmushSettings', this.onSavedNextGenSettingsHandler.bind( this ) );
document.addEventListener( 'on-smush-next-gen-activated-notice', this.showNextGenActivatedModal.bind( this ) );
document.addEventListener( 'on-smush-next-gen-conversion-changed-notice', this.showNextGenConversionChangedModal.bind( this ) );
}
registerEvents() {
this.maybeShowDeleteAllSuccessNotice();
/**
* Handles the "Deactivate" and "Get Started" buttons on the Next-Gen page.
*/
if ( this.toggleModuleButton ) {
this.toggleModuleButton.addEventListener( 'click', ( e ) =>
this.toggleModule( e )
);
}
/**
* Handles the "Delete Next-Gen images" button.
*/
if ( this.deleteAllButton ) {
this.deleteAllButton.addEventListener( 'click', ( e ) => this.deleteAll( e ) );
}
}
/**
* Toggle Next-Gen module.
*
* @param {Event} e
*/
toggleModule( e ) {
e.preventDefault();
const button = e.currentTarget,
doEnable = 'enable' === button.dataset.action;
button.classList.add( 'sui-button-onload' );
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + `?action=smush_${ this.nextGenFormat }_toggle`, true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
const res = JSON.parse( xhr.response );
if ( 200 === xhr.status ) {
if ( 'undefined' !== typeof res.success && res.success ) {
const scanPromise = this.runScan();
scanPromise.onload = () => {
this.redirectToNextGenPage();
};
} else if ( 'undefined' !== typeof res.data.message ) {
this.showNotice( res.data.message );
button.classList.remove( 'sui-button-onload' );
}
} else {
let message = window.wp_smush_msgs.generic_ajax_error;
if ( res && 'undefined' !== typeof res.data.message ) {
message = res.data.message;
}
this.showNotice( message );
button.classList.remove( 'sui-button-onload' );
}
};
xhr.send(
'param=' + doEnable + '&_ajax_nonce=' + this.nonceField[ 0 ].value
);
}
deleteAll( e ) {
const button = e.currentTarget;
button.classList.add( 'sui-button-onload' );
let message = false;
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + `?action=smush_${ this.nextGenFormat }_delete_all`, true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
const res = JSON.parse( xhr.response );
if ( 200 === xhr.status ) {
if ( 'undefined' !== typeof res.success && res.success ) {
const scanPromise = this.runScan();
scanPromise.onload = () => {
location.search =
location.search + `&smush-notice=${ this.nextGenFormat }-deleted`;
};
} else {
message = window.wp_smush_msgs.generic_ajax_error;
}
} else {
message = window.wp_smush_msgs.generic_ajax_error;
}
if ( res && res.data && res.data.message ) {
message = res.data.message;
}
if ( message ) {
button.classList.remove( 'sui-button-onload' );
const noticeMessage = `<p style="text-align: left;">${ message }</p>`;
const noticeOptions = {
type: 'error',
icon: 'info',
autoclose: {
show: false,
},
};
window.SUI.openNotice(
'wp-smush-next-gen-delete-all-error-notice',
noticeMessage,
noticeOptions
);
}
};
xhr.send( '_ajax_nonce=' + this.nonceField[ 0 ].value );
}
/**
* Triggers the scanning of images for updating the images to re-smush.
*
* @since 3.8.0
*/
runScan() {
const xhr = new XMLHttpRequest(),
nonceField = document.getElementsByName(
'wp_smush_options_nonce'
);
xhr.open( 'POST', ajaxurl + '?action=scan_for_resmush', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.send( '_ajax_nonce=' + nonceField[ 0 ].value );
return xhr;
}
/**
* Show message (notice).
*
* @param {string} message
* @param {string} type
*/
showNotice( message, type ) {
if ( 'undefined' === typeof message ) {
return;
}
const noticeMessage = `<p>${ message }</p>`;
const noticeOptions = {
type: type || 'error',
icon: 'info',
dismiss: {
show: true,
label: window.wp_smush_msgs.noticeDismiss,
tooltip: window.wp_smush_msgs.noticeDismissTooltip,
},
autoclose: {
show: false,
},
};
window.SUI.openNotice(
'wp-smush-ajax-notice',
noticeMessage,
noticeOptions
);
}
/**
* Show delete all webp success notice.
*/
maybeShowDeleteAllSuccessNotice() {
const deletedAllNoticeElementID = `wp-smush-${ this.nextGenFormat }-delete-all-notice`;
const deletedAllNoticeElement = document.getElementById( deletedAllNoticeElementID );
if ( ! deletedAllNoticeElement ) {
return;
}
const noticeMessage = `<p>${
deletedAllNoticeElement
.dataset.message
}</p>`;
const noticeOptions = {
type: 'success',
icon: 'check-tick',
dismiss: {
show: true,
},
};
window.SUI.openNotice(
deletedAllNoticeElementID,
noticeMessage,
noticeOptions
);
}
onSavedNextGenSettingsHandler( status ) {
if ( 'next-gen' === status?.detail?.page ) {
if ( status?.detail?.next_gen_format_changed ) {
this.redirectToNextGenPage( '&smush-notice=next-gen-conversion-changed' );
} else if ( status?.detail?.webp_method_changed ) {
this.redirectToNextGenPage();
}
}
}
showNextGenActivatedModal() {
if ( ! window.WP_Smush ) {
return;
}
const activatedModalId = 'smush-next-gen-activated-modal';
if ( ! document.getElementById( activatedModalId ) ) {
this.redirectToNextGenPage();
return;
}
window.WP_Smush.helpers.showModal( activatedModalId, {
isCloseOnEsc: true,
} );
}
showNextGenConversionChangedModal() {
if ( ! window.WP_Smush ) {
return;
}
const conversionChangedModalId = 'smush-next-gen-conversion-changed-modal';
if ( ! document.getElementById( conversionChangedModalId ) ) {
this.redirectToNextGenPage();
return;
}
window.WP_Smush.helpers.showModal( conversionChangedModalId, {
isCloseOnEsc: true,
} );
}
redirectToNextGenPage( noticeParam ) {
window.location.href = window.wp_smush_msgs.nextGenURL + ( noticeParam || '' );
}
}
@@ -0,0 +1,519 @@
import tracker from '../utils/tracker';
class ProductAnalytics {
troubleshootClicked = false;
resumeBulkSmushCount = 0;
missedEventsKey = 'wp_smush_missed_events';
init() {
if ( ! tracker.allowToTrack() ) {
return;
}
this.trackUltraLinks();
this.trackUpsellLinks();
this.registerTroubleshootClickEvent();
// Scan Interrupted Event from Scan Modal.
this.trackScanInterruptedEventOnStopScanningModal();
this.trackScanInterruptedEventOnRetryScanModal();
// Bulk Smush Interrupted Event from Bulk Smush Modal.
this.trackBulkSmushInterruptedEventOnStopBulkSmushModal();
this.trackBulkSmushInterruptedEventOnRetryBulkSmushModal();
this.registerBulkSmushResumeClickEvent();
// Bulk Smush Interrupted Event when exit ajax bulk smush.
this.trackBulkSmushInterruptedEventWhenExitingAjaxBulkSmush();
// Interrupted Event from Inline Notice.
this.trackInterruptedEventFromInlineNotice();
// Interrupted Event from Loopback Error Modal.
this.trackInterruptedEventFromLoopbackErrorModal();
this.maybeTrackMissedEventsOnLoad();
}
trackUltraLinks() {
const ultraUpsellLinks = document.querySelectorAll( '.wp-smush-upsell-ultra-compression' );
if ( ! ultraUpsellLinks ) {
return;
}
const getLocation = ( ultraLink ) => {
const locations = {
settings: 'bulksmush_settings',
dashboard: 'dash_summary',
bulk: 'bulksmush_summary',
directory: 'directory_summary',
'lazy-preload': 'lazy_summary',
cdn: 'cdn_summary',
'next-gen': 'webp_summary',
};
const locationId = ultraLink.classList.contains( 'wp-smush-ultra-compression-link' ) ? 'settings' : this.getCurrentPageSlug();
return locations[ locationId ] || 'bulksmush_settings';
};
ultraUpsellLinks.forEach( ( ultraLink ) => {
const eventName = 'ultra_upsell_modal';
ultraLink.addEventListener( 'click', ( e ) => {
tracker.track( eventName, {
Location: getLocation( e.target ),
'Modal Action': 'direct_cta',
} );
} );
} );
}
trackUpsellLinks() {
const upsellLinks = document.querySelectorAll( '[href*="utm_source=smush"]' );
if ( ! upsellLinks ) {
return;
}
upsellLinks.forEach( ( upsellLink ) => {
upsellLink.addEventListener( 'click', ( e ) => {
const params = new URL( e.target.href ).searchParams;
if ( ! params ) {
return;
}
const campaign = params.get( 'utm_campaign' );
const upsellLocations = {
// CDN.
summary_cdn: 'dash_summary',
'smush-dashboard-cdn-upsell': 'dash_widget',
smush_bulksmush_cdn: 'bulk_smush_progress',
smush_cdn_upgrade_button: 'cdn_page',
smush_bulksmush_library_gif_cdn: 'media_library',
smush_bulk_smush_complete_global: 'bulk_smush_complete',
// Local Next-Gen.
'summary_next-gen': 'dash_summary',
'smush-dashboard-next-gen-upsell': 'dash_widget',
'smush_next-gen_upgrade_button': 'Next-Gen Formats',// Moved from React WebP - free-content.jsx
};
if ( ! ( campaign in upsellLocations ) ) {
return;
}
const Location = upsellLocations[ campaign ];
const matches = campaign.match( /(cdn|next-gen)/i );
const upsellModule = matches && matches[ 0 ];
const eventName = 'next-gen' === upsellModule ? 'local_webp_upsell' : 'cdn_upsell';
tracker.track( eventName, { Location } );
} );
} );
}
trackScanInterruptedEventOnStopScanningModal() {
const stopScanningModal = document.getElementById( 'smush-stop-scanning-dialog' );
if ( ! stopScanningModal ) {
return;
}
const closeButtons = stopScanningModal.querySelectorAll( '[data-modal-close]' );
closeButtons.forEach( ( closeButton ) => {
closeButton.addEventListener( 'click', ( e ) => {
const action = e.target.dataset?.action || 'Close';
this.trackScanInterruptedEvent( {
Trigger: 'cancel_in_progress',
'Modal Action': action,
} );
} );
} );
}
trackBulkSmushInterruptedEventOnStopBulkSmushModal() {
const stopBulkSmushModal = document.getElementById( 'smush-stop-bulk-smush-modal' );
if ( ! stopBulkSmushModal ) {
return;
}
const closeButtons = stopBulkSmushModal.querySelectorAll( '[data-modal-close]' );
closeButtons.forEach( ( closeButton ) => {
closeButton.addEventListener( 'click', ( e ) => {
const action = e.target.dataset?.action || 'Close';
this.trackBulkSmushInterruptedEvent( {
Trigger: 'cancel_in_progress',
'Modal Action': action,
} );
} );
} );
}
trackScanInterruptedEventOnRetryScanModal() {
const retryScanModal = document.getElementById( 'smush-retry-scan-notice' );
if ( ! retryScanModal ) {
return;
}
const retryButton = retryScanModal.querySelector( '.smush-retry-scan-notice-button' );
if ( retryButton ) {
retryButton.addEventListener( 'click', ( e ) => {
const recheckImagesBtn = document.querySelector( '.wp-smush-scan' );
if ( recheckImagesBtn ) {
this.trackScanInterruptedEvent( {
Trigger: 'failed_modal',
'Modal Action': 'Retry',
} );
return;
}
e.preventDefault();
const event = 'Scan Interrupted';
const properties = this.getScanInterruptedEventProperties( {
Trigger: 'failed_modal',
'Modal Action': 'Retry',
} );
tracker.track( event, properties ).catch( () => {
this.cacheMissedEvent( {
event,
properties,
} );
} ).finally( () => {
window.location.href = e.target.href;
} );
} );
}
const closeButtons = retryScanModal.querySelectorAll( '[data-modal-close]' );
closeButtons.forEach( ( closeButton ) => {
closeButton.addEventListener( 'click', ( e ) => {
const action = e.target.dataset?.action || 'Close';
this.trackScanInterruptedEvent( {
Trigger: 'failed_modal',
'Modal Action': action,
} );
} );
} );
}
trackBulkSmushInterruptedEventOnRetryBulkSmushModal() {
const retryBulkModal = document.getElementById( 'smush-retry-bulk-smush-notice' );
if ( ! retryBulkModal ) {
return;
}
const retryButton = retryBulkModal.querySelector( '.smush-retry-bulk-smush-notice-button' );
if ( retryButton ) {
retryButton.addEventListener( 'click', () => {
this.trackBulkSmushInterruptedEvent( {
Trigger: 'failed_modal',
'Modal Action': 'Retry',
} );
} );
}
const closeButtons = retryBulkModal.querySelectorAll( '[data-modal-close]' );
closeButtons.forEach( ( closeButton ) => {
closeButton.addEventListener( 'click', ( e ) => {
const action = e.target.dataset?.action || 'Close';
this.trackBulkSmushInterruptedEvent( {
Trigger: 'failed_modal',
'Modal Action': action,
} );
} );
} );
}
trackScanInterruptedEvent( properties ) {
return tracker.track( 'Scan Interrupted', this.getScanInterruptedEventProperties( properties ) );
}
getScanInterruptedEventProperties( properties ) {
return Object.assign( {
Troubleshoot: this.troubleshootClicked ? 'Yes' : 'No',
}, properties );
}
trackBulkSmushInterruptedEventWhenExitingAjaxBulkSmush() {
if ( this.canUseBackgroundOptimization() ) {
return;
}
const progressBar = document.querySelector( '.wp-smush-bulk-progress-bar-wrapper' );
if ( ! progressBar ) {
return;
}
window.addEventListener( 'beforeunload', async () => {
const isBulkSmushInProgress = window.WP_Smush?.bulk.isBulkSmushInProgress() && ! progressBar.classList.contains( 'sui-hidden' );
if ( ! isBulkSmushInProgress ) {
return;
}
const isFreeExceeded = progressBar.classList.contains( 'wp-smush-exceed-limit' );
const event = 'Bulk Smush Interrupted';
const properties = this.getBulkSmushInterruptedEventProperties(
{
Trigger: isFreeExceeded ? 'exit_50_limit' : 'exit_in_progress',
'Modal Action': 'Exit',
'Retry Attempts': this.resumeBulkSmushCount,
}
);
await tracker.track( event, properties ).catch( () => {
this.cacheMissedEvent( {
event,
properties,
} );
} );
} );
}
cacheMissedEvent( eventData ) {
if ( window.localStorage ) {
// As now we only use it for one event, so let's keep it as a simple array.
window.localStorage.setItem( this.missedEventsKey, JSON.stringify( [ eventData ] ) );
}
}
getMissedEvents() {
if ( ! window.localStorage ) {
return [];
}
const properties = window.localStorage.getItem( this.missedEventsKey );
if ( ! properties ) {
return [];
}
return JSON.parse( properties );
}
clearMissedEvents() {
if ( window.localStorage ) {
window.localStorage.removeItem( this.missedEventsKey );
}
}
canUseBackgroundOptimization() {
return 'undefined' !== typeof window.wp_smushit_data?.bo_stats;
}
trackBulkSmushInterruptedEvent( properties ) {
return tracker.track( 'Bulk Smush Interrupted', this.getBulkSmushInterruptedEventProperties( properties ) );
}
getBulkSmushInterruptedEventProperties( properties ) {
return Object.assign(
{
Troubleshoot: this.troubleshootClicked ? 'Yes' : 'No',
},
this.getBulkSmushProcessStats(),
properties
);
}
getBulkSmushProcessStats() {
if ( this.canUseBackgroundOptimization() ) {
// Handled it via PHP.
return {};
}
const bulkSmushObject = window.WP_Smush?.bulk;
if ( ! bulkSmushObject ) {
return {};
}
return {
'Retry Attempts': this.resumeBulkSmushCount,
'Total Enqueued Images': bulkSmushObject.getTotalEnqueuedImages(),
'Completion Percentage': bulkSmushObject.getCompletionPercentage(),
};
}
trackInterruptedEventFromInlineNotice() {
this.trackInterruptedEventFromInlineNoticeOnDashboard();
this.trackBulkSmushInterruptedEventFromInlineNoticeOnBulkSmush();
this.trackScanInterruptedEventFromInlineNoticeOnBulkSmush();
}
trackInterruptedEventFromInlineNoticeOnDashboard() {
const dashboardBulkElement = document.getElementById( 'smush-box-dashboard-bulk' );
if ( ! dashboardBulkElement ) {
return;
}
this.trackBulkSmushInterruptedEventFromInlineNoticeOnDashboard( dashboardBulkElement );
this.trackScanInterruptedEventFromInlineNoticeOnDashboard( dashboardBulkElement );
}
trackBulkSmushInterruptedEventFromInlineNoticeOnDashboard( dashboardBulkElement ) {
const triggerBulkSmushButton = dashboardBulkElement.querySelector( '.wp-smush-retry-bulk-smush-link' );
if ( ! triggerBulkSmushButton ) {
return;
}
triggerBulkSmushButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
const event = 'Bulk Smush Interrupted';
const properties = this.getBulkSmushInterruptedEventProperties(
{
Trigger: 'failed_notice',
'Modal Action': 'Retry',
}
);
tracker.track( event, properties ).catch( () => {
this.cacheMissedEvent( {
event,
properties,
} );
} ).finally( () => {
window.location.href = e.target.href;
} );
} );
}
trackBulkSmushInterruptedInlineNoticeEvent() {
return this.trackBulkSmushInterruptedEvent( {
Trigger: 'failed_notice',
'Modal Action': 'Retry',
} );
}
trackBulkSmushInterruptedEventFromInlineNoticeOnBulkSmush() {
const triggerBulkSmushButton = document.querySelector( '.wp-smush-inline-retry-bulk-smush-notice .wp-smush-trigger-bulk-smush' );
if ( ! triggerBulkSmushButton ) {
return;
}
triggerBulkSmushButton.addEventListener( 'click', () => {
this.trackBulkSmushInterruptedInlineNoticeEvent();
} );
}
trackScanInterruptedEventFromInlineNoticeOnDashboard( dashboardBulkElement ) {
const triggerScanButton = dashboardBulkElement.querySelector( '.wp-smush-retry-scan-link' );
if ( ! triggerScanButton ) {
return;
}
triggerScanButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
const event = 'Scan Interrupted';
const properties = this.getScanInterruptedEventProperties( {
Trigger: 'failed_notice',
'Modal Action': 'Retry',
} );
tracker.track( event, properties ).catch( () => {
this.cacheMissedEvent( {
event,
properties,
} );
} ).finally( () => {
window.location.href = e.target.href;
} );
} );
}
trackScanInterruptedEventFromInlineNoticeOnBulkSmush() {
const recheckImagesNotice = document.querySelector( '.wp-smush-recheck-images-notice-box' );
if ( ! recheckImagesNotice ) {
return;
}
const triggerBackgroundScanImagesLink = recheckImagesNotice.querySelector( '.wp-smush-trigger-background-scan' );
if ( triggerBackgroundScanImagesLink ) {
triggerBackgroundScanImagesLink.addEventListener( 'click', () => {
// We are using the same frame for failed scan notice and generic required scan notice,
// so we need to verify the failed notice before tracking the event.
const containTroubleshootingLink = triggerBackgroundScanImagesLink?.previousElementSibling?.querySelector( 'a' );
if ( ! containTroubleshootingLink ) {
return;
}
this.trackScanInterruptedEvent( {
Trigger: 'failed_notice',
'Modal Action': 'Retry',
} );
} );
}
}
trackInterruptedEventFromLoopbackErrorModal() {
const loopbackErrorModal = document.getElementById( 'smush-loopback-error-dialog' );
if ( ! loopbackErrorModal ) {
return;
}
const loopbackErrorDocsLink = loopbackErrorModal.querySelector( 'a[href*="#loopback-request-issue"]' );
let isTroubleshootClicked = false;
if ( loopbackErrorDocsLink ) {
loopbackErrorDocsLink.addEventListener( 'click', () => {
isTroubleshootClicked = true;
}, { once: true } );
}
const trackLoopbackErrorEvent = ( action, processType ) => {
const properties = {
Trigger: 'loopback_error',
'Modal Action': action,
Troubleshoot: isTroubleshootClicked ? 'Yes' : 'No',
};
if ( 'scan' === processType ) {
this.trackScanInterruptedEvent( properties );
} else {
this.trackBulkSmushInterruptedEvent( properties );
}
};
const closeButtons = loopbackErrorModal.querySelectorAll( '[data-modal-close]' );
closeButtons.forEach( ( closeButton ) => {
closeButton.addEventListener( 'click', ( e ) => {
const action = e.target.dataset?.action || 'Close';
const processType = loopbackErrorModal.dataset?.processType || 'scan';
trackLoopbackErrorEvent( action, processType );
} );
} );
}
registerTroubleshootClickEvent() {
const troubleshootLinks = document.querySelectorAll( 'a[href*="#troubleshooting-guide"]' );
if ( ! troubleshootLinks ) {
return;
}
troubleshootLinks.forEach( ( troubleshootLink ) => {
troubleshootLink.addEventListener( 'click', () => {
this.troubleshootClicked = true;
}, { once: true } );
} );
}
maybeTrackMissedEventsOnLoad() {
window.addEventListener( 'load', () => {
const missedEvents = this.getMissedEvents();
if ( 0 === missedEvents.length ) {
return;
}
this.clearMissedEvents();
missedEvents.forEach( ( missedEvent ) => {
tracker.track( missedEvent.event, missedEvent.properties );
} );
} );
}
registerBulkSmushResumeClickEvent() {
const resumeBulkSmushButton = document.querySelector( '.wp-smush-resume-bulk-smush' );
if ( ! resumeBulkSmushButton ) {
return;
}
resumeBulkSmushButton.addEventListener( 'click', () => {
this.resumeBulkSmushCount += 1;
} );
}
getCurrentPageSlug() {
const searchParams = new URLSearchParams( document.location.search );
const pageSlug = searchParams.get( 'page' );
return 'smush' === pageSlug ? 'dashboard' : pageSlug.replace( 'smush-', '' );
}
}
( new ProductAnalytics() ).init();
@@ -0,0 +1,317 @@
/* global ajaxurl */
/* global wp_smush_msgs */
/* global WP_Smush */
/* global SUI */
( function( $ ) {
'use strict';
/**
* Bulk compress page.
*/
$( 'form#smush-bulk-form' ).on( 'submit', function( e ) {
e.preventDefault();
$( '#save-settings-button' ).addClass( 'sui-button-onload' );
saveSettings( $( this ).serialize(), 'bulk' );
// runReCheck();
} );
/**
* Lazy load page.
*/
const lazyLoadForm = $( 'form#smush-lazy-preload-form' );
lazyLoadForm.on( 'submit', function( e ) {
e.preventDefault();
const tabField = $( this ).find( '[name="tab"]' );
const isLazyLoadPage = tabField.length && 'lazy_load' === tabField.val();
$( '#save-settings-button' ).addClass( 'sui-button-onload-text' );
saveSettings( $( this ).serialize(), isLazyLoadPage ? 'lazy-load' : 'preload' );
} );
lazyLoadForm.on( 'keydown', function( e ) {
if ( e.key === 'Enter' ) {
lazyLoadForm.trigger( 'submit' );
}
} );
/**
* CDN page.
*/
$( 'form#smush-cdn-form' ).on( 'submit', function( e ) {
e.preventDefault();
$( '#save-settings-button' ).addClass( 'sui-button-onload-text' );
saveSettings( $( this ).serialize(), 'cdn' );
} );
/**
* Next-Gen page.
*/
$( 'form#smush-next-gen-form' ).on( 'submit', function( e ) {
e.preventDefault();
$( '#save-settings-button' ).addClass( 'sui-button-onload-text' );
saveSettings( $( this ).serialize(), 'next-gen' );
} );
/**
* Integrations page.
*/
$( 'form#smush-integrations-form' ).on( 'submit', function( e ) {
e.preventDefault();
$( '#save-settings-button' ).addClass( 'sui-button-onload-text' );
saveSettings( $( this ).serialize(), 'integrations' );
} );
/**
* Settings page.
*/
$( 'form#smush-settings-form' ).on( 'submit', function( e ) {
e.preventDefault();
$( '#save-settings-button' ).addClass( 'sui-button-onload-text' );
saveSettings( $( this ).serialize(), 'settings' );
} );
/**
* Save settings.
*
* @param {string} settings JSON string of settings.
* @param {string} page Settings page.
*/
function saveSettings( settings, page ) {
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=smush_save_settings', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
$( '#save-settings-button' ).removeClass(
'sui-button-onload-text sui-button-onload'
);
if ( 200 === xhr.status ) {
const res = JSON.parse( xhr.response );
if ( 'undefined' !== typeof res.success && res.success ) {
showSuccessNotice( wp_smush_msgs.settingsUpdated );
triggerSavedSmushSettingsEvent( res.data );
} else if ( res.data && res.data.message ) {
WP_Smush.helpers.showErrorNotice( res.data.message );
} else {
WP_Smush.helpers.showErrorNotice( 'Request failed.' );
}
} else {
WP_Smush.helpers.showErrorNotice( 'Request failed. Returned status of ' + xhr.status );
}
};
xhr.send( 'page=' + page + '&' + settings + '&_ajax_nonce=' + wp_smush_msgs.nonce );
}
function triggerSavedSmushSettingsEvent( status ) {
document.dispatchEvent(
new CustomEvent( 'onSavedSmushSettings', {
detail: status
} )
);
}
/**
* Show successful update notice.
*
* @param {string} msg Notice message.
*/
function showSuccessNotice( msg ) {
const noticeMessage = `<p>${ msg }</p>`,
noticeOptions = {
type: 'success',
icon: 'check',
};
SUI.openNotice( 'wp-smush-ajax-notice', noticeMessage, noticeOptions );
const loadingButton = document.querySelector( '.sui-button-onload' );
if ( loadingButton ) {
loadingButton.classList.remove( 'sui-button-onload' );
}
}
/**
* Re-check images from bulk smush and integrations pages.
*/
function runReCheck() {
$( '#save-settings-button' ).addClass( 'sui-button-onload' );
const param = {
action: 'scan_for_resmush',
wp_smush_options_nonce: $( '#wp_smush_options_nonce' ).val(),
type: 'media',
};
// Send ajax, Update Settings, And Check For resmush.
$.post( ajaxurl, $.param( param ) ).done( function() {
$( '#save-settings-button' ).removeClass( 'sui-button-onload' );
} );
}
/**
* Parse remove data change.
*/
$( 'input[name=keep_data]' ).on( 'change', function( e ) {
const otherClass =
'keep_data-true' === e.target.id
? 'keep_data-false'
: 'keep_data-true';
e.target.parentNode.classList.add( 'active' );
document
.getElementById( otherClass )
.parentNode.classList.remove( 'active' );
} );
/**
* Handle highlighting notice display based on detection checkbox state.
*/
function handleHighlightingNotice( isAfterSave = false ) {
const detectionEnabled = $( '#detection' ).is( ':checked' );
const $notice = $( '.smush-highlighting-notice' );
const $warning = $( '.smush-highlighting-warning' );
if ( ! detectionEnabled ) {
$notice.hide();
$warning.hide();
return;
}
if ( isAfterSave ) {
$notice.show();
$warning.hide();
} else {
// Show warning only if notice isn't visible yet.
const noticeVisible = $notice.is( ':visible' ) || $notice.css( 'display' ) !== 'none';
if ( noticeVisible ) {
$notice.show();
$warning.hide();
} else {
$notice.hide();
$warning.show();
}
}
}
/**
* Handle auto-detect checkbox toggle, to show/hide highlighting notice.
*/
$( 'input#detection' ).on( 'click', function () {
handleHighlightingNotice( false );
} );
/**
* Handle settings saved event.
*/
$( document ).on( 'onSavedSmushSettings', function () {
handleHighlightingNotice( true );
} );
/**
* Form notice via query var smush-notice handler.
*/
const formNoticeHandler = () => {
if ( ! window.URL || ! window.URLSearchParams ) {
return;
}
const url = new URL( window.location );
const noticeKey = url.searchParams.get( 'smush-notice' );
if ( ! noticeKey ) {
return;
}
document.dispatchEvent(
new CustomEvent( `on-smush-${ noticeKey }-notice` )
);
// Remove the smush-notice query parameter.
url.searchParams.delete( 'smush-notice' );
window.history.replaceState( {}, document.title, url.toString() );
};
formNoticeHandler();
// Handle toggle fields.
const toggleFieldVisibility = ( fieldVisibilitySettings, fieldWrapperClassName ) => {
for ( const fieldName in fieldVisibilitySettings ) {
const field = document.querySelector( `[name="${ fieldName }"]` );
if ( ! field ) {
continue;
}
const fieldWrapper = field.closest( `.${ fieldWrapperClassName }` );
if ( ! fieldWrapper ) {
continue;
}
if ( 'show' === fieldVisibilitySettings[ fieldName ] ) {
fieldWrapper.classList.remove( 'sui-hidden' );
} else {
fieldWrapper.classList.add( 'sui-hidden' );
}
}
};
const toggleFieldsHandler = () => {
const settingsForm = document.querySelector( '.wp-smush-settings-form' );
if ( ! settingsForm ) {
return;
}
const fieldWrapperClassName = 'sui-box-settings-row';
const conditionalFields = settingsForm.querySelectorAll( '[data-toggle-fields]' );
if ( conditionalFields ) {
conditionalFields.forEach( function( conditionalField ) {
const fieldVisibilitySettings = JSON.parse( conditionalField.dataset.toggleFields );
if ( ! fieldVisibilitySettings && 'object' !== typeof fieldVisibilitySettings ) {
return;
}
conditionalField.addEventListener( 'change', function() {
if ( ! this.checked ) {
return;
}
toggleFieldVisibility( fieldVisibilitySettings, fieldWrapperClassName );
} );
} );
}
if ( window.SUI?.tabs ) {
window.SUI.tabs(
{
callback( tab, panel ) {
const conditionalField = tab.next( '[data-toggle-fields]' );
if ( conditionalField.length ) {
const fieldVisibilitySettings = conditionalField.data( 'toggle-fields' );
if ( fieldVisibilitySettings && 'object' === typeof fieldVisibilitySettings ) {
toggleFieldVisibility( fieldVisibilitySettings, fieldWrapperClassName );
}
}
const childFields = panel.find( '[data-toggle-fields]' );
if ( childFields.length ) {
childFields.each( function() {
if ( $( this ).is( ':checked' ) ) {
const fieldVisibilitySettings = $( this ).data( 'toggle-fields' );
if ( fieldVisibilitySettings && 'object' === typeof fieldVisibilitySettings ) {
toggleFieldVisibility( fieldVisibilitySettings, fieldWrapperClassName );
}
}
} );
}
}
}
);
}
};
toggleFieldsHandler();
}( jQuery ) );
@@ -0,0 +1,133 @@
/* global WP_Smush */
/* global ajaxurl */
/**
* WebP functionality.
*
* @since 3.8.0
*/
import Fetcher from '../utils/fetcher';
import NextGen from './next-gen';
class WebP extends NextGen {
constructor() {
super( 'webp' );
this.recheckStatusButton = document.getElementById( 'smush-webp-recheck' );
this.recheckStatusLink = document.getElementById( 'smush-webp-recheck-link' );
this.showWizardButton = document.getElementById( 'smush-webp-toggle-wizard' );
this.switchWebpMethod = document.getElementById( 'smush-switch-webp-method' );
this.webpInit();
}
webpInit() {
/**
* Handle "RE-CHECK STATUS' button click on WebP page.
*/
if ( this.recheckStatusButton ) {
this.recheckStatusButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
this.recheckStatus();
} );
}
/**
* Handle "RE-CHECK STATUS' link click on WebP page.
*/
if ( this.recheckStatusLink ) {
this.recheckStatusLink.addEventListener( 'click', ( e ) => {
e.preventDefault();
this.recheckStatus();
} );
}
if ( this.showWizardButton ) {
this.showWizardButton.addEventListener(
'click',
this.toggleWizard
);
}
if ( this.switchWebpMethod ) {
this.switchWebpMethod.addEventListener(
'click',
( e ) => {
e.preventDefault();
e.target.classList.add( 'wp-smush-link-in-progress' );
this.switchMethod( this.switchWebpMethod.dataset.method );
}
);
}
}
switchMethod( newMethod ) {
Fetcher.webp.switchMethod( newMethod ).then( ( res ) => {
if ( ! res?.success ) {
WP_Smush.helpers.showNotice( res );
return;
}
window.location.reload();
} );
}
/**
* re-check server configuration for WebP.
*/
recheckStatus() {
this.recheckStatusButton.classList.add( 'sui-button-onload' );
const xhr = new XMLHttpRequest();
xhr.open( 'POST', ajaxurl + '?action=smush_webp_get_status', true );
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
this.recheckStatusButton.classList.remove( 'sui-button-onload' );
let message = false;
const res = JSON.parse( xhr.response );
if ( 200 === xhr.status ) {
const isConfigured = res.success ? '1' : '0';
if (
isConfigured !==
this.recheckStatusButton.dataset.isConfigured
) {
// Reload the page when the configuration status changed.
location.reload();
}
} else {
message = window.wp_smush_msgs.generic_ajax_error;
}
if ( res && res.data ) {
message = res.data;
}
if ( message ) {
this.showNotice( message );
}
};
xhr.send( '_ajax_nonce=' + window.wp_smush_msgs.webp_nonce );
}
toggleWizard( e ) {
e.currentTarget.classList.add( 'sui-button-onload' );
const xhr = new XMLHttpRequest();
xhr.open(
'GET',
ajaxurl +
'?action=smush_toggle_webp_wizard&_ajax_nonce=' +
window.wp_smush_msgs.webp_nonce,
true
);
xhr.onload = () => location.href = window.wp_smush_msgs.nextGenURL;
xhr.send();
}
}
( function() {
'use strict';
WP_Smush.WebP = new WebP();
}() );
@@ -0,0 +1,200 @@
/* global ajaxurl */
/**
* External dependencies
*/
import assign from 'lodash/assign';
/**
* Wrapper function for ajax calls to WordPress.
*
* @since 3.12.0
*/
function SmushFetcher() {
/**
* Request ajax with a promise.
* Use FormData Object as data if you need to upload file
*
* @param {string} action
* @param {Object|FormData} data
* @param {string} method
* @return {Promise<any>} Request results.
*/
function request(action, data = {}, method = 'POST') {
const args = {
url: ajaxurl,
method,
cache: false
};
if (data instanceof FormData) {
data.append('action', action);
data.append('_ajax_nonce', window.wp_smush_msgs.nonce);
args.contentType = false;
args.processData = false;
} else {
data._ajax_nonce = data._ajax_nonce || window.smush_global.nonce || window.wp_smush_msgs.nonce;
data.action = action;
}
args.data = data;
return new Promise((resolve, reject) => {
jQuery.ajax(args).done(resolve).fail(reject);
}).then((response) => {
if (typeof response !== 'object') {
response = JSON.parse(response);
}
return response;
}).catch((error) => {
console.error('Error:', error);
return error;
});
}
const methods = {
/**
* Manage ajax for background.
*/
background: {
/**
* Start background process.
*/
start: () => {
return request('bulk_smush_start');
},
/**
* Cancel background process.
*/
cancel: () => {
return request('bulk_smush_cancel');
},
/**
* Initial State - Get stats on the first time.
*/
initState: () => {
return request('bulk_smush_get_status');
},
/**
* Get stats.
*/
getStatus: () => {
return request('bulk_smush_get_status');
},
getStats: () => {
return request('bulk_smush_get_global_stats');
},
backgroundHealthyCheck: () => {
return request('smush_start_background_pre_flight_check');
},
backgroundHealthyStatus: () => {
return request('smush_get_background_pre_flight_status');
}
},
smush: {
/**
* Sync stats.
*/
syncStats: ( data ) => {
data = data || {};
return request('get_stats', data);
},
/**
* Ignore All.
*/
ignoreAll: ( type ) => {
return request('wp_smush_ignore_all_failed_items', {
type: type,
});
},
},
/**
* Manage ajax for other requests
*/
common: {
/**
* Dismiss Notice.
*
* @param {string} dismissId Notification id.
*/
dismissNotice: (dismissId) => {
return request('smush_dismiss_notice', {
key: dismissId
});
},
remindReviewPrompt: () => {
return request( 'wp_smush_review_prompts_remind_later' );
},
/**
* Hide the new features modal.
*
* @param {string} modalID Notification id.
*/
hideModal: (modalID) => request('hide_modal', {
modal_id: modalID,
}),
track: ( event, properties ) => request('smush_analytics_track_event', {
event,
properties
}),
/**
* Custom request.
*
* @param {Object} data
*/
request: (data) => data.action && request(data.action, data),
},
scanMediaLibrary: {
start: ( optimize_on_scan_completed = false ) => {
optimize_on_scan_completed = optimize_on_scan_completed ? 1 : 0;
const _ajax_nonce = window.wp_smushit_data.media_library_scan.nonce;
return request( 'wp_smush_start_background_scan', {
optimize_on_scan_completed,
_ajax_nonce,
} );
},
cancel: () => {
const _ajax_nonce = window.wp_smushit_data.media_library_scan.nonce;
return request( 'wp_smush_cancel_background_scan', {
_ajax_nonce,
} );
},
getScanStatus: () => {
const _ajax_nonce = window.wp_smushit_data.media_library_scan.nonce;
return request( 'wp_smush_get_background_scan_status', {
_ajax_nonce,
} );
},
},
webp: {
switchMethod: ( method ) => {
return request( 'webp_switch_method', { method } );
},
},
settings: {
disconnectSite: () => {
return request( 'wp_smush_disconnect_site' );
}
}
};
assign(this, methods);
}
const SmushAjax = new SmushFetcher();
export default SmushAjax;
@@ -0,0 +1,49 @@
import Fetcher from './fetcher';
class Tracker {
/* @private */
#doingEvents = new Set();
#allowToTrack;
track( event, properties = {} ) {
if ( ! this.allowToTrack() || this.inProgressEvent( event ) ) {
return;
}
this.setInProgressEvent( event );
return Fetcher.common.track( event, properties ).then( ( res ) => {
setTimeout( () => {
this.restoreInProgressEvent( event );
}, 1000 );
return res;
} );
}
inProgressEvent( event ) {
return this.#doingEvents.has( event );
}
setInProgressEvent( event ) {
this.#doingEvents.add( event );
}
restoreInProgressEvent( event ) {
this.#doingEvents.delete( event );
}
allowToTrack() {
return this.#allowToTrack || !! ( window.wp_smush_mixpanel?.opt_in );
}
setAllowToTrack( allowToTrack ) {
this.#allowToTrack = allowToTrack;
return this;
}
}
const tracker = new Tracker();
export default tracker;
@@ -0,0 +1,91 @@
import React, {useEffect, useRef, useState} from "react";
import {post} from "../utils/request";
import MediaLibraryScannerModal from "./media-library-scanner-modal";
export default function AjaxMediaLibraryScannerModal(
{
nonce = '',
onScanCompleted = () => false,
onClose = () => false,
focusAfterClose = ''
}
) {
const [inProgress, setInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [cancelled, setCancelled] = useState(false);
const cancelledRef = useRef();
useEffect(() => {
cancelledRef.current = cancelled;
}, [cancelled]);
function start() {
setInProgress(true);
post('wp_smush_before_scan_library', nonce).then((response) => {
const sliceCount = response?.slice_count;
const slicesList = range(sliceCount, 1);
const parallelRequests = response?.parallel_requests;
handleBatch(slicesList, sliceCount, parallelRequests)
.then(() => {
setTimeout(() => {
onScanCompleted();
}, 1000);
});
});
}
function handleBatch(remainingSlicesList, sliceCount, parallelRequests) {
const batchPromises = [];
const completedSliceCount = Math.max(sliceCount - remainingSlicesList.length, 0);
const batch = remainingSlicesList.splice(0, parallelRequests);
updateProgress(completedSliceCount, sliceCount);
batch.forEach((sliceNumber) => {
batchPromises.push(
post('wp_smush_scan_library_slice', nonce, {slice: sliceNumber})
);
});
return new Promise((resolve) => {
Promise.all(batchPromises)
.then(() => {
if (!cancelledRef.current) {
if (remainingSlicesList.length) {
handleBatch(remainingSlicesList, sliceCount, parallelRequests)
.then(resolve);
} else {
updateProgress(sliceCount, sliceCount);
resolve();
}
}
});
});
}
function range(size, startAt = 0) {
return [...Array(size).keys()].map(i => i + startAt);
}
function cancelScan() {
setCancelled(true);
setInProgress(false);
setProgress(0);
}
function updateProgress(completedSlices, totalSlices) {
const progress = (completedSlices / totalSlices) * 100;
setProgress(progress);
}
return <MediaLibraryScannerModal
inProgress={inProgress}
progress={progress}
onCancel={cancelScan}
focusAfterClose={focusAfterClose}
onClose={onClose}
onStart={start}
/>;
};
@@ -0,0 +1,76 @@
import React, {useRef, useState} from "react";
import {post} from "../utils/request";
import MediaLibraryScannerModal from "./media-library-scanner-modal";
export default function BackgroundMediaLibraryScannerModal(
{
nonce = '',
onScanCompleted = () => false,
onClose = () => false,
focusAfterClose = ''
}
) {
const [inProgress, setInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [cancelled, setCancelled] = useState(false);
const progressTimeoutId = useRef(0);
function start() {
post('wp_smush_start_background_scan', nonce).then(() => {
setInProgress(true);
progressTimeoutId.current = setTimeout(updateProgress, 2000);
});
}
function clearProgressTimeout() {
if (progressTimeoutId.current) {
clearTimeout(progressTimeoutId.current);
}
}
function updateProgress() {
post('wp_smush_get_background_scan_status', nonce).then(response => {
const isCompleted = response?.is_completed;
if (isCompleted) {
clearProgressTimeout();
onScanCompleted();
return;
}
const isCancelled = response?.is_cancelled;
if (isCancelled) {
clearProgressTimeout();
changeStateToCancelled();
return;
}
const totalItems = response?.total_items;
const processedItems = response?.processed_items;
const progress = (processedItems / totalItems) * 100;
setProgress(progress);
progressTimeoutId.current = setTimeout(updateProgress, 1000);
});
}
function cancelScan() {
clearProgressTimeout();
post('wp_smush_cancel_background_scan', nonce)
.then(changeStateToCancelled);
}
function changeStateToCancelled() {
setCancelled(true);
setProgress(0);
setInProgress(false);
}
return <MediaLibraryScannerModal
inProgress={inProgress}
progress={progress}
onCancel={cancelScan}
focusAfterClose={focusAfterClose}
onClose={onClose}
onStart={start}
/>;
};
@@ -0,0 +1,49 @@
import React, {useEffect, useRef, useState} from "react";
import Modal from "../common/modal";
import {post} from "../utils/request";
import Button from "../common/button";
import ProgressBar from "../common/progress-bar";
const {__} = wp.i18n;
export default function MediaLibraryScannerModal(
{
inProgress = false,
progress = 0,
onClose = () => false,
onStart = () => false,
onCancel = () => false,
focusAfterClose = ''
}
) {
function content() {
if (inProgress) {
return <>
<ProgressBar progress={progress}/>
<Button id="wp-smush-cancel-media-library-scan"
icon="sui-icon-close"
text={__('Cancel', 'wp-smushit')}
ghost={true}
onClick={onCancel}
/>
</>;
} else {
return <>
<Button id="wp-smush-start-media-library-scan"
icon="sui-icon-play"
text={__('Start', 'wp-smushit')}
onClick={onStart}
/>
</>;
}
}
return <Modal id="wp-smush-media-library-scanner-modal"
title={__('Scan Media Library', 'wp-smushit')}
description={__('Scans the media library to detect items to Smush.', 'wp-smushit')}
onClose={onClose}
focusAfterClose={focusAfterClose}
disableCloseButton={inProgress}>
{content()}
</Modal>;
};
@@ -0,0 +1,52 @@
import React, {useState} from "react";
import domReady from '@wordpress/dom-ready';
import ReactDOM from "react-dom";
import Button from "../common/button";
import FloatingNoticePlaceholder from "../common/floating-notice-placeholder";
import {showSuccessNotice} from "../utils/notices";
import AjaxMediaLibraryScannerModal from "./ajax-media-library-scanner-modal";
import BackgroundMediaLibraryScannerModal from "./background-media-library-scanner-modal";
const {__} = wp.i18n;
function MediaLibraryScanner({}) {
const [modalOpen, setModalOpen] = useState(false);
return <>
<FloatingNoticePlaceholder id="wp-smush-media-library-scanner-notice"/>
{modalOpen &&
<BackgroundMediaLibraryScannerModal
focusAfterClose="wp-smush-open-media-library-scanner"
nonce={mediaLibraryScan.nonce}
onScanCompleted={() => {
showSuccessNotice(
'wp-smush-media-library-scanner-notice',
__('Scan completed successfully!', 'wp-smushit'),
true
);
setModalOpen(false);
window.location.reload();
}}
onClose={() => setModalOpen(false)}
/>
}
<Button id="wp-smush-open-media-library-scanner" text={__('Re-Check Images', 'wp-smushit')}
className="wp-smush-scan"
icon="sui-icon-update"
disabled={modalOpen}
onClick={() => setModalOpen(true)}
/>
</>;
}
domReady(function () {
const scannerContainer = document.getElementById('wp-smush-media-library-scanner');
if (scannerContainer) {
ReactDOM.render(
<MediaLibraryScanner/>,
scannerContainer
);
}
});
@@ -0,0 +1,70 @@
import React from "react";
import classnames from "classnames";
export default function Button(
{
id = "",
text = "",
color = "",
dashed = false,
icon = '',
loading = false,
ghost = false,
disabled = false,
href = "",
target = "",
className = "",
onClick = () => false,
}
) {
function handleClick(e) {
e.preventDefault();
onClick();
}
function textTag() {
const iconTag = icon ? <span className={icon} aria-hidden="true"/> : "";
return (
<span className={classnames({"sui-loading-text": loading})}>
{iconTag} {text}
</span>
);
}
function loadingIcon() {
return loading
? <span className="sui-icon-loader sui-loading" aria-hidden="true"/>
: "";
}
let HtmlTag, props;
if (href) {
HtmlTag = 'a';
props = {href: href, target: target};
} else {
HtmlTag = 'button';
props = {
disabled: disabled,
onClick: e => handleClick(e)
};
}
const hasText = text && text.trim();
return (
<HtmlTag
{...props}
className={classnames(className, "sui-button-" + color, {
"sui-button-onload": loading,
"sui-button-ghost": ghost,
"sui-button-icon": !hasText,
"sui-button-dashed": dashed,
"sui-button": hasText
})}
id={id}
>
{textTag()}
{loadingIcon()}
</HtmlTag>
);
}
@@ -0,0 +1,11 @@
import React from "react";
export default function FloatingNoticePlaceholder({id = ''}) {
return <div className="sui-floating-notices">
<div role="alert"
id={id}
className="sui-notice"
aria-live="assertive">
</div>
</div>;
}
@@ -0,0 +1,133 @@
import React, {useEffect} from 'react';
import classnames from 'classnames';
import SUI from 'SUI';
import $ from 'jquery';
const {__} = wp.i18n;
export default function Modal(
{
id = '',
title = '',
description = '',
small = false,
headerActions = false,
focusAfterOpen = '',
focusAfterClose = 'container',
dialogClasses = [],
disableCloseButton = false,
enterDisabled = false,
beforeTitle = false,
onEnter = () => false,
onClose = () => false,
footer,
children
}
) {
useEffect(() => {
SUI.openModal(
id,
focusAfterClose,
focusAfterOpen ? focusAfterOpen : getTitleId(),
false,
false
);
return () => SUI.closeModal();
}, []);
const handleKeyDown = (event) => {
const isTargetInput = $(event.target).is('.sui-modal.sui-active input');
if (isTargetInput && event.keyCode === 13) {
event.preventDefault();
event.stopPropagation();
if (!enterDisabled && onEnter) {
onEnter(event);
}
}
}
function getTitleId() {
return id + '-modal-title';
}
function getHeaderActions() {
const closeButton = getCloseButton();
if (small) {
return closeButton;
} else if (headerActions) {
return headerActions;
} else {
return <div className="sui-actions-right">{closeButton}</div>
}
}
function getCloseButton() {
return <button id={id + '-close-button'}
type="button"
onClick={() => onClose()}
disabled={disableCloseButton}
className={classnames("sui-button-icon", {
'sui-button-float--right': small
})}>
<span className="sui-icon-close sui-md" aria-hidden="true"/>
<span className="sui-screen-reader-text">
{__('Close this dialog window', 'wds')}
</span>
</button>
}
function getDialogClasses() {
return Object.assign({}, {
'sui-modal-sm': small,
'sui-modal-lg': !small
}, dialogClasses);
}
return <div className={classnames('sui-modal', getDialogClasses())}
onKeyDown={e => handleKeyDown(e)}>
<div role="dialog"
id={id}
className={classnames('sui-modal-content', id + '-modal')}
aria-modal="true"
aria-labelledby={id + '-modal-title'}
aria-describedby={id + '-modal-description'}>
<div className="sui-box" role="document">
<div className={classnames('sui-box-header', {
'sui-flatten sui-content-center sui-spacing-top--40': small
})}>
{beforeTitle}
<h3 id={getTitleId()}
className={classnames('sui-box-title', {
'sui-lg': small
})}>
{title}
</h3>
{getHeaderActions()}
</div>
<div className={classnames('sui-box-body', {
'sui-content-center': small
})}>
{description &&
<p className="sui-description"
id={id + '-modal-description'}>
{description}
</p>}
{children}
</div>
{footer && <div className="sui-box-footer">
{footer}
</div>}
</div>
</div>
</div>;
}
@@ -0,0 +1,36 @@
import React from "react";
export default function ProgressBar(
{
progress = 0,
stateMessage = ''
}
) {
progress = Math.ceil(progress);
const progressPercentage = progress + "%";
return (
<React.Fragment>
<div className="sui-progress-block">
<div className="sui-progress">
<span className="sui-progress-icon" aria-hidden="true">
<span className="sui-icon-loader sui-loading"/>
</span>
<div className="sui-progress-text">{progressPercentage}</div>
<div className="sui-progress-bar">
<span
style={{
transition: progress === 0 ? false : "transform 0.4s linear 0s",
transformOrigin: "left center",
transform: `translateX(${progress - 100}%)`,
}}
/>
</div>
</div>
</div>
<div className="sui-progress-state">{stateMessage}</div>
</React.Fragment>
);
}
@@ -0,0 +1,189 @@
/**
* External dependencies
*/
import React from 'react';
import { createRoot } from 'react-dom/client';
/**
* WordPress dependencies
*/
import domReady from '@wordpress/dom-ready';
const { __, sprintf } = wp.i18n;
/**
* SUI dependencies
*/
import { Presets } from '@wpmudev/shared-presets';
export const Configs = ({ isWidget }) => {
// TODO: Handle the html interpolation and translation better.
const proDescription = (
<>
{__(
'You can easily apply configs to multiple sites at once via ',
'wp-smushit'
)}
<a
href={window.smushReact.links.hubConfigs}
target="_blank"
rel="noreferrer"
>
{__('the Hub.')}
</a>
</>
);
const closeIcon = __('Close this dialog window', 'wp-smushit'),
cancelButton = __('Cancel', 'wp-smushit');
const lang = {
title: __('Preset Configs', 'wp-smushit'),
upload: __('Upload', 'wp-smushit'),
save: __('Save config', 'wp-smushit'),
loading: __('Updating the config list…', 'wp-smushit'),
emptyNotice: __(
'You dont have any available config. Save preset configurations of Smushs settings, then upload and apply them to your other sites in just a few clicks!',
'wp-smushit'
),
baseDescription: __(
'Use configs to save preset configurations of Smushs settings, then upload and apply them to your other sites in just a few clicks!',
'wp-smushit'
),
freeNoticeMessage: __( 'Tired of saving, downloading and uploading your configs across your sites? WPMU DEV members use The Hub to easily apply configs to multiple sites at once… Try it today!', 'wp-smushit' ),
proDescription,
syncWithHubText: __(
'Created or updated configs via the Hub?',
'wp-smushit'
),
syncWithHubButton: __('Check again', 'wp-smushit'),
apply: __('Apply', 'wp-smushit'),
download: __('Download', 'wp-smushit'),
edit: __('Name and Description', 'wp-smushit'),
delete: __('Delete', 'wp-smushit'),
notificationDismiss: __('Dismiss notice', 'wp-smushit'),
freeButtonLabel: __('Try The Hub', 'wp-smushit'),
defaultRequestError: sprintf(
/* translators: %s request status */
__(
'Request failed. Status: %s. Please reload the page and try again.',
'wp-smushit'
),
'{status}'
),
uploadActionSuccessMessage: sprintf(
/* translators: %s request status */
__(
'%s config has been uploaded successfully you can now apply it to this site.',
'wp-smushit'
),
'{configName}'
),
uploadWrongPluginErrorMessage: sprintf(
/* translators: %s {pluginName} */
__(
'The uploaded file is not a %s Config. Please make sure the uploaded file is correct.',
'wp-smushit'
),
'{pluginName}'
),
applyAction: {
closeIcon,
cancelButton,
title: __('Apply Config', 'wp-smushit'),
description: sprintf(
/* translators: %s config name */
__(
'Are you sure you want to apply the %s config to this site? We recommend you have a backup available as your existing settings configuration will be overridden.',
'wp-smushit'
),
'{configName}'
),
actionButton: __('Apply', 'wp-smushit'),
successMessage: sprintf(
/* translators: %s. config name */
__('%s config has been applied successfully.', 'wp-smushit'),
'{configName}'
),
},
deleteAction: {
closeIcon,
cancelButton,
title: __('Delete Configuration File', 'wp-smushit'),
description: sprintf(
/* translators: %s config name */
__(
'Are you sure you want to delete %s? You will no longer be able to apply it to this or other connected sites.',
'wp-smushit'
),
'{configName}'
),
actionButton: __('Delete', 'wp-smushit'),
},
editAction: {
closeIcon,
cancelButton,
nameInput: __('Config name', 'wp-smushit'),
descriptionInput: __('Description', 'wp-smushit'),
emptyNameError: __('The config name is required', 'wp-smushit'),
actionButton: __('Save', 'wp-smushit'),
editTitle: __('Rename Config', 'wp-smushit'),
editDescription: __(
'Change your config name to something recognizable.',
'wp-smushit'
),
createTitle: __('Save Config', 'wp-smushit'),
createDescription: __(
'Save your current settings configuration. Youll be able to then download and apply it to your other sites.',
'wp-smushit'
),
successMessage: sprintf(
/* translators: %s. config name */
__('%s config created successfully.', 'wp-smushit'),
'{configName}'
),
},
settingsLabels: {
bulk_smush: __('Bulk Smush', 'wp-smushit'),
integrations: __('Integrations', 'wp-smushit'),
// Settings::LAZY_PRELOAD_MODULE_NAME.
lazy_load: __('Lazy Load & Preload', 'wp-smushit'),
cdn: __('CDN', 'wp-smushit'),
next_gen: __('Next-Gen Formats', 'wp-smushit'),
settings: __('Settings', 'wp-smushit'),
networkwide: __('Subsite Controls', 'wp-smushit'),
},
};
return (
<Presets
isWidget={isWidget}
isPro={false}
isWhitelabel={window.smushReact.hideBranding}
sourceLang={lang}
sourceUrls={window.smushReact.links}
requestsData={window.smushReact.requestsData}
proItems={[
'PNG to JPEG Conversion',
'Email Notification',
'CDN',
'Next-Gen Formats',
'Amazon S3',
'NextGen Gallery',
'Preload Critical Images',
'Auto Resizing',
'Add Missing Image Dimensions',
] }
/>
);
};
domReady(function () {
const configsPageBox = document.getElementById('smush-box-configs');
if (configsPageBox) {
createRoot(configsPageBox).render(<Configs isWidget={false} />);
}
const configsWidgetBox = document.getElementById('smush-widget-configs');
if (configsWidgetBox) {
createRoot(configsWidgetBox).render(<Configs isWidget={true} />);
}
});
@@ -0,0 +1,108 @@
/* global ajaxurl */
/**
* External dependencies
*/
import React from 'react';
import { createRoot } from 'react-dom/client';
/**
* WordPress dependencies
*/
import domReady from '@wordpress/dom-ready';
/**
* Internal dependencies
*/
import StepsBar from '../views/webp/steps-bar';
import StepContent from '../views/webp/step-content';
import FreeContent from '../views/webp/free-content';
import StepFooter from '../views/webp/step-footer';
export const WebpPage = ({ smushData }) => {
const [currentStep, setCurrentStep] = React.useState(
parseInt(smushData.startStep)
);
React.useEffect(() => {
if (2 === currentStep) {
window.SUI.suiCodeSnippet();
}
}, [currentStep]);
const [serverType, setServerType] = React.useState(
smushData.detectedServer
);
const [rulesMethod, setRulesMethod] = React.useState('automatic');
const [rulesError, setRulesError] = React.useState(false);
const makeRequest = (action, verb = 'GET') => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(
verb,
`${ajaxurl}?action=${action}&_ajax_nonce=${smushData.nonce}`,
true
);
xhr.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded'
);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(xhr);
}
};
xhr.onerror = () => reject(xhr);
xhr.send();
});
};
const stepContent = smushData.isPro ? (
<StepContent
currentStep={currentStep}
serverType={serverType}
rulesMethod={rulesMethod}
setRulesMethod={setRulesMethod}
rulesError={rulesError}
setServerType={setServerType}
smushData={smushData}
makeRequest={makeRequest}
/>
) : (
<FreeContent smushData={smushData} />
);
return (
<React.Fragment>
<div className="sui-box-body sui-no-padding">
<div className="sui-row-with-sidenav">
{ smushData.isPro && <StepsBar smushData={smushData} currentStep={currentStep} /> }
{stepContent}
</div>
</div>
{smushData.isPro && (
<StepFooter
currentStep={currentStep}
setCurrentStep={setCurrentStep}
serverType={serverType}
rulesMethod={rulesMethod}
setRulesError={setRulesError}
makeRequest={makeRequest}
/>
)}
</React.Fragment>
);
};
domReady(function () {
const webpPageBox = document.getElementById('smush-box-webp-wizard');
if (webpPageBox) {
const root = createRoot(webpPageBox);
root.render( <WebpPage smushData={window.smushReact} /> );
}
});
@@ -0,0 +1,35 @@
export function showSuccessNotice(id, message, dismissible = true) {
return showNotice(id, message, 'success', dismissible);
}
export function showErrorNotice(id, message, dismissible = true) {
return showNotice(id, message, 'error', dismissible);
}
export function showInfoNotice(id, message, dismissible = true) {
return showNotice(id, message, 'info', dismissible);
}
export function showWarningNotice(id, message, dismissible = true) {
return showNotice(id, message, 'warning', dismissible);
}
export function closeNotice(id) {
SUI.closeNotice(id);
}
export function showNotice(id, message, type = 'success', dismissible = true) {
const icons = {
error: 'warning-alert',
info: 'info',
warning: 'warning-alert',
success: 'check-tick'
};
SUI.closeNotice(id);
SUI.openNotice(id, '<p>' + message + '</p>', {
type: type,
icon: icons[type],
dismiss: {show: dismissible}
});
}
@@ -0,0 +1,23 @@
import $ from 'jquery';
import ajaxUrl from 'ajaxUrl';
export function post(action, nonce, data = {}) {
return new Promise(function (resolve, reject) {
const request = Object.assign({}, {
action: action,
_ajax_nonce: nonce
}, data);
$.post(ajaxUrl, request)
.done((response) => {
if (response.success) {
resolve(
response?.data
);
} else {
reject(response?.data?.message);
}
})
.fail(() => reject());
});
}
@@ -0,0 +1,92 @@
/**
* External dependencies
*/
import React from 'react';
import tracker from '../../../js/utils/tracker';
/**
* WordPress dependencies
*/
const {__} = wp.i18n;
export default ({smushData}) => {
return (
<React.Fragment>
<div className="sui-box-header">
<h3 className="sui-box-title">
{__('Next-Gen Formats', 'wp-smushit')}
</h3>
</div>
<div className="sui-box-body">
<div className="sui-message">
<img
className="sui-image"
src={smushData.urls.freeImg}
srcSet={smushData.urls.freeImg2x + ' 2x'}
alt={__('Smush WebP', 'wp-smushit')}
/>
<div className="sui-message-content">
<p>
{__(
'Fix the "Serve images in next-gen format" Google PageSpeed recommendation with a single click! Serve WebP and AVIF images directly from your server to supported browsers, while seamlessly switching to original images for those without WebP or AVIF support. All without relying on a CDN or any server configuration.',
'wp-smushit'
)}
</p>
<ol className="sui-upsell-list">
<li>
<span
className="sui-icon-check sui-sm"
aria-hidden="true"
/>
{__(
'Activate the Next-Gen Formats feature with a single click; no server configuration required.',
'wp-smushit'
)}
</li>
<li>
<span
className="sui-icon-check sui-sm"
aria-hidden="true"
/>
{__(
'Fix “Serve images in next-gen format" Google PageSpeed recommendation.',
'wp-smushit'
)}
</li>
<li>
<span
className="sui-icon-check sui-sm"
aria-hidden="true"
/>
{__(
'Serve WebP and AVIF version of images in the browsers that support it and fall back to JPEGs and PNGs for unsupported browsers.',
'wp-smushit'
)}
</li>
</ol>
<p className="sui-margin-top">
<a
href={smushData.urls.upsell}
className="sui-button sui-button-purple"
style={{marginRight: '30px'}}
target="_blank"
rel="noreferrer"
onClick={ () => {
tracker.track( 'local_webp_upsell', {
Location: 'Next-Gen Formats',
} );
} }
>
{__( 'UNLOCK NEXT-GEN FORMATS WITH PRO', 'wp-smushit')}
</a>
</p>
</div>
</div>
</div>
</React.Fragment>
);
};
@@ -0,0 +1,450 @@
/**
* External dependencies
*/
import React from 'react';
/**
* WordPress dependencies
*/
const { __, sprintf } = wp.i18n;
export default ({
currentStep,
serverType,
rulesMethod,
setRulesMethod,
setServerType,
rulesError,
smushData,
makeRequest,
}) => {
const stepsHeading = {
1: {
title: __('Choose Server Type', 'wp-smushit'),
description: __(
'Choose your server type. If you dont know this, please contact your hosting provider.',
'wp-smushit'
),
},
2: {
title: __('Add Rules', 'wp-smushit'),
description:
'apache' === serverType
? __(
'Smush can automatically apply WebP conversion rules for Apache servers by writing to your .htaccess file. Alternatively, switch to Manual to apply these rules yourself.',
'wp-smushit'
)
: __(
'The following configurations are for NGINX servers. If you do not have access to your NGINX config files you will need to contact your hosting provider to make these changes.',
'wp-smushit'
),
},
3: {
title: __('Finish Setup', 'wp-smushit'),
description: __(
'The rules have been applied successfully.',
'wp-smushit'
),
},
};
const getTopNotice = () => {
if (1 === currentStep && smushData.isS3Enabled) {
return (
<div className="sui-notice sui-notice-warning">
<div className="sui-notice-content">
<div className="sui-notice-message">
<span
className="sui-notice-icon sui-icon-info sui-md"
aria-hidden="true"
></span>
<p>
{__(
'We noticed the Amazon S3 Integration is enabled. Offloaded images will not be served in WebP format, but Smush will create local WebP copies of all images. If this is undesirable, you can quit the setup.',
'wp-smushit'
)}
</p>
</div>
</div>
</div>
);
}
if (2 === currentStep) {
const switchDirectConversionMethod = ( e ) => {
e.preventDefault();
WP_Smush.WebP.switchMethod( 'direct_conversion' );
};
const suggestionMessage = sprintf(
/* translators: 1: Opening <button> tag, 2: Closing button, 3: Opening support link, 4: Closing the link */
__( 'Please try the %1$sDirect Conversion%2$s method if you dont have server access, or %3$scontact support%4$s for further assistance.', 'wp-smushit' ),
'<button type="submit" style="text-decoration: none; color: #17A8E3; font-weight: 500; outline-color: transparent; outline-style: none; box-shadow: none;padding:0;margin:0;background:transparent;border:none;cursor:pointer;">',
'</button>',
'<a href="https://wpmudev.com/hub2/support/#get-support" target="_blank">',
'</a>'
);
return (
<div
role="alert"
className="sui-notice sui-notice-warning"
aria-live="assertive"
style={rulesError ? { display: 'block' } : {}}
>
{rulesError && (
<div className="sui-notice-content">
<div className="sui-notice-message">
<span
className="sui-notice-icon sui-icon-info sui-md"
aria-hidden="true"
></span>
<p
dangerouslySetInnerHTML={{
__html: rulesError,
}}
/>
<form onSubmit={ switchDirectConversionMethod }><p dangerouslySetInnerHTML={{ __html: suggestionMessage }} /></form>
</div>
</div>
)}
</div>
);
}
if (smushData.isWpmudevHost) {
const message = !smushData.isWhitelabel
? __(
'Since your site is hosted with WPMU DEV, we already have done the configurations steps for you. The only step for you would be to create WebP images below.',
'wp-smushit'
)
: __(
'WebP conversion is active and working well. Your hosting has automatically pre-configured the conversion for you. The only step for you would be to create WebP images below.',
'wp-smushit'
);
return (
<div className="sui-notice sui-notice-info">
<div className="sui-notice-content">
<div className="sui-notice-message">
<span
className="sui-notice-icon sui-icon-info sui-md"
aria-hidden="true"
></span>
<p>{message}</p>
</div>
</div>
</div>
);
}
};
const getStepContent = () => {
if (1 === currentStep) {
return (
<React.Fragment>
<div className="sui-box-selectors">
<ul>
<li>
<label
htmlFor="smush-wizard-server-type-apache"
className="sui-box-selector"
>
<input
id="smush-wizard-server-type-apache"
type="radio"
value="apache"
checked={'apache' === serverType}
onChange={(e) =>
setServerType(e.currentTarget.value)
}
/>
<span>{__('Apache', 'wp-smushit')}</span>
</label>
</li>
<li>
<label
htmlFor="smush-wizard-server-type-nginx"
className="sui-box-selector"
>
<input
id="smush-wizard-server-type-nginx"
type="radio"
value="nginx"
checked={'nginx' === serverType}
onChange={(e) =>
setServerType(e.currentTarget.value)
}
/>
<span>{__('NGINX', 'wp-smushit')}</span>
</label>
</li>
</ul>
</div>
<div className="sui-notice" style={{ textAlign: 'left' }}>
<div className="sui-notice-content">
<div className="sui-notice-message">
<span
className="sui-notice-icon sui-icon-info sui-md"
aria-hidden="true"
></span>
<p>
{sprintf(
/* translators: server type */
__(
"We've automatically detected your server type is %s. If this is incorrect, manually select your server type to generate the relevant rules and instructions.",
'wp-smushit'
),
'nginx' === smushData.detectedServer
? 'NGINX'
: 'Apache / LiteSpeed'
)}
</p>
</div>
</div>
</div>
</React.Fragment>
);
}
if (2 === currentStep) {
if ('nginx' === serverType) {
return (
<div className="smush-wizard-rules-wrapper">
<ol className="sui-description">
<li>
{__(
'Insert the following in the server context of your configuration file (usually found in /etc/nginx/sites-available). “The server context” refers to the part of the configuration that starts with “server {” and ends with the matching “}”.',
'wp-smushit'
)}
</li>
<li>
{__(
'Copy the generated code found below and paste it inside your http or server blocks.',
'wp-smushit'
)}
</li>
</ol>
<pre
className="sui-code-snippet"
style={{ marginLeft: '12px' }}
>
{smushData.nginxRules}
</pre>
<ol className="sui-description" start="3">
<li>{__('Reload NGINX.', 'wp-smushit')}</li>
</ol>
{!smushData.isWhitelabel && (
<p className="sui-description">
{__('Still having trouble?', 'wp_smushit')}{' '}
<a href={smushData.urls.support} target="_blank" rel="noreferrer">
{__('Get Support.', 'wp_smushit')}
</a>
</p>
)}
</div>
);
}
// TODO: The non-selected button isn't focusable this way. Why arrows don't workkkkkkk?
return (
<div className="sui-side-tabs sui-tabs">
<div role="tablist" className="sui-tabs-menu">
<button
type="button"
role="tab"
id="smush-tab-automatic"
className={
'sui-tab-item' +
('automatic' === rulesMethod ? ' active' : '')
}
aria-controls="smush-tab-content-automatic"
aria-selected={'automatic' === rulesMethod}
onClick={() => setRulesMethod('automatic')}
tabIndex={'automatic' === rulesMethod ? '0' : '-1'}
>
{__('Automatic', 'wp-smushit')}
</button>
<button
type="button"
role="tab"
id="smush-tab-manual"
className={
'sui-tab-item' +
('manual' === rulesMethod ? ' active' : '')
}
aria-controls="smush-tab-content-manual"
aria-selected={'manual' === rulesMethod}
onClick={() => setRulesMethod('manual')}
tabIndex={'manual' === rulesMethod ? '0' : '-1'}
>
{__('Manual', 'wp-smushit')}
</button>
</div>
<div className="sui-tabs-content">
<div
role="tabpanel"
tabIndex="0"
id="smush-tab-content-automatic"
className={
'sui-tab-content' +
('automatic' === rulesMethod ? ' active' : '')
}
aria-labelledby="smush-tab-automatic"
hidden={'automatic' !== rulesMethod}
>
<p
className="sui-description"
style={{ marginTop: '30px' }}
>
{__(
'Please note: Some servers have both Apache and NGINX software which may not begin serving WebP images after applying the .htaccess rules. If errors occur after applying the rules, we recommend adding NGINX rules manually.',
'wp-smushit'
)}
</p>
</div>
<div
role="tabpanel"
tabIndex="0"
id="smush-tab-content-manual"
className={
'sui-tab-content' +
('manual' === rulesMethod ? ' active' : '')
}
aria-labelledby="smush-tab-manual"
hidden={'manual' !== rulesMethod}
>
<p className="sui-description">
{__(
'If you are unable to get the automated method working, follow these steps:',
'wp-smushit'
)}
</p>
<div className="smush-wizard-rules-wrapper">
<ol className="sui-description">
<li>
{__(
'Copy the generated code below and paste it at the top of your .htaccess file (before any existing code) in the root directory.',
'wp-smushit'
)}
</li>
</ol>
<pre
className="sui-code-snippet"
style={{ marginLeft: '12px' }}
>
{smushData.apacheRules}
</pre>
<ol className="sui-description" start="2">
<li>
{__(
"Next, click Check Status button below to see if it's working.",
'wp-smushit'
)}
</li>
</ol>
<h5
className="sui-settings-label"
style={{
marginTop: '30px',
fontSize: '13px',
color: '#333333',
}}
>
{__('Troubleshooting', 'wp-smushit')}
</h5>
<p className="sui-description">
{__(
'If .htaccess does not work, and you have access to vhosts.conf or httpd.conf, try this:',
'wp-smushit'
)}
</p>
<ol className="sui-description">
<li>
{__(
'Look for your site in the file and find the line that starts with <Directory> - add the code above that line and into that section and save the file.',
'wp-smushit'
)}
</li>
<li>
{__('Reload Apache.', 'wp-smushit')}
</li>
<li>
{__(
"If you don't know where those files are, or you aren't able to reload Apache, you would need to consult with your hosting provider or a system administrator who has access to change the configuration of your server.",
'wp-smushit'
)}
</li>
</ol>
{!smushData.isWhitelabel && (
<p className="sui-description">
{__('Still having trouble?', 'wp_smushit')}{' '}
<a href={smushData.urls.support} target="_blank" rel="noreferrer">
{__('Get Support.', 'wp_smushit')}
</a>
</p>
)}
</div>
</div>
</div>
</div>
);
}
const hideWizard = (e) => {
e.preventDefault();
makeRequest('smush_toggle_webp_wizard').then(() => {
location.href = smushData.urls.bulkPage;
});
};
return (
<React.Fragment>
<p style={{ marginBottom: 0 }}><b>{__('Convert Images to WebP', 'wp-smushit')}</b></p>
<p className="sui-description" dangerouslySetInnerHTML={ { __html: smushData.thirdStepMsg } } />
{!smushData.isMultisite && (
<p>
<a href={smushData.urls.bulkPage} onClick={hideWizard}>
{__('Convert now', 'wp-smushit')}
</a>
</p>
)}
</React.Fragment>
);
};
const stepIndicatorText = sprintf(
/* translators: currentStep/totalSteps indicator */
__('Step %s', 'wp-smushit'),
currentStep + '/3'
);
return (
<div
className={`smush-wizard-steps-content-wrapper smush-wizard-step-${currentStep}`}
>
{getTopNotice()}
<div className="smush-wizard-steps-content">
<span className="smush-step-indicator">
{stepIndicatorText}
</span>
<h2>{stepsHeading[currentStep].title}</h2>
<p className="sui-description">
{stepsHeading[currentStep].description}
</p>
{getStepContent()}
</div>
</div>
);
};
@@ -0,0 +1,180 @@
/**
* External dependencies
*/
import React from 'react';
/**
* WordPress dependencies
*/
const { __ } = wp.i18n;
export default ({
currentStep,
setCurrentStep,
serverType,
rulesMethod,
setRulesError,
makeRequest,
}) => {
const genericRequestError = __(
'Something went wrong with the request.',
'wp-smushit'
);
const checkStatus = () => {
setRulesError(false);
makeRequest('smush_webp_get_status')
.then((res) => {
if (res.success) {
setCurrentStep(currentStep + 1);
} else {
setRulesError(res.data);
}
})
.catch(() => setRulesError(genericRequestError));
};
const applyRules = () => {
setRulesError(false);
makeRequest('smush_webp_apply_htaccess_rules')
.then((res) => {
if (res.success) {
return checkStatus();
}
setRulesError(res.data);
})
.catch(() => setRulesError(genericRequestError));
};
const hideWizard = (e) => {
e.currentTarget.classList.add(
'sui-button-onload',
'sui-button-onload-text'
);
makeRequest('smush_toggle_webp_wizard').then(() => location.reload());
};
// Markup stuff.
let buttonsLeft;
const quitButton = (
<button
type="button"
className="sui-button sui-button-ghost"
onClick={hideWizard}
>
<span className="sui-loading-text">
<span className="sui-icon-logout" aria-hidden="true"></span>
<span className="sui-hidden-xs">
{__('Quit setup', 'wp-smushit')}
</span>
<span className="sui-hidden-sm sui-hidden-md sui-hidden-lg">
{__('Quit', 'wp-smushit')}
</span>
</span>
<span
className="sui-icon-loader sui-loading"
aria-hidden="true"
></span>
</button>
);
if (1 !== currentStep) {
buttonsLeft = (
<button
type="button"
className="sui-button sui-button-compound sui-button-ghost"
onClick={() => setCurrentStep(currentStep - 1)}
>
<span className="sui-compound-desktop" aria-hidden="true">
<span className="sui-icon-arrow-left"></span>
{__('Previous', 'wp-smushit')}
</span>
<span className="sui-compound-mobile" aria-hidden="true">
<span className="sui-icon-arrow-left"></span>
</span>
<span className="sui-screen-reader-text">
{__('Previous', 'wp-smushit')}
</span>
</button>
);
}
const getButtonsRight = () => {
if (1 === currentStep) {
return (
<button
type="button"
className="sui-button sui-button-blue sui-button-icon-right"
onClick={() => setCurrentStep(currentStep + 1)}
>
{__('Next', 'wp-smushit')}
<span
className="sui-icon-arrow-right"
aria-hidden="true"
></span>
</button>
);
}
if (2 === currentStep) {
if ('apache' === serverType && 'automatic' === rulesMethod) {
return (
<button
type="button"
className="sui-button sui-button-blue"
onClick={applyRules}
>
{__('Apply rules', 'wp-smushit')}
</button>
);
}
return (
<button
type="button"
className="sui-button sui-button-blue"
onClick={checkStatus}
>
{__('Check status', 'wp-smushit')}
</button>
);
}
return (
<button
type="button"
className="sui-button sui-button-blue"
onClick={hideWizard}
>
<span className="sui-button-text-default">
{__('Finish', 'wp-smushit')}
</span>
<span className="sui-button-text-onload">
<span
className="sui-icon-loader sui-loading"
aria-hidden="true"
></span>
{__('Finishing setup…', 'wp-smushit')}
</span>
</button>
);
};
return (
<div className="sui-box-footer">
<div className="sui-actions-left">
{quitButton}
{buttonsLeft}
</div>
<div className="sui-actions-right">{getButtonsRight()}</div>
</div>
);
};
@@ -0,0 +1,114 @@
/**
* External dependencies
*/
import React from 'react';
/**
* WordPress dependencies
*/
const { __ } = wp.i18n;
export default ({ currentStep, smushData }) => {
const getStepClass = (step) => {
const stepClass = 'smush-wizard-bar-step';
if (!smushData.isPro) {
return stepClass + ' disabled';
}
if (step > currentStep) {
return stepClass;
}
return (
stepClass +
(step === currentStep ? ' current' : ' sui-tooltip done')
);
};
const getStepNumber = (step) => {
return currentStep > step ? (
<span className="sui-icon-check" aria-hidden="true"></span>
) : (
step
);
};
const steps = [
{ number: 1, title: __('Server Type', 'wp-smushit') },
{ number: 2, title: __('Add Rules', 'wp-smushit') },
{ number: 3, title: __('Finish Setup', 'wp-smushit') },
];
return (
<div className="sui-sidenav">
<span className="smush-wizard-bar-subtitle">
{__('Setup', 'wp-smushit')}
</span>
<div className="smush-sidenav-title">
<h4>{__('Local WebP', 'wp-smushit')}</h4>
{!smushData.isPro && (
<span className="sui-tag sui-tag-pro">
{__('Pro', 'wp-smushit')}
</span>
)}
</div>
<div className="smush-wizard-steps-container">
<svg
className="smush-svg-mobile"
focusable="false"
aria-hidden="true"
>
<line
x1="0"
x2="50%"
stroke={1 !== currentStep ? '#1ABC9C' : '#E6E6E6'}
/>
<line
x1="50%"
x2="100%"
stroke={3 === currentStep ? '#1ABC9C' : '#E6E6E6'}
/>
</svg>
<ul>
{steps.map((step) => (
<React.Fragment key={step.number}>
<li
className={getStepClass(step.number)}
data-tooltip={__(
'This stage is already completed.',
'wp-smushit'
)}
>
<div className="smush-wizard-bar-step-number">
{getStepNumber(step.number)}
</div>
{step.title}
</li>
{3 !== step.number && (
<svg
data={step.number}
data2={currentStep}
className="smush-svg-desktop"
focusable="false"
aria-hidden="true"
>
<line
y1="0"
y2="40px"
stroke={
step.number < currentStep
? '#1ABC9C'
: '#E6E6E6'
}
/>
</svg>
)}
</React.Fragment>
))}
</ul>
</div>
</div>
);
};
@@ -0,0 +1,50 @@
@include body-class(true) {
&.sui-color-accessible {
.smush-final-log .smush-bulk-error-row {
box-shadow: inset 2px 0 0 0 $accessible-dark;
.smush-bulk-image-data:before {
color: $accessible-dark;
}
}
// Bulk Smush Fancy Tree
ul.fancytree-container {
.fancytree-selected {
background-color: #F8F8F8;
span.fancytree-checkbox {
border: 1px solid $accessible-dark;
background-color: $accessible-dark;
}
}
span.fancytree-expander:before,
span.fancytree-icon:before,
span.fancytree-title {
color: $accessible-dark;
}
}
// CDN
.smush-filename-extension {
background-color: $accessible-dark;
}
// Check images button.
.sui-button {
&.smush-button-check-success:before {
color: $accessible-light;
}
}
// Smush submit note.
.smush-submit-note {
color: $accessible-dark;
}
// Hightlight lazyload spinner.
.sui-lazyload .sui-box-selector [name="animation[spinner-icon]"]:checked+span {
background-color: rgba(220,220,222, 0.7)!important;
}
}
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.sui-wrap .sui-toggle-slider {
-ms-high-contrast-adjust: none;
}
}
@@ -0,0 +1,59 @@
// This needs to be here.
@import "modules/variables";
// Share UI styles
@import "~@wpmudev/shared-ui/scss/functions";
@import "~@wpmudev/shared-ui/scss/colors";
@import "~@wpmudev/shared-ui/scss/variables";
$google-fonts-url: 'https://fonts.bunny.net/css?family=Roboto:400,500,700';
@import "~@wpmudev/shared-ui/scss/mixins";
@import "~@wpmudev/shared-ui/scss/accessibility";
@import "~@wpmudev/shared-ui/scss/animations";
@import "~@wpmudev/shared-ui/scss/typography";
@import "~@wpmudev/shared-ui/scss/icons";
@import "~@wpmudev/shared-ui/scss/buttons";
@import "~@wpmudev/shared-ui/scss/toggles";
@import "~@wpmudev/shared-ui/scss/boxes";
@import "~@wpmudev/shared-ui/scss/box-settings";
@import "~@wpmudev/shared-ui/scss/layout";
@import "~@wpmudev/shared-ui/scss/notifications";
@import "~@wpmudev/shared-ui/scss/header";
@import "~@wpmudev/shared-ui/scss/summary";
@import "~@wpmudev/shared-ui/scss/list";
@import "~@wpmudev/shared-ui/scss/tooltips";
@import "~@wpmudev/shared-ui/scss/select2";
@import "~@wpmudev/shared-ui/scss/tags";
@import "~@wpmudev/shared-ui/scss/forms";
@import "~@wpmudev/shared-ui/scss/radio-checkbox";
@import "~@wpmudev/shared-ui/scss/tabs";
@import "~@wpmudev/shared-ui/scss/sidenav";
@import "~@wpmudev/shared-ui/scss/dropdowns";
@import "~@wpmudev/shared-ui/scss/scores";
@import "~@wpmudev/shared-ui/scss/footer";
@import "~@wpmudev/shared-ui/scss/progress-bars";
@import "~@wpmudev/shared-ui/scss/modals";
@import "~@wpmudev/shared-ui/scss/utility";
@import "~@wpmudev/shared-ui/scss/wp-admin-notices";
@import "~@wpmudev/shared-ui/scss/tables";
@import "~@wpmudev/shared-ui/scss/accordions";
// Used on lazy loading page (since 3.2.2).
@import "~@wpmudev/shared-ui/scss/box-selectors";
@import "~@wpmudev/shared-ui/scss/upload";
@import "~@wpmudev/shared-ui/scss/_colorpickers.scss";
// Upgrade page (since 3.2.3).
@import "~@wpmudev/shared-ui/scss/upgrade-page";
@import "~@wpmudev/shared-ui/scss/reviews";
// Used on WebP page (since 3.8.0).
@import "~@wpmudev/shared-ui/scss/_code-snippet.scss";
// Upsells (since 3.9.1).
@import "~@wpmudev/shared-ui/scss/upsells";
// App styles
@import "modules/admin";
@import "modules/directory-smush";
@import "modules/cdn";
@import "modules/webp";
// SUI Color Accessibility
@import "~@wpmudev/shared-ui/scss/color-accessibility";
@import "accessibility/color-accessibility";
@@ -0,0 +1,603 @@
/**
* Common styles that are used on all the WP pages in the backend
*/
@import "modules/media";
.sui-wrap .smush-upsell-link,
.sui-wrap a.smush-upsell-link {
color: $purple;
> span:before {
color: $purple;
}
&:hover:not(.sui-button),
&:focus:not(.sui-button),
&:active:not(.sui-button) {
color: #64007e;
> span:before {
color: #64007e;
}
}
}
/**
* Media details (grid layout)
* @since 3.4.0
*/
.attachment-info .smush-stats .value {
display: flex;
flex-wrap: wrap;
.smush-status {
margin: 0 0 10px;
flex-basis: 100%;
font-size: 12px;
line-height: 1.33333;
}
.smush-status-links {
width: 100%;
}
span.sui-tooltip {
float: none;
}
a {
margin-left: 5px;
}
a:first-of-type {
margin-left: 0;
margin-right: 5px;
}
}
.attachment-info .smush-status-links,
.column-smushit .smush-status-links {
color: #ddd;
}
.column-smushit .smush-status-links > a {
box-shadow: none;
outline: none;
}
.wp-smush-progress {
padding-left: 25px;
margin: 0;
background-size: 17px 17px;
visibility: visible;
vertical-align: initial !important; /* prevent from jumping on a line */
display: inline;
color: #32373c;
cursor: default;
}
// Fix grid view links.
.attachment-details .setting span.wp-smush-progress {
width: auto;
line-height: 0;
margin-right: 5px;
}
/** Settings Page **/
.smush-status.fail {
color: #dd3d36;
}
.smush-status.success {
color: #0074a2;
}
.smush-status.error {
color: red;
}
#wpbody-content .wp-smush-error {
color: red;
}
.wp-smush-action[disabled] {
opacity: 0.6;
}
#post-body-content .smush-status {
margin: 4px 0;
}
.attachment-info .wp-smush-error-message {
margin: 0 0 1em;
}
.smush-stats-wrapper .row {
padding: 8px 0;
}
.smush-stats-wrapper .row:first-child {
padding-top: 0;
}
.smush-stats-wrapper td, .smush-stats-wrapper th {
font-size: 11px;
}
.smush-skipped .dashicons-editor-help {
margin-top: -2px;
margin-left: 5px;
}
.smush-skipped {
a:focus { box-shadow: 0 0 black; }
.sui-tag.sui-tag-purple {
min-height: 18px;
padding: 2px 10px;
font-size: 10px;
line-height: 12px;
font-weight: 700;
background-color: #8d00b1;
color: #fff;
border: 2px solid transparent;
border-radius: 13px;
}
}
/** Help Tip **/
.ui-tooltip-content {
font-size: 12px;
}
/** All Smushed **/
.wp-smush-notice {
background-color: #D1F1EA;
border-radius: 5px;
color: #333333;
font-family: 'Roboto', sans-serif;
font-size: 15px;
line-height: 30px;
margin-bottom: 30px;
padding: 15px 30px;
letter-spacing: -0.015em;
}
div.smush-notice-cta a.smush-notice-act.button-primary {
padding: 3px 23px;
background-color: #00B0DB;
box-shadow: none;
border-radius: 4px;
border: none;
text-shadow: none;
font-weight: normal;
-webkit-font-smoothing: antialiased;
&:hover {
border: none;
}
}
a.wp-smush-resize-enable:hover,
a.wp-smush-lossy-enable:hover {
color: #0A9BD6;
}
.wp-smush-bulk-wrapper {
#wp-smush-bulk-image-count {
color: #333333;
font-size: 28px;
line-height: 40px;
letter-spacing: -0.5px;
font-weight: 600;
}
#wp-smush-bulk-image-count-description {
color: #333333;
font-size: 13px;
margin-top: 0;
margin-bottom: 10px;
}
.sui-tooltip,
.sui-tooltip > .sui-icon-info {
vertical-align: top;
}
}
/** Image Remaining **/
div.wp-smush-dir-limit,
div.smush-s3-setup-message {
background-color: #FFF5D5;
border: none;
color: #333333;
line-height: 30px;
font-size: 15px;
letter-spacing: -0.015em;
}
div.smush-s3-setup-message {
background-color: #DFF6FA;
}
div.wp-smush-dir-limit {
background-color: #dff6fa;
}
.wp-smush-count {
color: #888888;
font-size: 13px;
line-height: 1.5;
margin-top: 15px;
}
/** Stats Container **/
a.wp-smush-lossy-enable {
cursor: pointer;
}
/** Re Smush **/
.wp-smush-settings-changed {
background: #dff6fa;
border-radius: 5px;
font-size: 13px;
line-height: 1.7;
padding: 20px;
}
.compat-item .compat-field-wp_smush {
display: table-row;
}
.manage-column.column-smushit {
width: 265px;
}
.smushit [tooltip],
label.setting.smush-stats [tooltip],
.compat-field-wp_smush [tooltip] {
position: relative;
overflow: visible;
}
.smushit [tooltip]:before,
label.setting.smush-stats [tooltip]:before,
.compat-field-wp_smush [tooltip]:before {
content: '';
position: absolute;
border: 5px solid transparent;
border-top-color: #0B2F3F;
bottom: 100%;
left: 50%;
margin-left: -5px;
margin-bottom: -5px;
opacity: 0;
z-index: -1;
transition: margin .2s, opacity .2s, z-index .2s linear .2s;
pointer-events: none;
}
.smushit [tooltip]:after,
label.setting.smush-stats [tooltip]:after,
.compat-field-wp_smush [tooltip]:after {
background: #0B2F3F;
border-radius: 4px;
bottom: 100%;
color: #FFF;
content: attr(tooltip);
font-size: 13px;
font-weight: 400;
left: 50%;
line-height: 20px;
margin-left: -100px;
margin-bottom: 5px;
opacity: 0;
padding: 5px;
pointer-events: none;
position: absolute;
width: 180px;
text-align: center;
transition: margin .2s, opacity .2s, z-index .2s linear .2s;
white-space: pre-wrap;
z-index: -1;
}
.smushit .smush-skipped [tooltip]:before,
label.setting.smush-stats .smush-skipped [tooltip]:before,
.compat-field-wp_smush .smush-skipped [tooltip]:before {
border-top-color: transparent;
border-left-color: #0B2F3F;
bottom: 0;
left: 0;
}
.smushit .smush-skipped [tooltip]:after,
label.setting.smush-stats .smush-skipped [tooltip]:after,
.compat-field-wp_smush .smush-skipped [tooltip]:after {
margin-left: 0;
left: -195px;
top: -35px;
bottom: inherit;
margin-bottom: 5px;
}
label.setting.smush-stats .smush-skipped [tooltip]:after {
top: -98px;
}
div.media-sidebar label.setting.smush-stats .smush-skipped [tooltip]:after {
left: -188px;
padding-left: 10px;
width: 170px;
}
div.media-sidebar label.setting.smush-stats .smush-skipped [tooltip]:before {
margin-left: -3px;
}
.smushit [tooltip].tooltip-s:after,
label.setting.smush-stats [tooltip].tooltip-s:after,
.compat-field-wp_smush [tooltip].tooltip-s:after {
width: 150px;
margin-left: -75px;
}
.smushit [tooltip].tooltip-l:after,
label.setting.smush-stats [tooltip].tooltip-l:after,
.compat-field-wp_smush [tooltip].tooltip-l:after {
width: 280px;
margin-left: -140px;
}
.smushit [tooltip].tooltip-right:after, .compat-field-wp_smush [tooltip].tooltip-right:after {
margin-left: -180px;
}
.smushit [tooltip].tooltip-s.tooltip-right:after, .compat-field-wp_smush [tooltip].tooltip-s.tooltip-right:after {
margin-left: -130px;
}
.smushit [tooltip].tooltip-l.tooltip-right:after, .compat-field-wp_smush [tooltip].tooltip-l.tooltip-right:after {
margin-left: -260px;
}
.smushit [tooltip].tooltip-bottom:before, .compat-field-wp_smush [tooltip].tooltip-bottom:before {
border-color: transparent;
border-bottom-color: #0B2F3F;
top: 100%;
bottom: auto;
margin-top: -5px;
margin-bottom: 0;
}
.smushit [tooltip].tooltip-bottom:after, .compat-field-wp_smush [tooltip].tooltip-bottom:after {
bottom: auto;
top: 100%;
margin-top: 5px;
margin-bottom: 0;
}
.smushit [tooltip]:hover:before,
label.setting.smush-stats [tooltip]:hover:before,
.compat-field-wp_smush [tooltip]:hover:before {
z-index: 1;
margin-bottom: 0;
opacity: 1;
transition: margin .2s, opacity .2s;
}
.smushit [tooltip]:hover:after,
label.setting.smush-stats [tooltip]:hover:after,
.compat-field-wp_smush [tooltip]:hover:after {
opacity: 1;
z-index: 1;
margin-bottom: 10px;
transition: margin .2s, opacity .2s;
}
.smushit .disabled[tooltip]:before,
.smushit .disabled[tooltip]:after,
label.setting.smush-stats .disabled[tooltip]:before,
label.setting.smush-stats .disabled[tooltip]:after,
.compat-field-wp_smush .disabled[tooltip]:before,
.compat-field-wp_smush .disabled[tooltip]:after {
display: none;
}
/** Image List **/
div.wp-smush-scan-result {
background: white;
div.wp-smush-notice {
margin-top: 14px;
padding: 15px 30px;
}
div.content {
overflow: hidden;
width: 100%;
}
}
div.wp-smush-info.notice {
font-size: 15px;
letter-spacing: -0.015em;
margin: 0 0 30px;
padding: 15px;
}
/** Media Queries **/
@media screen and (max-width: 1024px) and (min-width: 800px) {
/** Stats Section **/
.smush-stats-wrapper h3 {
padding: 6px 0;
}
}
/** Media Queries for resolution below 782px **/
@media only screen and (max-width: 800px) {
.dev-box.bulk-smush-wrapper.wp-smush-container {
padding: 20px 10px;
}
}
/**
* CSS styles used Admin notice
*/
.smush-notice.notice {
padding: 0;
margin: 5px 0 10px;
border: 1px solid #E5E5E5;
background: #FFF;
overflow: hidden;
-webkit-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.05);
position: relative;
z-index: 1;
min-height: 80px;
display: table; /* The magic ingredient! */
font: 13px "Roboto", sans-serif;
}
.smush-notice.notice.loading:before {
content: attr(data-message);
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
z-index: 5;
text-align: center;
line-height: 80px;
font-size: 22px;
font-weight: bold;
}
.smush-notice > div {
display: table-cell; /* The magic ingredient! */
vertical-align: middle;
cursor: default;
line-height: 1.5;
}
.smush-notice.notice.loading > div {
-webkit-filter: blur(2px);
-moz-filter: blur(2px);
-o-filter: blur(2px);
-ms-filter: blur(2px);
filter: blur(2px);
}
.smush-notice-logo {
padding-left: 30px;
}
.smush-notice-message {
color: #23282D;
font-size: 13px;
font-weight: normal;
line-height: 20px;
padding: 20px;
-webkit-font-smoothing: antialiased;
width: 100%;
}
.smush-notice-cta {
background: #F8F8F8;
padding: 0 30px;
position: relative;
white-space: nowrap;
}
.wp-core-ui .smush-notice-cta button,
.wp-core-ui .smush-notice-cta .button-primary:active {
vertical-align: middle;
}
.wp-core-ui .smush-notice-cta input[type="email"] {
vertical-align: middle;
line-height: 20px;
margin: 0;
min-width: 50px;
max-width: 320px;
text-align: center;
padding-left: 0;
padding-right: 0;
}
/**
* Upsell lists.
* @since 3.9.1
*/
#smush-box-cdn-upsell,
#smush-box-webp-wizard,
#smush-box-next-gen-upsell .smush-box-next-gen-upsell {
.sui-upsell-list {
max-width: 495px;
text-align: left;
margin: 0 auto;
li {
font-size:12px;
border-bottom:1px solid #f2f2f2;
padding-bottom: 10px;
margin-bottom: 15px;
letter-spacing: -0.23px;
&:last-child {
border-bottom: none;
}
}
}
}
#smush-box-cdn-upsell .sui-upsell-list {
max-width:350px;
}
@media only all and (max-width: 1000px) {
.smush-notice.notice {
display: block;
font-size: 13px;
}
.smush-notice > .smush-notice-logo {
float: left;
display: inline-block;
height: 80px;
margin: 10px;
border-radius: 4px;
}
.smush-notice > .smush-notice-message {
width: auto;
display: block;
min-height: 80px;
}
.smush-notice > .smush-notice-cta {
display: block;
border-top: 1px solid #E5E5E5;
border-left: 0;
text-align: center;
white-space: normal;
line-height: 30px;
padding: 10px 20px;
}
.wp-core-ui .smush-notice > .smush-notice-cta > input[type="email"],
.smush-notice > .smush-notice-cta > button {
font-size: 14px;
}
}
@media only all and (max-width: 500px) {
.wp-core-ui .smush-notice > .smush-notice-cta > input[type="email"],
.smush-notice > .smush-notice-cta > button {
display: block;
width: 100% !important;
max-width: none;
margin-bottom: 4px;
font-size: 16px;
height: 34px;
}
}
@@ -0,0 +1,68 @@
@import "variables";
/**
* CDN styles
*
* @since 3.0
*/
@include body-class {
.sui-wrap {
.sui-box-settings-row .sui-box-settings-col-1 {
vertical-align: top;
}
&.wrap-smush-cdn {
.sui-box-header .sui-actions-right .sui-icon-info{
font-size: 16px;
position: relative;
top: 1.5px;
}
}
.sui-cdn {
form p:first-of-type {
margin-top: 0;
}
}
.wp-smush-stats {
display: flex;
align-items: center;
line-height: 0;
.sui-tooltip {
line-height: 10px;
margin-right: 10px;
}
}
/* Filename Extensions Icons */
.smush-filename-extension {
border-radius: 4px;
display: inline-block;
font-size: 9px;
font-weight: 600;
color: #fff;
text-transform: uppercase;
text-align: center;
line-height: 43px;
height: 30px;
margin: 0 5px 0 0;
width: 30px;
&.smush-extension-jpeg,
&.smush-extension-jpg { background-color: #F7E100; }
&.smush-extension-png { background-color: #FFB694; }
&.smush-extension-gif { background-color: #72D5D4; }
&.smush-extension-webp { background-color: #72ADD5; }
&.smush-extension-svg { background-color: #88D572; }
&.smush-extension-iframe {
background-color: #8772D5;
font-size: 7px;
}
}
}
}
@@ -0,0 +1,338 @@
/* ****************************************************************************
* MODULE: Directory Smush styles.
*/
@include body-class {
.wp-smush-progress-dialog,
.wp-smush-list-dialog {
text-align: left;
}
.sui-directory.sui-message {
text-align: left;
.sui-message-content {
text-align: center;
}
}
.sui-directory .smush-final-log {
margin-top: 30px;
.sui-description {
margin-top: 10px;
}
}
ul.fancytree-container {
color: #666;
font-family: "Roboto", sans-serif;
font-size: 13px;
font-weight: 500;
letter-spacing: -0.25px;
line-height: 40px;
padding: 0;
margin: 0;
outline: 0 solid transparent;
min-height: 0%;
position: relative;
ul {
padding: 0 0 0 16px;
margin: 0;
display: block;
}
/*------------------------------------------------------------------------------
* Expander icon
*
* Note: IE6 doesn't correctly evaluate multiples class names,
* so we create combined class names that can be used in the CSS.
*
* Prefix: fancytree-exp-
* 1st character: 'e': expanded, 'c': collapsed, 'n': no children
* 2nd character (optional): 'd': lazy (Delayed)
* 3rd character (optional): 'l': Last sibling
*----------------------------------------------------------------------------*/
span.fancytree-expander {
cursor: pointer;
font-size: 12px;
margin-left: 13px;
width: 15px;
&:before {
font-family: wpmudev-plugin-icons, sans-serif;
}
}
.fancytree-exp-c span.fancytree-expander,
.fancytree-exp-cd:not(.fancytree-unselectable) span.fancytree-expander,
.fancytree-exp-cf:not(.fancytree-unselectable) span.fancytree-expander {
margin-left: 13px;
}
// --- End nodes (use connectors instead of expanders)
.fancytree-expanded.fancytree-exp-n span.fancytree-expander,
.fancytree-expanded.fancytree-exp-nl span.fancytree-expander {
width: 13px;
display: inline-block;
&:before {
background-image: none;
cursor: default;
}
}
.fancytree-exp-n span.fancytree-expander,
.fancytree-exp-nl span.fancytree-expander {
width: 12px;
display: inline-block;
}
.fancytree-exp-nl span.fancytree-expander:before {
content: "\131";
cursor: default;
}
span.fancytree-ico-c span.fancytree-expander:before {
content: '';
cursor: default;
}
// --- Collapsed
.fancytree-exp-c span.fancytree-expander:before,
.fancytree-exp-cl span.fancytree-expander:before,
.fancytree-exp-cd span.fancytree-expander:before,
.fancytree-exp-cdl span.fancytree-expander:before,
.fancytree-exp-e span.fancytree-expander:before,
.fancytree-exp-ed span.fancytree-expander:before,
.fancytree-exp-el span.fancytree-expander:before,
.fancytree-exp-edl span.fancytree-expander:before {
color: #888888;
content: "\2DC";
}
// --- Expanded
.fancytree-exp-e span.fancytree-expander:before,
.fancytree-exp-ed span.fancytree-expander:before,
.fancytree-exp-el span.fancytree-expander:before,
.fancytree-exp-edl span.fancytree-expander:before {
content: "\131";
}
// --- Unselectable
.fancytree-unselectable span.fancytree-expander:before {
content: "9";
}
/* Fade out expanders, when container is not hovered or active */
.fancytree-fade-expander {
span.fancytree-expander:before {
transition: opacity 1.5s;
opacity: 0;
}
&:hover span.fancytree-expander:before,
&.fancytree-treefocus span.fancytree-expander:before,
.fancytree-treefocus span.fancytree-expander:before,
[class*='fancytree-statusnode-'] span.fancytree-expander:before {
transition: opacity 0.6s;
opacity: 1;
}
}
/*------------------------------------------------------------------------------
* Checkbox icon
*----------------------------------------------------------------------------*/
span.fancytree-checkbox {
margin-right: 5px;
margin-left: 12px;
border-radius: 3px;
border: 1px solid #ddd;
background-color: #e6e6e6;
display: inline-block;
width: 16px;
height: 16px;
top: 2px;
position: relative;
transition: .2s;
@include icon( before, check );
&:before {
opacity: 0;
color: #fff;
font-size: 10px;
line-height: 14px;
position: absolute;
width: 100%;
text-align: center;
transition: .2s;
}
}
.fancytree-selected span.fancytree-checkbox {
border: 1px solid #17a8e3;
background-color: #17a8e3;
&:before {
opacity: 1;
}
}
.fancytree-expanded span.fancytree-checkbox {
margin-left: 11px;
}
// Unselectable is dimmed, without hover effects
.fancytree-unselectable {
background-color: transparent !important;
// Fix for bug in library.
&.fancytree-selected {
margin-left: -9px;
span.fancytree-expander {
margin-left: 10px;
}
span.fancytree-checkbox {
border: 1px solid #ddd;
background-color: #e6e6e6;
&:before {
color: #e6e6e6 !important;
}
}
span.fancytree-title {
color: #666;
}
}
span.fancytree-expander,
span.fancytree-icon,
span.fancytree-checkbox,
span.fancytree-title {
opacity: 0.4;
filter: alpha(opacity=40);
&:before {
color: #666 !important;
}
}
}
/*------------------------------------------------------------------------------
* Node type icon
* Note: IE6 doesn't correctly evaluate multiples class names,
* so we create combined class names that can be used in the CSS.
*
* Prefix: fancytree-ico-
* 1st character: 'e': expanded, 'c': collapsed
* 2nd character (optional): 'f': folder
*----------------------------------------------------------------------------*/
span.fancytree-icon:before { // Default icon
margin-left: 10px;
font-family: wpmudev-plugin-icons, sans-serif;
font-size: 16px;
color: #AAA;
content: 'D';
position: relative;
top: 1px;
}
/* Documents */
.fancytree-ico-c span.fancytree-icon:before { // Collapsed folder (empty)
content: 'D';
}
.fancytree-has-children.fancytree-ico-c span.fancytree-icon:before { // Collapsed folder (not empty)
content: 'D';
}
.fancytree-ico-e span.fancytree-icon:before { // Expanded folder
content: '\BB';
}
/* Folders */
.fancytree-exp-n.fancytree-ico-ef span.fancytree-icon:before,
.fancytree-exp-nl.fancytree-ico-ef span.fancytree-icon:before,
.fancytree-ico-cf span.fancytree-icon:before { // Collapsed folder (empty)
content: '\2D8';
}
.fancytree-has-children.fancytree-ico-cf span.fancytree-icon:before { // Collapsed folder (not empty)
content: '\2D8';
}
.fancytree-ico-ef span.fancytree-icon:before { // Expanded folder
content: '\BB';
}
// 'Loading' status overrides all others
.fancytree-loading span.fancytree-expander:before,
.fancytree-statusnode-loading span.fancytree-icon:before {
content: 'N';
display: inline-block;
animation: spin 1.3s linear infinite;
}
/*------------------------------------------------------------------------------
* Node titles and highlighting
*----------------------------------------------------------------------------*/
span.fancytree-node {
display: inherit;
width: 100%;
margin-top: 5px;
min-height: 40px;
&:not(.fancytree-unselectable):hover {
background-color: #F8F8F8;
}
}
span.fancytree-title {
color: #666; // inherit doesn't work on IE
cursor: pointer;
display: inline-block; // Better alignment, when title contains <br>
vertical-align: top;
min-height: 20px;
padding: 0 3px 0 3px; // Otherwise italic font will be outside right bounds
margin: 0 0 0 5px;
border: 1px solid transparent; // avoid jumping, when a border is added on hover
border-radius: 4px;
font-weight: 500;
}
span.fancytree-node.fancytree-error span.fancytree-title {
//color: @fancy-font-error-color;
}
span.fancytree-expanded,
span.fancytree-selected {
border-radius: 4px;
background-color: #F8F8F8;
color: #17A8E3;
span.fancytree-title {
color: #666666;
}
}
span.fancytree-selected {
background-color: #E1F6FF;
span.fancytree-expander:before,
span.fancytree-icon:before,
span.fancytree-title {
color: #17A8E3;
}
}
span.fancytree-focused {
background-color: #e1e1e1 !important;
}
}
}
@@ -0,0 +1,316 @@
/* ****************************************************************************
* MEDIA AREA SCSS FILE
*/
@import "~@wpmudev/shared-ui/scss/functions";
@import "~@wpmudev/shared-ui/scss/colors";
@import "~@wpmudev/shared-ui/scss/variables";
// Override body class
$sui-version: 'smush-media';
$sui-wrap-class: false;
@import "~@wpmudev/shared-ui/scss/mixins";
@import "~@wpmudev/shared-ui/scss/tooltips";
@import "~@wpmudev/shared-ui/scss/notifications";
@import "~@wpmudev/shared-ui/scss/buttons";
/* ****************************************************************************
* MEDIA AREA STYLES
*/
// Set column width.
.manage-column.column-smushit {
width: 260px;
}
// Margin for buttons.
.sui-smush-media {
.button {
&:last-of-type {
margin-right: 0;
}
}
&.smush-status-links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
.smush-stats-details {
height: 24px;
span {
&.stats-toggle {
color: #666666;
font-size: 18px;
margin-left: 4px;
font-weight: 500;
&::before {
content: '+';
}
}
}
&.smush-stats-expanded {
.stats-toggle::before {
content: '-';
}
&.sui-tooltip[data-tooltip] {
&::after,
&::before {
display: none;
}
}
}
}
&:has(.wp-smush-restore) {
.smush-stats-details +,
.wp-smush-resmush + {
.smush-ignore-image {
margin-right: 30px;
}
}
}
}
}
td {
&.smushit {
&.column-smushit {
padding: 8px 0px;
}
}
}
// Smush button loading icon.
#ngg-listimages,
.column-smushit {
.spinner {
float: none;
&.visible {
visibility: visible;
}
}
}
.smush-status-links{
.smush-upgrade-link {
color: #8D00B1;
font-size: 12px;
}
.smush-ignore-utm,.smush-revert-utm{
display: block;
margin: 6px 0 4px;
}
a {
text-decoration: none;
}
span {
float: none !important;;
}
.smush-cdn-notice {
color: #50575E;
a {
color:#2271B1;
&:focus {
box-shadow: none;
opacity: 0.7;
}
}
}
}
.smush-status {
&.smush-warning,&.smush-ignored,&.smush-success{
padding-left:17px;
position: relative;
&:before{
content:"";
background: url('../images/icon-warning.png' ) no-repeat 0 0;
position: absolute;
width:12px;
height:12px;
background-size: contain;
left: 0;
top:3px;
}
}
&.smush-ignored{
&:before{
background-image: url('../images/icon-ignored.png' ) !important;
}
}
&.smush-success{
&:before{
background-image: url('../images/icon-success.png' ) !important;
}
}
.sui-icon-warning-media-lib {
margin-right:4px;
position:relative;
top:1px;
}
}
.column-smushit .smush-status{
color:#50575E;
}
// Stats table.
.sui-smush-media {
table.wp-smush-stats-holder {
width: 100%;
border: 1px solid #E6E6E6;
border-radius: 4px;
margin-top: 6px;
border-collapse: collapse;
border-spacing: 0;
thead {
th.smush-stats-header {
padding: 8px 10px;
border-bottom: 1px solid #E6E6E6 !important;
color: #32373D;
font-size: 12px;
font-weight: bold;
letter-spacing: -0.23px;
line-height: 16px;
text-align: left;
}
}
tr {
border: 1px solid #E6E6E6;
}
td {
overflow-wrap: break-word;
vertical-align: middle;
padding: 8px 10px;
color: #555555;
font-size: 11px;
letter-spacing: -0.21px;
line-height: 16px;
border-bottom: 1px solid #E6E6E6;
&:first-of-type {
max-width: 110px;
font-weight: 500;
}
}
}
}
// Override !important set from WordPress.
#the-list {
.sui-smush-media {
thead {
th.smush-stats-header {
border-bottom: 1px solid #E6E6E6 !important;
}
}
}
}
// Responsive table for list mode.
@media screen and (max-width: 1024px) {
.wp-list-table .smushit {
table.wp-smush-stats-holder {
th {
display: table-cell;
box-sizing: border-box;
}
tr td {
word-wrap: break-word;
display: table-cell !important;
&:first-child {
border-right: none;
box-sizing: border-box;
}
&:last-child {
box-sizing: border-box;
float: none;
overflow: visible;
}
}
}
}
}
// NextGen Integration.
.iedit .wp-smush-action,
.iedit .smush-stats-details {
font-size: 11px;
}
/*NextGen Gallery stats*/
#ngg-listimages {
table.wp-smush-stats-holder {
table-layout: fixed;
border: 1px solid lightgray;
border-collapse: collapse;
width: 100%;
td,
th {
border: 1px solid #CECECE;
}
}
.column-7 {
width: 300px;
}
.spinner {
width: auto;
padding-left: 30px;
}
}
/** NextGen Gallery tr height, to show the progress bar properly for alternate rows **/
.alternate.iedit {
height: 120px;
}
/** Allows to click on button, otherwise row-actions from NextGen interferes **/
.wp-smush-nextgen-send {
position: relative;
z-index: 2;
}
.smush-hub-connect-media-notice {
.sui-notice {
&-content {
box-shadow: inset 2px 0 0 0 #17A8E3, inset 0 0 0 1px #E6E6E6 !important;
border-radius: unset !important;
h4 {
font-weight: 700;
margin-bottom: 5px;
margin-top: 0;
}
a {
font-size: 13px;
color: #007CBA;
&.sui-button {
background-color: #007CBA !important;
color: #ffffff;
text-transform: inherit;
&:hover {
background-color: darken(#007CBA, 6%) !important;
}
}
}
.smus-media-notification-skip {
text-decoration: none;
}
}
}
}
.wp-smush-restore {
&.button {
&.disabled {
cursor: help !important;
background: #eaeaea !important;
border: none !important;
color: #828282 !important;
}
}
}
@@ -0,0 +1,22 @@
/* ****************************************************************************
* MODULE: VARIABLES
*/
$font--path: "../fonts" !default;
$img--path: "../images" !default;
$sui-font-path: '~@wpmudev/shared-ui/dist/fonts/';
$summary-image: '#{$img--path}/smush-graphic-dashboard-summary.svg';
// Promo banners for free footer
$cross-sell-1: 'hummingbird';
$cross-sell-2: 'defender';
$cross-sell-3: 'smartcrawl';
$main-color: #17A8E3;
$text-color: #333;
$border-radius: 4px;
$upgrade-image: '../../app/assets/images/hero@2x.png';
$upgrade-image-mobile: '../../app/assets/images/hero.png';
@@ -0,0 +1,207 @@
/**
* Webp styles
*
* @since 3.8.0
*/
@include body-class(true) {
#smush-box-next-gen-next-gen {
.sui-box-header {
.smush-sui-tag-new {
font-size: 10px;
line-height: 12px;
}
}
.sui-box-settings-row.sui-hidden {
display: none !important;
}
}
#smush-box-webp-wizard {
.sui-row-with-sidenav {
margin-bottom: 0;
.sui-sidenav {
padding: 30px;
border-top-left-radius: $border-radius;
background-color: #F8F8F8;
.smush-wizard-bar-subtitle {
font-size: 11px;
line-height: 20px;
font-weight: 700;
color: #AAAAAA;
text-transform: uppercase;
}
.smush-sidenav-title {
display: flex;
align-items: center;
margin-bottom: 33px;
h4 {
margin: 0;
line-height: 20px;
}
}
.smush-wizard-steps-container {
ul {
margin: 0;
@include media(max-width, lg) {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
.smush-wizard-bar-step {
display: inline-block;
font-size: 13px;
color: #AAAAAA;
line-height: 22px;
font-weight: 500;
margin-bottom: 0;
.smush-wizard-bar-step-number {
display: inline-block;
margin-right: 10px;
text-align: center;
border-radius: 50%;
width: 22px;
height: 22px;
font-size: 11px;
background-color: #F2F2F2;
border: 1px solid #DDDDDD;
@include media(max-width, lg) {
display: block;
margin: 0 auto 5px auto;
}
}
&.disabled {
color: #DDDDDD;
}
&.current {
color: #333333;
.smush-wizard-bar-step-number {
background-color: #FFFFFF;
border-color: #333333;
}
}
&.done .smush-wizard-bar-step-number {
background-color: #1ABC9C;
border-color: #1ABC9C;
.sui-icon-check::before {
color: #FFFFFF;
}
}
@include media(min-width, lg) {
display: block;
}
@include media(max-width, lg) {
width: 70px;
text-align: center;
}
}
svg {
line {
stroke-width: 4px;
}
&.smush-svg-desktop {
height: 40px;
width: 22px;
margin-left: 10px;
display: none;
@include media(min-width, lg) {
display: block;
}
}
&.smush-svg-mobile {
width: 100%;
height: 4px;
display: block;
margin-bottom: -14px;
padding: 0 35px;
@include media(min-width, lg) {
display: none;
}
}
}
}
@include media(max-width, sm) {
padding: 20px;
}
}
.smush-wizard-steps-content-wrapper {
padding: 20px;
.smush-wizard-steps-content {
padding: 0 70px;
text-align: center;
&:first-child {
padding-top: 30px;
}
.smush-step-indicator {
font-size: 11px;
font-weight: 500;
color: #888888;
}
h2 {
margin: 0;
}
@include media(max-width, sm) {
padding: 0;
}
}
&.smush-wizard-step-1 {
.sui-box-selectors {
padding-left: 115px;
padding-right: 115px;
margin-bottom: 15px;
}
}
&.smush-wizard-step-2 {
.smush-wizard-rules-wrapper {
text-align: left;
}
.sui-tabs-menu {
justify-content: center;
}
}
@include media(min-width, sm) {
padding: 30px;
}
}
}
}
}
@@ -0,0 +1,360 @@
@import url('https://fonts.bunny.net/css?family=Roboto:400,500,700');
@import "modules/variables";
$font--path: '~@wpmudev/shared-ui/dist/fonts/';
@font-face {
font-family: "wpmudev-plugin-icons";
src: url('#{$font--path}/wpmudev-plugin-icons.eot?');
src: url('#{$font--path}/wpmudev-plugin-icons.eot?') format('embedded-opentype'),
url('#{$font--path}/wpmudev-plugin-icons.ttf') format('truetype'),
url('#{$font--path}/wpmudev-plugin-icons.woff') format('woff'),
url('#{$font--path}/wpmudev-plugin-icons.woff2') format('woff2'),
url('#{$font--path}/wpmudev-plugin-icons.svg') format('svg');
font-weight: 400;
font-style: normal
}
@media screen and ( max-width: 800px ) {
#smush-image-bar-toggle,
#smush-image-bar {
display: none;
}
}
@media screen and ( min-width: 800px ) {
.smush-detected-img {
border-radius: 5px;
transition: all 0.5s ease;
box-shadow: 0 0 0 5px #FECF2F;
}
#smush-image-bar-toggle {
position: fixed;
top: 60px;
right: 330px;
height: 50px;
width: 60px;
z-index: 9999999;
border-radius: 4px 0 0 4px;
background-color: #FFF;
box-shadow: inset 2px 0 0 0 #FECF2F, -13px 5px 20px 0 rgba(0, 0, 0, 0.1);
text-align: center;
cursor: pointer;
transition-property: all;
transition-duration: .5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
&.closed {
right: 0;
}
&.smush-toggle-success {
box-shadow: inset 2px 0 0 0 #1abc9c, -13px 5px 20px 0 rgba(0, 0, 0, 0.1);
}
i.sui-icon-info,
i.sui-icon-loader {
font-family: "wpmudev-plugin-icons" !important;
font-style: normal;
font-size: 16px;
line-height: 50px;
color: #FECF2F;
}
i.sui-icon-info:before {
content: "I";
}
&.smush-toggle-success i.sui-icon-info {
color: #1abc9c;
&:before {
content: "_";
}
}
i.sui-icon-loader {
&:before {
display: block;
content: "N";
-webkit-animation: spin 1.3s linear infinite;
animation: spin 1.3s linear infinite;
}
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
}
#smush-image-bar {
position: fixed;
top: 0;
right: 0;
width: 330px;
height: 100%;
background-color: #FFF;
box-shadow: 0 0 40px 0 rgba(0,0,0,0.1);
z-index: 999999;
padding: 0 0 20px;
overflow-y: auto;
overflow-x: hidden;
max-width: 330px;
transition-property: all;
transition-duration: .5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
&.closed {
max-width: 0;
overflow-y: hidden;
}
h3, p, strong, span {
font-family: 'Roboto', sans-serif;
letter-spacing: -0.25px;
}
h3 {
color: #333333;
font-size: 15px;
font-weight: bold;
line-height: 30px;
background-color: #FAFAFA;
padding: 15px 20px;
margin: 0;
}
p {
color: #888888;
font-size: 13px;
line-height: 22px;
padding: 0 20px;
}
strong {
color: #AAAAAA;
font-size: 12px;
font-weight: bold;
line-height: 22px;
padding: 0 20px;
}
.smush-resize-box {
background-color: #F8F8F8;
&:first-of-type {
border-top: 1px solid #E6E6E6;
margin-top: 5px;
}
&:last-of-type {
margin-bottom: 20px;
}
span:first-of-type {
color: #888;
height: 34px;
width: 40px;
font-size: 13px;
font-weight: bold;
line-height: 32px;
text-align: center;
border: 1px solid #DDDDDD;
border-radius: 50%;
margin-right: 10px;
}
.smush-image-info {
background-color: #FFF;
display: flex;
align-items: center;
align-content: center;
justify-content: space-between;
padding: 17px 20px;
border-bottom: 1px solid #E6E6E6;
cursor: pointer;
}
.smush-front-icons {
margin: 0 10px;
line-height: 5px;
&:before {
font-family: "wpmudev-plugin-icons" !important;
speak: none;
font-size: 12px;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
text-rendering: auto;
color: #AAA;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
&.smush-front-icon-arrows-in {
&:before {
content: '\2264';
}
}
}
.smush-tag {
background-color: #fecf2f;
color: #333;
border-radius: 13px;
height: 26px;
width: 116px;
font-size: 12px;
letter-spacing: -0.25px;
line-height: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
&.smush-tag-success {
background-color: #1abc9c;
color: #fff;
}
}
&.smush-tooltip {
position: relative;
&:before,
&:after {
content: "";
opacity: 0;
backface-visibility: hidden;
pointer-events: none;
position: absolute;
z-index: 1;
transition: margin .2s, opacity .2s;
}
&:before {
border: 5px solid transparent;
bottom: 100%;
left: 50%;
border-top-color: #000000;
transform: translateX(-50%);
}
&:after {
content: attr(data-tooltip);
min-width: 40px;
padding: 8px 12px;
border-radius: 4px;
background: #000000;
box-sizing: border-box;
color: #FFFFFF;
font: 400 12px/18px "Roboto", Arial, sans-serif;
text-transform: none;
text-align: center;
white-space: nowrap;
bottom: 100%;
left: 50%;
margin: 0 0 10px;
transform: translateX(-50%);
}
&.smush-tooltip-constrained {
&:after {
min-width: 240px;
white-space: normal;
}
}
&:not(.show-description):hover {
&:before,
&:after {
opacity: 1;
}
}
}
&:not(.show-description):hover,
&.show-description {
.smush-image-info { background-color: #F8F8F8; }
span:first-of-type {
background-color: #E6E6E6;
color: transparent;
&:before {
font-family: "wpmudev-plugin-icons" !important;
font-weight: 400;
content: "";
color: #666;
margin-right: -7px;
}
}
}
.smush-image-description {
display: none;
border-radius: 4px;
background-color: #FFFFFF;
box-shadow: 0 2px 0 0 #DDDDDD;
margin: 0 20px 20px;
padding: 20px;
color: #888888;
font-family: 'Roboto', sans-serif;
font-size: 13px;
letter-spacing: -0.25px;
line-height: 22px;
}
&.show-description {
padding-bottom: 1px;
border-bottom: 1px solid #E6E6E6;
.smush-image-info { border-bottom: 0; }
.smush-image-description { display: block; }
span:first-of-type {
background-color: #FECF2F;
border-color: #FECF2F;
&:before { color: #333; }
}
}
}
#smush-image-bar-notice {
display: none;
margin: 20px;
border: 1px solid #E6E6E6;
border-left: 2px solid #1abc9c;
border-radius: 4px;
padding: 15px 20px 15px 25px;
p:before {
position: absolute;
left: 42px;
font-family: "wpmudev-plugin-icons" !important;
font-weight: 400;
content: "_";
color: #1abc9c;
margin-right: -7px;
}
}
}
}