|
import { |
|
createStore, |
|
createEvent, |
|
is, |
|
clearNode, |
|
forward, |
|
sample, |
|
Store, |
|
Event, |
|
launch |
|
} from 'effector' |
|
import {beginMark, endMark} from './mark' |
|
import { |
|
planTaskOnce, |
|
debounceRaf, |
|
noDebounceRaf, |
|
planTask, |
|
deleteTask, |
|
startTime |
|
} from './renderer' |
|
|
|
import { |
|
NSType, |
|
PropertyMap, |
|
HElement, |
|
KeyedRecord, |
|
TransformMap, |
|
StoreOrData, |
|
StylePropertyMap |
|
} from './index.h' |
|
import { |
|
nodeStack, |
|
signalStack, |
|
typeStack, |
|
svgRootStack, |
|
stackAppendHead, |
|
getSignal, |
|
stackHead |
|
} from './stack' |
|
|
|
function isFalse(val) { |
|
return val === false || val === undefined || val === null |
|
} |
|
|
|
export function using(node: any, cb: () => any): void |
|
export function using(node, cb) { |
|
nodeStack.push({node, append: []}) |
|
try { |
|
return cb() |
|
} finally { |
|
appendBatch(nodeStack.pop()) |
|
} |
|
} |
|
function usingWithSignal(node: any, signal: Event<void>, cb: () => any) { |
|
nodeStack.push({node, append: []}) |
|
signalStack.push(signal) |
|
try { |
|
return cb() |
|
} finally { |
|
appendBatch(nodeStack.pop()) |
|
signalStack.pop() |
|
} |
|
} |
|
function appendBatch({node, append, reverse = false}) { |
|
if (append.length === 0) return |
|
const frag = document.createDocumentFragment() |
|
if (!reverse) { |
|
for (let i = 0; i < append.length; i++) { |
|
frag.appendChild(append[i]) |
|
} |
|
node.appendChild(frag) |
|
} else { |
|
for (let i = append.length - 1; i >= 0; i--) { |
|
frag.appendChild(append[i]) |
|
} |
|
node.prepend(frag) |
|
} |
|
} |
|
|
|
export function h(tag: string, cb: (node: HElement) => void): HElement |
|
export function h( |
|
tag: string, |
|
opts: {type?: 'svg'; noAppend?: boolean}, |
|
cb?: (node: HElement) => void |
|
): HElement |
|
export function h(tag, opts, cb?: any) { |
|
if (typeof opts === 'function') { |
|
cb = opts |
|
opts = {} |
|
} |
|
if (opts === undefined) opts = {} |
|
const {noAppend = false} = opts |
|
const parentNS = typeStack[typeStack.length - 1] |
|
let type = 'html' |
|
if ('type' in opts) { |
|
type = opts.type |
|
} else { |
|
type = parentNS === 'svg' ? 'svg' : 'html' |
|
} |
|
if (tag === 'svg') type = 'svg' |
|
const node = |
|
type === 'svg' |
|
? document.createElementNS('http://www.w3.org/2000/svg', tag) |
|
: document.createElement(tag) |
|
let needToPop = false |
|
let isSvgRoot = false |
|
if (parentNS === 'foreignObject') { |
|
node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml') |
|
typeStack.push('html') |
|
needToPop = true |
|
} else if (tag === 'svg') { |
|
node.setAttribute('xmlns', 'http://www.w3.org/2000/svg') |
|
typeStack.push('svg') |
|
needToPop = true |
|
svgRootStack.push(node) |
|
isSvgRoot = true |
|
} else if (tag === 'foreignObject') { |
|
typeStack.push('foreignObject') |
|
needToPop = true |
|
} |
|
node.__SIGNAL__ = createSignal() |
|
if (cb) { |
|
try { |
|
usingWithSignal(node, node.__SIGNAL__, () => cb(node)) |
|
} finally { |
|
if (needToPop) typeStack.pop() |
|
if (isSvgRoot) svgRootStack.pop() |
|
} |
|
} |
|
if (!noAppend) { |
|
planAppendChild(node) |
|
} |
|
return node |
|
} |
|
|
|
function createSignal() { |
|
const signal = createEvent() |
|
signal.watch(() => { |
|
planTaskOnce(signal, clearNode) |
|
}) |
|
const parent = getSignal() |
|
if (parent) { |
|
forward({ |
|
from: parent, |
|
to: signal |
|
}) |
|
} |
|
return signal |
|
} |
|
function planAppendChild(node) { |
|
if (nodeStack.length > 0) { |
|
stackAppendHead().push(node) |
|
} |
|
} |
|
|
|
function domOperation( |
|
immediate: boolean, |
|
signal: Event<void>, |
|
data: any, |
|
handler: (data: any) => void |
|
) { |
|
if (is.unit(data)) { |
|
;(immediate ? noDebounceRaf : debounceRaf)(signal, data, handler) |
|
} else { |
|
handler(data) |
|
} |
|
} |
|
|
|
export function Handler(map: {[event: string]: Event<globalThis.Event>}): void |
|
export function Handler( |
|
options: {passive?: boolean; capture?: boolean}, |
|
map: {[event: string]: Event<globalThis.Event>} |
|
): void |
|
export function Handler(options, map?: any) { |
|
if (map === undefined) { |
|
map = options |
|
options = {} |
|
} |
|
const node = stackHead() |
|
const {passive = true, capture = false} = options |
|
const handlerOptions = {passive, capture} |
|
for (const key in map) { |
|
node.addEventListener(key, map[key], handlerOptions) |
|
} |
|
const stop = getSignal().watch(() => { |
|
stop() |
|
for (const key in map) { |
|
node.removeEventListener(key, map[key], handlerOptions) |
|
} |
|
}) |
|
} |
|
|
|
export function Data(dataset: PropertyMap): void |
|
export function Data(dataset: PropertyMap) { |
|
const node = stackHead() |
|
const signal = getSignal() |
|
for (const field in dataset) { |
|
domOperation(false, signal, dataset[field], value => { |
|
if (isFalse(value)) { |
|
delete node.dataset[field] |
|
} else { |
|
node.dataset[field] = `${value}` |
|
} |
|
}) |
|
} |
|
} |
|
export function Visible(visible: Store<boolean>): void |
|
export function Visible(visible: any) { |
|
const node = stackHead() |
|
const parent = nodeStack[nodeStack.length - 2].node |
|
debounceRaf(getSignal(), visible, visible => { |
|
if (visible) { |
|
if (!parent.contains(node)) { |
|
parent.appendChild(node) |
|
} |
|
} else { |
|
node.remove() |
|
} |
|
}) |
|
} |
|
export function Style({ |
|
prop = {}, |
|
val = {} |
|
}: { |
|
prop?: StylePropertyMap |
|
val?: PropertyMap |
|
}) { |
|
const node = stackHead() as any |
|
const signal = getSignal() |
|
const style: CSSStyleDeclaration = node.style |
|
for (const propName in prop) { |
|
domOperation(false, signal, prop[propName], value => { |
|
if (isFalse(value)) { |
|
style.removeProperty(propName) |
|
} else { |
|
style[propName] = value |
|
} |
|
}) |
|
} |
|
for (const varShortName in val) { |
|
const variableName = `--${varShortName}` |
|
domOperation(false, signal, val[varShortName], value => { |
|
if (isFalse(value)) { |
|
style.removeProperty(variableName) |
|
} else { |
|
style.setProperty(variableName, value) |
|
} |
|
}) |
|
} |
|
} |
|
export function Attr(map: PropertyMap): void |
|
export function Attr(map: PropertyMap) { |
|
const node = stackHead() |
|
const signal = getSignal() |
|
for (const attr in map) { |
|
domOperation(attr !== 'value', signal, map[attr], value => { |
|
if (isFalse(value)) { |
|
node.removeAttribute(attr) |
|
} else { |
|
node.setAttribute(attr, `${value}`) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
function applyTransform<T>( |
|
svg: SVGSVGElement, |
|
signal: Event<void>, |
|
transformList: SVGTransformList, |
|
data: StoreOrData<T>, |
|
handler: (data: T, transform: SVGTransform) => void |
|
) { |
|
const transform = svg.createSVGTransform() |
|
domOperation(false, signal, data, function transformHandler(data) { |
|
handler(data, transform) |
|
}) |
|
transformList.appendItem(transform) |
|
} |
|
const transformResolvers = { |
|
translate({x = 0, y = 0}: {x?: number; y?: number}, transform: SVGTransform) { |
|
transform.setTranslate(x, y) |
|
}, |
|
scale({x = 0, y = 0}: {x?: number; y?: number}, transform: SVGTransform) { |
|
transform.setScale(x, y) |
|
}, |
|
rotate( |
|
data: |
|
| number |
|
| { |
|
angle?: number |
|
x?: number |
|
y?: number |
|
}, |
|
transform: SVGTransform |
|
) { |
|
if (typeof data === 'number') { |
|
transform.setRotate(data, 0, 0) |
|
} else { |
|
transform.setRotate(data.angle || 0, data.x || 0, data.y || 0) |
|
} |
|
}, |
|
skewX(angle: number, transform: SVGTransform) { |
|
transform.setSkewX(angle) |
|
}, |
|
skewY(angle: number, transform: SVGTransform) { |
|
transform.setSkewY(angle) |
|
} |
|
} |
|
export function Transform(operations: Partial<TransformMap>) { |
|
const node = stackHead() |
|
const signal = getSignal() |
|
const transformList: SVGTransformList = node.transform.baseVal |
|
const svg = svgRootStack[svgRootStack.length - 1] |
|
for (const key in operations) { |
|
applyTransform( |
|
svg, |
|
signal, |
|
transformList, |
|
operations[key], |
|
transformResolvers[key] |
|
) |
|
} |
|
} |
|
function setText(node, text) { |
|
const textNode = new Text(`${text}`) |
|
const firstChild = node.firstChild |
|
if (firstChild) { |
|
firstChild.replaceWith(textNode) |
|
} else { |
|
node.appendChild(textNode) |
|
} |
|
} |
|
function DOMText( |
|
value: Store<string | number> | Event<string | number> | string | number |
|
): void |
|
function DOMText(store: any) { |
|
const node = stackHead() |
|
if (is.unit(store)) { |
|
debounceRaf(getSignal(), store, setText.bind(null, node)) |
|
} else { |
|
setText(node, store) |
|
} |
|
} |
|
|
|
function planRemoveKeyedNode({nodes, signal}: KeyedRecord<any>) { |
|
for (let i = 0; i < nodes.length; i++) { |
|
nodes[i].remove() |
|
} |
|
clearNode(signal, {deep: true}) |
|
} |
|
function withSavedStack(context: ListContext, list: KeyedRecord<any>[]) { |
|
const {savedTypeStack, savedSVGStack, signal, shortName} = context |
|
beginMark('append [' + shortName + ']') |
|
const currentNodeStack = [...nodeStack] |
|
const currentTypeStack = [...typeStack] |
|
const currentSignalStack = [...signalStack] |
|
const currentSvgStack = [...svgRootStack] |
|
nodeStack.length = 0 |
|
typeStack.length = 0 |
|
signalStack.length = 0 |
|
svgRootStack.length = 0 |
|
typeStack.push(...savedTypeStack) |
|
svgRootStack.push(...savedSVGStack) |
|
signalStack.push(signal) |
|
try { |
|
return appendNewKeyedRecords(context, list) |
|
} finally { |
|
nodeStack.push(...currentNodeStack) |
|
typeStack.length = 0 |
|
typeStack.push(...currentTypeStack) |
|
signalStack.length = 0 |
|
signalStack.push(...currentSignalStack) |
|
svgRootStack.length = 0 |
|
svgRootStack.push(...currentSvgStack) |
|
endMark('append [' + shortName + ']') |
|
} |
|
} |
|
function appendNewKeyedRecords( |
|
{parentNode, cb, reverse}: ListContext, |
|
list: KeyedRecord<any>[] |
|
) { |
|
const nodes: HElement[] = [] |
|
const appended: HElement[] = [] |
|
nodeStack.push({ |
|
node: parentNode, |
|
append: appended |
|
}) |
|
let ret = -1 |
|
for (let i = 0; i < list.length; i++) { |
|
const item: KeyedRecord<any> = list[i] |
|
if (!item.active) continue |
|
if (Date.now() - startTime >= 5) { |
|
ret = i |
|
break |
|
} |
|
cb(item) |
|
for (let j = 0; j < appended.length; j++) { |
|
const child = appended[j] |
|
forward({ |
|
from: item.signal, |
|
to: child.__SIGNAL__ |
|
}) |
|
item.nodes.push(child) |
|
nodes.push(child) |
|
} |
|
appended.length = 0 |
|
} |
|
nodeStack.pop() |
|
planTaskOnce( |
|
{ |
|
node: parentNode, |
|
append: nodes, |
|
reverse |
|
}, |
|
appendBatch |
|
) |
|
return ret |
|
} |
|
function getInitialUpdate<T>( |
|
signal, |
|
update, |
|
source |
|
): { |
|
newRecords: KeyedRecord<T>[] |
|
resultRecords: KeyedRecord<T>[] |
|
} { |
|
signalStack.push(signal) |
|
try { |
|
return update([], source.getState()) |
|
} finally { |
|
signalStack.pop() |
|
} |
|
} |
|
function applyNewRecords<T>(context: ListContext, list: KeyedRecord<T>[]) { |
|
if (list.length === 0) return |
|
const continueFrom = withSavedStack(context, list) |
|
if (continueFrom !== -1) { |
|
planTaskOnce(list.slice(continueFrom), applyNewRecords.bind(null, context)) |
|
} |
|
} |
|
type ListContext = { |
|
parentNode: Element |
|
cb: (opts: any) => void |
|
shortName: string |
|
reverse: boolean |
|
savedTypeStack: NSType[] |
|
savedSVGStack: SVGSVGElement[] |
|
signal: Event<void> |
|
} |
|
export function List<T>( |
|
source: Store<T[]>, |
|
cb: (opts: {store: Store<T>; index: number; signal: Event<void>}) => void |
|
): void |
|
export function List<T, K extends keyof T>( |
|
{ |
|
key, |
|
source, |
|
reverse |
|
}: { |
|
key: T[K] extends (string | number | symbol) ? K : never |
|
source: Store<T[]> |
|
reverse?: boolean |
|
}, |
|
cb: (opts: {store: Store<T>; key: T[K]; signal: Event<void>}) => void |
|
): void |
|
export function List<T>(opts, cb: (opts: any) => void) { |
|
let source |
|
let reverse = false |
|
let getID: (item: T, i: number) => string | number | symbol |
|
if (is.store(opts)) { |
|
getID = (item, i) => i |
|
source = opts |
|
} else { |
|
const key = opts.key |
|
getID = (item, i) => item[key] |
|
source = opts.source |
|
reverse = !!opts.reverse |
|
} |
|
const parentNode: Element = stackHead() |
|
const parentSignal = createSignal() |
|
const savedTypeStack = [...typeStack] |
|
const savedSVGStack = [...svgRootStack] |
|
|
|
const triggerUpdate = createEvent<T[]>() |
|
const {taskID, updater} = planTask(triggerUpdate) |
|
const updates = createStore(getInitialUpdate<T>(parentSignal, update, source)) |
|
const resultRecords = updates.map(({resultRecords}) => resultRecords) |
|
const newRecords = updates.map(({newRecords}) => newRecords) |
|
own(parentSignal, [updates, resultRecords, newRecords, triggerUpdate]) |
|
const stop = source.updates.watch(updater) |
|
parentSignal.watch(() => { |
|
stop() |
|
deleteTask(taskID) |
|
}) |
|
sample({ |
|
source: resultRecords, |
|
clock: triggerUpdate, |
|
fn: update, |
|
target: updates |
|
}) |
|
const context: ListContext = { |
|
parentNode, |
|
cb, |
|
shortName: source.shortName, |
|
reverse, |
|
savedTypeStack, |
|
savedSVGStack, |
|
signal: parentSignal |
|
} |
|
newRecords.watch(applyNewRecords.bind(null, context)) |
|
|
|
function update(records: Array<KeyedRecord<T>>, input: T[]) { |
|
beginMark('list [' + source.shortName + ']') |
|
const skipNode: boolean[] = Array(input.length).fill(false) |
|
const keys = input.map(getID) |
|
const newRecords: Array<KeyedRecord<T>> = [] |
|
const resultRecords: Array<KeyedRecord<T>> = [] |
|
for (let i = 0; i < records.length; i++) { |
|
const record = records[i] |
|
const index = keys.indexOf(record.key) |
|
if (index !== -1) { |
|
resultRecords.push(record) |
|
skipNode[index] = true |
|
if (record.store.getState() !== input[index]) |
|
//@ts-ignore |
|
launch(record.store, input[index], true) |
|
} else { |
|
//@ts-ignore |
|
launch(record.signal) |
|
} |
|
} |
|
for (let i = 0; i < input.length; i++) { |
|
if (skipNode[i]) continue |
|
const item = input[i] |
|
const store = createStore(item) |
|
const signal = createSignal() |
|
own(signal, [store]) |
|
signal.watch(() => { |
|
keyed.active = false |
|
planTaskOnce(keyed, planRemoveKeyedNode) |
|
}) |
|
const id = getID(item, i) |
|
const keyed: KeyedRecord<T> = { |
|
key: id, |
|
index: id, |
|
store, |
|
signal, |
|
nodes: [], |
|
active: true |
|
} |
|
newRecords.push(keyed) |
|
resultRecords.push(keyed) |
|
} |
|
endMark('list [' + source.shortName + ']') |
|
return { |
|
newRecords, |
|
resultRecords |
|
} |
|
} |
|
} |
|
|
|
export {DOMText as Text} |
|
|
|
|
|
|
|
function own(ownerUnit, ownedUnits) { |
|
const owner = getGraph(ownerUnit) |
|
for (let i = 0; i < ownedUnits.length; i++) { |
|
const link = getGraph(ownedUnits[i]) |
|
link.family.type = 'crosslink' |
|
const owners = getOwners(link) |
|
const links = getLinks(owner) |
|
if (!owners.includes(owner)) owners.push(owner) |
|
if (!links.includes(link)) links.push(link) |
|
} |
|
} |
|
|
|
const getGraph = unit => unit.graphite || unit |
|
const getOwners = node => node.family.owners |
|
const getLinks = node => node.family.links |
|
|