Skip to content

Commit bf6ac9c

Browse files
authored
Improve the QE comment integration (#82)
* Improve the QE comment integration Handles the case of someone opening the QE for one email, closing it, changing the email, and then trying to open the QE again. * Match the loading skeleton size to the hovercard * Bump QE and hovercards * Clear any previous timer
1 parent 0d9fab7 commit bf6ac9c

File tree

11 files changed

+1954
-1926
lines changed

11 files changed

+1954
-1926
lines changed

classes/quick-editor/class-quick-editor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public function add_quick_editor_link() {
145145
<div class="gravatar-hovercard">
146146
<div class="gravatar-hovercard__inner">
147147
<div class="gravatar-hovercard__header">
148-
<img class="gravatar-hovercard__avatar" src="$avatar_url" width="72" height="72" />
148+
<img class="gravatar-hovercard__avatar" src="$avatar_url" width="104" height="104" />
149149
<h4 class="gravatar-hovercard__name"></h4>
150150
</div>
151151
<div class="gravatar-hovercard__body">

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
"typescript": "^5.8.2"
3838
},
3939
"dependencies": {
40-
"@gravatar-com/hovercards": "^0.10.6",
41-
"@gravatar-com/quick-editor": "^0.6.0",
40+
"@gravatar-com/hovercards": "^0.10.7",
41+
"@gravatar-com/quick-editor": "^0.7.0",
4242
"@wordpress/editor": "^14.19.0",
4343
"@wordpress/url": "^4.19.0",
4444
"clsx": "^2.1.1",

src/comments/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const GRAVATAR_CONTAINER = '.gravatar-enhanced-profile';
2+
export const COMMENT_EMAIL_WRAPPER = '.comment-form-email';
3+
export const COMMENT_EMAIL_FIELD = '#email';

src/comments/index.ts

Lines changed: 40 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,33 @@
11
import { sha256 } from 'js-sha256';
2-
import showQuickEditor from '../shared/show-quick-editor';
2+
import { GravatarQuickEditorCore } from '@gravatar-com/quick-editor';
33
import { Hovercards } from '@gravatar-com/hovercards';
4+
import trackEvent from '../shared/analytics';
5+
import updateAvatars from '../shared/update-avatars';
6+
import { adjustGravatarPosition, fetchUserProfile, suggestProfile, hideProfile, showProfile } from './profile';
7+
import { GRAVATAR_CONTAINER, COMMENT_EMAIL_WRAPPER, COMMENT_EMAIL_FIELD } from './constants';
48
import './style.scss';
59

6-
const BASE_API_URL = 'https://api.gravatar.com/v3/profiles';
7-
const GRAVATAR_CONTAINER = '.gravatar-enhanced-profile';
8-
const COMMENT_EMAIL_WRAPPER = '.comment-form-email';
9-
const COMMENT_EMAIL_FIELD = '#email';
1010
const INPUT_TIMEOUT = 1000;
1111

12-
const hovercards = new Hovercards();
13-
14-
function isEmail( email ) {
12+
function isEmail( email: string ) {
1513
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1614
return emailRegex.test( email );
1715
}
1816

19-
async function fetchUserProfile( email ) {
20-
const hash = sha256( email.trim().toLowerCase() );
21-
22-
try {
23-
// Get profile data
24-
const response = await fetch( `${ BASE_API_URL }/${ hash }?source=hovercard` );
25-
if ( ! response.ok ) {
26-
return null;
27-
}
28-
29-
return await response.json();
30-
} catch ( error ) {
31-
// eslint-disable-next-line no-console
32-
console.error( error );
33-
}
34-
35-
return null;
36-
}
37-
38-
function suggestProfile( profile ) {
39-
const author = document.getElementById( 'author' ) as HTMLInputElement;
40-
const url = document.getElementById( 'url' ) as HTMLInputElement;
41-
const profileUrl = profile.profile_url;
42-
43-
if ( author && author.value === '' ) {
44-
author.value = profile.display_name;
45-
}
46-
47-
if ( url && url.value === '' && profileUrl ) {
48-
url.value = profileUrl;
49-
}
50-
}
51-
52-
function hideProfile() {
53-
const emailContainer = document.querySelector( '.comment-form-email' ) as HTMLInputElement;
54-
const emailField = document.querySelector( COMMENT_EMAIL_FIELD ) as HTMLInputElement;
55-
56-
if ( ! emailField || ! emailContainer ) {
57-
return;
58-
}
59-
60-
emailContainer.classList.remove( 'gravatar-enhanced-comments' );
61-
emailField.style.paddingLeft = '';
62-
}
63-
64-
function adjustGravatarPosition() {
65-
const gravatarProfile = document.querySelector( GRAVATAR_CONTAINER ) as HTMLSpanElement;
66-
const emailContainer = document.querySelector( '.comment-form-email' ) as HTMLInputElement;
67-
const emailField = document.querySelector( COMMENT_EMAIL_FIELD ) as HTMLInputElement;
68-
69-
if ( ! gravatarProfile || ! emailField || ! emailContainer ) {
70-
return;
71-
}
72-
73-
// Measure the email field
74-
const computedStyle = getComputedStyle( emailField );
75-
const padding = parseInt( computedStyle.paddingTop, 10 ) + parseInt( computedStyle.borderTopWidth, 10 );
76-
const emailFieldRect = emailField.getBoundingClientRect();
77-
const emailContainerRect = emailContainer.getBoundingClientRect();
78-
const topRectOffset = emailFieldRect.top - emailContainerRect.top;
79-
const leftRectOffset = emailFieldRect.left - emailContainerRect.left;
80-
const height = parseFloat( ( emailFieldRect.height * 0.8 ).toFixed( 1 ) );
81-
const heightDifference = parseFloat( ( emailFieldRect.height - height ).toFixed( 1 ) );
82-
const leftOffset = parseFloat(
83-
( leftRectOffset + padding / 2 + parseInt( computedStyle.borderLeftWidth ) ).toFixed( 1 )
84-
);
85-
const topOffset = parseFloat( ( topRectOffset + heightDifference / 2 ).toFixed( 1 ) );
86-
87-
// Position the Gravatar inside the text field
88-
gravatarProfile.style.height = height + 'px';
89-
gravatarProfile.style.width = height + 'px';
90-
gravatarProfile.style.top = topOffset + 'px';
91-
gravatarProfile.style.left = leftOffset + 'px';
92-
93-
// Move the text up to allow the Gravatar to fit
94-
emailField.style.paddingLeft = parseFloat( ( height + padding * 1.3 ).toFixed( 1 ) ) + 'px';
95-
}
96-
97-
function showProfile( profile, isShowingEditor ) {
98-
const gravatarImg = document.querySelector( GRAVATAR_CONTAINER + ' img' ) as HTMLImageElement;
99-
const emailContainer = document.querySelector( COMMENT_EMAIL_WRAPPER ) as HTMLInputElement;
100-
101-
if ( ! gravatarImg || ! emailContainer ) {
102-
return;
103-
}
104-
105-
gravatarImg.src = profile.avatar_url;
106-
emailContainer.classList.add( 'gravatar-enhanced-comments' );
107-
108-
adjustGravatarPosition();
109-
110-
// Hook up to hovercard
111-
hovercards.attach( gravatarImg, {
112-
onCanShowHovercard: () => {
113-
return ! isShowingEditor();
114-
},
115-
} );
116-
}
117-
11817
document.addEventListener( 'DOMContentLoaded', () => {
11918
const email = document.querySelector( COMMENT_EMAIL_FIELD ) as HTMLInputElement;
12019
const qeButton = document.querySelector( GRAVATAR_CONTAINER + ' img' );
12120
let lastRequestEmail = '';
21+
let lastRequestUrl = '';
22+
let lastRequestName = '';
12223
let debounceProfileTimeout: NodeJS.Timeout;
123-
let isShowingEditor = false;
24+
let quickEditor = null;
25+
26+
const hovercards = new Hovercards( {
27+
onCanShowHovercard: () => {
28+
return quickEditor === null || ! quickEditor.isOpen();
29+
},
30+
} );
12431

12532
const loadProfile = async ( event ) => {
12633
clearTimeout( debounceProfileTimeout );
@@ -141,16 +48,19 @@ document.addEventListener( 'DOMContentLoaded', () => {
14148
lastRequestEmail = emailValue;
14249

14350
if ( profile ) {
144-
suggestProfile( profile );
145-
showProfile( profile, () => isShowingEditor );
51+
suggestProfile( profile, lastRequestUrl, lastRequestName );
52+
showProfile( profile, hovercards );
53+
54+
lastRequestUrl = profile.profile_url;
55+
lastRequestName = profile.display_name;
14656
} else {
14757
showProfile(
14858
{
14959
display_name: '',
15060
profile_url: '',
15161
avatar_url: 'https://gravatar.com/avatar/' + sha256( emailValue.trim().toLowerCase() ),
15262
},
153-
() => isShowingEditor
63+
hovercards
15464
);
15565
}
15666
};
@@ -159,21 +69,29 @@ document.addEventListener( 'DOMContentLoaded', () => {
15969
email?.addEventListener( 'input', ( ev ) => {
16070
clearTimeout( debounceProfileTimeout );
16171
debounceProfileTimeout = setTimeout( () => loadProfile( ev ), INPUT_TIMEOUT );
72+
73+
// If the email is changed then close any QE and clear the instance
74+
if ( quickEditor ) {
75+
quickEditor.close();
76+
quickEditor = null;
77+
}
16278
} );
16379

16480
// Hook up the image to the QE
16581
qeButton?.addEventListener( 'click', () => {
166-
isShowingEditor = true;
167-
168-
showQuickEditor(
169-
email?.value || gravatarEnhancedComments?.email || '',
170-
gravatarEnhancedComments?.locale || 'en',
171-
[ 'avatars' ],
172-
GRAVATAR_CONTAINER + ' img',
173-
() => {
174-
isShowingEditor = false;
175-
}
176-
);
82+
if ( ! quickEditor ) {
83+
quickEditor = new GravatarQuickEditorCore( {
84+
email: email?.value || gravatarEnhancedComments?.email || '',
85+
scope: [ 'avatars' ],
86+
locale: gravatarEnhancedComments?.locale || 'en',
87+
onProfileUpdated: () => {
88+
trackEvent( 'gravatar_enhanced_qe_avatar_updated' );
89+
updateAvatars( GRAVATAR_CONTAINER + ' img' );
90+
},
91+
} );
92+
}
93+
94+
quickEditor.open();
17795
} );
17896

17997
// Reposition the avatar on resize - it can get slightly out of place

src/comments/profile.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { sha256 } from 'js-sha256';
2+
import { GRAVATAR_CONTAINER, COMMENT_EMAIL_WRAPPER, COMMENT_EMAIL_FIELD } from './constants';
3+
4+
const BASE_API_URL = 'https://api.gravatar.com/v3/profiles';
5+
6+
export function adjustGravatarPosition() {
7+
const gravatarProfile = document.querySelector( GRAVATAR_CONTAINER ) as HTMLSpanElement;
8+
const emailContainer = document.querySelector( COMMENT_EMAIL_WRAPPER ) as HTMLInputElement;
9+
const emailField = document.querySelector( COMMENT_EMAIL_FIELD ) as HTMLInputElement;
10+
11+
if ( ! gravatarProfile || ! emailField || ! emailContainer ) {
12+
return;
13+
}
14+
15+
// Measure the email field
16+
const computedStyle = getComputedStyle( emailField );
17+
const padding = parseInt( computedStyle.paddingTop, 10 ) + parseInt( computedStyle.borderTopWidth, 10 );
18+
const emailFieldRect = emailField.getBoundingClientRect();
19+
const emailContainerRect = emailContainer.getBoundingClientRect();
20+
const topRectOffset = emailFieldRect.top - emailContainerRect.top;
21+
const leftRectOffset = emailFieldRect.left - emailContainerRect.left;
22+
const height = parseFloat( ( emailFieldRect.height * 0.8 ).toFixed( 1 ) );
23+
const heightDifference = parseFloat( ( emailFieldRect.height - height ).toFixed( 1 ) );
24+
const leftOffset = parseFloat(
25+
( leftRectOffset + padding / 2 + parseInt( computedStyle.borderLeftWidth ) ).toFixed( 1 )
26+
);
27+
const topOffset = parseFloat( ( topRectOffset + heightDifference / 2 ).toFixed( 1 ) );
28+
29+
// Position the Gravatar inside the text field
30+
gravatarProfile.style.height = height + 'px';
31+
gravatarProfile.style.width = height + 'px';
32+
gravatarProfile.style.top = topOffset + 'px';
33+
gravatarProfile.style.left = leftOffset + 'px';
34+
35+
// Move the text up to allow the Gravatar to fit
36+
emailField.style.paddingLeft = parseFloat( ( height + padding * 1.3 ).toFixed( 1 ) ) + 'px';
37+
}
38+
39+
export async function fetchUserProfile( email: string ) {
40+
const hash = sha256( email.trim().toLowerCase() );
41+
42+
try {
43+
// Get profile data
44+
const response = await fetch( `${ BASE_API_URL }/${ hash }?source=hovercard` );
45+
if ( ! response.ok ) {
46+
return null;
47+
}
48+
49+
return await response.json();
50+
} catch ( error ) {
51+
// eslint-disable-next-line no-console
52+
console.error( error );
53+
}
54+
55+
return null;
56+
}
57+
58+
export function suggestProfile( profile, lastUrl, lastName ) {
59+
const author = document.getElementById( 'author' ) as HTMLInputElement;
60+
const url = document.getElementById( 'url' ) as HTMLInputElement;
61+
const profileUrl = profile.profile_url;
62+
63+
if ( author && ( author.value === '' || author.value === lastName ) ) {
64+
author.value = profile.display_name;
65+
}
66+
67+
if ( url && profileUrl && ( url.value === '' || url.value === lastUrl ) ) {
68+
url.value = profileUrl;
69+
}
70+
}
71+
72+
export function hideProfile() {
73+
const emailContainer = document.querySelector( COMMENT_EMAIL_WRAPPER ) as HTMLInputElement;
74+
const emailField = document.querySelector( COMMENT_EMAIL_FIELD ) as HTMLInputElement;
75+
76+
if ( ! emailField || ! emailContainer ) {
77+
return;
78+
}
79+
80+
emailContainer.classList.remove( 'gravatar-enhanced-comments' );
81+
emailField.style.paddingLeft = '';
82+
}
83+
84+
export function showProfile( profile, hovercards ) {
85+
const gravatarImg = document.querySelector( GRAVATAR_CONTAINER + ' img' ) as HTMLImageElement;
86+
const emailContainer = document.querySelector( COMMENT_EMAIL_WRAPPER ) as HTMLInputElement;
87+
88+
if ( ! gravatarImg || ! emailContainer ) {
89+
return;
90+
}
91+
92+
gravatarImg.src = profile.avatar_url;
93+
emailContainer.classList.add( 'gravatar-enhanced-comments' );
94+
95+
adjustGravatarPosition();
96+
97+
// Hook up to hovercard
98+
hovercards.attach( gravatarImg );
99+
}

src/comments/style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ $gravatar-blue: #1d4fc4;
3535
&.avatar {
3636
position: inherit !important;
3737
}
38+
39+
&.avatar-loading {
40+
animation: gravatar-pulse 2s infinite ease-in-out;
41+
}
3842
}
3943
}
4044

src/hovercards/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Hovercards } from '@gravatar-com/hovercards';
22
import '@gravatar-com/hovercards/dist/style.css';
33

4-
const ignoreSelector = '#wpadminbar img';
4+
const ignoreSelector = '#wpadminbar img, img.gravatar-hovercard__avatar';
55

66
document.addEventListener( 'DOMContentLoaded', () => {
77
const hovercards = new Hovercards();

0 commit comments

Comments
 (0)