@pinegrow/piny-vite
Version:
A Vite plugin that implements Piny integration in dev mode.
479 lines (403 loc) • 15.1 kB
JavaScript
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();