UNPKG

@pinegrow/piny-vite

Version:

A Vite plugin that implements Piny integration in dev mode.

479 lines (403 loc) 15.1 kB
class PinegrowPhone { constructor() { const _this = this; this.runs_in_piny = false; this.snapTimer = null; this.observer = null; this.observer_enabled = false; this.current_element_observer = null; this.debug = true; this.log('PinyPhone is loaded. Not yet active.') this.id_count = 1; this.id_to_el = {} this.react_key = null; window.addEventListener("message", function(event) { const m = event.data; if(m?.from === 'pinegrow') { switch(m.message) { case 'hello': _this.runs_in_piny = true; _this.sendMessage('hello', {}); break; case 'scroll': document.scrollingElement.scrollTop = m.scrollTop; document.scrollingElement.scrollLeft = m.scrollLeft; break; case 'request_snap': _this.snap(false); break; case 'enable_observer': _this.enableObserver(); break; case 'disable_observer': _this.disableObserver(); break; case 'request_sync_scroll': _this.sendMessage('sync_scroll', { scrollTop: document.scrollingElement.scrollTop, scrollLeft: document.scrollingElement.scrollLeft }) break; case 'observe_element': if(this.current_element_observer) { this.current_element_observer.destroy(); this.current_element_observer = null; } const el = _this.getElementById(m.id); if(el) { this.current_element_observer = new PinegrowElementObserver(el, function(el) { //_this.sendMessage('element_changed', _this.getElementId(el)) _this.snap(true); }) } break; } } }, false); let current_route_url = null; setInterval(function() { if(_this.runs_in_piny) { const href = window.location.pathname + window.location.search + window.location.hash; if (current_route_url !== href) { _this.sendMessage('route_change', href) current_route_url = href; } } }, 1000) } enableObserver() { const _this = this; const config = {attributes: true, childList: true, subtree: true}; const callback = (mutationList, observer) => { if (_this.snapTimer) { clearTimeout(_this.snapTimer) } _this.snapTimer = setTimeout(function () { _this.snap(true) }, 500) }; if(!this.observer) { this.observer = new MutationObserver(callback); } this.observer.observe(document.documentElement, config); this.observer_enabled = true; this.log('Observer is enabled.') } disableObserver() { if(this.observer) { this.observer.disconnect() } this.observer_enabled = false; this.log('Observer is disabled.') } sendMessage(msg, data) { window.parent.postMessage({ from: 'pinegrowPhone', message: msg, data: data }, '*') } log(msg, a) { this.debug && console.log('pinegrowPhone: ' + msg, a) } getReactKey(el) { if(this.react_key) return this.react_key; for (let attr in el) { if (attr.startsWith('__reactFiber')) { this.react_key = attr; return attr; } } } snap(observer) { if(this.snapTimer) { clearTimeout(this.snapTimer) this.snapTimer = null; } const st = Date.now(); this.id_to_el = {} const scroll_top = document.scrollingElement.scrollTop; const scroll_left = document.scrollingElement.scrollLeft; const root = { route: window.location.pathname + window.location.search + window.location.hash, children: [], rect: { x: 0, y: 0, ox: 0, oy: 0 }, sl: scroll_left, st: scroll_top, css: {}, source_observer: observer } function getReactContainer(el) { for (let attr in el) { if (attr.startsWith('__reactContainer')) { return { el: el, f: el[attr], attr: attr }; } } } function findRoot(el) { let c = null; if(c = getReactContainer(el)) { return c; } if(el.children) { for(let i = 0; i < el.children.length; i++) { if(c = findRoot(el.children[i])) { return c; } } } return null; } const app_root = findRoot(document) if(!app_root) { console.log('pgPhone - app root not found'); } let root_fiber = app_root.f; if(root_fiber?.stateNode?.current) { root_fiber = root_fiber.stateNode.current; } this.getReactTree(root_fiber, root, scroll_left, scroll_top); const data = root; data.scrollHeight = document.scrollingElement.scrollHeight; data.scrollWidth = document.scrollingElement.scrollWidth; this.sendMessage('snap', data) this.log(`snap took ${Date.now() - st}ms`, data); if(this.debug && false) { let s = ''; function dolevel(d, prefix) { s += `\n${prefix} ${d.name || d.tag} ${d.source ? `${d.source.file.split('/').pop()}, ${d.source.line}, ${d.source.character}` : '-'}` if(d.debug) { s += ` (${d.debug.rect.x}, ${d.debug.rect.y}, ${d.debug.rect.width}, ${d.debug.rect.height})` } d.children.forEach(function (ch) { dolevel(ch, prefix + '--'); }) } dolevel(data, '') this.log(s) } } getElementById(el_id) { return this.id_to_el[el_id] || null; } getElementId(el) { return el.__pinegrow_id || null; } getReactTree(app_root_f, root, scroll_left, scroll_top) { const _this = this; function firstChildWithNode(f) { let i = 0; while(f) { if(f.stateNode?.getBoundingClientRect) { return f; } f = f.child; i++; if(i === 20) break; } return null; } function walk(f, current) { let add_child_to = current; const fiber_with_state = firstChildWithNode(f) if(!f.elementType && !f.ref && !f.type && !f.stateNode && !f._debugInfo) { //skip this one } else if(fiber_with_state) { const d = _this.doElement(fiber_with_state.stateNode, current, scroll_left, scroll_top, [], f, f === fiber_with_state); if(d) { add_child_to.children.push(d) add_child_to = d; } } if(f.child && f.type !== 'svg') { walk(f.child, add_child_to) } if(f.sibling) { walk(f.sibling, current); } } walk(app_root_f, root) } getComponentNameFromFiber(fiber) { if(!fiber) return null; if(fiber._debugInfo) { return fiber._debugInfo?.name; } if(typeof fiber.type === 'function') { return fiber.type.name; } if(typeof fiber.type === 'object') { if(fiber.type?.render) { return fiber.type.displayName || fiber.type.render.name || 'Anonymous'; } } return null; } getDebugInfo(fiber, ignore_fibers) { if(!fiber) return null; //if(ignore_fibers && ignore_fibers.indexOf(fiber) >= 0) return null; if(fiber._debugInfo) { ignore_fibers.push(fiber) return fiber._debugInfo; } const name = this.getComponentNameFromFiber(fiber) if(name) { return [{ name: name }] } return; while(fiber._debugOwner) { fiber = fiber._debugOwner; const di = this.getDebugInfo(fiber, ignore_fibers); if(di) return di; } } doElement(element, current, scroll_left, scroll_top, parent_fibers, fiber, use_elid) { const rect = element.getBoundingClientRect(); let data = null; let subdata = null; const tag = element.nodeName.toLowerCase(); if(tag === 'script') return; const elid = element.__pinegrow_id || ('el' + (++this.id_count)); element.__pinegrow_id = elid; this.id_to_el[elid] = element; const computedStyle = window.getComputedStyle(element); const isFixedOrSticky = (computedStyle.position === 'fixed' || computedStyle.position === 'sticky'); const css = {}; if(use_elid && isFixedOrSticky) { css.position = computedStyle.position; } if(computedStyle.zIndex !== 'auto') { css['z-index'] = computedStyle.zIndex; } const props = {}; if(typeof fiber.memoizedProps === 'object') { for(let prop in fiber.memoizedProps) { if(prop !== 'className' && prop !== 'children') { let val = fiber.memoizedProps[prop]; if(typeof val !== 'string' && typeof val !== 'number') { val = '...'; } props[prop] = val; } } } if(fiber.key) { props.key = fiber.key.toString(); } let source = null; let _debugSource = fiber._debugSource; if(!_debugSource && fiber._debugOwner?._debugSource) { _debugSource = fiber._debugOwner._debugSource; } if(_debugSource) { source = { line: _debugSource.lineNumber - 1, character: _debugSource.columnNumber - 1, file: _debugSource.fileName }; } const owner_name = fiber._debugOwner ? this.getComponentNameFromFiber(fiber._debugOwner) : null; let debug = null; if(this.debug) { debug = { rect: rect }; } const dis = this.getDebugInfo(fiber, parent_fibers); // Compute final x,y. If 'fixed' or 'sticky', don't subtract parent's offsets. const offsetX = isFixedOrSticky ? rect.x + scroll_left : rect.x + scroll_left - current.rect.ox; const offsetY = isFixedOrSticky ? rect.y + scroll_top : rect.y + scroll_top - current.rect.oy; // We gather all debug info "frames" from the fiber, if any if(dis?.length) { let first = true; dis.forEach((di) => { const el_data = { name: di.name, pgid: (first && use_elid) ? elid : null, rect: { x: first ? offsetX : 0, y: first ? offsetY : 0, w: rect.width, h: rect.height, ox: rect.x + scroll_left, oy: rect.y + scroll_top }, class: element.getAttribute('class'), source: source, children: [], owner: owner_name, st: first ? element.scrollTop : 0, sl: first ? element.scrollLeft : 0, props, css, debug }; first = false; if(!data) { data = el_data; } else { subdata = el_data; current.children.push(subdata); } // push future "frames" into the new object current = el_data; }); } else { // Fallback for normal DOM element data = { tag: tag, pgid: use_elid ? elid : null, rect: { x: offsetX, y: offsetY, w: rect.width, h: rect.height, ox: rect.x + scroll_left, oy: rect.y + scroll_top }, class: element.getAttribute('class'), source: source, children: [], owner: owner_name, sl: element.scrollLeft, st: element.scrollTop, props, css, debug }; } return data; } } class PinegrowElementObserver { constructor(element, on_changed) { const _this = this; this.element = element; let prev_rect = element.getBoundingClientRect(); this.interval = setInterval(function() { const rect = element.getBoundingClientRect(); if(prev_rect.x !== rect.x || prev_rect.y || rect.y || prev_rect.width !== rect.width || prev_rect.height !== rect.height) { on_changed(element); } }, 200) } destroy() { clearInterval(this.interval); } } const pinegrowPhone = new PinegrowPhone();