mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
Webamp optionally fully contained into a DOM element
This commit is contained in:
parent
61476591f8
commit
88464a0bb7
13 changed files with 189 additions and 27 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
node_modules
|
||||
.vscode
|
||||
.idea
|
||||
dist
|
||||
|
||||
# Turborepo cache
|
||||
|
|
|
|||
24
examples/contained/.gitignore
vendored
Normal file
24
examples/contained/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
examples/contained/README.md
Normal file
3
examples/contained/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `webamp/contained` Example
|
||||
|
||||
Example for Webamp fully contained into a DOM element. Uses [Vite](https://vitejs.dev/) for development and bundling.
|
||||
13
examples/contained/index.html
Normal file
13
examples/contained/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Webamp (contained)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
examples/contained/package.json
Normal file
18
examples/contained/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "contained",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"webamp": "^2.2.0"
|
||||
}
|
||||
}
|
||||
36
examples/contained/src/main.ts
Normal file
36
examples/contained/src/main.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Webamp from "../../../packages/webamp/js/webamp";
|
||||
|
||||
const webamp = new Webamp({
|
||||
initialTracks: [
|
||||
{
|
||||
metaData: {
|
||||
artist: "DJ Mike Llama",
|
||||
title: "Llama Whippin' Intro",
|
||||
},
|
||||
// NOTE: Your audio file must be served from the same domain as your HTML
|
||||
// file, or served with permissive CORS HTTP headers:
|
||||
// https://docs.webamp.org/docs/guides/cors
|
||||
url: "https://cdn.jsdelivr.net/gh/captbaritone/webamp@43434d82cfe0e37286dbbe0666072dc3190a83bc/mp3/llama-2.91.mp3",
|
||||
duration: 5.322286,
|
||||
},
|
||||
],
|
||||
windowLayout: {
|
||||
main: { position: { left: 0, top: 0 } },
|
||||
equalizer: { position: { left: 0, top: 116 } },
|
||||
playlist: {
|
||||
position: { left: 0, top: 232 },
|
||||
size: { extraHeight: 4, extraWidth: 0 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Container smaller than body
|
||||
const container = document.getElementById("app");
|
||||
container!.style.position = "absolute";
|
||||
container!.style.left = "20px";
|
||||
container!.style.top = "20px";
|
||||
container!.style.right = "120px";
|
||||
container!.style.bottom = "120px";
|
||||
container!.style.backgroundColor = "lightyellow";
|
||||
|
||||
webamp.renderWhenReady(container!, true);
|
||||
1
examples/contained/src/vite-env.d.ts
vendored
Normal file
1
examples/contained/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
25
examples/contained/tsconfig.json
Normal file
25
examples/contained/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -104,20 +104,27 @@ export function updateWindowPositions(
|
|||
return { type: "UPDATE_WINDOW_POSITIONS", positions, absolute };
|
||||
}
|
||||
|
||||
export function centerWindowsInContainer(container: HTMLElement): Thunk {
|
||||
export function centerWindowsInContainer(
|
||||
container: HTMLElement,
|
||||
contained?: false
|
||||
): Thunk {
|
||||
return (dispatch, getState) => {
|
||||
if (!Selectors.getPositionsAreRelative(getState())) {
|
||||
return;
|
||||
}
|
||||
const { left, top } = container.getBoundingClientRect();
|
||||
const { left, top } = contained
|
||||
? { left: 0, top: 0 }
|
||||
: container.getBoundingClientRect();
|
||||
const { scrollWidth: width, scrollHeight: height } = container;
|
||||
dispatch(centerWindows({ left, top, width, height }));
|
||||
};
|
||||
}
|
||||
|
||||
export function centerWindowsInView(): Thunk {
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
export function centerWindowsInView(parentDomNode?: HTMLElement): Thunk {
|
||||
const { width, height } =
|
||||
parentDomNode === document.body || !parentDomNode
|
||||
? { width: window.innerWidth, height: window.innerHeight }
|
||||
: Utils.getElementSize(parentDomNode);
|
||||
return centerWindows({ left: 0, top: 0, width, height });
|
||||
}
|
||||
|
||||
|
|
@ -168,13 +175,16 @@ export function centerWindows({ left, top, width, height }: Box): Thunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function browserWindowSizeChanged(size: {
|
||||
height: number;
|
||||
width: number;
|
||||
}): Thunk {
|
||||
export function browserWindowSizeChanged(
|
||||
size: {
|
||||
height: number;
|
||||
width: number;
|
||||
},
|
||||
parentDomNode?: HTMLElement
|
||||
): Thunk {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch({ type: "BROWSER_WINDOW_SIZE_CHANGED", ...size });
|
||||
dispatch(ensureWindowsAreOnScreen());
|
||||
dispatch(ensureWindowsAreOnScreen(parentDomNode));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -234,13 +244,16 @@ export function setWindowLayout(layout?: WindowLayout): Thunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function ensureWindowsAreOnScreen(): Thunk {
|
||||
export function ensureWindowsAreOnScreen(parentDomNode?: HTMLElement): Thunk {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const windowsInfo = Selectors.getWindowsInfo(state);
|
||||
const getOpen = Selectors.getWindowOpen(state);
|
||||
const { height, width } = Utils.getWindowSize();
|
||||
const { height, width } =
|
||||
parentDomNode === document.body || !parentDomNode
|
||||
? Utils.getWindowSize()
|
||||
: Utils.getElementSize(parentDomNode);
|
||||
const bounding = Utils.calculateBoundingBox(
|
||||
windowsInfo.filter((w) => getOpen(w.key))
|
||||
);
|
||||
|
|
@ -294,6 +307,6 @@ export function ensureWindowsAreOnScreen(): Thunk {
|
|||
// I give up. Just reset everything.
|
||||
dispatch(resetWindowSizes());
|
||||
dispatch(stackWindows());
|
||||
dispatch(centerWindowsInView());
|
||||
dispatch(centerWindowsInView(parentDomNode));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default function App({
|
|||
webampNode.style.right = "0";
|
||||
webampNode.style.bottom = "0";
|
||||
webampNode.style.overflow = "hidden";
|
||||
browserWindowSizeChanged(Utils.getWindowSize());
|
||||
browserWindowSizeChanged(Utils.getWindowSize(), parentDomNode);
|
||||
webampNode.style.right = "auto";
|
||||
webampNode.style.bottom = "auto";
|
||||
webampNode.style.overflow = "visible";
|
||||
|
|
@ -100,7 +100,7 @@ export default function App({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [browserWindowSizeChanged, webampNode]);
|
||||
}, [parentDomNode, browserWindowSizeChanged, webampNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onMount != null) {
|
||||
|
|
@ -151,7 +151,10 @@ export default function App({
|
|||
<ContextMenuWrapper
|
||||
renderContents={() => <MainContextMenu filePickers={filePickers} />}
|
||||
>
|
||||
<WindowManager windows={renderWindows()} />
|
||||
<WindowManager
|
||||
windows={renderWindows()}
|
||||
parentDomNode={parentDomNode}
|
||||
/>
|
||||
</ContextMenuWrapper>
|
||||
</div>
|
||||
</StrictMode>,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const abuts = (a: Box, b: Box) => {
|
|||
|
||||
interface Props {
|
||||
windows: { [windowId: string]: ReactNode };
|
||||
parentDomNode: HTMLElement;
|
||||
}
|
||||
|
||||
type DraggingState = {
|
||||
|
|
@ -25,9 +26,12 @@ type DraggingState = {
|
|||
mouseStart: Point;
|
||||
};
|
||||
|
||||
function useHandleMouseDown(propsWindows: {
|
||||
[windowId: string]: ReactNode;
|
||||
}): (
|
||||
function useHandleMouseDown(
|
||||
propsWindows: {
|
||||
[windowId: string]: ReactNode;
|
||||
},
|
||||
parentDomNode: HTMLElement
|
||||
): (
|
||||
key: WindowId,
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => void {
|
||||
|
|
@ -69,7 +73,9 @@ function useHandleMouseDown(propsWindows: {
|
|||
|
||||
const withinDiff = SnapUtils.snapWithinDiff(
|
||||
proposedBox,
|
||||
browserWindowSize
|
||||
parentDomNode === document.body || !parentDomNode
|
||||
? browserWindowSize
|
||||
: Utils.getElementSize(parentDomNode)
|
||||
);
|
||||
|
||||
const finalDiff = SnapUtils.applyMultipleDiffs(
|
||||
|
|
@ -102,7 +108,7 @@ function useHandleMouseDown(propsWindows: {
|
|||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
window.removeEventListener("touchend", handleMouseUp);
|
||||
};
|
||||
}, [browserWindowSize, draggingState, updateWindowPositions]);
|
||||
}, [parentDomNode, browserWindowSize, draggingState, updateWindowPositions]);
|
||||
|
||||
// Mouse down handler
|
||||
return useCallback(
|
||||
|
|
@ -149,10 +155,13 @@ function useHandleMouseDown(propsWindows: {
|
|||
);
|
||||
}
|
||||
|
||||
export default function WindowManager({ windows: propsWindows }: Props) {
|
||||
export default function WindowManager({
|
||||
windows: propsWindows,
|
||||
parentDomNode,
|
||||
}: Props) {
|
||||
const windowsInfo = useTypedSelector(Selectors.getWindowsInfo);
|
||||
const setFocusedWindow = useActionCreator(Actions.setFocusedWindow);
|
||||
const handleMouseDown = useHandleMouseDown(propsWindows);
|
||||
const handleMouseDown = useHandleMouseDown(propsWindows, parentDomNode);
|
||||
|
||||
const windows = windowsInfo.filter((w) => propsWindows[w.key]);
|
||||
|
||||
|
|
|
|||
|
|
@ -370,6 +370,16 @@ export function findLastIndex<T>(arr: T[], cb: (val: T) => boolean) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
export function getElementSize(domNode: HTMLElement): {
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
return {
|
||||
width: Math.max(domNode.scrollWidth, domNode.offsetWidth),
|
||||
height: Math.max(domNode.scrollHeight, domNode.offsetHeight),
|
||||
};
|
||||
}
|
||||
|
||||
export function getWindowSize(): { width: number; height: number } {
|
||||
// Apparently this is crazy across browsers.
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -479,12 +479,14 @@ class Webamp {
|
|||
*
|
||||
* Webamp is rendered into a new DOM node at the end of the <body> tag with the id `#webamp`.
|
||||
*
|
||||
* If a domNode is passed, Webamp will place itself in the center of that DOM node.
|
||||
* A domNode must be passed, Webamp will place itself in the center of that DOM node.
|
||||
*
|
||||
* @param contained TRUE to render Webamp inside the passed DOM node, and to fully contain its position inside of it
|
||||
*
|
||||
* @returns A promise is returned which will resolve after the render is complete.
|
||||
*/
|
||||
async renderWhenReady(node: HTMLElement): Promise<void> {
|
||||
this.store.dispatch(Actions.centerWindowsInContainer(node));
|
||||
async renderWhenReady(node: HTMLElement, contained?: false): Promise<void> {
|
||||
this.store.dispatch(Actions.centerWindowsInContainer(node, contained));
|
||||
await this.skinIsLoaded();
|
||||
if (this._disposable.disposed) {
|
||||
return;
|
||||
|
|
@ -505,13 +507,17 @@ class Webamp {
|
|||
onMount = resolve;
|
||||
});
|
||||
|
||||
if (contained && getComputedStyle(node)?.position === "static") {
|
||||
node.style.position = "relative";
|
||||
}
|
||||
|
||||
this._root.render(
|
||||
<Provider store={this.store}>
|
||||
<App
|
||||
media={this.media}
|
||||
filePickers={this.options.filePickers || []}
|
||||
onMount={onMount}
|
||||
parentDomNode={document.body}
|
||||
parentDomNode={contained ? node : document.body}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue