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