-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from contentful/feat/tooltip-btn
feat: add tooltip bttn [TOL-937]
- Loading branch information
Showing
16 changed files
with
636 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const DATA_CURR_FIELD_ID = 'current-data-contentful-field-id'; | ||
export const DATA_CURR_ENTRY_ID = 'current-data-contentful-entry-id'; | ||
export const DATA_CURR_LOCALE = 'current-data-contentful-locale'; | ||
export const TOOLTIP_CLASS = 'contentful-tooltip'; | ||
|
||
export const TOOLTIP_HEIGHT = 32; | ||
export const TOOLTIP_PADDING_LEFT = 5; |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { | ||
DATA_CURR_ENTRY_ID, | ||
DATA_CURR_FIELD_ID, | ||
DATA_CURR_LOCALE, | ||
TOOLTIP_CLASS, | ||
TOOLTIP_HEIGHT, | ||
TOOLTIP_PADDING_LEFT, | ||
} from './constants'; | ||
import { TagAttributes } from './types'; | ||
|
||
export default class FieldTagging { | ||
private tooltip: HTMLButtonElement | null = null; // this tooltip scrolls to the correct field in the entry editor | ||
private currentElementBesideTooltip: HTMLElement | null = null; // this element helps to position the tooltip | ||
|
||
constructor() { | ||
this.tooltip = null; | ||
this.currentElementBesideTooltip = null; | ||
|
||
this.resolveIncomingMessage = this.resolveIncomingMessage.bind(this); | ||
this.updateTooltipPosition = this.updateTooltipPosition.bind(this); | ||
this.addTooltipOnHover = this.addTooltipOnHover.bind(this); | ||
this.createTooltip = this.createTooltip.bind(this); | ||
this.clickHandler = this.clickHandler.bind(this); | ||
|
||
this.createTooltip(); | ||
window.addEventListener('message', this.resolveIncomingMessage); | ||
window.addEventListener('scroll', this.updateTooltipPosition); | ||
window.addEventListener('mouseover', this.addTooltipOnHover); | ||
} | ||
|
||
// Handles incoming messages from Contentful | ||
private resolveIncomingMessage(e: MessageEvent) { | ||
if (typeof e.data !== 'object') return; | ||
if (e.data.from !== 'live-preview') return; | ||
// Toggle the contentful-inspector--active class on the body element based on the isInspectorActive boolean | ||
document.body.classList.toggle('contentful-inspector--active', e.data.isInspectorActive); | ||
} | ||
|
||
// Updates the position of the tooltip | ||
private updateTooltipPosition() { | ||
if (!this.currentElementBesideTooltip || !this.tooltip) return false; | ||
|
||
const currentRectOfElement = this.currentElementBesideTooltip.getBoundingClientRect(); | ||
const currentRectOfParentOfElement = this.tooltip.parentElement?.getBoundingClientRect(); | ||
|
||
if (currentRectOfElement && currentRectOfParentOfElement) { | ||
let upperBoundOfTooltip = currentRectOfElement.top - TOOLTIP_HEIGHT; | ||
const left = currentRectOfElement.left - TOOLTIP_PADDING_LEFT; | ||
|
||
if (upperBoundOfTooltip < 0) { | ||
if (currentRectOfElement.top < 0) upperBoundOfTooltip = currentRectOfElement.top; | ||
else upperBoundOfTooltip = 0; | ||
} | ||
|
||
this.tooltip.style.top = upperBoundOfTooltip + 'px'; | ||
this.tooltip.style.left = left + 'px'; | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private addTooltipOnHover(e: MouseEvent) { | ||
const eventTargets = e.composedPath(); | ||
|
||
for (const eventTarget of eventTargets) { | ||
const element = eventTarget as HTMLElement; | ||
if (element.nodeName === 'BODY') break; | ||
if (typeof element?.getAttribute !== 'function') continue; | ||
|
||
const currFieldId = element.getAttribute(TagAttributes.FIELD_ID); | ||
const currEntryId = element.getAttribute(TagAttributes.ENTRY_ID); | ||
const currLocale = element.getAttribute(TagAttributes.LOCALE); | ||
|
||
if (currFieldId && currEntryId && currLocale) { | ||
this.currentElementBesideTooltip = element; | ||
|
||
if (this.updateTooltipPosition()) { | ||
this.tooltip?.setAttribute(DATA_CURR_FIELD_ID, currFieldId); | ||
this.tooltip?.setAttribute(DATA_CURR_ENTRY_ID, currEntryId); | ||
this.tooltip?.setAttribute(DATA_CURR_LOCALE, currLocale); | ||
} | ||
|
||
break; | ||
} | ||
} | ||
} | ||
|
||
private createTooltip() { | ||
if (!document.querySelector(`.${TOOLTIP_CLASS}`)) { | ||
const tooltip = document.createElement('button'); | ||
tooltip.classList.add(TOOLTIP_CLASS); | ||
tooltip.innerHTML = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5325 2.22242C13.825 2.51492 13.825 2.98742 13.5325 3.27992L12.16 4.65242L9.3475 1.83992L10.72 0.467422C11.0125 0.174922 11.485 0.174922 11.7775 0.467422L13.5325 2.22242ZM0.25 13.7499V10.9374L8.545 2.64243L11.3575 5.45493L3.0625 13.7499H0.25Z" fill="white"/> | ||
</svg>Edit`; | ||
window.document.body.insertAdjacentElement('beforeend', tooltip); | ||
tooltip.addEventListener('click', this.clickHandler); | ||
this.tooltip = tooltip; | ||
} | ||
this.updateTooltipPosition(); | ||
} | ||
|
||
// responsible for handling the event when the user clicks on the edit button in the tooltip | ||
private clickHandler() { | ||
if (!this.tooltip) { | ||
return; | ||
} | ||
const fieldId = this.tooltip.getAttribute(DATA_CURR_FIELD_ID); | ||
const entryId = this.tooltip.getAttribute(DATA_CURR_ENTRY_ID); | ||
const locale = this.tooltip.getAttribute(DATA_CURR_LOCALE); | ||
|
||
window.top?.postMessage( | ||
{ | ||
from: 'live-preview', | ||
fieldId, | ||
entryId, | ||
locale, | ||
}, | ||
//todo: check if there is any security risk with this | ||
'*' | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import './styles.css'; | ||
import FieldTagging from './field-tagging'; | ||
import { LivePreviewProps, TagAttributes } from './types'; | ||
|
||
export class ContentfulLivePreview { | ||
static fieldTagging: FieldTagging | null = null; | ||
|
||
// Static method to initialize the LivePreview SDK | ||
static init(): Promise<FieldTagging> | undefined { | ||
// Check if running in a browser environment | ||
if (typeof window !== 'undefined') { | ||
if (ContentfulLivePreview.fieldTagging) { | ||
console.log('You have already initialized the Live Preview SDK.'); | ||
return Promise.resolve(ContentfulLivePreview.fieldTagging); | ||
} else { | ||
ContentfulLivePreview.fieldTagging = new FieldTagging(); | ||
return Promise.resolve(ContentfulLivePreview.fieldTagging); | ||
} | ||
} | ||
} | ||
|
||
// Static method to render live preview data-attributes to HTML element output | ||
static getProps({ | ||
fieldId, | ||
entryId, | ||
locale, | ||
}: LivePreviewProps): Record<TagAttributes, string | null | undefined> { | ||
return { | ||
[TagAttributes.FIELD_ID]: fieldId, | ||
[TagAttributes.ENTRY_ID]: entryId, | ||
[TagAttributes.LOCALE]: locale, | ||
}; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
[data-contentful-field-id] { | ||
outline: 1px dashed rgba(64, 160, 255, 0) !important; | ||
transition: outline-color 0.3s ease-in-out; | ||
} | ||
|
||
.contentful-inspector--active [data-contentful-field-id] { | ||
outline: 1px dashed rgba(64, 160, 255, 1) !important; | ||
} | ||
|
||
button.contentful-tooltip { | ||
padding: 0; | ||
display: none; | ||
outline: none; | ||
border: none; | ||
z-index: 999999 !important; | ||
position: fixed; | ||
margin: 0; | ||
height: 32px; | ||
width: 72px; | ||
background: rgb(3, 111, 227); | ||
font-weight: 500 !important; | ||
color: #ffffff !important; | ||
transition: background 0.2s; | ||
text-align: center !important; | ||
border-radius: 6px !important; | ||
font-size: 14px !important; | ||
justify-content: center; | ||
align-items: center; | ||
box-shadow: 0px 1px 0px rgba(17, 27, 43, 0.05); | ||
box-sizing: border-box; | ||
cursor: pointer; | ||
gap: 6px; | ||
} | ||
|
||
button.contentful-tooltip:hover { | ||
background: rgb(0, 89, 200); | ||
} | ||
|
||
button.contentful-tooltip:active:hover { | ||
background: rgb(0, 65, 171); | ||
} | ||
|
||
.contentful-inspector--active button.contentful-tooltip { | ||
display: flex; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { describe, it, expect } from 'vitest'; | ||
import { ContentfulLivePreview } from '../index'; | ||
import { TagAttributes } from '../types'; | ||
|
||
describe('getProps', () => { | ||
it('returns the expected props with a given entryId, fieldId and locale', () => { | ||
const entryId = 'test-entry-id'; | ||
const fieldId = 'test-field-id'; | ||
const locale = 'test-locale'; | ||
|
||
const result = ContentfulLivePreview.getProps({ | ||
entryId, | ||
fieldId, | ||
locale, | ||
}); | ||
|
||
expect(result).toStrictEqual({ | ||
[TagAttributes.FIELD_ID]: fieldId, | ||
[TagAttributes.ENTRY_ID]: entryId, | ||
[TagAttributes.LOCALE]: locale, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// @vitest-environment jsdom | ||
import { describe, it, expect } from 'vitest'; | ||
import { ContentfulLivePreview } from '../index'; | ||
import FieldTagging from '../field-tagging'; | ||
|
||
describe('init', () => { | ||
it('returns a Promise that resolves to a LivePreview instance when running in a browser environment', async () => { | ||
const livePreviewInstance = await ContentfulLivePreview.init(); | ||
expect(livePreviewInstance).toBeInstanceOf(FieldTagging); | ||
}); | ||
|
||
it('returns undefined when not running in a browser environment', () => { | ||
const windowBackup = global.window; | ||
(global as any).window = undefined; | ||
const result = ContentfulLivePreview.init(); | ||
expect(result).toBeUndefined(); | ||
global.window = windowBackup; | ||
}); | ||
|
||
it('returns a Promise that resolves to the same LivePreview instance when called multiple times', async () => { | ||
const livePreviewInstance1 = await ContentfulLivePreview.init(); | ||
const livePreviewInstance2 = await ContentfulLivePreview.init(); | ||
expect(livePreviewInstance1).toBe(livePreviewInstance2); | ||
}); | ||
}); |
Oops, something went wrong.