reorganize files

This commit is contained in:
Géry Debongnie
2019-01-22 19:25:12 +01:00
parent 5652ef65ad
commit 3766fbc7b8
101 changed files with 31 additions and 43 deletions
+64
View File
@@ -0,0 +1,64 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
// because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237
declare global {
interface Element {
setAttribute(name: string, value: string | number | boolean): void;
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string | number | boolean): void;
}
}
export type Attrs = Record<string, string | number | boolean>
const xlinkNS = 'http://www.w3.org/1999/xlink';
const xmlNS = 'http://www.w3.org/XML/1998/namespace';
const colonChar = 58;
const xChar = 120;
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
var key: string, elm: Element = vnode.elm as Element,
oldAttrs = (oldVnode.data as VNodeData).attrs,
attrs = (vnode.data as VNodeData).attrs;
if (!oldAttrs && !attrs) return;
if (oldAttrs === attrs) return;
oldAttrs = oldAttrs || {};
attrs = attrs || {};
// update modified attributes, add new attributes
for (key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
if (old !== cur) {
if (cur === true) {
elm.setAttribute(key, "");
} else if (cur === false) {
elm.removeAttribute(key);
} else {
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur);
} else if (key.charCodeAt(3) === colonChar) {
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur);
} else if (key.charCodeAt(5) === colonChar) {
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur);
} else {
elm.setAttribute(key, cur);
}
}
}
}
// remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key);
}
}
}
export const attributesModule = {create: updateAttrs, update: updateAttrs} as Module;
export default attributesModule;
+30
View File
@@ -0,0 +1,30 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type Classes = Record<string, boolean>
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any, name: string, elm: Element = vnode.elm as Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? 'add' : 'remove'](name);
}
}
}
export const classModule = {create: updateClass, update: updateClass} as Module;
export default classModule;
+43
View File
@@ -0,0 +1,43 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type Dataset = Record<string, string>;
const CAPS_REGEX = /[A-Z]/g;
function updateDataset(oldVnode: VNode, vnode: VNode): void {
let elm: HTMLElement = vnode.elm as HTMLElement,
oldDataset = (oldVnode.data as VNodeData).dataset,
dataset = (vnode.data as VNodeData).dataset,
key: string;
if (!oldDataset && !dataset) return;
if (oldDataset === dataset) return;
oldDataset = oldDataset || {};
dataset = dataset || {};
const d = elm.dataset;
for (key in oldDataset) {
if (!dataset[key]) {
if (d) {
if (key in d) {
delete d[key];
}
} else {
elm.removeAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase());
}
}
}
for (key in dataset) {
if (oldDataset[key] !== dataset[key]) {
if (d) {
d[key] = dataset[key];
} else {
elm.setAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase(), dataset[key]);
}
}
}
}
export const datasetModule = {create: updateDataset, update: updateDataset} as Module;
export default datasetModule;
+111
View File
@@ -0,0 +1,111 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type On = {
[N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void
} & {
[event: string]: EventListener
};
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
if (typeof handler === "function") {
// call function handler
handler.call(vnode, event, vnode);
} else if (typeof handler === "object") {
// call handler with arguments
if (typeof handler[0] === "function") {
// special case for single argument for performance
if (handler.length === 2) {
handler[0].call(vnode, handler[1], event, vnode);
} else {
var args = handler.slice(1);
args.push(event);
args.push(vnode);
handler[0].apply(vnode, args);
}
} else {
// call multiple handlers
for (var i = 0; i < handler.length; i++) {
invokeHandler(handler[i], vnode, event);
}
}
}
}
function handleEvent(event: Event, vnode: VNode) {
var name = event.type,
on = (vnode.data as VNodeData).on;
// call event handler(s) if exists
if (on && on[name]) {
invokeHandler(on[name], vnode, event);
}
}
function createListener() {
return function handler(event: Event) {
handleEvent(event, (handler as any).vnode);
}
}
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
var oldOn = (oldVnode.data as VNodeData).on,
oldListener = (oldVnode as any).listener,
oldElm: Element = oldVnode.elm as Element,
on = vnode && (vnode.data as VNodeData).on,
elm: Element = (vnode && vnode.elm) as Element,
name: string;
// optimization for reused immutable handlers
if (oldOn === on) {
return;
}
// remove existing listeners which no longer used
if (oldOn && oldListener) {
// if element changed or deleted we remove all existing listeners unconditionally
if (!on) {
for (name in oldOn) {
// remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false);
}
} else {
for (name in oldOn) {
// remove listener if existing listener removed
if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false);
}
}
}
}
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
var listener = (vnode as any).listener = (oldVnode as any).listener || createListener();
// update vnode for listener
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
}
} else {
for (name in on) {
// add listener if new listener added
if (!oldOn[name]) {
elm.addEventListener(name, listener, false);
}
}
}
}
}
export const eventListenersModule = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
} as Module;
export default eventListenersModule;
+165
View File
@@ -0,0 +1,165 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type Hero = { id: string }
var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout;
var nextFrame = function(fn: any) { raf(function() { raf(fn); }); };
function setNextFrame(obj: any, prop: string, val: any): void {
nextFrame(function() { obj[prop] = val; });
}
function getTextNodeRect(textNode: Text): ClientRect | undefined {
var rect: ClientRect | undefined;
if (document.createRange) {
var range = document.createRange();
range.selectNodeContents(textNode);
if (range.getBoundingClientRect) {
rect = range.getBoundingClientRect();
}
}
return rect;
}
function calcTransformOrigin(isTextNode: boolean,
textRect: ClientRect | undefined,
boundingRect: ClientRect): string {
if (isTextNode) {
if (textRect) {
//calculate pixels to center of text from left edge of bounding box
var relativeCenterX = textRect.left + textRect.width/2 - boundingRect.left;
var relativeCenterY = textRect.top + textRect.height/2 - boundingRect.top;
return relativeCenterX + 'px ' + relativeCenterY + 'px';
}
}
return '0 0'; //top left
}
function getTextDx(oldTextRect: ClientRect | undefined,
newTextRect: ClientRect | undefined): number {
if (oldTextRect && newTextRect) {
return ((oldTextRect.left + oldTextRect.width/2) - (newTextRect.left + newTextRect.width/2));
}
return 0;
}
function getTextDy(oldTextRect: ClientRect | undefined,
newTextRect: ClientRect | undefined): number {
if (oldTextRect && newTextRect) {
return ((oldTextRect.top + oldTextRect.height/2) - (newTextRect.top + newTextRect.height/2));
}
return 0;
}
function isTextElement(elm: Element | Text): elm is Text {
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3;
}
var removed: any, created: any;
function pre() {
removed = {};
created = [];
}
function create(oldVnode: VNode, vnode: VNode): void {
var hero = (vnode.data as VNodeData).hero;
if (hero && hero.id) {
created.push(hero.id);
created.push(vnode);
}
}
function destroy(vnode: VNode): void {
var hero = (vnode.data as VNodeData).hero;
if (hero && hero.id) {
var elm = vnode.elm;
(vnode as any).isTextNode = isTextElement(elm as Element | Text); //is this a text node?
(vnode as any).boundingRect = (elm as Element).getBoundingClientRect(); //save the bounding rectangle to a new property on the vnode
(vnode as any).textRect = (vnode as any).isTextNode ? getTextNodeRect((elm as Element).childNodes[0] as Text) : null; //save bounding rect of inner text node
var computedStyle = window.getComputedStyle(elm as Element, void 0); //get current styles (includes inherited properties)
(vnode as any).savedStyle = JSON.parse(JSON.stringify(computedStyle)); //save a copy of computed style values
removed[hero.id] = vnode;
}
}
function post() {
var i: number, id: any, newElm: Element, oldVnode: VNode, oldElm: Element,
hRatio: number, wRatio: number,
oldRect: ClientRect, newRect: ClientRect, dx: number, dy: number,
origTransform: string | null, origTransition: string | null,
newStyle: CSSStyleDeclaration, oldStyle: CSSStyleDeclaration,
newComputedStyle: CSSStyleDeclaration, isTextNode: boolean,
newTextRect: ClientRect | undefined, oldTextRect: ClientRect | undefined;
for (i = 0; i < created.length; i += 2) {
id = created[i];
newElm = created[i+1].elm;
oldVnode = removed[id];
if (oldVnode) {
isTextNode = (oldVnode as any).isTextNode && isTextElement(newElm); //Are old & new both text?
newStyle = (newElm as HTMLElement).style;
newComputedStyle = window.getComputedStyle(newElm, void 0); //get full computed style for new element
oldElm = oldVnode.elm as Element;
oldStyle = (oldElm as HTMLElement).style;
//Overall element bounding boxes
newRect = newElm.getBoundingClientRect();
oldRect = (oldVnode as any).boundingRect; //previously saved bounding rect
//Text node bounding boxes & distances
if (isTextNode) {
newTextRect = getTextNodeRect(newElm.childNodes[0] as Text);
oldTextRect = (oldVnode as any).textRect;
dx = getTextDx(oldTextRect, newTextRect);
dy = getTextDy(oldTextRect, newTextRect);
} else {
//Calculate distances between old & new positions
dx = oldRect.left - newRect.left;
dy = oldRect.top - newRect.top;
}
hRatio = newRect.height / (Math.max(oldRect.height, 1));
wRatio = isTextNode ? hRatio : newRect.width / (Math.max(oldRect.width, 1)); //text scales based on hRatio
// Animate new element
origTransform = newStyle.transform;
origTransition = newStyle.transition;
if (newComputedStyle.display === 'inline') //inline elements cannot be transformed
newStyle.display = 'inline-block'; //this does not appear to have any negative side effects
newStyle.transition = origTransition + 'transform 0s';
newStyle.transformOrigin = calcTransformOrigin(isTextNode, newTextRect, newRect);
newStyle.opacity = '0';
newStyle.transform = origTransform + 'translate('+dx+'px, '+dy+'px) ' +
'scale('+1/wRatio+', '+1/hRatio+')';
setNextFrame(newStyle, 'transition', origTransition);
setNextFrame(newStyle, 'transform', origTransform);
setNextFrame(newStyle, 'opacity', '1');
// Animate old element
for (var key in (oldVnode as any).savedStyle) { //re-apply saved inherited properties
if (parseInt(key) != key as any as number) {
var ms = key.substring(0,2) === 'ms';
var moz = key.substring(0,3) === 'moz';
var webkit = key.substring(0,6) === 'webkit';
if (!ms && !moz && !webkit) //ignore prefixed style properties
(oldStyle as any)[key] = (oldVnode as any).savedStyle[key];
}
}
oldStyle.position = 'absolute';
oldStyle.top = oldRect.top + 'px'; //start at existing position
oldStyle.left = oldRect.left + 'px';
oldStyle.width = oldRect.width + 'px'; //Needed for elements who were sized relative to their parents
oldStyle.height = oldRect.height + 'px'; //Needed for elements who were sized relative to their parents
oldStyle.margin = '0'; //Margin on hero element leads to incorrect positioning
oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect);
oldStyle.transform = '';
oldStyle.opacity = '1';
document.body.appendChild(oldElm);
setNextFrame(oldStyle, 'transform', 'translate('+ -dx +'px, '+ -dy +'px) scale('+wRatio+', '+hRatio+')'); //scale must be on far right for translate to be correct
setNextFrame(oldStyle, 'opacity', '0');
oldElm.addEventListener('transitionend', function (ev: TransitionEvent) {
if (ev.propertyName === 'transform')
document.body.removeChild(ev.target as Node);
});
}
}
removed = created = undefined;
}
export const heroModule = {pre, create, destroy, post} as Module;
export default heroModule;
+10
View File
@@ -0,0 +1,10 @@
import {PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook} from '../hooks';
export interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
+31
View File
@@ -0,0 +1,31 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type Props = Record<string, any>;
function updateProps(oldVnode: VNode, vnode: VNode): void {
var key: string, cur: any, old: any, elm = vnode.elm,
oldProps = (oldVnode.data as VNodeData).props,
props = (vnode.data as VNodeData).props;
if (!oldProps && !props) return;
if (oldProps === props) return;
oldProps = oldProps || {};
props = props || {};
for (key in oldProps) {
if (!props[key]) {
delete (elm as any)[key];
}
}
for (key in props) {
cur = props[key];
old = oldProps[key];
if (old !== cur && (key !== 'value' || (elm as any)[key] !== cur)) {
(elm as any)[key] = cur;
}
}
}
export const propsModule = {create: updateProps, update: updateProps} as Module;
export default propsModule;
+103
View File
@@ -0,0 +1,103 @@
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type VNodeStyle = Record<string, string> & {
delayed?: Record<string, string>
remove?: Record<string, string>
}
// Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
var raf = (typeof window !== 'undefined' && (window.requestAnimationFrame).bind(window)) || setTimeout;
var nextFrame = function(fn: any) { raf(function() { raf(fn); }); };
var reflowForced = false;
function setNextFrame(obj: any, prop: string, val: any): void {
nextFrame(function() { obj[prop] = val; });
}
function updateStyle(oldVnode: VNode, vnode: VNode): void {
var cur: any, name: string, elm = vnode.elm,
oldStyle = (oldVnode.data as VNodeData).style,
style = (vnode.data as VNodeData).style;
if (!oldStyle && !style) return;
if (oldStyle === style) return;
oldStyle = oldStyle || {} as VNodeStyle;
style = style || {} as VNodeStyle;
var oldHasDel = 'delayed' in oldStyle;
for (name in oldStyle) {
if (!style[name]) {
if (name[0] === '-' && name[1] === '-') {
(elm as any).style.removeProperty(name);
} else {
(elm as any).style[name] = '';
}
}
}
for (name in style) {
cur = style[name];
if (name === 'delayed' && style.delayed) {
for (let name2 in style.delayed) {
cur = style.delayed[name2];
if (!oldHasDel || cur !== (oldStyle.delayed as any)[name2]) {
setNextFrame((elm as any).style, name2, cur);
}
}
} else if (name !== 'remove' && cur !== oldStyle[name]) {
if (name[0] === '-' && name[1] === '-') {
(elm as any).style.setProperty(name, cur);
} else {
(elm as any).style[name] = cur;
}
}
}
}
function applyDestroyStyle(vnode: VNode): void {
var style: any, name: string, elm = vnode.elm, s = (vnode.data as VNodeData).style;
if (!s || !(style = s.destroy)) return;
for (name in style) {
(elm as any).style[name] = style[name];
}
}
function applyRemoveStyle(vnode: VNode, rm: () => void): void {
var s = (vnode.data as VNodeData).style;
if (!s || !s.remove) {
rm();
return;
}
if(!reflowForced) {
getComputedStyle(document.body).transform;
reflowForced = true;
}
var name: string, elm = vnode.elm, i = 0, compStyle: CSSStyleDeclaration,
style = s.remove, amount = 0, applied: Array<string> = [];
for (name in style) {
applied.push(name);
(elm as any).style[name] = style[name];
}
compStyle = getComputedStyle(elm as Element);
var props = (compStyle as any)['transition-property'].split(', ');
for (; i < props.length; ++i) {
if(applied.indexOf(props[i]) !== -1) amount++;
}
(elm as Element).addEventListener('transitionend', function (ev: TransitionEvent) {
if (ev.target === elm) --amount;
if (amount === 0) rm();
});
}
function forceReflow() {
reflowForced = false;
}
export const styleModule = {
pre: forceReflow,
create: updateStyle,
update: updateStyle,
destroy: applyDestroyStyle,
remove: applyRemoveStyle
} as Module;
export default styleModule;