/*
Adapted from svelte-tag by Chris Ward
*/
// witchcraft from svelte issue - https://github.com/sveltejs/svelte/issues/2588
import { detach, insert, noop } from 'svelte/internal'
function createSlots (slots) {
const svelteSlots = {}
for (const slotName in slots) {
svelteSlots[slotName] = [createSlotFn(slots[slotName])]
}
function createSlotFn (element) {
return function () {
return {
c: noop,
m: function mount (target, anchor) {
insert(target, element.cloneNode(true), anchor)
},
d: function destroy (detaching) {
if (detaching && element.innerHTML) {
detach(element)
}
},
l: noop
}
}
}
return svelteSlots
}
/**
* Convert a Svelte component into a Web Component for A-Frame.
*
* Provides the wrapperElement context to the Svelte component with a reference to the containing
* custom element instance.
* @param {object} opts
* @param {function} opts.Component - the Svelte component constructor
* @param {string} opts.tagname - the element tag for the Web Component, must contain a '-'
* @param {string[]} [opts.props] - the prop names from the Svelte copmonent which will be settable via HTML attributes (auto-converted between camelCase an dash-case)
* @param {HTMLElement} [opts.baseClass] - base class that Web Component element will inherit from, default's to AEntity
* @param {boolean} [opts.noWraper] - EXPERIMENTAL: render the Svelte component output as siblings to the Web Component element instead of as children
* @example <caption>Basic usage</caption>
* // creates and registers the <a-person> custom element from the APerson.svelte component
* import { registerWebComponent } from 'svawc'
* import APerson from "./APerson.svelte"
* registerWebComponent({ component: APerson, tagname: "a-person", attributes: ["skin", "shirt", "pants"] })
* @example <caption>Using context to modify the containing element from inside the Svelte component</caption>
* import { getContext } from "svelte";
* getContext('wrapperElement').setAttribute('shadow', '')
*/
export function registerWebComponent (opts) {
const BaseClass = opts.baseClass ?? window.AFRAME.AEntity
opts.props ??= []
// setup camel/dash case conversions
const attributes = opts.props.map(prop => dashify(prop))
const toDash = Object.fromEntries(opts.props.map((prop, i) => [prop, attributes[i]]))
const toCamel = Object.fromEntries(opts.props.map((prop, i) => [attributes[i], prop]))
class Wrapper extends BaseClass {
constructor () {
super()
this.addEventListener('nodeready', () => this.init())
}
static get observedAttributes () {
return (attributes).concat(BaseClass.observedAttributes || [])
}
// use init on nodeready instead of connectedCallback to avoid
// issues with multiple calls due to A-Frame's initialization delay
init () {
const props = {}
props.$$scope = {}
opts.props.forEach(prop => {
if (this.hasAttribute(toDash[prop])) {
props[prop] = this.getAttribute(toDash[prop])
}
})
props.$$scope = {}
const slots = this.getSlots()
props.$$slots = createSlots(slots)
const context = new Map([['wrapperElement', opts.noWrapper ? null : this]])
this.elem = new opts.Component({ target: opts.noWrapper ? this.parentElement : this, props, context })
}
disconnectedCallback () {
super.disconnectedCallback?.()
if (this.observe) {
this.observer.disconnect()
}
try { this.elem.$destroy() } catch (err) {} // detroy svelte element when removed from dom
}
unwrap (from) {
if (!from.content) {
console.warn('svawc: entities in slots should be wrapped in a template element')
const frag = document.createDocumentFragment()
frag.appendChild(from)
return frag
}
from.remove()
return from.content
}
getSlots () {
const namedSlots = this.querySelectorAll('[slot]')
const slots = {}
namedSlots.forEach(n => {
slots[n.slot] = this.unwrap(n)
})
const defaultSlot = this.firstElementChild
if (defaultSlot) {
slots.default = this.unwrap(defaultSlot)
} else if (this.textContent.trim().length) {
// if the only child is text, wrap in fragment and use as default slot
slots.default = document.createDocumentFragment()
slots.default.textContent = this.textContent.trim()
}
this.innerHTML = ''
return slots
}
attributeChangedCallback (name, oldValue, newValue) {
if (!attributes.includes(name)) {
// passthrough for inherited attrs
return super.attributeChangedCallback?.()
}
if (this.elem && newValue !== oldValue) {
this.elem.$set({ [toCamel[name]]: newValue })
}
}
}
window.customElements.define(opts.tagname, Wrapper)
return Wrapper
}
/*!
* dashify <https://github.com/jonschlinkert/dashify>
*
* Copyright (c) 2015-2017, Jon Schlinkert.
* Released under the MIT License.
*/
function dashify (str, options) {
if (typeof str !== 'string') throw new TypeError('expected a string')
return str.trim()
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/\W/g, m => /[À-ž]/.test(m) ? m : '-')
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, m => options && options.condense ? '-' : m)
.toLowerCase()
};
Source