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