@flourish/layout
Advanced tools
| /** | ||
| * Iframe Credit Component | ||
| * | ||
| * Displays a localized Flourish credit inside iframe embeds for Free/Presenter users. | ||
| * - Shows template-specific and localized credit text (e.g., "Made with Flourish • Create a parliament chart") | ||
| * - Can be hidden via postMessage in platform code (embedded.js) when script embed handles credit externally | ||
| * - Credit text and URL are provided by server in Flourish.metadata | ||
| */ | ||
| let credit_element = null; | ||
| /** | ||
| * Determines whether we should show the internal iframe credit. | ||
| * Returns true when Flourish.metadata.show_internal_credit is set (Free/Presenter users in embed environment). | ||
| */ | ||
| function shouldShowCredit() { | ||
| if (typeof Flourish === "undefined") return false; | ||
| const metadata = Flourish.metadata || {}; | ||
| const should_show = metadata.show_internal_credit === true; | ||
| return should_show; | ||
| } | ||
| const DEFAULT_CREDIT_URL = "https://flourish.studio"; | ||
| const DEFAULT_CREDIT_TEXT = "Made with Flourish • Create your own"; | ||
| /** | ||
| * Gets localized credit text and URL from server-provided metadata. | ||
| * Server calculates template-specific and localized credits based on template categories and language. | ||
| * Falls back to default English text/URL if metadata is not available. | ||
| */ | ||
| function getCreditTextAndUrl() { | ||
| const metadata = (typeof Flourish !== "undefined" && Flourish.metadata) || {}; | ||
| return { | ||
| credit_url: metadata.credit_url || DEFAULT_CREDIT_URL, | ||
| credit_text: metadata.credit_text || DEFAULT_CREDIT_TEXT, | ||
| }; | ||
| } | ||
| /** | ||
| * Creates the credit element with styling matching the external embed credit. | ||
| * Matches the style from common/embed/credit.js createFlourishCredit() | ||
| */ | ||
| function createCreditElement() { | ||
| const credit_info = getCreditTextAndUrl(); | ||
| const credit_url = credit_info.credit_url; | ||
| const query_string = "?utm_source=embed&utm_campaign=visualisation"; | ||
| const credit_text = credit_info.credit_text; | ||
| const credit = document.createElement("div"); | ||
| credit.setAttribute("class", "flourish-credit"); | ||
| credit.setAttribute( | ||
| "style", | ||
| "margin:4px 8px;text-align:right;font-family:system-ui,sans-serif;color:inherit;opacity:0.8;font-size:12px;font-weight:normal;font-style:normal;-webkit-font-smoothing:antialiased;box-shadow:none;order:50;", | ||
| ); | ||
| // Split credit text into prefix and CTA (linked part) | ||
| const parts = credit_text.split("•"); | ||
| const UNICODE_NO_BREAK_SPACE = "\u00A0"; | ||
| const prefix_text = | ||
| parts.length > 1 | ||
| ? parts[0].trim() + UNICODE_NO_BREAK_SPACE + "•" + UNICODE_NO_BREAK_SPACE | ||
| : ""; | ||
| const cta_text = parts.length > 1 ? parts[1].trim() : credit_text; | ||
| if (prefix_text) { | ||
| const prefix = document.createElement("span"); | ||
| prefix.setAttribute( | ||
| "style", | ||
| "font:inherit;color:inherit;vertical-align:middle;display:inline-block;box-shadow:none;", | ||
| ); | ||
| prefix.appendChild(document.createTextNode(prefix_text)); | ||
| credit.appendChild(prefix); | ||
| } | ||
| const a = document.createElement("a"); | ||
| a.setAttribute("href", credit_url + query_string); | ||
| a.setAttribute("target", "_blank"); | ||
| a.setAttribute("aria-label", cta_text + " (opens in new tab)"); | ||
| a.setAttribute("rel", "noopener noreferrer"); | ||
| a.setAttribute( | ||
| "style", | ||
| "display:inline-block;text-decoration:none;font:inherit;color:inherit;border:none;box-shadow:none;", | ||
| ); | ||
| credit.appendChild(a); | ||
| const span = document.createElement("span"); | ||
| span.setAttribute( | ||
| "style", | ||
| "font:inherit;color:inherit;vertical-align:middle;display:inline-block;box-shadow:none;", | ||
| ); | ||
| span.appendChild(document.createTextNode(cta_text)); | ||
| a.appendChild(span); | ||
| span.addEventListener("mouseover", function () { | ||
| span.style.textDecoration = "underline"; | ||
| }); | ||
| span.addEventListener("mouseout", function () { | ||
| span.style.textDecoration = "none"; | ||
| }); | ||
| return credit; | ||
| } | ||
| export function init() { | ||
| if (credit_element) return; // Already initialized | ||
| if (!shouldShowCredit()) return; | ||
| credit_element = createCreditElement(); | ||
| // Append after footer container (outside it to avoid being hidden when footer is empty) | ||
| const footer_container = document.getElementById( | ||
| "fl-layout-footer-container", | ||
| ); | ||
| const wrapper = document.getElementById("fl-layout-wrapper"); | ||
| if (footer_container && footer_container.parentNode) { | ||
| footer_container.parentNode.insertBefore( | ||
| credit_element, | ||
| footer_container.nextSibling, | ||
| ); | ||
| } else if (wrapper) { | ||
| wrapper.appendChild(credit_element); | ||
| } else { | ||
| document.body.appendChild(credit_element); | ||
| } | ||
| } | ||
| // used for testing purposes | ||
| export function destroy() { | ||
| if (credit_element && credit_element.parentNode) { | ||
| credit_element.parentNode.removeChild(credit_element); | ||
| } | ||
| credit_element = null; | ||
| } |
| /* global global */ | ||
| import { expect } from "chai"; | ||
| import { init, destroy } from "./iframe-credit.js"; | ||
| describe("iframe-credit", function () { | ||
| beforeEach(function () { | ||
| // Clean up any existing credit elements | ||
| destroy(); | ||
| const existing_credit = document.querySelector(".flourish-credit"); | ||
| if (existing_credit) existing_credit.remove(); | ||
| // Create footer container for DOM insertion tests | ||
| const footer = document.createElement("div"); | ||
| footer.id = "fl-layout-footer-container"; | ||
| document.body.appendChild(footer); | ||
| }); | ||
| afterEach(function () { | ||
| // Clean up global | ||
| delete global.Flourish; | ||
| // Clean up DOM | ||
| destroy(); | ||
| const footer = document.getElementById("fl-layout-footer-container"); | ||
| if (footer) footer.remove(); | ||
| }); | ||
| describe("shouldShowCredit", function () { | ||
| it("should show credit when show_internal_credit is true", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| }, | ||
| }; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| expect(credit).to.not.be.null; | ||
| }); | ||
| it("should not show credit when conditions are not met", function () { | ||
| const scenarios = [ | ||
| { Flourish: { metadata: { show_internal_credit: false } } }, | ||
| { Flourish: { metadata: {} } }, | ||
| { Flourish: undefined }, | ||
| ]; | ||
| scenarios.forEach((scenario) => { | ||
| global.Flourish = scenario.Flourish; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| expect(credit).to.be.null; | ||
| destroy(); | ||
| }); | ||
| }); | ||
| it("should append credit after footer container", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| }, | ||
| }; | ||
| init(); | ||
| const footer = document.getElementById("fl-layout-footer-container"); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| expect(footer.nextSibling).to.equal(credit); | ||
| }); | ||
| }); | ||
| describe("getCreditTextAndUrl", function () { | ||
| it("should return server-provided credit text and URL when available", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| credit_text: "Made with Flourish • Create a parliament chart", | ||
| credit_url: "https://flourish.studio/parliament", | ||
| }, | ||
| }; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| const link = credit.querySelector("a"); | ||
| expect(link.href).to.include("https://flourish.studio/parliament"); | ||
| expect(credit.textContent).to.include("Create a parliament chart"); | ||
| }); | ||
| it("should fall back to default text and URL when metadata missing", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| }, | ||
| }; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| const link = credit.querySelector("a"); | ||
| expect(link.href).to.include("https://flourish.studio"); | ||
| expect(credit.textContent).to.include("Made with Flourish"); | ||
| expect(credit.textContent).to.include("Create your own"); | ||
| }); | ||
| }); | ||
| describe("createCreditElement", function () { | ||
| afterEach(function () { | ||
| destroy(); | ||
| }); | ||
| it("should create correct DOM structure and attributes", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| }, | ||
| }; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| const link = credit.querySelector("a"); | ||
| expect(credit).to.not.be.null; | ||
| expect(credit.tagName).to.equal("DIV"); | ||
| expect(credit.getAttribute("class")).to.equal("flourish-credit"); | ||
| expect(link.getAttribute("target")).to.equal("_blank"); | ||
| expect(link.getAttribute("rel")).to.equal("noopener noreferrer"); | ||
| }); | ||
| it("should split text with bullet into prefix and CTA", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| credit_text: "Made with Flourish • Create your own", | ||
| }, | ||
| }; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| const spans = credit.querySelectorAll("span"); | ||
| expect(spans.length).to.equal(2); | ||
| expect(spans[0].textContent).to.include("Made with Flourish"); | ||
| expect(spans[0].textContent).to.include("•"); | ||
| expect(spans[1].textContent).to.equal("Create your own"); | ||
| }); | ||
| it("should handle text without bullet as CTA only", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| credit_text: "Create your own visualization", | ||
| }, | ||
| }; | ||
| init(); | ||
| const credit = document.querySelector(".flourish-credit"); | ||
| const spans = credit.querySelectorAll("span"); | ||
| expect(spans.length).to.equal(1); | ||
| expect(spans[0].textContent).to.equal("Create your own visualization"); | ||
| }); | ||
| it("should set link href with UTM parameters and aria-label", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| credit_url: "https://flourish.studio/", | ||
| credit_text: "Create your own", | ||
| }, | ||
| }; | ||
| init(); | ||
| const link = document.querySelector(".flourish-credit a"); | ||
| expect(link.href).to.include("https://flourish.studio/"); | ||
| expect(link.href).to.include("utm_source=embed"); | ||
| expect(link.href).to.include("utm_campaign=visualisation"); | ||
| expect(link.getAttribute("aria-label")).to.equal( | ||
| "Create your own (opens in new tab)", | ||
| ); | ||
| }); | ||
| it("should not create duplicate on second call", function () { | ||
| global.Flourish = { | ||
| metadata: { | ||
| show_internal_credit: true, | ||
| }, | ||
| }; | ||
| init(); | ||
| init(); | ||
| const credits = document.querySelectorAll(".flourish-credit"); | ||
| expect(credits.length).to.equal(1); | ||
| }); | ||
| }); | ||
| }); |
+6
-6
| { | ||
| "name": "@flourish/layout", | ||
| "type": "module", | ||
| "version": "8.7.3", | ||
| "version": "8.8.0", | ||
| "private": false, | ||
@@ -40,3 +40,3 @@ "description": "Flourish module to control layouts", | ||
| "uglify-js": "^3.19.3", | ||
| "@flourish/cypress": "^9.2.0", | ||
| "@flourish/cypress": "^9.3.0", | ||
| "@flourish/mocha-env": "^1.0.0" | ||
@@ -49,7 +49,7 @@ }, | ||
| "file-saver-es": "^2.0.5", | ||
| "@flourish/interpreter": "^9.2.0", | ||
| "@flourish/formatters": "^2.3.0", | ||
| "@flourish/pocket-knife": "^2.3.0", | ||
| "@flourish/formatters": "^2.4.0", | ||
| "@flourish/number-formatter": "^2.0.0", | ||
| "@flourish/utils-color": "^2.0.0" | ||
| "@flourish/utils-color": "^2.0.0", | ||
| "@flourish/interpreter": "^9.3.0", | ||
| "@flourish/pocket-knife": "^2.3.0" | ||
| }, | ||
@@ -56,0 +56,0 @@ "exports": "./src/index.js", |
+16
-0
@@ -0,2 +1,18 @@ | ||
| # 8.8.0 | ||
| * Change the default formatting for floating point numbers to remove non-significant trailing zeros while keeping two-decimal rounding | ||
| # 8.7.6 | ||
| * Added before:browser:launch event to cypress to enable cpu webGL rendering in CI | ||
| # 8.7.5 | ||
| * Add cacheable_for_standalone_downloads to URL fields for standalone HTML downloads | ||
| # 8.7.4 | ||
| * Renders a localized Flourish credit bar after the footer | ||
| # 8.7.3 | ||
| * Renders a localized Flourish credit bar after the footer when Flourish.metadata.show_internal_credit is true | ||
| * New tests cover show/hide logic, metadata fallbacks, DOM placement, and duplicate guarding | ||
| # 8.7.3 | ||
| * Font loading should now work with relative as well as absolute URLs | ||
@@ -3,0 +19,0 @@ |
+3
-0
@@ -37,2 +37,3 @@ - Layout | ||
| width: half | ||
| cacheable_for_standalone_downloads: true | ||
| show_if: background_image_enabled | ||
@@ -646,2 +647,3 @@ - property: background_image_size | ||
| type: url | ||
| cacheable_for_standalone_downloads: true | ||
| show_if: header_logo_enabled | ||
@@ -865,2 +867,3 @@ - property: header_logo_alt | ||
| width: half | ||
| cacheable_for_standalone_downloads: true | ||
| show_if: footer_logo_enabled | ||
@@ -867,0 +870,0 @@ - property: footer_logo_src_light |
+2
-0
@@ -13,2 +13,3 @@ import { createScreenshotSVG } from "@flourish/pocket-knife"; | ||
| import { remToPx } from "../lib/utils.js"; | ||
| import * as iframeCredit from "../lib/iframe-credit.js"; | ||
@@ -353,2 +354,3 @@ // Globals ------------------------------------------------------ | ||
| createOverlay(); | ||
| iframeCredit.init(); | ||
@@ -355,0 +357,0 @@ return { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
447503
4.11%27
8%8573
4.83%Updated
Updated