Jelajahi Sumber

make content types pluggable

Adam Shaw 5 tahun lalu
induk
melakukan
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
   innerContent: ComponentChildren // if falsy, means it wasn't specified
 ) => ComponentChildren
 ) => ComponentChildren
 
 
+export interface ContentTypeHandlers {
+  [contentKey: string]: () => (el: HTMLElement, contentVal: any) => void
+}
+
 // TODO: use capitalizeFirstLetter util
 // TODO: use capitalizeFirstLetter util
 
 
 
 
@@ -77,9 +81,14 @@ export interface ContentHookProps<HookProps> {
 export class ContentHook<HookProps> extends Component<ContentHookProps<HookProps>> {
 export class ContentHook<HookProps> extends Component<ContentHookProps<HookProps>> {
 
 
   static contextType = ComponentContextType
   static contextType = ComponentContextType
+  context: ComponentContext
 
 
   private innerElRef = createRef()
   private innerElRef = createRef()
-  private customContentHandler: ContentHandler<any>
+  private customContentInfo: {
+    contentKey: string
+    contentVal: any
+    handler: (el: HTMLElement, contentVal: any) => void
+  }
 
 
 
 
   render(props: ContentHookProps<HookProps>) {
   render(props: ContentHookProps<HookProps>) {
@@ -98,44 +107,53 @@ export class ContentHook<HookProps> extends Component<ContentHookProps<HookProps
 
 
 
 
   private renderInnerContent() {
   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 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 {
       } 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() {
   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
     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 ArrayEventSourcePlugin from './event-sources/array-event-source'
 import FuncEventSourcePlugin from './event-sources/func-event-source'
 import FuncEventSourcePlugin from './event-sources/func-event-source'
 import JsonFeedEventSourcePlugin from './event-sources/json-feed-event-source'
 import JsonFeedEventSourcePlugin from './event-sources/json-feed-event-source'
 import SimpleRecurrencePlugin from './structs/recurring-event-simple'
 import SimpleRecurrencePlugin from './structs/recurring-event-simple'
 import DefaultOptionChangeHandlers from './option-change-handlers'
 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.
 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,
   FuncEventSourcePlugin,
   JsonFeedEventSourcePlugin,
   JsonFeedEventSourcePlugin,
   SimpleRecurrencePlugin,
   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 { guid } from './util/misc'
 import { ComponentChildren } from './vdom'
 import { ComponentChildren } from './vdom'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
 import { ScrollGridImpl } from './scrollgrid/ScrollGridImpl'
+import { ContentTypeHandlers } from './common/render-hook'
 
 
 // TODO: easier way to add new hooks? need to update a million things
 // TODO: easier way to add new hooks? need to update a million things
 
 
@@ -52,6 +53,7 @@ export interface PluginDefInput {
   elementDraggingImpl?: ElementDraggingClass
   elementDraggingImpl?: ElementDraggingClass
   optionChangeHandlers?: OptionChangeHandlerMap
   optionChangeHandlers?: OptionChangeHandlerMap
   scrollGridImpl?: ScrollGridImpl
   scrollGridImpl?: ScrollGridImpl
+  contentTypeHandlers?: ContentTypeHandlers
 }
 }
 
 
 export interface PluginHooks {
 export interface PluginHooks {
@@ -81,6 +83,7 @@ export interface PluginHooks {
   elementDraggingImpl?: ElementDraggingClass
   elementDraggingImpl?: ElementDraggingClass
   optionChangeHandlers: OptionChangeHandlerMap
   optionChangeHandlers: OptionChangeHandlerMap
   scrollGridImpl: ScrollGridImpl | null
   scrollGridImpl: ScrollGridImpl | null
+  contentTypeHandlers: ContentTypeHandlers
 }
 }
 
 
 export interface PluginDef extends PluginHooks {
 export interface PluginDef extends PluginHooks {
@@ -126,7 +129,8 @@ export function createPlugin(input: PluginDefInput): PluginDef {
     defaultView: input.defaultView || '',
     defaultView: input.defaultView || '',
     elementDraggingImpl: input.elementDraggingImpl,
     elementDraggingImpl: input.elementDraggingImpl,
     optionChangeHandlers: input.optionChangeHandlers || {},
     optionChangeHandlers: input.optionChangeHandlers || {},
-    scrollGridImpl: input.scrollGridImpl || null
+    scrollGridImpl: input.scrollGridImpl || null,
+    contentTypeHandlers: input.contentTypeHandlers || {}
   }
   }
 }
 }
 
 
@@ -162,7 +166,8 @@ export class PluginSystem {
       defaultView: '',
       defaultView: '',
       elementDraggingImpl: null,
       elementDraggingImpl: null,
       optionChangeHandlers: {},
       optionChangeHandlers: {},
-      scrollGridImpl: null
+      scrollGridImpl: null,
+      contentTypeHandlers: {}
     }
     }
     this.addedHash = {}
     this.addedHash = {}
   }
   }
@@ -208,6 +213,7 @@ function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
     defaultView: hooks0.defaultView || hooks1.defaultView, // put earlier plugins FIRST
     defaultView: hooks0.defaultView || hooks1.defaultView, // put earlier plugins FIRST
     elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
     elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl, // "
     optionChangeHandlers: { ...hooks0.optionChangeHandlers, ...hooks1.optionChangeHandlers },
     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 {
 export function htmlToElement(html: string): HTMLElement {
   html = html.trim()
   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) {
   if (el.parentNode) {
     el.parentNode.removeChild(el)
     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
 // Querying
 // ----------------------------------------------------------------------------------------------------------------
 // ----------------------------------------------------------------------------------------------------------------