Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject styles into SilverbackIframe #1484

Merged
merged 2 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/silverback-gatsby/src/templates/webform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ const Webform: React.FC<
marginLeft: '-0.25em',
marginRight: '-0.25em',
}}
cssStylesToInject={
location.search.includes('test-inject-css=true')
? `
/*
comment with special chars
#$@;\`'()*
*/
* {
color: green;
}
.form-item-optional-text-field {
margin-bottom: 200px;
}
`
: undefined
}
/>
</StandardLayout>
);
Expand Down
117 changes: 80 additions & 37 deletions packages/composer/amazeelabs/silverback_iframe_theme/iframeCommand.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
(function ($, Drupal, drupalSettings) {
"use strict";
'use strict';

function removeIframeFromUrl(url) {
// This is a simple implementation which does not consider hash and edge
// cases. But it should be enough.
return url
.replace(/\?iframe=true$/, "")
.replace(/\?iframe=true&/, "?")
.replace(/&iframe=true$/, "")
.replace(/&iframe=true&/, "&");
.replace(/\?iframe=true$/, '')
.replace(/\?iframe=true&/, '?')
.replace(/&iframe=true$/, '')
.replace(/&iframe=true&/, '&');
}

function prependBaseUrl(url, baseUrl) {
// Again, a very simple implementation which only considers relative paths
// starting from a slash.
if (url.indexOf("/") === 0 && url.indexOf("//") !== 0) {
if (url.indexOf('/') === 0 && url.indexOf('//') !== 0) {
return baseUrl + url;
}
}
Expand All @@ -36,17 +36,19 @@

// Pass the iframe command to the parent iframe.
if (drupalSettings.iframeCommand) {
var iframeCommands = !Array.isArray(drupalSettings.iframeCommand) ? new Array(drupalSettings.iframeCommand) : drupalSettings.iframeCommand;
var iframeCommands = !Array.isArray(drupalSettings.iframeCommand)
? new Array(drupalSettings.iframeCommand)
: drupalSettings.iframeCommand;
if (iframeCommands.length > 0) {
waitForParentIframe(function (parentIFrame) {
iframeCommands.forEach(function(iframeCommand) {
iframeCommands.forEach(function (iframeCommand) {
var command = iframeCommand;
if (command.action === "redirect") {
if (command.action === 'redirect') {
command = $.extend(true, {}, command, {
path: removeIframeFromUrl(command.path),
});
}
parentIFrame.sendMessage(command, "*");
parentIFrame.sendMessage(command, '*');
});
});
return;
Expand All @@ -57,61 +59,49 @@
Drupal.behaviors.silverbackIframeRedirect = {
attach: function (context) {
// Check if there is a link with class `.js-iframe-parent-message`.
var $messageElements = $(".js-iframe-parent-message", context);
var $messageElements = $('.js-iframe-parent-message', context);

// Check if there is a link with class `.js-iframe-parent-redirect` in
// the current content, and trigger a parent redirect to that path.
var $redirectLinkElement = $(".js-iframe-parent-redirect", context);
var $redirectLinkElement = $('.js-iframe-parent-redirect', context);
if ($redirectLinkElement.length > 0) {
var redirectLink = $redirectLinkElement.get(0).pathname;
waitForParentIframe(function (parentIFrame) {
parentIFrame.sendMessage(
{
action: "redirect",
action: 'redirect',
path: redirectLink,
messages: $messageElements.toArray().map(function (el) {
return el.innerText;
}),
},
"*"
'*',
);
});
}
},
};

// Ask parent for the base URL to adjust links.
waitForParentIframe(function (parentIFrame) {
parentIFrame.sendMessage({ action: "getBaseUrl" }, "*");
parentIFrame.sendMessage({ action: 'init' }, '*');
});

// Update links using the given base URL.
window.addEventListener("message", (event) => {
// The message looks like this:
// [iFrameSizer]message:"silverback-iframe-base-url:http://localhost:8000"
var prefix = '[iFrameSizer]message:"silverback-iframe-base-url:';
if (typeof event.data !== "string" || event.data.indexOf(prefix) !== 0) {
return;
}
var baseUrl = event.data.substr(
prefix.length,
event.data.length - prefix.length - 1
);
$("a:visible").each(function () {
var updateBaseUrlInLinks = (baseUrl) => {
$('a:visible').each(function () {
var $this = $(this);
var href = $this.attr("href");
var href = $this.attr('href');
if (!href) {
return true;
}

// Exclude some links.
if (
// Drupal a11y links.
$this.hasClass("visually-hidden") ||
$this.hasClass('visually-hidden') ||
// Commerce checkout "Edit" links.
$this.closest(".checkout-pane-review").length ||
$this.closest('.checkout-pane-review').length ||
// Multistep form navigation links.
$this.closest(".form-actions").length
$this.closest('.form-actions').length
) {
return true;
}
Expand All @@ -121,14 +111,67 @@
href = removeIframeFromUrl(href);
// 2. Use parent frame base URL for relative links.
href = prependBaseUrl(href, baseUrl);
$this.attr("href", href);
$this.attr('href', href);
// 3. Open links in parent frame.
if ($this.attr("target") !== "_blank") {
$this.attr("target", "_parent");
if ($this.attr('target') !== '_blank') {
$this.attr('target', '_parent');
}
return true;
});
// This class is used by integration tests.
$("body").addClass("silverback-iframe-links-processed");
$('body').addClass('silverback-iframe-links-processed');
};

var injectCssStyles = (styles) => {
var id = 'silverback-iframe-injected-styles';
var el = document.getElementById(id);
if (!el) {
el = document.createElement('style');
el.id = id;
document.head.appendChild(el);
}
el.textContent = styles;
};

window.addEventListener('message', (event) => {
var parsed = parseMessage(event.data);
if (!parsed) {
return;
}
if (parsed.type === 'init') {
updateBaseUrlInLinks(parsed.baseUrl);
if (parsed.injectStyles) {
injectCssStyles(parsed.injectStyles);
}
}
});

/**
*
* @param {string} message
* @returns {{type: 'init', baseUrl: string, injectStyles: string | undefined} | null}
*/
function parseMessage(message) {
if (typeof message !== 'string') {
return null;
}
var prefix = '[iFrameSizer]message:';
if (!message.startsWith(prefix)) {
return null;
}
var parsed = null;
try {
parsed = JSON.parse(message.substring(prefix.length));
} catch (e) {
return null;
}
if (
typeof parsed !== 'object' ||
typeof parsed.silverbackIframe !== 'object' ||
parsed.silverbackIframe.type !== 'init'
) {
return null;
}
return parsed.silverbackIframe;
}
})(jQuery, Drupal, drupalSettings);
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import React, { useRef, useState } from 'react';
import {
IframeCommandOther,
IframeCommandScroll,
isIframeCommand
isIframeCommand,
} from '../types/iframe-command';

type OwnProps = {
buildMessages: (htmlMessages: Array<string>) => JSX.Element | null;
redirect: (url: string, htmlMessages?: Array<string>) => void;
scroll?: (to: string, iframeWrapper: HTMLElement) => void
scroll?: (to: string, iframeWrapper: HTMLElement) => void;
/**
* Not recommended for using in production.
* Because injecting CSS takes time and produces flashing.
*/
cssStylesToInject?: string;
};

type Props = OwnProps & IframeResizer.IframeResizerProps;
Expand All @@ -19,12 +24,15 @@ export const SilverbackIframe = ({
buildMessages,
redirect,
scroll,
cssStylesToInject,
...iframeResizerProps
}: Props) => {
const silverbackIframeReference = useRef<HTMLDivElement>(null);
const iframeRef = useRef<IFrameObject>(null);
const [iframeSeed, setIframeSeed] = useState<string | null>(null);
const [currentCommand, setCurrentCommand] = useState<IframeCommandOther | IframeCommandScroll>();
const [currentCommand, setCurrentCommand] = useState<
IframeCommandOther | IframeCommandScroll
>();

return (
<div className="silverback-iframe" ref={silverbackIframeReference}>
Expand All @@ -48,9 +56,15 @@ export const SilverbackIframe = ({
if (!isIframeCommand(message)) {
return;
}
if (message.action === 'getBaseUrl') {
if (message.action === 'init') {
iframeRef.current?.sendMessage(
`silverback-iframe-base-url:${window.location.origin}`,
{
silverbackIframe: {
type: 'init',
baseUrl: window.location.origin,
injectStyles: cssStylesToInject,
},
},
'*',
);
return;
Expand All @@ -65,12 +79,19 @@ export const SilverbackIframe = ({
} else {
setCurrentCommand(message);
}
if (message.action === 'scroll' && silverbackIframeReference && silverbackIframeReference.current) {
if (
message.action === 'scroll' &&
silverbackIframeReference &&
silverbackIframeReference.current
) {
// If the component received a scroll handler, then just call it.
// Otherwise we fallback to a very simple scroll implementation.
scroll
? scroll(message.scroll, silverbackIframeReference.current)
: scrollIframe(message.scroll, silverbackIframeReference.current);
: scrollIframe(
message.scroll,
silverbackIframeReference.current,
);
}
}}
/>
Expand All @@ -84,9 +105,9 @@ const scrollIframe = (to: string, iframeWrapper: HTMLElement) => {
switch (to) {
case 'top':
default:
iframeWrapper.scrollIntoView({behavior: "smooth"});
iframeWrapper.scrollIntoView({ behavior: 'smooth' });
}
}
};

const updateUrlParameters = (
uri: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const sets = [
result: false,
},
{
case: 'getBaseUrl',
data: { action: 'getBaseUrl' },
case: 'init',
data: { action: 'init' },
result: true,
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type IframeCommandGetBaseUrl = {
action: 'getBaseUrl';
export type IframeCommandInit = {
action: 'init';
};

export type IframeCommandRedirect = {
Expand All @@ -17,17 +17,20 @@ export type IframeCommandOther = {
export type IframeCommandScroll = {
action: 'scroll';
scroll: string;
}
};

export type IframeCommand =
| IframeCommandGetBaseUrl
| IframeCommandInit
| IframeCommandRedirect
| IframeCommandOther
| IframeCommandScroll;

export const isIframeCommand = (variable: any): variable is IframeCommand => {
if (typeof variable === 'object' && typeof variable.action === 'string') {
if (['getBaseUrl', 'scroll'].includes(variable.action)) {
if (variable.action === 'init') {
return true;
}
if (variable.action === 'scroll' && typeof variable.scroll === 'string') {
return true;
}
if (
Expand Down
2 changes: 2 additions & 0 deletions packages/tests/silverback-gatsby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"test:integration": "playwright install chromium && pnpm test:readonly && pnpm test:mutating",
"test:readonly": "playwright test --pass-with-no-tests",
"test:mutating": "playwright test --pass-with-no-tests -c playwright.config.mutating.ts",
"headed:readonly": "pnpm test:readonly --headed",
"headed:mutating": "pnpm test:mutating --headed",
"dev:readonly": "DEBUG=pw:api pnpm test:readonly --ui",
"dev:mutating": "DEBUG=pw:api pnpm test:mutating --ui"
}
Expand Down
29 changes: 29 additions & 0 deletions packages/tests/silverback-gatsby/specs/webform.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { gatsby } from '@amazeelabs/silverback-playwright';
import { expect, test } from '@playwright/test';

import { getIframe } from '../../silverback-drupal/common';

test('injected CSS styles', async ({ page }) => {
// Check if it isn't green by default.
await page.goto(`${gatsby.baseUrl}/en/form/for-testing-confirmation-options`);
await expect((await getIframe(page)).locator('h1')).not.toHaveCSS(
'color',
'rgb(0, 128, 0)',
);

// Inject CSS.
await page.goto(
`${gatsby.baseUrl}/en/form/for-testing-confirmation-options?test-inject-css=true`,
);
// Check if it's green.
await expect((await getIframe(page)).locator('h1')).toHaveCSS(
'color',
'rgb(0, 128, 0)',
);
// Check if we have additional margin at the bottom of the field.
await expect(
(await getIframe(page)).locator('.form-item-optional-text-field'),
).toHaveCSS('margin-bottom', '200px');
// Check if the iframe was resized. We should still see the Submit button.
await expect((await getIframe(page)).locator('text=Submit')).toBeVisible();
});
Loading