c a n d l a n d . n e t

JS Widgets

Dusty Candland | | javascript, webpack4

Basic Webpack setup for creating a JS widget w/ a loading script.

Package.json

Include Webpack & Webpack CLI

{
"name": "widget",
"version": "1.0.0",
"description": "Widget widget",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack && cp dist/*.js ../public/js/",
"watch": "webpack --watch"
},
"author": "",
"license": "None",
"devDependencies": {
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2"
}
}

Webpack

Basic config for Webpack 4. We need to output two files, one for the loader, which shouldn't change much and one for the actual widget.

const debug = process.env.NODE_ENV !== 'production';
const Webpack = require('webpack');
const path = require('path');

const commonPlugins = [
new Webpack.DefinePlugin({
WIDGET_HOST: debug ?
JSON.stringify("http://widget.test") :
JSON.stringify("https:/widget.io")
})
];

module.exports = {
entry: {
widget: './src/widget.js',
viewer: './src/viewer.js',
},
devtool: debug ? 'inline-sourcemap' : false,
mode: process.env.NODE_ENV || 'development',
plugins: debug ? commonPlugins : [
...commonPlugins,
// Add production plugins here!
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}
};

Loader

The loaders main job is to load the actual widget script. It's other job is to pass config info to the widget. This can be done with data-X attributes or a JS object.

'use strict';

(function() {
function load_widget(script) {
if (!script.attributes["data-id"]) {
console && console.log("Please put a data-id");
return;
}
const id = script.attributes["data-id"].value;
const pid = script.attributes["data-pid"].value;

const js = document.createElement('script');
js.src = WIDGET_HOST + '/js/viewer.js';
js.onload = function() {
widget.inject(id, pid);
};
script.parentNode.insertBefore(js, script);
}

if (document.currentScript) {
var script = document.currentScript;
} else {
const scripts = document.getElementsByTagName('script');
const index = scripts.length - 1;
var script = scripts[index];
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => { load_widget(script); });
} else {
load_widget(script);
}
})()

Widget

This is the functional part of the widget. This one loads an iframe, but it could manipulate the DOM directly.

'use strict';

window.widget = (function() {

let rmr = {
init: function(id, pid) {
this.id = id;
this.pid = pid;
this.widget = document.getElementById('widget');
if ( !this.widget) {
console && console.log("widget div not found.");
return false;
}
return true;
},

createButton: function() {
var button = document.createElement('button');
button.setAttribute('id', 'btn-widget');
button.innerHTML = "Widget Has It";
button.addEventListener('click', (e) => {
e.preventDefault();
this.createFrame();
});
this.widget.appendChild(button);
},

createFrame: function() {
if ( document.getElementById("widget-view") ) {
return false;
}

var iframe = document.createElement('iframe');
iframe.setAttribute('src', WIDGET_HOST + '/sch/' + this.pid + '/locate');
iframe.setAttribute('class', 'widget-iframe');
iframe.setAttribute('data-id', this.id);
iframe.setAttribute('data-pid', this.pid);
iframe.setAttribute('width', '100%');
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('scrolling', 'auto');
iframe.style.border = 'none';
iframe.style.width = '100%';
iframe.style.height = '375px';
iframe.style.position = 'relative';
iframe.style.overflow = 'scroll';

let view = document.createElement("div");
view.setAttribute("id", "widget-view");

let header = document.createElement("div");
header.setAttribute("id", "widget-header");
header.innerHTML = "Widget";

let close = document.createElement("span");
close.setAttribute("id", "widget-close");

let closeA = document.createElement("a");
closeA.setAttribute("href", "#");
closeA.innerHTML = "CLOSE";
closeA.addEventListener('click', (e) => {
e.preventDefault();
let view = document.getElementById("widget-view");
document.getElementsByTagName("body")[0].removeChild(view);
return false;
});

close.appendChild(closeA);
header.appendChild(close);
view.appendChild(header);
view.appendChild(iframe);

document.getElementsByTagName("body")[0].appendChild(view);
},

injectStyles: function() {
let styles = `
#widget-view {
font-family: sans-serif;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 640px;
height: 400px;
border: solid 5px black;
z-index: 5000;
}
#widget-close a {
color: white;
text-decoration: none;
font-size: 12px;
}
#widget-header {
font-size: 18px;
line-height: 22px;
color: white;
border-bottom: solid 5px black;
height: 20px;
padding: 0 10px;
background-color: #353535;
}
#btn-widget {
border: solid 2px #353535;
background: #ddd;
color: #353535;
font-weight: bold;
font-size: 14px;
border-radius: 4px;
padding: 2px 10px;
cursor: pointer;
}
#widget-close {
float: right;
border-left: solid 5px black;
height: 20px;
padding: 0 10px;
}
`
;
let style = document.createElement("style");
style.innerHTML = styles;
document.getElementsByTagName("body")[0].appendChild(style);
}
};

return {
inject: function(id, pid) {
if (rmr.init(id, pid)) {
rmr.injectStyles();
rmr.createButton();
}
}
};

})();

References

Webmentions

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: