1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16 17type Style = string|Partial<CSSStyleDeclaration>; 18 19export interface MountOptions { 20 // Optionally specify an element in which to place our portal. 21 // Defaults to body. 22 container?: Element; 23} 24 25export interface PortalAttrs { 26 // Space delimited class list forwarded to our portal element. 27 className?: string; 28 // Inline styles forwarded to our portal element. 29 style?: Style; 30 // Called before our portal is created, allowing customization of where in the 31 // DOM the portal is mounted. 32 // The dom parameter is a dummy element representing where the portal would be 33 // located if it were rendered into the normal tree hierarchy. 34 onBeforeContentMount?: (dom: Element) => MountOptions; 35 // Called after our portal is created and its content rendered. 36 onContentMount?: (portalElement: HTMLElement) => void; 37 // Called after our portal's content is updated. 38 onContentUpdate?: (portalElement: HTMLElement) => void; 39 // Called before our portal is removed. 40 onContentUnmount?: (portalElement: HTMLElement) => void; 41} 42 43// A portal renders children into a a div outside of the normal hierarchy of the 44// parent component, usually in order to stack elements on top of others. 45// Useful for creating overlays, dialogs, and popups. 46export class Portal implements m.ClassComponent<PortalAttrs> { 47 private portalElement?: HTMLElement; 48 private containerElement?: Element; 49 50 view() { 51 // Dummy element renders nothing but permits DOM access in lifecycle hooks. 52 return m('span', {style: {display: 'none'}}); 53 } 54 55 oncreate({attrs, children, dom}: m.VnodeDOM<PortalAttrs, this>) { 56 const { 57 onContentMount = () => {}, 58 onBeforeContentMount = (): MountOptions => ({}), 59 } = attrs; 60 61 const {container = document.body} = onBeforeContentMount(dom); 62 this.containerElement = container; 63 64 this.portalElement = document.createElement('div'); 65 container.appendChild(this.portalElement); 66 this.applyPortalProps(attrs); 67 68 m.render(this.portalElement, children); 69 70 onContentMount(this.portalElement); 71 } 72 73 onupdate({attrs, children}: m.VnodeDOM<PortalAttrs, this>) { 74 const {onContentUpdate = () => {}} = attrs; 75 if (this.portalElement) { 76 this.applyPortalProps(attrs); 77 m.render(this.portalElement, children); 78 onContentUpdate(this.portalElement); 79 } 80 } 81 82 private applyPortalProps(attrs: PortalAttrs) { 83 if (this.portalElement) { 84 this.portalElement.className = attrs.className ?? ''; 85 Object.assign(this.portalElement.style, attrs.style); 86 } 87 } 88 89 onremove({attrs}: m.VnodeDOM<PortalAttrs, this>) { 90 const {onContentUnmount = () => {}} = attrs; 91 const container = this.containerElement ?? document.body; 92 if (this.portalElement) { 93 if (container.contains(this.portalElement)) { 94 onContentUnmount(this.portalElement); 95 // Rendering null ensures previous vnodes are removed properly. 96 m.render(this.portalElement, null); 97 container.removeChild(this.portalElement); 98 } 99 } 100 } 101} 102