| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- import {
- type App,
- type Component,
- type ComponentCustomElementInterface,
- type ComponentInjectOptions,
- type ComponentInternalInstance,
- type ComponentObjectPropsOptions,
- type ComponentOptions,
- type ComponentOptionsBase,
- type ComponentOptionsMixin,
- type ComponentProvideOptions,
- type ComponentPublicInstance,
- type ComputedOptions,
- type ConcreteComponent,
- type CreateAppFunction,
- type CreateComponentPublicInstanceWithMixins,
- type DefineComponent,
- type Directive,
- type EmitsOptions,
- type EmitsToProps,
- type ExtractPropTypes,
- type MethodOptions,
- type RenderFunction,
- type SetupContext,
- type SlotsType,
- type VNode,
- type VNodeProps,
- createVNode,
- defineComponent,
- getCurrentInstance,
- nextTick,
- unref,
- warn,
- } from '@vue/runtime-core'
- import {
- camelize,
- extend,
- hasOwn,
- hyphenate,
- isArray,
- isPlainObject,
- toNumber,
- } from '@vue/shared'
- import { createApp, createSSRApp, render } from '.'
- // marker for attr removal
- const REMOVAL = {}
- export type VueElementConstructor<P = {}> = {
- new (initialProps?: Record<string, any>): VueElement & P
- }
- export interface CustomElementOptions {
- styles?: string[]
- shadowRoot?: boolean
- nonce?: string
- configureApp?: (app: App) => void
- }
- // defineCustomElement provides the same type inference as defineComponent
- // so most of the following overloads should be kept in sync w/ defineComponent.
- // overload 1: direct setup function
- export function defineCustomElement<Props, RawBindings = object>(
- setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
- options?: Pick<ComponentOptions, 'name' | 'inheritAttrs' | 'emits'> &
- CustomElementOptions & {
- props?: (keyof Props)[]
- },
- ): VueElementConstructor<Props>
- export function defineCustomElement<Props, RawBindings = object>(
- setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
- options?: Pick<ComponentOptions, 'name' | 'inheritAttrs' | 'emits'> &
- CustomElementOptions & {
- props?: ComponentObjectPropsOptions<Props>
- },
- ): VueElementConstructor<Props>
- // overload 2: defineCustomElement with options object, infer props from options
- export function defineCustomElement<
- // props
- RuntimePropsOptions extends
- ComponentObjectPropsOptions = ComponentObjectPropsOptions,
- PropsKeys extends string = string,
- // emits
- RuntimeEmitsOptions extends EmitsOptions = {},
- EmitsKeys extends string = string,
- // other options
- Data = {},
- SetupBindings = {},
- Computed extends ComputedOptions = {},
- Methods extends MethodOptions = {},
- Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
- Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
- InjectOptions extends ComponentInjectOptions = {},
- InjectKeys extends string = string,
- Slots extends SlotsType = {},
- LocalComponents extends Record<string, Component> = {},
- Directives extends Record<string, Directive> = {},
- Exposed extends string = string,
- Provide extends ComponentProvideOptions = ComponentProvideOptions,
- // resolved types
- InferredProps = string extends PropsKeys
- ? ComponentObjectPropsOptions extends RuntimePropsOptions
- ? {}
- : ExtractPropTypes<RuntimePropsOptions>
- : { [key in PropsKeys]?: any },
- ResolvedProps = InferredProps & EmitsToProps<RuntimeEmitsOptions>,
- >(
- options: CustomElementOptions & {
- props?: (RuntimePropsOptions & ThisType<void>) | PropsKeys[]
- } & ComponentOptionsBase<
- ResolvedProps,
- SetupBindings,
- Data,
- Computed,
- Methods,
- Mixin,
- Extends,
- RuntimeEmitsOptions,
- EmitsKeys,
- {}, // Defaults
- InjectOptions,
- InjectKeys,
- Slots,
- LocalComponents,
- Directives,
- Exposed,
- Provide
- > &
- ThisType<
- CreateComponentPublicInstanceWithMixins<
- Readonly<ResolvedProps>,
- SetupBindings,
- Data,
- Computed,
- Methods,
- Mixin,
- Extends,
- RuntimeEmitsOptions,
- EmitsKeys,
- {},
- false,
- InjectOptions,
- Slots,
- LocalComponents,
- Directives,
- Exposed
- >
- >,
- extraOptions?: CustomElementOptions,
- ): VueElementConstructor<ResolvedProps>
- // overload 3: defining a custom element from the returned value of
- // `defineComponent`
- export function defineCustomElement<
- // this should be `ComponentPublicInstanceConstructor` but that type is not exported
- T extends { new (...args: any[]): ComponentPublicInstance<any> },
- >(
- options: T,
- extraOptions?: CustomElementOptions,
- ): VueElementConstructor<
- T extends DefineComponent<infer P, any, any, any> ? P : unknown
- >
- /*@__NO_SIDE_EFFECTS__*/
- export function defineCustomElement(
- options: any,
- extraOptions?: ComponentOptions,
- /**
- * @internal
- */
- _createApp?: CreateAppFunction<Element>,
- ): VueElementConstructor {
- let Comp = defineComponent(options, extraOptions) as any
- if (isPlainObject(Comp)) Comp = extend({}, Comp, extraOptions)
- class VueCustomElement extends VueElement {
- static def = Comp
- constructor(initialProps?: Record<string, any>) {
- super(Comp, initialProps, _createApp)
- }
- }
- return VueCustomElement
- }
- /*@__NO_SIDE_EFFECTS__*/
- export const defineSSRCustomElement = ((
- options: any,
- extraOptions?: ComponentOptions,
- ) => {
- // @ts-expect-error
- return defineCustomElement(options, extraOptions, createSSRApp)
- }) as typeof defineCustomElement
- const BaseClass = (
- typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
- ) as typeof HTMLElement
- type InnerComponentDef = ConcreteComponent & CustomElementOptions
- export class VueElement
- extends BaseClass
- implements ComponentCustomElementInterface
- {
- _isVueCE = true
- /**
- * @internal
- */
- _instance: ComponentInternalInstance | null = null
- /**
- * @internal
- */
- _app: App | null = null
- /**
- * @internal
- */
- _root: Element | ShadowRoot
- /**
- * @internal
- */
- _nonce: string | undefined = this._def.nonce
- /**
- * @internal
- */
- _teleportTargets?: Set<Element>
- private _connected = false
- private _resolved = false
- private _numberProps: Record<string, true> | null = null
- private _styleChildren = new WeakSet()
- private _pendingResolve: Promise<void> | undefined
- private _parent: VueElement | undefined
- /**
- * dev only
- */
- private _styles?: HTMLStyleElement[]
- /**
- * dev only
- */
- private _childStyles?: Map<string, HTMLStyleElement[]>
- private _ob?: MutationObserver | null = null
- private _slots?: Record<string, Node[]>
- constructor(
- /**
- * Component def - note this may be an AsyncWrapper, and this._def will
- * be overwritten by the inner component when resolved.
- */
- private _def: InnerComponentDef,
- private _props: Record<string, any> = {},
- private _createApp: CreateAppFunction<Element> = createApp,
- ) {
- super()
- if (this.shadowRoot && _createApp !== createApp) {
- this._root = this.shadowRoot
- } else {
- if (__DEV__ && this.shadowRoot) {
- warn(
- `Custom element has pre-rendered declarative shadow root but is not ` +
- `defined as hydratable. Use \`defineSSRCustomElement\`.`,
- )
- }
- if (_def.shadowRoot !== false) {
- this.attachShadow({ mode: 'open' })
- this._root = this.shadowRoot!
- } else {
- this._root = this
- }
- }
- }
- connectedCallback(): void {
- // avoid resolving component if it's not connected
- if (!this.isConnected) return
- // avoid re-parsing slots if already resolved
- if (!this.shadowRoot && !this._resolved) {
- this._parseSlots()
- }
- this._connected = true
- // locate nearest Vue custom element parent for provide/inject
- let parent: Node | null = this
- while (
- (parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
- ) {
- if (parent instanceof VueElement) {
- this._parent = parent
- break
- }
- }
- if (!this._instance) {
- if (this._resolved) {
- this._mount(this._def)
- } else {
- if (parent && parent._pendingResolve) {
- this._pendingResolve = parent._pendingResolve.then(() => {
- this._pendingResolve = undefined
- this._resolveDef()
- })
- } else {
- this._resolveDef()
- }
- }
- }
- }
- private _setParent(parent = this._parent) {
- if (parent) {
- this._instance!.parent = parent._instance
- this._inheritParentContext(parent)
- }
- }
- private _inheritParentContext(parent = this._parent) {
- // #13212, the provides object of the app context must inherit the provides
- // object from the parent element so we can inject values from both places
- if (parent && this._app) {
- Object.setPrototypeOf(
- this._app._context.provides,
- parent._instance!.provides,
- )
- }
- }
- disconnectedCallback(): void {
- this._connected = false
- nextTick(() => {
- if (!this._connected) {
- if (this._ob) {
- this._ob.disconnect()
- this._ob = null
- }
- // unmount
- this._app && this._app.unmount()
- if (this._instance) this._instance.ce = undefined
- this._app = this._instance = null
- if (this._teleportTargets) {
- this._teleportTargets.clear()
- this._teleportTargets = undefined
- }
- }
- })
- }
- private _processMutations(mutations: MutationRecord[]) {
- for (const m of mutations) {
- this._setAttr(m.attributeName!)
- }
- }
- /**
- * resolve inner component definition (handle possible async component)
- */
- private _resolveDef() {
- if (this._pendingResolve) {
- return
- }
- // set initial attrs
- for (let i = 0; i < this.attributes.length; i++) {
- this._setAttr(this.attributes[i].name)
- }
- // watch future attr changes
- this._ob = new MutationObserver(this._processMutations.bind(this))
- this._ob.observe(this, { attributes: true })
- const resolve = (def: InnerComponentDef, isAsync = false) => {
- this._resolved = true
- this._pendingResolve = undefined
- const { props, styles } = def
- // cast Number-type props set before resolve
- let numberProps
- if (props && !isArray(props)) {
- for (const key in props) {
- const opt = props[key]
- if (opt === Number || (opt && opt.type === Number)) {
- if (key in this._props) {
- this._props[key] = toNumber(this._props[key])
- }
- ;(numberProps || (numberProps = Object.create(null)))[
- camelize(key)
- ] = true
- }
- }
- }
- this._numberProps = numberProps
- this._resolveProps(def)
- // apply CSS
- if (this.shadowRoot) {
- this._applyStyles(styles)
- } else if (__DEV__ && styles) {
- warn(
- 'Custom element style injection is not supported when using ' +
- 'shadowRoot: false',
- )
- }
- // initial mount
- this._mount(def)
- }
- const asyncDef = (this._def as ComponentOptions).__asyncLoader
- if (asyncDef) {
- this._pendingResolve = asyncDef().then((def: InnerComponentDef) => {
- def.configureApp = this._def.configureApp
- resolve((this._def = def), true)
- })
- } else {
- resolve(this._def)
- }
- }
- private _mount(def: InnerComponentDef) {
- if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
- // @ts-expect-error
- def.name = 'VueElement'
- }
- this._app = this._createApp(def)
- // inherit before configureApp to detect context overwrites
- this._inheritParentContext()
- if (def.configureApp) {
- def.configureApp(this._app)
- }
- this._app._ceVNode = this._createVNode()
- this._app.mount(this._root)
- // apply expose after mount
- const exposed = this._instance && this._instance.exposed
- if (!exposed) return
- for (const key in exposed) {
- if (!hasOwn(this, key)) {
- // exposed properties are readonly
- Object.defineProperty(this, key, {
- // unwrap ref to be consistent with public instance behavior
- get: () => unref(exposed[key]),
- })
- } else if (__DEV__) {
- warn(`Exposed property "${key}" already exists on custom element.`)
- }
- }
- }
- private _resolveProps(def: InnerComponentDef) {
- const { props } = def
- const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
- // check if there are props set pre-upgrade or connect
- for (const key of Object.keys(this)) {
- if (key[0] !== '_' && declaredPropKeys.includes(key)) {
- this._setProp(key, this[key as keyof this])
- }
- }
- // defining getter/setters on prototype
- for (const key of declaredPropKeys.map(camelize)) {
- Object.defineProperty(this, key, {
- get() {
- return this._getProp(key)
- },
- set(val) {
- this._setProp(key, val, true, true)
- },
- })
- }
- }
- protected _setAttr(key: string): void {
- if (key.startsWith('data-v-')) return
- const has = this.hasAttribute(key)
- let value = has ? this.getAttribute(key) : REMOVAL
- const camelKey = camelize(key)
- if (has && this._numberProps && this._numberProps[camelKey]) {
- value = toNumber(value)
- }
- this._setProp(camelKey, value, false, true)
- }
- /**
- * @internal
- */
- protected _getProp(key: string): any {
- return this._props[key]
- }
- /**
- * @internal
- */
- _setProp(
- key: string,
- val: any,
- shouldReflect = true,
- shouldUpdate = false,
- ): void {
- if (val !== this._props[key]) {
- if (val === REMOVAL) {
- delete this._props[key]
- } else {
- this._props[key] = val
- // support set key on ceVNode
- if (key === 'key' && this._app) {
- this._app._ceVNode!.key = val
- }
- }
- if (shouldUpdate && this._instance) {
- this._update()
- }
- // reflect
- if (shouldReflect) {
- const ob = this._ob
- if (ob) {
- this._processMutations(ob.takeRecords())
- ob.disconnect()
- }
- if (val === true) {
- this.setAttribute(hyphenate(key), '')
- } else if (typeof val === 'string' || typeof val === 'number') {
- this.setAttribute(hyphenate(key), val + '')
- } else if (!val) {
- this.removeAttribute(hyphenate(key))
- }
- ob && ob.observe(this, { attributes: true })
- }
- }
- }
- private _update() {
- const vnode = this._createVNode()
- if (this._app) vnode.appContext = this._app._context
- render(vnode, this._root)
- }
- private _createVNode(): VNode<any, any> {
- const baseProps: VNodeProps = {}
- if (!this.shadowRoot) {
- baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
- this._renderSlots.bind(this)
- }
- const vnode = createVNode(this._def, extend(baseProps, this._props))
- if (!this._instance) {
- vnode.ce = instance => {
- this._instance = instance
- instance.ce = this
- instance.isCE = true // for vue-i18n backwards compat
- // HMR
- if (__DEV__) {
- instance.ceReload = newStyles => {
- // always reset styles
- if (this._styles) {
- this._styles.forEach(s => this._root.removeChild(s))
- this._styles.length = 0
- }
- this._applyStyles(newStyles)
- this._instance = null
- this._update()
- }
- }
- const dispatch = (event: string, args: any[]) => {
- this.dispatchEvent(
- new CustomEvent(
- event,
- isPlainObject(args[0])
- ? extend({ detail: args }, args[0])
- : { detail: args },
- ),
- )
- }
- // intercept emit
- instance.emit = (event: string, ...args: any[]) => {
- // dispatch both the raw and hyphenated versions of an event
- // to match Vue behavior
- dispatch(event, args)
- if (hyphenate(event) !== event) {
- dispatch(hyphenate(event), args)
- }
- }
- this._setParent()
- }
- }
- return vnode
- }
- private _applyStyles(
- styles: string[] | undefined,
- owner?: ConcreteComponent,
- ) {
- if (!styles) return
- if (owner) {
- if (owner === this._def || this._styleChildren.has(owner)) {
- return
- }
- this._styleChildren.add(owner)
- }
- const nonce = this._nonce
- for (let i = styles.length - 1; i >= 0; i--) {
- const s = document.createElement('style')
- if (nonce) s.setAttribute('nonce', nonce)
- s.textContent = styles[i]
- this.shadowRoot!.prepend(s)
- // record for HMR
- if (__DEV__) {
- if (owner) {
- if (owner.__hmrId) {
- if (!this._childStyles) this._childStyles = new Map()
- let entry = this._childStyles.get(owner.__hmrId)
- if (!entry) {
- this._childStyles.set(owner.__hmrId, (entry = []))
- }
- entry.push(s)
- }
- } else {
- ;(this._styles || (this._styles = [])).push(s)
- }
- }
- }
- }
- /**
- * Only called when shadowRoot is false
- */
- private _parseSlots() {
- const slots: VueElement['_slots'] = (this._slots = {})
- let n
- while ((n = this.firstChild)) {
- const slotName =
- (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
- ;(slots[slotName] || (slots[slotName] = [])).push(n)
- this.removeChild(n)
- }
- }
- /**
- * Only called when shadowRoot is false
- */
- private _renderSlots() {
- const outlets = this._getSlots()
- const scopeId = this._instance!.type.__scopeId
- for (let i = 0; i < outlets.length; i++) {
- const o = outlets[i] as HTMLSlotElement
- const slotName = o.getAttribute('name') || 'default'
- const content = this._slots![slotName]
- const parent = o.parentNode!
- if (content) {
- for (const n of content) {
- // for :slotted css
- if (scopeId && n.nodeType === 1) {
- const id = scopeId + '-s'
- const walker = document.createTreeWalker(n, 1)
- ;(n as Element).setAttribute(id, '')
- let child
- while ((child = walker.nextNode())) {
- ;(child as Element).setAttribute(id, '')
- }
- }
- parent.insertBefore(n, o)
- }
- } else {
- while (o.firstChild) parent.insertBefore(o.firstChild, o)
- }
- parent.removeChild(o)
- }
- }
- /**
- * @internal
- */
- private _getSlots(): HTMLSlotElement[] {
- const roots: Element[] = [this]
- if (this._teleportTargets) {
- roots.push(...this._teleportTargets)
- }
- return roots.reduce<HTMLSlotElement[]>((res, i) => {
- res.push(...Array.from(i.querySelectorAll('slot')))
- return res
- }, [])
- }
- /**
- * @internal
- */
- _injectChildStyle(comp: ConcreteComponent & CustomElementOptions): void {
- this._applyStyles(comp.styles, comp)
- }
- /**
- * @internal
- */
- _removeChildStyle(comp: ConcreteComponent): void {
- if (__DEV__) {
- this._styleChildren.delete(comp)
- if (this._childStyles && comp.__hmrId) {
- // clear old styles
- const oldStyles = this._childStyles.get(comp.__hmrId)
- if (oldStyles) {
- oldStyles.forEach(s => this._root.removeChild(s))
- oldStyles.length = 0
- }
- }
- }
- }
- }
- export function useHost(caller?: string): VueElement | null {
- const instance = getCurrentInstance()
- const el = instance && (instance.ce as VueElement)
- if (el) {
- return el
- } else if (__DEV__) {
- if (!instance) {
- warn(
- `${caller || 'useHost'} called without an active component instance.`,
- )
- } else {
- warn(
- `${caller || 'useHost'} can only be used in components defined via ` +
- `defineCustomElement.`,
- )
- }
- }
- return null
- }
- /**
- * Retrieve the shadowRoot of the current custom element. Only usable in setup()
- * of a `defineCustomElement` component.
- */
- export function useShadowRoot(): ShadowRoot | null {
- const el = __DEV__ ? useHost('useShadowRoot') : useHost()
- return el && el.shadowRoot
- }
|