Initial work on projector
A stripped down renderer from starlight.
This commit is contained in:
parent
37d9c2ed79
commit
110879829b
18
packages/projector/README.md
Normal file
18
packages/projector/README.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Projector
|
||||||
|
|
||||||
|
A DOM-abstraction communicator. Projector consumes patches to update the DOM in _frames_ and
|
||||||
|
provides sanitized event objects to subscribers.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
## Instantiation
|
||||||
|
|
||||||
|
```js
|
||||||
|
const projector = Projector(rootElement):
|
||||||
|
|
||||||
|
projector.subscribe(evt => {
|
||||||
|
console.log(`Received ${evt.type} event from:`, projector.getElement(evt.target));
|
||||||
|
);
|
||||||
|
|
||||||
|
projector.queuePatch(/* array of patches */);
|
||||||
|
```
|
||||||
39
packages/projector/package.json
Normal file
39
packages/projector/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "projector",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "A DOM-abstraction communicator",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"jsnext:main": "src/index.js",
|
||||||
|
"files": ["dist", "lib", "src"],
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint src",
|
||||||
|
"clean": "rimraf dist lib",
|
||||||
|
"build:lib": "NODE_ENV=production babel src --presets=\"stage-0,es2015\" --out-dir lib",
|
||||||
|
"build:umd": "npm run build:lib && NODE_ENV=production rollup -c",
|
||||||
|
"build:umd:min":
|
||||||
|
"npm run build:umd && uglifyjs -m --screw-ie8 -c -o dist/projector.min.js dist/projector.js",
|
||||||
|
"build:umd:gzip":
|
||||||
|
"npm run build:umd:min && gzip -c9 dist/projector.min.js > dist/projector.min.js.gz",
|
||||||
|
"build": "npm run build:umd:gzip && ls -l dist/",
|
||||||
|
"prepublish": "npm run clean && npm run build",
|
||||||
|
"test": "npm run build:lib && jasmine --verbose"
|
||||||
|
},
|
||||||
|
"keywords": ["router"],
|
||||||
|
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "6.18.0",
|
||||||
|
"babel-core": "6.21.0",
|
||||||
|
"babel-preset-es2015": "6.18.0",
|
||||||
|
"babel-preset-es2015-rollup": "3.0.0",
|
||||||
|
"babel-preset-stage-0": "6.16.0",
|
||||||
|
"jasmine": "^2.5.3",
|
||||||
|
"rimraf": "2.5.4",
|
||||||
|
"rollup-plugin-json": "2.1.0",
|
||||||
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
|
"uglifyjs": "2.4.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"trimkit": "^1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/projector/rollup.config.js
Normal file
19
packages/projector/rollup.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import json from 'rollup-plugin-json';
|
||||||
|
import babel from 'rollup-plugin-babel';
|
||||||
|
|
||||||
|
const babelConfig = {
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
browser: true
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
presets: ['stage-0', 'es2015-rollup']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
entry: 'src/index.js',
|
||||||
|
format: 'umd',
|
||||||
|
moduleName: 'Projector',
|
||||||
|
plugins: [json(), babel(babelConfig)],
|
||||||
|
dest: 'dist/projector.js'
|
||||||
|
};
|
||||||
3
packages/projector/spec/helpers/globals.js
Normal file
3
packages/projector/spec/helpers/globals.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
self = window = {
|
||||||
|
location: {}
|
||||||
|
};
|
||||||
1
packages/projector/spec/projector.spec.js
Normal file
1
packages/projector/spec/projector.spec.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
const { Projector } = require('../lib/index.js');
|
||||||
7
packages/projector/spec/support/jasmine.json
Normal file
7
packages/projector/spec/support/jasmine.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": ["**/*[sS]pec.js"],
|
||||||
|
"helpers": ["helpers/**/*.js"],
|
||||||
|
"stopSpecOnExpectationFailure": false,
|
||||||
|
"random": false
|
||||||
|
}
|
||||||
168
packages/projector/src/index.js
Normal file
168
packages/projector/src/index.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import { isFunction } from 'trimkit';
|
||||||
|
|
||||||
|
import { sanitizeObject, supportsPassive } from './utils.js';
|
||||||
|
|
||||||
|
|
||||||
|
const OVERRIDING_EVENTS = ['contextmenu','dragover','drop'];
|
||||||
|
function getEventList(element) {
|
||||||
|
return (element.getAttribute('evl') || '').split(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Projector(domRoot) {
|
||||||
|
const elementMap = new Map();
|
||||||
|
const pendingFrames = [];
|
||||||
|
const eventCallbacks = [];
|
||||||
|
let runningNextFrame;
|
||||||
|
|
||||||
|
function eventHandler(evt) {
|
||||||
|
if (OVERRIDING_EVENTS.includes(eventName)) {
|
||||||
|
evt.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeEvt = sanitizeObject(evt);
|
||||||
|
if (evt.target) {
|
||||||
|
fakeEvt.target = evt.target._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventCallbacks.forEach(cb => cb(fakeEvt));
|
||||||
|
}
|
||||||
|
function removeEvent(eventSet, id, eventName) {
|
||||||
|
eventSet.remove(element._id);
|
||||||
|
if (!eventSet.size) {
|
||||||
|
domRoot.removeEventListener(eventName, eventHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAttributes(element, props) {
|
||||||
|
Object.entries((name, value) => {
|
||||||
|
if (name in element) {
|
||||||
|
if (name.startsWith('on')) {
|
||||||
|
const eventName = name.substr(2);
|
||||||
|
const eventSet = eventMap.get(eventName) || new Set();
|
||||||
|
const eventList = getEventList(element);
|
||||||
|
if (value === null) {
|
||||||
|
// remove event
|
||||||
|
eventList.splice(eventList.indexOf(eventName), 1);
|
||||||
|
removeEvent(eventSet, element._id, eventName);
|
||||||
|
} else {
|
||||||
|
// add event
|
||||||
|
if (!eventSet.size) {
|
||||||
|
domRoot.addEventListener(
|
||||||
|
eventName,
|
||||||
|
eventHandler,
|
||||||
|
(
|
||||||
|
(supportsPassive && !OVERRIDING_EVENTS.includes(eventName))
|
||||||
|
? { passive: true, capture: false }
|
||||||
|
: false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eventList.push(eventName);
|
||||||
|
eventSet.add(element._id);
|
||||||
|
if (!eventMap.has(eventName)) {
|
||||||
|
eventMap.set(eventName, eventSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.setAttribute('evl', eventList.join(';'));
|
||||||
|
} else {
|
||||||
|
element[name] = value;
|
||||||
|
}
|
||||||
|
} else if (value === null) {
|
||||||
|
element.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
element.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement({
|
||||||
|
t as type,
|
||||||
|
n as name,
|
||||||
|
p as props,
|
||||||
|
i as id,
|
||||||
|
c as children
|
||||||
|
}) {
|
||||||
|
let element;
|
||||||
|
if (type === 3) {
|
||||||
|
element = document.createTextNode(props.textContent);
|
||||||
|
} else if (type === 1) {
|
||||||
|
element = document.createElement(name);
|
||||||
|
}
|
||||||
|
setAttributes(element, props);
|
||||||
|
elementMap.set(element._id = id, element);
|
||||||
|
|
||||||
|
for (let i=0; i<children.length; i++) {
|
||||||
|
element.appendChild(createElement(children[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _removeElement(parent, childrenToRemove) {
|
||||||
|
childrenToRemove.forEach(function (id) {
|
||||||
|
const child = getElement(id);
|
||||||
|
_removeElement(child, asArray(child.childNodes).map(c => c._id)))
|
||||||
|
|
||||||
|
getEventList(child).forEach(eventName => {
|
||||||
|
removeEvent(
|
||||||
|
eventMap.get(eventName),
|
||||||
|
child._id,
|
||||||
|
eventName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.removeChild(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_METHODS = [
|
||||||
|
function addElement(parentId, data, nextSiblingId) {
|
||||||
|
getElement(parentId)
|
||||||
|
.insertBefore(
|
||||||
|
createElement(data),
|
||||||
|
getElement(nextSiblingId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setAttributes,
|
||||||
|
function removeElement(parentId, childrenToRemove) {
|
||||||
|
_removeElement(getElement(parentId), childrenToRemove);
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const queuePatch = (patchFrame) => {
|
||||||
|
if (!patchFrame || !patchFrame.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingFrames.unshift(patchFrame);
|
||||||
|
if (!runningNextFrame) {
|
||||||
|
updateFrame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFrame = () => {
|
||||||
|
const patches = pendingFrames.pop();
|
||||||
|
// console.group('PatchSet');
|
||||||
|
let patch;
|
||||||
|
while (patch = patchSet.shift()) {
|
||||||
|
// console.log(ACTION_METHODS[patch[0]].name, JSON.stringify(patch));
|
||||||
|
ACTION_METHODS[patch[0]](patchParams[1], patchParams[2], patchParams[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while(postProcessing.length) { postProcessing.pop()(); }
|
||||||
|
// console.groupEnd('PatchSet');
|
||||||
|
|
||||||
|
if (pendingFrames.length) {
|
||||||
|
runningNextFrame = requestAnimationFrame(updateFrame, domRoot);
|
||||||
|
} else {
|
||||||
|
runningNextFrame = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getElement(id) {
|
||||||
|
return elementMap.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queuePatch,
|
||||||
|
getElement,
|
||||||
|
subscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
23
packages/projector/src/utils.js
Normal file
23
packages/projector/src/utils.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export const NORMAL_OBJECT_PROP_TYPES = ['number', 'string', 'boolean'];
|
||||||
|
|
||||||
|
export function sanitizeObject(obj) {
|
||||||
|
const output = {};
|
||||||
|
for (const key in evt) {
|
||||||
|
const value = evt[key];
|
||||||
|
if (NORMAL_OBJECT_PROP_TYPES.includes(typeof value)) {
|
||||||
|
output[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrapolated from https://github.com/zzarcon/default-passive-events/blob/master/default-passive-events.js
|
||||||
|
export let supportsPassive = false;
|
||||||
|
try {
|
||||||
|
const opts = Object.defineProperty({}, 'passive', {
|
||||||
|
get: function() {
|
||||||
|
supportsPassive = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addEventListener('test', null, opts);
|
||||||
|
} catch (e) {}
|
||||||
Loading…
x
Reference in New Issue
Block a user