Adam Shaw 5 лет назад
Родитель
Сommit
980d0d613f

+ 42 - 91
packages/core/src/common/render-hook.tsx

@@ -20,6 +20,10 @@ export type RenderHookPropsChildren = (
   innerContent: ComponentChildren // if falsy, means it wasn't specified
 ) => ComponentChildren
 
+export interface ContentTypeHandlers {
+  [contentKey: string]: () => (el: HTMLElement, contentVal: any) => void
+}
+
 // TODO: use capitalizeFirstLetter util
 
 
@@ -77,9 +81,14 @@ export interface ContentHookProps<HookProps> {
 export class ContentHook<HookProps> extends Component<ContentHookProps<HookProps>> {
 
   static contextType = ComponentContextType
+  context: ComponentContext
 
   private innerElRef = createRef()
-  private customContentHandler: ContentHandler<any>
+  private customContentInfo: {
+    contentKey: string
+    contentVal: any
+    handler: (el: HTMLElement, contentVal: any) => void
+  }
 
 
   render(props: ContentHookProps<HookProps>) {
@@ -98,44 +107,53 @@ export class ContentHook<HookProps> extends Component<ContentHookProps<HookProps
 
 
   private renderInnerContent() {
-    let { props } = this
-    let innerContent: ComponentChildren = null
+    let { contentTypeHandlers } = this.context.pluginHooks
+    let { props, customContentInfo } = this
     let rawVal = (this.props.options || this.context.options)[props.name ? props.name + 'Content' : 'content']
-    let innerContentRaw = normalizeContent(rawVal, props.hookProps)
+    let innerContent = normalizeContent(rawVal, props.hookProps)
+    let innerContentVDom: ComponentChildren = null
 
-    if (innerContentRaw === undefined) {
-      innerContentRaw = normalizeContent(props.defaultContent, props.hookProps)
+    if (innerContent === undefined) { // use the default
+      innerContent = normalizeContent(props.defaultContent, props.hookProps)
     }
 
-    if (innerContentRaw !== undefined) {
+    if (innerContent !== undefined) { // we allow custom content handlers to return nothing
 
-      if (isComponentChildren(innerContentRaw)) {
-        innerContent = innerContentRaw
+      if (customContentInfo) {
+        customContentInfo.contentVal = innerContent[customContentInfo.contentKey]
 
       } else {
-        innerContent = [] // signal that something was specified
-
-        if (this.customContentHandler) {
-          this.customContentHandler.meta = innerContentRaw
-
-        // after this point, we know innerContentRaw is not null nor undefined
-        // would have been caught by isComponentChildren
-        } else if ('html' in innerContentRaw) {
-          this.customContentHandler = new HtmlContentHandler(innerContentRaw)
-
-        } else if ('domNodes' in innerContentRaw) {
-          this.customContentHandler = new DomContentHandler(innerContentRaw)
+        // look for a prop that would indicate a custom content handler is needed
+        for (let contentKey in contentTypeHandlers) {
+
+          if (innerContent[contentKey] !== undefined) {
+            customContentInfo = this.customContentInfo = {
+              contentKey,
+              contentVal: innerContent[contentKey],
+              handler: contentTypeHandlers[contentKey]()
+            }
+            break
+          }
         }
       }
+
+      if (customContentInfo) {
+        innerContentVDom = [] // signal that something was specified
+      } else {
+        innerContentVDom = innerContent // assume a [p]react vdom node. use it
+      }
     }
 
-    return innerContent
+    return innerContentVDom
   }
 
 
   private updateCustomContent() {
-    if (this.customContentHandler) {
-      this.customContentHandler.updateEl(this.innerElRef.current || this.props.backupElRef.current)
+    if (this.customContentInfo) {
+      this.customContentInfo.handler(
+        this.innerElRef.current || this.props.backupElRef.current, // the element to render into
+        this.customContentInfo.contentVal
+      )
     }
   }
 
@@ -247,70 +265,3 @@ function normalizeContent(input, hookProps) {
     return input
   }
 }
-
-
-function isComponentChildren(input) { // TODO: make this a general util
-  let type = typeof input
-
-  return (type === 'object')
-    ? (
-      !input || // null
-      Array.isArray(input) || // DOM node list
-      input.type // a virtual DOM node
-    )
-    : type.match(/^(undefined|string|number|boolean)$/)
-}
-
-
-abstract class ContentHandler<RenderMeta> {
-
-  private el: HTMLElement
-
-  constructor(public meta: RenderMeta) {
-  }
-
-  updateEl(el: HTMLElement) {
-    this.render(el, this.meta, this.el !== el)
-    this.el = el
-  }
-
-  abstract render(el: HTMLElement, meta: RenderMeta, isInitial: boolean)
-
-}
-
-
-type HtmlMeta = { html: string }
-
-class HtmlContentHandler extends ContentHandler<HtmlMeta> {
-
-  render(el: HTMLElement, meta: HtmlMeta) {
-    el.innerHTML = meta.html
-  }
-
-}
-
-
-type DomMeta = { domNodes: Node[] | NodeList }
-
-class DomContentHandler extends ContentHandler<DomMeta> {
-
-  render(el: HTMLElement, meta: DomMeta) {
-    removeAllChildren(el)
-
-    let { domNodes } = meta
-
-    for (let i = 0; i < domNodes.length; i++) {
-      el.appendChild(domNodes[i])
-    }
-  }
-
-}
-
-
-function removeAllChildren(parentEl: HTMLElement) { // TODO: move to util file
-  let { childNodes } = parentEl
-
-  while (childNodes.length) {
-    parentEl.removeChild(childNodes[0])
-  }
-}

+ 9 - 3
packages/core/src/global-plugins.ts

@@ -1,10 +1,10 @@
-import { PluginDef } from './plugin-system'
-
+import { PluginDef, createPlugin } from './plugin-system'
 import ArrayEventSourcePlugin from './event-sources/array-event-source'
 import FuncEventSourcePlugin from './event-sources/func-event-source'
 import JsonFeedEventSourcePlugin from './event-sources/json-feed-event-source'
 import SimpleRecurrencePlugin from './structs/recurring-event-simple'
 import DefaultOptionChangeHandlers from './option-change-handlers'
+import { injectHtml, injectDomNodes } from './util/dom-manip'
 
 /*
 this array is exposed on the root namespace so that UMD plugins can add to it.
@@ -15,5 +15,11 @@ export let globalPlugins: PluginDef[] = [
   FuncEventSourcePlugin,
   JsonFeedEventSourcePlugin,
   SimpleRecurrencePlugin,
-  DefaultOptionChangeHandlers
+  DefaultOptionChangeHandlers,
+  createPlugin({
+    contentTypeHandlers: {
+      html: () => injectHtml,
+      domNodes: () => injectDomNodes
+    }
+  })
 ]

+ 9 - 3
packages/core/src/plugin-system.ts

@@ -21,6 +21,7 @@ import { ElementDraggingClass } from './interactions/ElementDragging'
 import { guid } from './util/misc'
 import { ComponentChildren } from './vdom'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
+import { ContentTypeHandlers } from './common/render-hook'
 
 // TODO: easier way to add new hooks? need to update a million things
 
@@ -52,6 +53,7 @@ export interface PluginDefInput {
   elementDraggingImpl?: ElementDraggingClass
   optionChangeHandlers?: OptionChangeHandlerMap
   scrollGridImpl?: ScrollGridImpl
+  contentTypeHandlers?: ContentTypeHandlers
 }
 
 export interface PluginHooks {
@@ -81,6 +83,7 @@ export interface PluginHooks {
   elementDraggingImpl?: ElementDraggingClass
   optionChangeHandlers: OptionChangeHandlerMap
   scrollGridImpl: ScrollGridImpl | null
+  contentTypeHandlers: ContentTypeHandlers
 }
 
 export interface PluginDef extends PluginHooks {
@@ -126,7 +129,8 @@ export function createPlugin(input: PluginDefInput): PluginDef {
     defaultView: input.defaultView || '',
     elementDraggingImpl: input.elementDraggingImpl,
     optionChangeHandlers: input.optionChangeHandlers || {},
-    scrollGridImpl: input.scrollGridImpl || null
+    scrollGridImpl: input.scrollGridImpl || null,
+    contentTypeHandlers: input.contentTypeHandlers || {}
   }
 }
 
@@ -162,7 +166,8 @@ export class PluginSystem {
       defaultView: '',
       elementDraggingImpl: null,
       optionChangeHandlers: {},
-      scrollGridImpl: null
+      scrollGridImpl: null,
+      contentTypeHandlers: {}
     }
     this.addedHash = {}
   }
@@ -208,6 +213,7 @@ function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
     defaultView: hooks0.defaultView || hooks1.defaultView, // put earlier plugins FIRST
     elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
     optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
-    scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl
+    scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
+    contentTypeHandlers: { ...hooks0.contentTypeHandlers, ...hooks1.contentTypeHandlers }
   }
 }

+ 21 - 1
packages/core/src/util/dom-manip.ts

@@ -1,3 +1,5 @@
+import { isArraysEqual } from './array'
+
 
 export function htmlToElement(html: string): HTMLElement {
   html = html.trim()
@@ -7,13 +9,31 @@ export function htmlToElement(html: string): HTMLElement {
 }
 
 
-export function removeElement(el: HTMLElement) {
+export function removeElement(el: HTMLElement) { // removes nodes in addition to elements. bad name
   if (el.parentNode) {
     el.parentNode.removeChild(el)
   }
 }
 
 
+export function injectHtml(el: HTMLElement, html: string) {
+  el.innerHTML = html
+}
+
+
+export function injectDomNodes(el: HTMLElement, domNodes: Node[] | NodeList) {
+  let oldNodes = Array.prototype.slice.call(el.childNodes) // TODO: use array util
+  let newNodes = Array.prototype.slice.call(domNodes) // TODO: use array util
+
+  if (!isArraysEqual(oldNodes, newNodes)) {
+    for (let newNode of newNodes) {
+      el.appendChild(newNode)
+    }
+    oldNodes.forEach(removeElement)
+  }
+}
+
+
 // Querying
 // ----------------------------------------------------------------------------------------------------------------