Initial work on projector

A stripped down renderer from starlight.
This commit is contained in:
Timothy Farrell 2017-01-27 22:41:05 -06:00
parent 37d9c2ed79
commit 110879829b
8 changed files with 278 additions and 0 deletions

View 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 */);
```

View 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"
}
}

View 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'
};

View File

@ -0,0 +1,3 @@
self = window = {
location: {}
};

View File

@ -0,0 +1 @@
const { Projector } = require('../lib/index.js');

View File

@ -0,0 +1,7 @@
{
"spec_dir": "spec",
"spec_files": ["**/*[sS]pec.js"],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": false
}

View 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,
};
}

View 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) {}