@nitrogenbuilder/client
Version:
Nitrogen Builder JS Client
777 lines (656 loc) • 19.1 kB
JavaScript
// TODO: 2 things:
// 1. Compile nitrogen-client.js to a CDN and use that instead
// 2. Make a nextjs plugin that automatically injects this script properly
// 3. Make an astro integration that automatically injects this script properly
const isIframe = window !== window.parent;
const isNitrogen = window.location.href.includes('nitrogen-builder=true');
if (isIframe && isNitrogen) {
initNitrogenClient();
}
function initNitrogenClient() {
console.log('initNitrogenClient');
function loadCSS() {
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href =
'https://cdn.jsdelivr.net/npm/@nitrogenbuilder/client@0.2/nitrogen-client.css';
document.head.appendChild(css);
}
loadCSS();
//#region State
window.nitrogen = {
// Send these messages to the parent window (Nitrogen Builder)
// once the parent window tells us it's ready.
queuedMessages: [],
// Has parent window (Nitrogen Builder) told us it's ready yet?
parentInit: false,
// List of screen usages that are currently being tracked
// We are using it here to keep track of the current module that the bobs are over
_screenUsage: [],
get screenUsage() {
return this._screenUsage;
},
set screenUsage(screenUsage) {
console.log('setting screenUsage', screenUsage);
this._screenUsage = screenUsage;
},
// List of users that are currently connected to the page
_users: {},
get users() {
return this._users;
},
set users(users) {
console.log('setting users', users);
// Get users that were deleted
const deletedUsers = Object.keys(this._users).filter(
(socketId) => !users[socketId]
);
// Get users that were added
const addedUsers = Object.keys(users).filter(
(socketId) => !this._users[socketId]
);
console.log('mySocketId', mySocketId);
console.log('deletedUsers', deletedUsers);
console.log('addedUsers', addedUsers);
deletedUsers.forEach((socketId) => {
if (socketId === mySocketId) return;
console.log('deleteCurrBobForUser', socketId);
deleteCurrBobForUser(this._users[socketId]);
});
addedUsers.forEach((socketId) => {
if (socketId === mySocketId) return;
console.log('createCurrBobForUser', socketId);
createCurrBobForUser(users[socketId]);
});
this._users = users;
},
};
// TODO: This should change depending on what we are editing in nitrogen (page, header, footer, etc.)
const nitrogenLocation = 'page';
/**
* Store current mod element for use in resize events, scroll events etc to send to parent frame.
*/
let currentModuleEl = null;
let mySocketId = '';
let lastScrollX = 0;
let lastScrollY = 0;
let lastMousePositionX = 0;
let lastMousePositionY = 0;
function getOS() {
let userAgent = window.navigator.userAgent.toLowerCase(),
macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i,
windowsPlatforms = /(win32|win64|windows|wince)/i,
iosPlatforms = /(iphone|ipad|ipod)/i,
os = null;
if (macosPlatforms.test(userAgent)) {
os = 'macos';
} else if (iosPlatforms.test(userAgent)) {
os = 'ios';
} else if (windowsPlatforms.test(userAgent)) {
os = 'windows';
} else if (/android/.test(userAgent)) {
os = 'android';
} else if (!os && /linux/.test(userAgent)) {
os = 'linux';
}
return os;
}
const os = getOS();
let keyListeners = [];
//#endregion
//#region General Utilities
function scrollToModule(el) {
if (!el) return;
const rect = el.getBoundingClientRect();
const parentRect = el.parentElement.getBoundingClientRect();
window.scrollTo({
top: rect.top - parentRect.top,
left: rect.left - parentRect.left,
behavior: 'instant',
});
}
//#endregion
//#region Window Listeners
window.addEventListener(
'message',
(event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
return;
}
if (data.channel !== 'nitrogen-builder') {
return;
}
if (data.type === 'init') {
window.nitrogen.parentInit = true;
console.log('initializing');
window.dispatchEvent(new CustomEvent('nitrogen-initialized'));
window.nitrogen.queuedMessages.forEach((message) => {
postMessage(message);
});
mySocketId = data.socketId;
return;
}
if (data.type === 'selectModule') {
const el = document.getElementById(data.moduleId);
selectModule(el);
scrollToModule(el);
return;
}
if (data.type === 'keydown') {
// don't add duplicate key listener
if (
keyListeners.some((keyListener) => {
return (
keyListener.keys.every((k) => data.keys.includes(k)) &&
keyListener.modifiers === data.modifiers
);
})
) {
return;
}
keyListeners.push({
keys: data.keys,
modifiers: data.modifiers,
});
return;
}
},
false
);
function postMessage(data) {
const message = JSON.stringify({
channel: 'nitrogen-builder',
...data,
});
if (!window.nitrogen.parentInit) {
window.nitrogen.queuedMessages.push(data);
return;
}
window.parent.postMessage(message, '*');
}
// Take registerModules from project and forward to nitrogen builder
window.addEventListener('nitrogen-modules', (event) => {
const data = event.detail;
postMessage({
type: 'nitrogen-modules',
modules: data.modules ?? [],
});
});
// TODO: ensure this happens after re-render of nitrogen elements
// Sometimes this event fires before the elements are rendered and causes the newly rendered elements to not have listeners
window.addEventListener('nitrogen-page-data', (event) => {
setNitrogenElementsListeners();
// TODO: This is a hack to ensure that the listeners are set after the elements are rendered
setTimeout(() => {
setNitrogenElementsListeners();
}, 1000);
});
//#endregion
//#region Element Utilities
/**
*
* @param {HTMLElement} el
* @returns
*/
function getModDetailsFromEl(el) {
if (!el) {
return;
}
const boundingRect = el.getBoundingClientRect();
return {
id: el.id,
name: el.dataset.nitrogenModule,
position: {
// x: el.offsetLeft - window.scrollX + el.offsetParent.offsetLeft,
// y: el.offsetTop - window.scrollY + el.offsetParent.offsetTop,
x: boundingRect.left + window.scrollX,
y: boundingRect.top + window.scrollY,
},
size: {
width: boundingRect.width,
height: boundingRect.height,
},
};
}
function selectModule(el) {
if (!el) {
currentModuleEl = null;
updateCurrBob({ display: 'none' });
return postMessage({
type: 'selectModule',
module: null,
});
}
currentModuleEl = el;
const modDetails = getModDetailsFromEl(el);
updateCurrBob({
x: modDetails?.position.x,
y: modDetails?.position.y,
height: modDetails?.size.height,
width: modDetails?.size.width,
display: 'block',
});
postMessage({
type: 'selectModule',
module: modDetails,
});
}
function hoverModule(el) {
if (!el) {
updateBob({
display: 'none',
});
return;
}
const offset = el.getBoundingClientRect();
updateBob({
display: 'block',
x: offset.left + window.scrollX,
y: offset.top + window.scrollY,
width: el.offsetWidth,
height: el.offsetHeight,
});
}
function recursiveGetNitrogenElement(el) {
if (!el.classList.contains('nitrogen-module')) {
if (!el.parentElement) {
return null;
}
return recursiveGetNitrogenElement(el.parentElement);
}
return el;
}
//#endregion
//#region Element Listeners
function onElementClick(e, a) {
// if a tag, prevent default
if (e.target.tagName === 'A') {
e.preventDefault();
}
if (!e.computedRootEvent) {
const el = recursiveGetNitrogenElement(e.target);
selectModule(el);
}
e.computedRootEvent = true;
}
function onElementMouseMove(e) {
lastMousePositionX = e.clientX;
lastMousePositionY = e.clientY;
if (!e.computedRootEvent) {
const el = recursiveGetNitrogenElement(e.target);
hoverModule(el);
}
e.computedRootEvent = true;
}
function onElementMouseLeave(e) {
if (!e.toElement) {
hoverModule(null);
}
}
function setNitrogenElementsListeners() {
// get all elements with nitrogenLocation === 'page'
const nitrogenElements = document.querySelectorAll(
`[data-nitrogen-location="${nitrogenLocation}"] .nitrogen-module`
);
// add event listeners to all elements with nitrogenLocation === 'page'
nitrogenElements.forEach((el) => {
if (el.hasAttribute('data-nitrogen-listener')) {
return;
}
//console.log('setting listeners on new element', el);
el.setAttribute('data-nitrogen-listener', true);
el.addEventListener('click', onElementClick);
el.addEventListener('mousemove', onElementMouseMove);
el.addEventListener('mouseleave', onElementMouseLeave);
});
}
setNitrogenElementsListeners();
//#endregion
//#region Document Listeners
function setDocListeners() {
document.addEventListener('mousemove', onDocMouseMove);
document.documentElement.addEventListener('mouseleave', onDocMouseLeave);
document.addEventListener('click', onDocClick);
// window.addEventListener('resize', onWindowResize);
document.addEventListener('scroll', onDocScroll);
document.addEventListener('keydown', onDocKeyDown);
}
function onDocScroll(e) {
// console.log(e);
lastMousePositionX -= window.scrollX;
lastMousePositionY -= window.scrollY;
lastScrollX = window.scrollX;
lastScrollY = window.scrollY;
lastMousePositionX += window.scrollX;
lastMousePositionY += window.scrollY;
// console.log(lastMousePositionX, lastMousePositionY);
}
function onDocMouseLeave(e) {
hoverModule(null);
}
function onDocMouseMove(e) {
if (e.computedRootEvent) {
return;
}
hoverModule(null);
}
function onDocClick(e) {
// if a tag, prevent default
if (e.target.tagName === 'A') {
e.preventDefault();
}
if (!e.computedRootEvent) {
selectModule(null);
}
}
function onDocKeyDown(event) {
keyListeners.forEach((keyListener) => {
const keys = keyListener.keys;
const modifiers = keyListener.modifiers;
// check if one of the key is part of the ones we want
if (keys.some((key) => event.key === key)) {
if (modifiers) {
if (
modifiers === 'ctrl' &&
(event.ctrlKey || (os === 'macos' && event.metaKey))
) {
if (keys.length === 1 && keys[0] === 's') {
event.preventDefault();
}
postMessage({
type: 'keydown',
keys,
modifiers,
});
} else if (modifiers === 'shift' && event.shiftKey) {
postMessage({
type: 'keydown',
keys,
modifiers,
});
} else if (modifiers === 'alt' && event.altKey) {
postMessage({
type: 'keydown',
keys,
modifiers,
});
} else if (modifiers === 'meta' && event.metaKey) {
postMessage({
type: 'keydown',
keys,
modifiers,
});
}
} else {
postMessage({
type: 'keydown',
keys,
modifiers,
});
}
}
});
}
setDocListeners();
//#endregion
//#region Bob
/**
* Update bob element position, size and/or display
* @param {object} options
* @param {number} [options.x]
* @param {number} [options.y]
* @param {number} [options.width]
* @param {number} [options.height]
* @param {'block'|'none'} [options.display]
*/
function updateBob({ x, y, width, height, display }) {
const bob = document.getElementById('nitrogen-bob');
if (!bob) {
return;
}
if (display !== undefined) {
bob.style.display = display;
}
if (x !== undefined) {
bob.style.left = x + 'px';
}
if (y !== undefined) {
bob.style.top = y + 'px';
}
if (width !== undefined) {
bob.style.width = width + 'px';
}
if (height !== undefined) {
bob.style.height = height + 'px';
}
}
/**
* Update curr bob element position, size and/or display
* @param {object} options
* @param {number} [options.x]
* @param {number} [options.y]
* @param {number} [options.width]
* @param {number} [options.height]
* @param {'block'|'none'} [options.display]
*/
function updateCurrBob({ x, y, width, height, display }) {
const bob = document.getElementById('nitrogen-curr-bob');
const bobBorder = document.getElementById('nitrogen-curr-bob-border');
if (!bob || !bobBorder) {
return;
}
if (display !== undefined) {
bob.style.display = display;
bobBorder.style.display = display;
}
if (x !== undefined) {
bob.style.left = x + 'px';
bobBorder.style.left = x + 'px';
}
if (y !== undefined) {
bob.style.top = y + 'px';
bobBorder.style.top = y + 'px';
}
if (width !== undefined) {
bobBorder.style.width = width + 'px';
}
if (height !== undefined) {
bob.style.height = height + 'px';
bobBorder.style.height = height + 'px';
}
}
/**
* Update curr bob element position, size and/or display
* @param {object} options
* @param {object} [options.user]
* @param {string} [options.user.socketId]
* @param {string} [options.user.cmsId]
* @param {string} [options.user.color]
* @param {number} [options.x]
* @param {number} [options.y]
* @param {number} [options.width]
* @param {number} [options.height]
* @param {'block'|'none'} [options.display]
*/
function updateCurrBobForUser({ user, x, y, width, height, display }) {
const bob = document.getElementById(
`nitrogen-curr-bob-border-${user.socketId}`
);
if (!bob) {
return;
}
if (display !== undefined) {
bob.style.display = display;
}
if (x !== undefined) {
bob.style.left = x + 'px';
}
if (y !== undefined) {
bob.style.top = y + 'px';
}
if (width !== undefined) {
bob.style.width = width + 'px';
}
if (height !== undefined) {
bob.style.height = height + 'px';
}
}
function createBob() {
const bobHtml = /* html */ `
<div id="nitrogen-bob">
<div></div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', bobHtml);
}
createBob();
/**
*
* @param {object} user
* @param {string} user.socketId
* @param {string} user.cmsId
* @param {string} user.color
*/
function createCurrBobForUser(user) {
const bobHtml = /* html */ `
<div id="nitrogen-curr-bob-border-${user.socketId}" class="nitrogen-curr-bob-border nitrogen-curr-bob-border-user" style="--bob-border:${user.color};">
<div class="nitrogen-curr-bob-border-content">${user.cmsId}</div>
<div class="nitrogen-curr-bob-border-border"></div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', bobHtml);
}
function deleteCurrBobForUser(user) {
const bob = document.getElementById(
`nitrogen-curr-bob-border-${user.socketId}`
);
if (bob) {
bob.remove();
}
}
function createCurrBob() {
const bobHtml = /* html */ `
<div id="nitrogen-curr-bob-border">
<div></div>
</div>
<div id="nitrogen-curr-bob">
<div id="nitrogen-curr-bob-actions">
<div
id="nitrogen-curr-bob-move-module-up"
>
<svg style="height:1em;fill:currentColor;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"/></svg>
</div>
<div
id="nitrogen-curr-bob-move-module-down"
>
<svg style="height:1em;fill:currentColor;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"/></svg>
</div>
<div
id="nitrogen-curr-bob-clone-module"
>
<svg style="height:1em;fill:currentColor;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M288 448H64V224h64V160H64c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384H288v64zm-64-96H448c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224c-35.3 0-64 28.7-64 64V288c0 35.3 28.7 64 64 64z"/></svg>
</div>
<div
id="nitrogen-curr-bob-delete-module"
>
<svg style="height:1em;fill:currentColor;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', bobHtml);
const nitrogenCurrBob = document.getElementById('nitrogen-curr-bob');
nitrogenCurrBob.addEventListener('click', (e) => {
e.stopPropagation();
});
document
.getElementById('nitrogen-curr-bob-move-module-up')
.addEventListener('click', (e) => {
e.stopPropagation();
const modDetails = getModDetailsFromEl(currentModuleEl);
postMessage({
type: 'moveModuleUp',
moduleId: modDetails.id,
});
});
document
.getElementById('nitrogen-curr-bob-move-module-down')
.addEventListener('click', (e) => {
e.stopPropagation();
const modDetails = getModDetailsFromEl(currentModuleEl);
postMessage({
type: 'moveModuleDown',
moduleId: modDetails.id,
});
});
document
.getElementById('nitrogen-curr-bob-clone-module')
.addEventListener('click', (e) => {
e.stopPropagation();
const modDetails = getModDetailsFromEl(currentModuleEl);
postMessage({
type: 'duplicateModule',
moduleId: modDetails.id,
});
});
document
.getElementById('nitrogen-curr-bob-delete-module')
.addEventListener('click', (e) => {
e.stopPropagation();
const modDetails = getModDetailsFromEl(currentModuleEl);
postMessage({
type: 'deleteModule',
moduleId: modDetails.id,
});
});
}
createCurrBob();
setInterval(() => {
if (currentModuleEl) {
const modDetails = getModDetailsFromEl(currentModuleEl);
if (!modDetails) return;
updateCurrBob({
display: 'block',
x: modDetails?.position.x,
y: modDetails?.position.y,
height: modDetails?.size.height,
width: modDetails?.size.width,
});
}
Object.values(window.nitrogen.users ?? {}).forEach((user) => {
if (!user.socketId) return;
const screenUsage = window.nitrogen.screenUsage.find(
(usage) => usage.socketId === user.socketId
);
if (!screenUsage) {
updateCurrBobForUser({
user,
display: 'none',
});
return;
}
const split = screenUsage.screen.split('inspector-panel__');
if (split.length !== 2) {
updateCurrBobForUser({
user,
display: 'none',
});
return;
}
const el = document.getElementById(split[1]);
const modDetails = getModDetailsFromEl(el);
updateCurrBobForUser({
user,
display: 'block',
x: modDetails?.position.x,
y: modDetails?.position.y,
height: modDetails?.size.height,
width: modDetails?.size.width,
});
});
}, 100);
//#endregion
}