UNPKG

@nitrogenbuilder/client

Version:

Nitrogen Builder JS Client

777 lines (656 loc) 19.1 kB
// 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 }