Files
e-cosplay/assets/functions/preact.js
Serreau Jovann 5cf3da1488 ```
 feat(newsletter): Ajoute l'éditeur de template d'email avec Preact

Crée un nouvel éditeur de template d'email en utilisant Preact et
react-email-editor, et l'intègre au contrôleur et aux vues.
```
2025-08-02 10:45:16 +02:00

153 lines
5.3 KiB
JavaScript

import { h, cloneElement, render, hydrate } from 'preact'
export default function preactCustomElement (tagName, Component, propNames, options) {
function PreactElement () {
const inst = Reflect.construct(HTMLElement, [], PreactElement)
inst._vdomComponent = Component
inst._root = options && options.shadow ? inst.attachShadow({ mode: 'open' }) : inst
return inst
}
PreactElement.prototype = Object.create(HTMLElement.prototype)
PreactElement.prototype.constructor = PreactElement
PreactElement.prototype.connectedCallback = connectedCallback
PreactElement.prototype.attributeChangedCallback = attributeChangedCallback
PreactElement.prototype.disconnectedCallback = disconnectedCallback
propNames = propNames || Component.observedAttributes || Object.keys(Component.propTypes || {})
PreactElement.observedAttributes = propNames
// Keep DOM properties and Preact props in sync
propNames.forEach(name => {
Object.defineProperty(PreactElement.prototype, name, {
get () {
return this._vdom.props[name]
},
set (v) {
if (this._vdom) {
this.attributeChangedCallback(name, null, v)
} else {
if (!this._props) this._props = {}
this._props[name] = v
this.connectedCallback()
}
// Reflect property changes to attributes if the value is a primitive
const type = typeof v
if (v == null || type === 'string' || type === 'boolean' || type === 'number') {
this.setAttribute(name, v)
}
}
})
})
return customElements.define(tagName || Component.tagName || Component.displayName || Component.name, PreactElement)
}
function ContextProvider (props) {
this.getChildContext = () => props.context
// eslint-disable-next-line no-unused-vars
const { context, children, ...rest } = props
return cloneElement(children, rest)
}
function connectedCallback () {
// Obtain a reference to the previous context by pinging the nearest
// higher up node that was rendered with Preact. If one Preact component
// higher up receives our ping, it will set the `detail` property of
// our custom event. This works because events are dispatched
// synchronously.
const event = new CustomEvent('_preact', {
detail: {},
bubbles: true,
cancelable: true
})
this.dispatchEvent(event)
const context = event.detail.context
this._vdom = h(ContextProvider, { ...this._props, context }, toVdom(this, this._vdomComponent))
;(this.hasAttribute('hydrate') ? hydrate : render)(this._vdom, this._root)
}
function toCamelCase (str) {
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}
function attributeChangedCallback (name, oldValue, newValue) {
if (!this._vdom) return
// Attributes use `null` as an empty value whereas `undefined` is more
// common in pure JS components, especially with default parameters.
// When calling `node.removeAttribute()` we'll receive `null` as the new
// value. See issue #50.
newValue = newValue == null ? undefined : newValue
const props = {}
props[name] = newValue
props[toCamelCase(name)] = newValue
this._vdom = cloneElement(this._vdom, props)
render(this._vdom, this._root)
}
function disconnectedCallback () {
render((this._vdom = null), this._root)
}
/**
* Pass an event listener to each `<slot>` that "forwards" the current
* context value to the rendered child. The child will trigger a custom
* event, where will add the context value to. Because events work
* synchronously, the child can immediately pull of the value right
* after having fired the event.
*/
function Slot (props, context) {
const ref = r => {
if (!r) {
this.ref.removeEventListener('_preact', this._listener)
} else {
this.ref = r
if (!this._listener) {
this._listener = event => {
event.stopPropagation()
event.detail.context = context
}
r.addEventListener('_preact', this._listener)
}
}
}
return h('slot', { ...props, ref })
}
function toVdom (element, nodeName) {
if (element.nodeType === Node.TEXT_NODE) {
const data = element.data
element.data = ''
return data
}
if (element.nodeType !== Node.ELEMENT_NODE) return null
const children = []
const props = {}
let i = 0
const a = element.attributes
const cn = element.childNodes
for (i = a.length; i--; ) {
if (a[i].name !== 'slot') {
props[a[i].name] = a[i].value
props[toCamelCase(a[i].name)] = a[i].value
}
}
props.parent = element
for (i = cn.length; i--; ) {
const vnode = toVdom(cn[i], null)
// Move slots correctly
const name = cn[i].slot
if (name) {
props[name] = h(Slot, { name }, vnode)
} else {
children[i] = vnode
}
}
// Only wrap the topmost node with a slot
const wrappedChildren = nodeName ? h(Slot, null, children) : children
return h(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren)
}