-
Notifications
You must be signed in to change notification settings - Fork 26
WordPress Integration
Adam Doe edited this page Dec 4, 2024
·
41 revisions
These docs attempt to share how to create the editor in WordPress.
Click to expand package.json
{
"private": true,
"scripts": {
"start": "webpack serve --mode development",
"build": "npx webpack --mode production; cd ../../../; gulp contrib; cd contrib/widgets/openVizWrapper"
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@babel/preset-react": "^7.6.3",
"babel-loader": "^8.0.6",
"core-js": "^3.8.3",
"css-loader": "^6.8.1",
"eslint": "^7.16.0",
"eslint-config-airbnb-typescript": "12.0.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "^6.1.0",
"html-webpack-plugin": "^5.3.1",
"husky": "^8.0.3",
"lint-staged": "^13.2.1",
"mini-svg-data-uri": "^1.2.3",
"papaparse": "^5.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^1.3.0",
"terser-webpack-plugin": "^5.1.1",
"url-loader": "^4.1.1",
"webpack": "^5.94.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^5.0.4",
"whatwg-fetch": "^3.6.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"./packages/*/src/**/*.{js,jsx,ts,tsx}": [
"eslint --config .eslintrc.js"
]
}
}
Click to expand webpack.config.js
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = (env, { mode }) => ({
mode,
entry: './src/index.js',
devtool: mode === 'development' ? 'inline-source-map' : false,
performance: {
hints: mode === 'development' ? false : 'error',
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
plugins: [
// Used for widget loader
new HtmlWebpackPlugin({
templateParameters: (compilation, assets, assetTags, options) => {
return {
compilation,
webpackConfig: compilation.options,
htmlWebpackPlugin: {
tags: assetTags,
files: assets,
options,
},
fileName: assets.js[0],
mode: mode,
};
},
template: './src/index.html',
inject: false, // Don't auto inject, we custom inject with the template
}),
// SSI used inside WCMS for maps
new HtmlWebpackPlugin({
filename: 'ssi.html',
template: './src/ssi.html',
publicPath: '/TemplatePackage/contrib/widgets/openVizWrapper/dist/',
inject: 'body',
}),
],
resolve: {
alias: {
react: path.resolve('./node_modules/react'), // https://github.com/facebook/react/issues/13991 - Needed or else you won't be able to link a package locally
},
},
stats: 'normal',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: mode === 'development' ? '/' : '/TemplatePackage/contrib/widgets/openVizWrapper/dist/', // Needed for IE11
filename: '[name].js',
environment: {
arrowFunction: false,
bigIntLiteral: false,
const: false,
destructuring: false,
dynamicImport: false,
forOf: false,
module: false,
},
clean: true,
},
devServer: {
open: true,
overlay: {
warnings: false,
errors: true,
},
},
module: {
rules: [
{
test: /\.(png|jp(e*)g|gif)$/,
use: [
{
loader: 'url-loader',
options: {
name: 'images/[name].[ext]',
},
},
],
},
{
exclude: [
/node_modules/,
// Due to symlinking, we have to explicitly exclude these files or Babel will try to parse them.
/cdcchart.js/,
/cdcmap.js/,
/cdceditor.js/,
/cdcdashboard.js/,
/cdcdatabite.js/,
/cdcwafflechart.js/,
/cdcmarkupinclude.js/,
],
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: '3.8',
targets: {
browsers: ['IE 11'],
},
},
],
'@babel/preset-react',
],
},
},
},
{
test: /\.(sa|sc|c)ss$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader',
],
},
{
test: /\.svg$/i,
use: [
{
loader: 'url-loader',
options: {
generator: (content) => svgToMiniDataURI(content.toString()),
},
},
],
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
},
});
Click to expand index.js
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import React, { StrictMode } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import Wrapper from './Wrapper';
const loadViz = () => {
const vizContainers = Array.from(document.querySelectorAll('.wcms-viz-container'));
vizContainers.forEach((container) => {
// Remove existing if there is one so you can start fresh
unmountComponentAtNode(container);
// Grab data attributes from the container we're going to be rendering inside of and set defaults.
let {
configUrl: relativePath = null,
host: hostName = null,
standalone = false,
language = 'en',
config = null,
editor: isEditor = false,
} = container.dataset;
let constructedURL = null;
let sharePath = container.getAttribute('data-sharepath');
//If we are not in the context of syndication, use the current host, not the data-host attribute value
if (!document.body.classList.contains('syndicated-content')) {
hostName = location.host;
}
// Transform values to type boolean
standalone = standalone === 'true';
// Only allow URL properties if we're running this in standalone mode (widget loader or development environment.)
if (true === standalone) {
const params = new URLSearchParams(window.location.search);
// Set Editor Flag
if ('true' === params.get('editor')) {
isEditor = true;
}
let queryStringRelativePath = params.get('configUrl');
let queryStringHostName = params.get('host');
let queryStringSharePath = params.get('sharePath');
let queryStringConfigURL = `https://` + queryStringHostName + queryStringRelativePath;
// Config file load method: URL parameter
if (queryStringHostName && queryStringRelativePath) {
const configURLObject = new URL(queryStringConfigURL);
// We can load URLs this way from either cdc.gov or localhost for local development.
if (true === configURLObject.hostname.endsWith('cdc.gov') || 'localhost' === configURLObject.hostname) {
constructedURL = queryStringConfigURL;
} else {
const errorMsg = new Error(
'Invalid JSON file provided to URL query. Must be from cdc.gov or localhost.'
);
throw errorMsg;
}
}
}
// If we received a config instead of the URL
if ('string' === typeof config) {
config = JSON.parse(config);
}
if (null === config && null !== relativePath) {
constructedURL = `https://` + hostName + relativePath;
try {
const configURLObject = new URL(constructedURL);
configURLObject.protocol = window.location.protocol;
constructedURL = configURLObject.toString();
} catch (err) {
new Error(err);
}
}
if (constructedURL && window.hasOwnProperty('CDC') && standalone) {
initMetrics(constructedURL);
}
render(
<StrictMode>
<Wrapper
language={language}
configURL={constructedURL}
config={config}
isEditor={isEditor}
sharePath={sharePath}
/>
</StrictMode>,
container
);
});
};
// Assign to CDC object for external use
window.CDC_Load_Viz = loadViz;
// Call on load
if (document.readyState !== 'loading') {
loadViz();
} else {
document.addEventListener('DOMContentLoaded', function () {
loadViz();
});
}
Click to expand wrapper.s
import React, { useEffect, useState, useCallback, Suspense } from 'react'
import 'whatwg-fetch'
import './styles.scss'
import cdcLogo from './cdc-hhs.svg'
const CdcMap = React.lazy(() => import('@cdc/map'));
const CdcChart = React.lazy(() => import('@cdc/chart'));
const CdcEditor = React.lazy(() => import('@cdc/editor'));
const CdcDashboard = React.lazy(() => import('@cdc/dashboard'));
const CdcDataBite = React.lazy(() => import('@cdc/data-bite'));
const CdcWaffleChart = React.lazy(() => import('@cdc/waffle-chart'));
const CdcMarkupInclude = React.lazy(() => import('@cdc/markup-include'));
const Loading = ({viewport = "lg"}) => {
return (
<section className="loading">
<div className={`la-ball-beat la-dark ${viewport}`}>
<div />
<div />
<div />
</div>
</section>
)
}
const Wrapper = ({configURL, language, config: configObj, isEditor, hostname, sharePath}) => {
const [ config, setConfig ] = useState(configObj)
const [ type, setType ] = useState(null)
const metricsCall = useCallback((type, url) => {
const s = window.s || {}
if(true === s.hasOwnProperty('tl')) {
let newObj = {...s}
newObj.pageURL = window.location.href
newObj.linkTrackVars = "pageURL";
newObj.linkURL = url // URL We are navigating to
s.tl( true, type, null, newObj )
}
})
const iframeCheck = () => {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
const navigationHandler = useCallback((urlString = '') => {
// Abort if value is blank
if(0 === urlString.length) {
throw Error("Blank string passed as URL. Navigation aborted.");
}
// Make sure this isn't loading through an iFrame.
const inIframe = iframeCheck();
// Determine if link is a relative hash link
const isHashLink = urlString.startsWith('#');
// Smooth scrolling for hash links on the same page as the map
if(true === isHashLink && false === inIframe) {
let hashName = urlString.substr(1);
let scrollSection = window.document.querySelector(`*[id="${hashName}"]`) || window.document.querySelector(`*[name="${hashName}"]`)
if(scrollSection) {
scrollSection.scrollIntoView({
behavior: 'smooth'
})
return true;
} else {
throw Error("Internal hash link detected but unable to find element on page. Navigation aborted.");
}
}
// Metrics Call
const extension = urlString.substring( urlString.lastIndexOf( '.' ) + 1 )
const s = window.s || {}
let metricsParam = 'e';
if ( s.hasOwnProperty('linkDownloadFileTypes') && s.linkDownloadFileTypes.includes(extension) ) {
metricsParam = 'd'; // Different parameter for downloads
}
let urlObj;
// If we're not loading through iframe (ex: widget loader)
if(false === inIframe) {
// Insert proper base for relative URLs
const parentUrlObj = new URL(window.location.href);
// Only insert a dynamic base if this is on a CDC.gov page, regardless of environment.
// This prevents security concerns where a party could embed a CDC visualization on their own site and have the relative URLs go to their own content making it look like its endorsed by the CDC.
let urlBase = parentUrlObj.host.endsWith('cdc.gov') ? parentUrlObj.origin : 'https://www.cdc.gov/';
urlObj = new URL(urlString, urlBase);
} else {
urlObj = new URL(urlString);
}
// Set the string to the newly constructed string.
urlString = urlObj.toString();
// Don't make a metrics call if it's a link to cdc.gov and does not have a download extension (ex. pdf) or if we're inside the editor.
if( false === ( 'e' === metricsParam && urlString.includes('cdc.gov') ) && false === isEditor ) {
metricsCall(metricsParam, urlString);
}
// Open constructed link in new tab/window
window.open(urlString, '_blank');
})
useEffect(() => {
if(null === configURL) {
console.warn('No configuration URL detected.');
return;
}
const grabConfigObj = async () => {
try {
const response = await fetch(configURL);
const data = await response.json();
let tempConfigObj = {language, ...data}
setConfig(tempConfigObj);
setLoading(false);
} catch (err) {
new Error(err)
}
};
grabConfigObj();
}, [configURL]);
useEffect(() => {
if(config && config.hasOwnProperty('type')) {
setType(config.type)
}
}, [config])
// WCMS Admin
if(isEditor && config) {
// This either passes an existing config or starts with a blank editor
return (
<Suspense fallback={<Loading />}>
<CdcEditor config={config} hostname={hostname} sharepath={sharePath} />
</Suspense>)
}
// Standalone mode when you run `npm run start` just so it isn't blank
if(!config && !configURL) {
return (<Suspense fallback={<Loading />}>
<CdcEditor hostname={hostname} sharepath={sharePath} />
</Suspense>)
}
switch (type) {
case 'map':
return (
<Suspense fallback={<Loading />}>
<CdcMap config={config} hostname={hostname} navigationHandler={navigationHandler} logo={cdcLogo} />
</Suspense>
)
case 'chart':
return (
<Suspense fallback={<Loading />}>
<CdcChart config={config} hostname={hostname} />
</Suspense>
)
case 'dashboard':
return (
<Suspense fallback={<Loading />}>
<CdcDashboard config={config} hostname={hostname} />
</Suspense>
)
case 'data-bite':
return (
<Suspense fallback={<Loading />}>
<CdcDataBite config={config} hostname={hostname} />
</Suspense>
)
case 'waffle-chart':
return (
<Suspense fallback={<Loading />}>
<CdcWaffleChart config={config} hostname={hostname} />
</Suspense>
)
case 'markup-include':
return (
<Suspense fallback={<Loading />}>
<CdcMarkupInclude config={config} hostname={hostname} />
</Suspense>
)
default:
return <Loading />
}
}
export default Wrapper
Click to expand listener.js
jQuery( document ).ready( function( $ ) {
var $temporaryInput = $(".cdc-viz-editor-hidden-temp-input");
var $persistedInput = $(".cdc-viz-editor-hidden-input");
var $vizContainer = $(".wcms-viz-container");
// Store the data in the temporary input every time the viz sends an event from it's editor.
window.addEventListener('updateVizConfig', function(e) {
$temporaryInput.val(e.detail);
updateDetailsOnScreen( e.detail );
}, false)
function updateDetailsOnScreen( config ) {
//Get config object
var configObject;
if ( undefined !== config ) {
configObject = JSON.parse( config );
} else if ( undefined !== vizVariables.config && vizVariables.config.length > 0 ) {
configObject = JSON.parse( vizVariables.config );
} else { //New viz
return false;
}
const getPropHtml = (pathArr, title, defaultVal = 'Not Set', alwaysShow = true) => {
let val = getConfigProp( pathArr )
if ( ( undefined === val ) && alwaysShow ) {
val = defaultVal;
}
return undefined !== val ? getHtml( title, val) : '';
}
const friendlies = {
'us': 'U.S.',
'world': 'World',
'chart': 'Chart',
'equalnumber': 'Equal Number',
'equalinterval': 'Equal Interval',
'category': 'Categorical',
'data': 'Data',
'navigation': 'Navigation',
'map': 'Map'
}
const getHtml = (title,val) => {
if ( friendlies.hasOwnProperty( val ) ) {
val = friendlies[val];
}
return '<div>' + title + ':</div><div>' + val + '</div>';
}
const getConfigProp = (pathArr) => {
return pathArr.reduce((configObject, key) =>
(configObject && configObject[key] !== 'undefined') ? configObject[key] : undefined, configObject);
}
let summaryInfo = '';
const vizType = configObject.type;
if ( 'chart' === vizType ) { //Handle Charts
summaryInfo += getPropHtml( ['title'], 'Title', 'Not Set' );
summaryInfo += getPropHtml( ['type'], 'Type', 'Not Set' );
summaryInfo += getPropHtml( ['visualizationType'], 'Sub Type', 'Not Set' );
if ( configObject.hasOwnProperty('series') && Array.isArray( configObject.series ) && configObject.series.length ) {
let dataSeries = configObject.series.map(a => a.dataKey);
summaryInfo += getHtml( 'Data Series', dataSeries.join(', ') );
}
summaryInfo += getHtml('Number of Rows', configObject.data.length);
} else if ( 'map' === vizType ) { //Handle Maps
summaryInfo += getPropHtml( ['general', 'title'], 'Title', 'Not Set' );
summaryInfo += getPropHtml( ['type'], 'Type', 'Not Set' );
summaryInfo += getPropHtml( ['general', 'type'], 'Sub Type', 'Not Set' );
summaryInfo += getPropHtml( ['general', 'geoType'], 'Geo Type', 'Not Set' );
var displayAsHex = getConfigProp( ['general', 'displayAsHex'] );
if ( displayAsHex ) {
summaryInfo += getHtml('Is Hex Tile', 'Yes');
}
summaryInfo += getHtml('Number of Rows', configObject.data.length);
summaryInfo += getPropHtml( ['legend', 'type'], 'Classification Type', 'Not Set' );
summaryInfo += getPropHtml( ['legend', 'numberOfItems'], 'Number of Classes (Legend Items)', 'Not Set', true );
}
$('.viz-details').html( summaryInfo );
}
function saveVizData() {
// Apply the temporary value
window.SAVEVIZ = true
$persistedInput.val( $temporaryInput.val() );
$vizContainer.attr('data-config', $persistedInput.val());
$('body').css('overflow', 'auto')
}
function cancelVizEdit() {
// This is so this function can work every time the modal closes - ESC button or clicking outside the modal.
if(window.SAVEVIZ) {
return;
}
// Blank out temporary value
$temporaryInput.val('');
$('body').css('overflow', 'auto');
var dataVal = $persistedInput.val()
if(dataVal) {
// Revert to persisted configuration behind the scenes
$vizContainer.attr('data-config', dataVal);
window.CDC_Load_Viz();
}
}
var vizModal = CDC_Modal.create({
title: 'Visualization Editor',
// data-viz uses the older modal center method 'modal-transform'
classNames: ['cdc-cove-editor modal-transform'],
closed: true,
fullbody: true,
fullscreen: true,
onOpen: function() {
$('body').css('overflow', 'hidden'); // Disable scrolling
window.SAVEVIZ = false;
},
onClose: cancelVizEdit,
message: $vizContainer,
buttons: [
{
text: 'OK',
click: saveVizData,
className: 'button-primary'
},
{
text: 'Cancel',
className: 'button-secondary'
}
]
})
$( '.open-viz-editor-anchor').on( 'click', function( e ) {
e.preventDefault();
vizModal.open();
} );
updateDetailsOnScreen();
return false;
});