mirror of
https://github.com/rio-labs/rio.git
synced 2026-05-24 21:48:31 -05:00
implement Webview
This commit is contained in:
@@ -25,7 +25,6 @@ import { FundamentalRootComponent } from "./components/fundamentalRootComponent"
|
||||
import { GridComponent } from "./components/grid";
|
||||
import { HeadingListItemComponent } from "./components/headingListItem";
|
||||
import { HighLevelComponent as HighLevelComponent } from "./components/highLevelComponent";
|
||||
import { HtmlComponent } from "./components/html";
|
||||
import { IconComponent } from "./components/icon";
|
||||
import { ImageComponent } from "./components/image";
|
||||
import { KeyEventListenerComponent } from "./components/keyEventListener";
|
||||
@@ -62,7 +61,7 @@ import { TextComponent } from "./components/text";
|
||||
import { TextInputComponent } from "./components/textInput";
|
||||
import { ThemeContextSwitcherComponent } from "./components/themeContextSwitcher";
|
||||
import { TooltipComponent } from "./components/tooltip";
|
||||
import { WebsiteComponent } from "./components/website";
|
||||
import { WebviewComponent } from "./components/webview";
|
||||
import { GraphEditorComponent } from "./components/graphEditor/graphEditor";
|
||||
|
||||
const COMPONENT_CLASSES = {
|
||||
@@ -90,7 +89,6 @@ const COMPONENT_CLASSES = {
|
||||
"Grid-builtin": GridComponent,
|
||||
"HeadingListItem-builtin": HeadingListItemComponent,
|
||||
"HighLevelComponent-builtin": HighLevelComponent,
|
||||
"Html-builtin": HtmlComponent,
|
||||
"Icon-builtin": IconComponent,
|
||||
"IconButton-builtin": IconButtonComponent,
|
||||
"Image-builtin": ImageComponent,
|
||||
@@ -128,7 +126,7 @@ const COMPONENT_CLASSES = {
|
||||
"TextInput-builtin": TextInputComponent,
|
||||
"ThemeContextSwitcher-builtin": ThemeContextSwitcherComponent,
|
||||
"Tooltip-builtin": TooltipComponent,
|
||||
"Website-builtin": WebsiteComponent,
|
||||
"Webview-builtin": WebviewComponent,
|
||||
};
|
||||
|
||||
globalThis.COMPONENT_CLASSES = COMPONENT_CLASSES;
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { ComponentBase, ComponentState } from "./componentBase";
|
||||
|
||||
export type HtmlState = ComponentState & {
|
||||
_type_: "Html-builtin";
|
||||
html?: string;
|
||||
enable_pointer_events?: boolean;
|
||||
};
|
||||
|
||||
export class HtmlComponent extends ComponentBase {
|
||||
declare state: Required<HtmlState>;
|
||||
|
||||
private isInitialized = false;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement("div");
|
||||
element.classList.add("rio-html");
|
||||
return element;
|
||||
}
|
||||
|
||||
runScriptsInElement(): void {
|
||||
for (let oldScriptElement of this.element.querySelectorAll("script")) {
|
||||
// Create a new script element
|
||||
const newScriptElement = document.createElement("script");
|
||||
|
||||
// Copy over all attributes
|
||||
for (let i = 0; i < oldScriptElement.attributes.length; i++) {
|
||||
const attr = oldScriptElement.attributes[i];
|
||||
newScriptElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
|
||||
// And the source itself
|
||||
newScriptElement.appendChild(
|
||||
document.createTextNode(oldScriptElement.innerHTML)
|
||||
);
|
||||
|
||||
// Finally replace the old script element with the new one so
|
||||
// the browser executes it
|
||||
oldScriptElement.parentNode!.replaceChild(
|
||||
newScriptElement,
|
||||
oldScriptElement
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: HtmlState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.html !== undefined) {
|
||||
// If the HTML hasn't actually changed from last time, don't do
|
||||
// anything. This is important so scripts don't get re-executed each
|
||||
// time the component is updated.
|
||||
if (deltaState.html === this.state.html && this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresIframe(deltaState.html)) {
|
||||
this.element.innerHTML = "";
|
||||
|
||||
let iframe = document.createElement("iframe");
|
||||
iframe.srcdoc = deltaState.html;
|
||||
|
||||
this.element.appendChild(iframe);
|
||||
} else {
|
||||
// Load the HTML
|
||||
this.element.innerHTML = deltaState.html;
|
||||
|
||||
// Just setting the innerHTML doesn't run scripts. Do that manually.
|
||||
this.runScriptsInElement();
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
if (deltaState.enable_pointer_events !== undefined) {
|
||||
this.element.style.pointerEvents = deltaState.enable_pointer_events
|
||||
? "auto"
|
||||
: "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requiresIframe(html: string): boolean {
|
||||
return html.match(/^\s*(<!doctype |<html[ >])/i) !== null;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ComponentBase, ComponentState } from "./componentBase";
|
||||
|
||||
export type WebsiteState = ComponentState & {
|
||||
_type_: "Website-builtin";
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export class WebsiteComponent extends ComponentBase {
|
||||
declare state: Required<WebsiteState>;
|
||||
element: HTMLIFrameElement;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement("iframe");
|
||||
element.classList.add("rio-website");
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: WebsiteState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (
|
||||
deltaState.url !== undefined &&
|
||||
deltaState.url !== this.element.src
|
||||
) {
|
||||
this.element.src = deltaState.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { ComponentBase, ComponentState } from "./componentBase";
|
||||
|
||||
export type WebviewState = ComponentState & {
|
||||
_type_: "Webview-builtin";
|
||||
content?: string; // Url or Html code
|
||||
enable_pointer_events?: boolean;
|
||||
resize_to_fit_content?: boolean;
|
||||
};
|
||||
|
||||
export class WebviewComponent extends ComponentBase {
|
||||
declare state: Required<WebviewState>;
|
||||
|
||||
private iframe: HTMLIFrameElement | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private isInitialized = false;
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement("div");
|
||||
element.classList.add("rio-webview");
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: WebviewState,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
if (deltaState.content !== undefined) {
|
||||
// If the URL/HTML hasn't actually changed from last time, don't do
|
||||
// anything. This is important so scripts don't get re-executed each
|
||||
// time the component is updated.
|
||||
if (
|
||||
deltaState.content !== this.state.content ||
|
||||
!this.isInitialized
|
||||
) {
|
||||
if (isUrl(deltaState.content)) {
|
||||
this.element.innerHTML = "";
|
||||
|
||||
this.iframe = this.createIframe();
|
||||
this.iframe.src = deltaState.content;
|
||||
|
||||
this.element.appendChild(this.iframe);
|
||||
} else if (requiresIframe(deltaState.content)) {
|
||||
this.element.innerHTML = "";
|
||||
|
||||
this.iframe = this.createIframe();
|
||||
this.iframe.srcdoc = deltaState.content;
|
||||
|
||||
this.element.appendChild(this.iframe);
|
||||
} else {
|
||||
// Clean up stuff we no longer need
|
||||
this.iframe = null;
|
||||
this.resizeObserver = null;
|
||||
|
||||
// Load the HTML
|
||||
this.element.innerHTML = deltaState.content;
|
||||
|
||||
// Just setting the innerHTML doesn't run scripts. Do that manually.
|
||||
this.runScriptsInElement();
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (deltaState.enable_pointer_events !== undefined) {
|
||||
this.element.style.pointerEvents = deltaState.enable_pointer_events
|
||||
? "auto"
|
||||
: "none";
|
||||
}
|
||||
|
||||
if (
|
||||
deltaState.resize_to_fit_content !== undefined &&
|
||||
this.iframe !== null
|
||||
) {
|
||||
if (deltaState.resize_to_fit_content) {
|
||||
if (this.resizeObserver === null) {
|
||||
this.resizeObserver = tryCreateIframeResizeObserver(
|
||||
this.iframe
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this.resizeObserver !== null) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
|
||||
this.iframe.style.removeProperty("width");
|
||||
this.iframe.style.removeProperty("height");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createIframe(): HTMLIFrameElement {
|
||||
let iframe = document.createElement("iframe");
|
||||
|
||||
let self = this;
|
||||
iframe.addEventListener("load", function () {
|
||||
// Careful, this code runs with a delay! If this iframe has
|
||||
// already been replaced by other content, do nothing.
|
||||
if (
|
||||
self.iframe !== iframe ||
|
||||
self.resizeObserver !== null ||
|
||||
!self.state.resize_to_fit_content
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.resizeObserver = tryCreateIframeResizeObserver(iframe);
|
||||
});
|
||||
|
||||
return iframe;
|
||||
}
|
||||
|
||||
runScriptsInElement(): void {
|
||||
for (let oldScriptElement of this.element.querySelectorAll("script")) {
|
||||
// Create a new script element
|
||||
const newScriptElement = document.createElement("script");
|
||||
|
||||
// Copy over all attributes
|
||||
for (let i = 0; i < oldScriptElement.attributes.length; i++) {
|
||||
const attr = oldScriptElement.attributes[i];
|
||||
newScriptElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
|
||||
// And the source itself
|
||||
newScriptElement.appendChild(
|
||||
document.createTextNode(oldScriptElement.innerHTML)
|
||||
);
|
||||
|
||||
// Finally replace the old script element with the new one so
|
||||
// the browser executes it
|
||||
oldScriptElement.parentNode!.replaceChild(
|
||||
newScriptElement,
|
||||
oldScriptElement
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isUrl(urlOrHtml: string): boolean {
|
||||
try {
|
||||
new URL(urlOrHtml);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function requiresIframe(html: string): boolean {
|
||||
return html.match(/^\s*(<!doctype |<html[ >])/i) !== null;
|
||||
}
|
||||
|
||||
function tryCreateIframeResizeObserver(
|
||||
iframe: HTMLIFrameElement
|
||||
): ResizeObserver | null {
|
||||
let contentDoc = iframe.contentDocument;
|
||||
if (contentDoc === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let docElement = contentDoc.documentElement;
|
||||
|
||||
let resizeObserver = new ResizeObserver(function () {
|
||||
iframe.style.width = `${docElement.scrollWidth}px`;
|
||||
iframe.style.height = `${docElement.scrollHeight}px`;
|
||||
});
|
||||
resizeObserver.observe(docElement);
|
||||
|
||||
return resizeObserver;
|
||||
}
|
||||
+56
-99
@@ -167,6 +167,14 @@ $monospace-fonts: var(--rio-global-monospace-font), monospace;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Fallback dialog for requestFileUpload function
|
||||
.request-file-upload-fallback-dialog {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
// General
|
||||
a {
|
||||
color: var(--rio-local-level-2-bg);
|
||||
@@ -196,7 +204,17 @@ body {
|
||||
|
||||
font-family: var(--rio-global-font, sans-serif);
|
||||
|
||||
@include single-container();
|
||||
// It's pretty common for random elements to be added to the <body> by
|
||||
// browser extensions or JS libraries, so we can't simply use
|
||||
// `@single-container()`.
|
||||
//
|
||||
// I'm not really sure what purpose(s) those elements serve, but it seems
|
||||
// unwise to simply hide them. For now, we'll turn the body into a Stack.
|
||||
display: inline-grid;
|
||||
& > * {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Force input elements to use the font-family we specified. For some reason
|
||||
@@ -355,9 +373,7 @@ select {
|
||||
background-color: transparent;
|
||||
opacity: 0;
|
||||
|
||||
transition:
|
||||
opacity 0.3s ease-in-out,
|
||||
background-color 1s ease-in-out;
|
||||
transition: opacity 0.3s ease-in-out, background-color 1s ease-in-out;
|
||||
|
||||
& > * {
|
||||
transform: translateY(-5rem);
|
||||
@@ -843,9 +859,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
|
||||
outline: 0.15rem solid var(--rio-global-disabled-bg-variant);
|
||||
|
||||
transition:
|
||||
all 0.3s ease-in-out,
|
||||
outline 0.15s linear;
|
||||
transition: all 0.3s ease-in-out, outline 0.15s linear;
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -969,9 +983,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
}
|
||||
|
||||
.rio-dropdown-popup {
|
||||
transition:
|
||||
max-height 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
transition: max-height 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-dropdown-popup:not(.rio-popup-manager-open) {
|
||||
@@ -1215,9 +1227,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
// styles depend on them.
|
||||
--outer-text-color: var(--rio-local-text-color);
|
||||
--outer-bg-active-color: var(--rio-local-bg-active);
|
||||
transition:
|
||||
color 0.1s ease-in-out,
|
||||
border-color 0.1s ease-in-out;
|
||||
transition: color 0.1s ease-in-out, border-color 0.1s ease-in-out;
|
||||
|
||||
// Create a stacking context. This is needed so the `colored-text` and
|
||||
// `plain-text` styles can reliably create an ::after element behind the
|
||||
@@ -1236,9 +1246,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
background-color: var(--rio-local-bg);
|
||||
|
||||
box-shadow: 0 0 0 transparent;
|
||||
transition:
|
||||
background-color 0.1s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
transition: background-color 0.1s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-buttonstyle-major:hover {
|
||||
@@ -1407,9 +1415,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
|
||||
transition:
|
||||
opacity 0.45s ease-in-out,
|
||||
transform 0.35s ease;
|
||||
transition: opacity 0.45s ease-in-out, transform 0.35s ease;
|
||||
}
|
||||
|
||||
.rio-revealer-open > * > .rio-revealer-content-inner {
|
||||
@@ -1519,10 +1525,8 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
background-color: var(--rio-local-level-2-bg);
|
||||
opacity: 0%;
|
||||
|
||||
transition:
|
||||
left var(--rio-slider-position-transition-time) ease-in-out,
|
||||
width 0.15s ease-in-out,
|
||||
height 0.15s ease-in-out,
|
||||
transition: left var(--rio-slider-position-transition-time) ease-in-out,
|
||||
width 0.15s ease-in-out, height 0.15s ease-in-out,
|
||||
opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -1547,8 +1551,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
|
||||
box-shadow: 0 0.1rem 0.2rem var(--rio-global-shadow-color);
|
||||
|
||||
transition:
|
||||
left var(--rio-slider-position-transition-time) ease-in-out,
|
||||
transition: left var(--rio-slider-position-transition-time) ease-in-out,
|
||||
background-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -1699,9 +1702,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
transition:
|
||||
width 0.2s ease-in-out,
|
||||
height 0.2s ease-in-out;
|
||||
transition: width 0.2s ease-in-out, height 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-media-player-timeline:hover .rio-media-player-timeline-knob {
|
||||
@@ -1897,7 +1898,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
// directions. That's not what we want - we want to increase the parent's
|
||||
// width instead.
|
||||
&[data-scroll-x="never"][data-scroll-y="auto"] > * {
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-gutter: stable !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2377,9 +2378,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
padding: 0.3rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
transition:
|
||||
opacity 0.1s ease-in-out,
|
||||
color 0.1s ease-in-out,
|
||||
transition: opacity 0.1s ease-in-out, color 0.1s ease-in-out,
|
||||
background-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -2405,9 +2404,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
linear-gradient(45deg, var(--checker-color) 25%, transparent 25%);
|
||||
|
||||
background-size: var(--checker-size) var(--checker-size);
|
||||
background-position:
|
||||
0 0,
|
||||
0 0,
|
||||
background-position: 0 0, 0 0,
|
||||
calc(var(--checker-size) * -0.5) calc(var(--checker-size) * -0.5),
|
||||
calc(var(--checker-size) * 0.5) calc(var(--checker-size) * 0.5);
|
||||
}
|
||||
@@ -2611,9 +2608,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
background-color: var(--rio-local-bg);
|
||||
box-shadow: 0 0 0 var(--rio-global-shadow-color);
|
||||
|
||||
transition:
|
||||
box-shadow 0.15s ease-out,
|
||||
background-color 0.1s ease-out;
|
||||
transition: box-shadow 0.15s ease-out, background-color 0.1s ease-out;
|
||||
}
|
||||
|
||||
.rio-card-elevate-on-hover:hover {
|
||||
@@ -2667,9 +2662,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
|
||||
color: var(--rio-local-text-color);
|
||||
|
||||
transition:
|
||||
background-color 0.1s ease-out,
|
||||
color 0.1s ease-out;
|
||||
transition: background-color 0.1s ease-out, color 0.1s ease-out;
|
||||
}
|
||||
|
||||
.rio-switcher-bar-option > .rio-switcher-bar-icon {
|
||||
@@ -3136,11 +3129,8 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
|
||||
position: fixed;
|
||||
z-index: $z-index-dev-tools-highlighter;
|
||||
|
||||
transition:
|
||||
left 0.3s ease-in-out,
|
||||
top 0.3s ease-in-out,
|
||||
width 0.3s ease-in-out,
|
||||
height 0.3s ease-in-out;
|
||||
transition: left 0.3s ease-in-out, top 0.3s ease-in-out,
|
||||
width 0.3s ease-in-out, height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -3279,8 +3269,7 @@ html.picking-component * {
|
||||
}
|
||||
|
||||
.rio-switcher-resizer {
|
||||
transition:
|
||||
min-width var(--rio-switcher-transition-time) ease-in-out,
|
||||
transition: min-width var(--rio-switcher-transition-time) ease-in-out,
|
||||
min-height var(--rio-switcher-transition-time) ease-in-out;
|
||||
}
|
||||
|
||||
@@ -3427,12 +3416,8 @@ html.picking-component * {
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition:
|
||||
opacity 0.3s ease-in-out,
|
||||
left 0.3s ease-in-out,
|
||||
top 0.3s ease-in-out,
|
||||
width 0.3s ease-in-out,
|
||||
height 0.3s ease-in-out;
|
||||
transition: opacity 0.3s ease-in-out, left 0.3s ease-in-out,
|
||||
top 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-code-explorer-highlighter::after {
|
||||
@@ -3506,11 +3491,8 @@ html.picking-component * {
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
transition:
|
||||
top var(--rio-ripple-duration),
|
||||
left var(--rio-ripple-duration),
|
||||
width var(--rio-ripple-duration),
|
||||
height var(--rio-ripple-duration),
|
||||
transition: top var(--rio-ripple-duration), left var(--rio-ripple-duration),
|
||||
width var(--rio-ripple-duration), height var(--rio-ripple-duration),
|
||||
opacity var(--rio-ripple-duration);
|
||||
}
|
||||
|
||||
@@ -3527,17 +3509,14 @@ html.picking-component * {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
|
||||
transition:
|
||||
transform 0.2s linear,
|
||||
opacity 0.1s ease-in-out;
|
||||
transition: transform 0.2s linear, opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-popup-animation-scale.rio-popup-manager-open {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
|
||||
transition:
|
||||
transform 0.2s $transition-timing-overshoot,
|
||||
transition: transform 0.2s $transition-timing-overshoot,
|
||||
opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -3696,9 +3675,7 @@ html.picking-component * {
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
||||
transition:
|
||||
background-color 0.1s ease-out,
|
||||
box-shadow 0.15s ease-out;
|
||||
transition: background-color 0.1s ease-out, box-shadow 0.15s ease-out;
|
||||
}
|
||||
|
||||
.rio-layout-display-child:not(.rio-layout-display-target) {
|
||||
@@ -3757,11 +3734,8 @@ html.picking-component * {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
border-width 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out;
|
||||
transition: opacity 0.2s ease-in-out, border-width 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-checkbox.is-on .rio-checkbox-border {
|
||||
@@ -3787,10 +3761,9 @@ html.picking-component * {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
// Html
|
||||
.rio-html {
|
||||
// Disable pointer events, the user can always turn them back on if desired
|
||||
pointer-events: none;
|
||||
// Webview
|
||||
.rio-webview {
|
||||
// `pointer-events` is controlled via JS.
|
||||
|
||||
@include single-container(); // FIXME: Should we do this?
|
||||
}
|
||||
@@ -3810,9 +3783,7 @@ html.picking-component * {
|
||||
opacity: 0;
|
||||
|
||||
// Theses durations are also referenced in code!
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
background-color 0.5s ease-in-out;
|
||||
transition: opacity 0.2s ease-in-out, background-color 0.5s ease-in-out;
|
||||
|
||||
& > * {
|
||||
transform: translateY(-2rem);
|
||||
@@ -3831,12 +3802,7 @@ html.picking-component * {
|
||||
}
|
||||
}
|
||||
|
||||
// Website
|
||||
.rio-website {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// File Picker Area
|
||||
// Upload Area
|
||||
.rio-file-picker-area {
|
||||
pointer-events: auto;
|
||||
|
||||
@@ -3865,9 +3831,7 @@ html.picking-component * {
|
||||
|
||||
color: var(--rio-local-fg);
|
||||
|
||||
transition:
|
||||
background-color 0.1s ease-in-out,
|
||||
color 0.1s ease-in-out;
|
||||
transition: background-color 0.1s ease-in-out, color 0.1s ease-in-out;
|
||||
|
||||
&::after {
|
||||
// Using the full foreground color for the outline is brutal. The ::after
|
||||
@@ -4059,9 +4023,7 @@ html.picking-component * {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
|
||||
transition:
|
||||
background-color 0.1s ease-in-out,
|
||||
opacity 0.1s ease-in-out,
|
||||
transition: background-color 0.1s ease-in-out, opacity 0.1s ease-in-out,
|
||||
transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -4166,9 +4128,7 @@ $graph-editor-port-size: 1.4rem;
|
||||
|
||||
outline: 0 solid var(--rio-global-primary-bg);
|
||||
|
||||
transition:
|
||||
background-color 0.1s ease-in-out,
|
||||
box-shadow 0.1s ease-in-out,
|
||||
transition: background-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out,
|
||||
outline 0.1s $transition-timing-overshoot;
|
||||
|
||||
&:has(.rio-graph-editor-node-header:hover) {
|
||||
@@ -4258,11 +4218,8 @@ $graph-editor-port-size: 1.4rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--port-color);
|
||||
|
||||
transition:
|
||||
left 0.1s ease-in-out,
|
||||
top 0.1s ease-in-out,
|
||||
right 0.1s ease-in-out,
|
||||
bottom 0.1s ease-in-out;
|
||||
transition: left 0.1s ease-in-out, top 0.1s ease-in-out,
|
||||
right 0.1s ease-in-out, bottom 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.rio-graph-editor-port-circle:hover::after {
|
||||
|
||||
+10
-4
@@ -1,13 +1,16 @@
|
||||
import dataclasses
|
||||
import typing as t
|
||||
|
||||
from .fundamental_component import FundamentalComponent
|
||||
from ..deprecations import deprecated
|
||||
from .component import Component
|
||||
from .webview import Webview
|
||||
|
||||
__all__ = ["Html"]
|
||||
|
||||
|
||||
@t.final
|
||||
class Html(FundamentalComponent):
|
||||
@deprecated(since="0.11", replacement=Webview)
|
||||
class Html(Component):
|
||||
"""
|
||||
Displays raw HTML.
|
||||
|
||||
@@ -42,5 +45,8 @@ class Html(FundamentalComponent):
|
||||
_: dataclasses.KW_ONLY
|
||||
enable_pointer_events: bool = True
|
||||
|
||||
|
||||
Html._unique_id_ = "Html-builtin"
|
||||
def build(self):
|
||||
return Webview(
|
||||
self.html,
|
||||
enable_pointer_events=self.enable_pointer_events,
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ class Switch(FundamentalComponent):
|
||||
|
||||
if "is_on" in delta_state and not self.is_sensitive:
|
||||
raise AssertionError(
|
||||
f"Frontend tried to set `Switch.is_on` even though `is_sensitive` is `False`"
|
||||
"Frontend tried to set `Switch.is_on` even though `is_sensitive` is `False`"
|
||||
)
|
||||
|
||||
async def _call_event_handlers_for_delta_state(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import typing as t
|
||||
|
||||
from uniserde import JsonDoc
|
||||
|
||||
from ..deprecations import deprecated
|
||||
from ..utils import URL
|
||||
from .fundamental_component import FundamentalComponent
|
||||
from .component import Component
|
||||
from .webview import Webview
|
||||
|
||||
__all__ = [
|
||||
"Website",
|
||||
@@ -11,7 +11,8 @@ __all__ = [
|
||||
|
||||
|
||||
@t.final
|
||||
class Website(FundamentalComponent):
|
||||
@deprecated(since="0.11", replacement=Webview)
|
||||
class Website(Component):
|
||||
"""
|
||||
Displays a website.
|
||||
|
||||
@@ -37,10 +38,5 @@ class Website(FundamentalComponent):
|
||||
|
||||
url: URL
|
||||
|
||||
def _custom_serialize_(self) -> JsonDoc:
|
||||
return {
|
||||
"url": str(self.url),
|
||||
}
|
||||
|
||||
|
||||
Website._unique_id_ = "Website-builtin"
|
||||
def build(self):
|
||||
return Webview(self.url)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import dataclasses
|
||||
import typing as t
|
||||
|
||||
from uniserde import JsonDoc
|
||||
|
||||
from ..utils import URL
|
||||
from .fundamental_component import FundamentalComponent
|
||||
|
||||
__all__ = ["Webview"]
|
||||
|
||||
|
||||
@t.final
|
||||
class Webview(FundamentalComponent):
|
||||
"""
|
||||
Displays a website or renders HTML.
|
||||
|
||||
`Webview` takes a URL or HTML markup as input and displays the website
|
||||
or the rendered HTML in your app.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
`content`: The URL of the website you want to display, or the HTML
|
||||
you want to render.
|
||||
|
||||
`enable_pointer_events`: Whether the `Webview` component (and its contents)
|
||||
are clickable.
|
||||
|
||||
`resize_to_fit_content`: Whether the `Webview` component should automatically
|
||||
update its size to match the size of its content. Note that this won't
|
||||
work if the displayed website's domain doesn't match your own domain.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
This will display a website based on its URL:
|
||||
|
||||
```python
|
||||
rio.Webview(
|
||||
rio.URL("https://www.example.com"),
|
||||
)
|
||||
```
|
||||
|
||||
While this will render the given HTML markup:
|
||||
|
||||
```python
|
||||
rio.Webview('<html><body>Hello World</body></html>')
|
||||
```
|
||||
|
||||
The HTML doesn't necessarily have to be an entire website; something
|
||||
like this will also work just fine:
|
||||
|
||||
```python
|
||||
rio.Webview('<p>Hello World</p>')
|
||||
```
|
||||
"""
|
||||
|
||||
content: URL | str
|
||||
_: dataclasses.KW_ONLY
|
||||
enable_pointer_events: bool = True
|
||||
resize_to_fit_content: bool = True
|
||||
|
||||
def _custom_serialize_(self) -> JsonDoc:
|
||||
return {
|
||||
"content": str(self.content),
|
||||
}
|
||||
|
||||
|
||||
Webview._unique_id_ = "Webview-builtin"
|
||||
Reference in New Issue
Block a user