summaryrefslogtreecommitdiff
path: root/src/index.ts
blob: 7e73cf5afa4fbf39ae4e87f29023c2dc82dcaf81 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { EvalActions, evalExpr } from './eval'
import { parse } from './parser'

const UNSET_PROPERTY_VALUE = '<unset>'
const EVENT_HANDLERS = {
  click: '--cssx-on-click',
  load: '--cssx-on-load',
}

const injectStyles = () => {
  const STYLE_TAG_CLASS = 'cssx-style-root'
  if (document.querySelector(`.${STYLE_TAG_CLASS}`)) return

  const $style = document.createElement('style')
  $style.className = STYLE_TAG_CLASS

  const properties = ['--cssx-children', ...Object.values(EVENT_HANDLERS)]

  $style.textContent = `.cssx-layer {
    ${properties.map((p) => `${p}: ${UNSET_PROPERTY_VALUE};`).join(' ')}
  }`

  document.body.appendChild($style)
}

const getPropertyValue = ($element: Element, prop: string) => {
  const value = `${getComputedStyle($element).getPropertyValue(prop)}`.trim()
  return !value || value === UNSET_PROPERTY_VALUE ? '' : value
}

const getChildrenIds = ($element: Element) => {
  const value = getPropertyValue($element, '--cssx-children')
  return value.split(/(\s*,\s*)|\s+/g).filter(Boolean)
}

const getEvalActions = ($element: Element): EvalActions => ({
  addClass: async (id, cls) => document.getElementById(id)?.classList.add(cls),
  removeClass: async (id, cls) =>
    document.getElementById(id)?.classList.remove(cls),
  delay: (delay) => new Promise((res) => setTimeout(res, delay)),
  jsEval: async (js) => (0, eval)(js),
  loadCssx: async (id, url) =>
    new Promise((resolve, reject) => {
      const $link = Object.assign(document.createElement('link'), {
        href: url,
        rel: 'stylesheet',
      })
      $link.onload = () => {
        const $el = document.getElementById(id)
        // NOTE: Maybe create and append to body if no root?
        if ($el) {
          manageElement($el)
          resolve(id)
        } else {
          console.error(`[CSSX] Unable to find root for ${id}`)
          reject(`[CSSX] Unable to find root for ${id}`)
        }
      }
      document.body.appendChild($link)
    }),
  getVariable: async (varName) => getPropertyValue($element, varName),
  updateVariable: async (targetId, varName, value) => {
    const $el = document.getElementById(targetId)
    if ($el) {
      $el.style.setProperty(varName, JSON.stringify(value))
    }
  },
})

const handleEvents = async ($element: Element) => {
  for (const [event, property] of Object.entries(EVENT_HANDLERS)) {
    const handlerExpr = getPropertyValue($element, property)

    if (handlerExpr) {
      ;($element as any)[`on${event}`] = async () => {
        console.log(`Triggered event: ${event}`)
        const exprs = parse(handlerExpr)
        for (const expr of exprs) {
          await evalExpr(expr, getEvalActions($element))
        }
      }
    }
  }
}

let nodeCount = 0
const manageElement = async ($element: Element) => {
  if (nodeCount++ > 100) return // NOTE: Temporary. To prevent infinite rec

  await handleEvents($element)
  const childrenIds = getChildrenIds($element)

  if (childrenIds.length > 0) {
    const LAYER_CLASS = 'cssx-layer'
    const $childrenRoot =
      $element.querySelector(`:scope > .${LAYER_CLASS}`) ??
      Object.assign(document.createElement('div'), { className: LAYER_CLASS })
    $element.appendChild($childrenRoot)

    for (const childId of childrenIds) {
      const [tag, id] = childId.split('#')
      const $child =
        $childrenRoot.querySelector(`:scope > #${id}`) ??
        Object.assign(document.createElement(tag || 'div'), { id })
      $childrenRoot.appendChild($child)
      await manageElement($child)
    }
  }
}

interface Options {
  root?: HTMLElement
}
const render = async ({ root = document.body }: Options = {}) => {
  injectStyles()
  await manageElement(root)
}

render()