diff --git a/.gitignore b/.gitignore
index 829ecf68..0f1fe805 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules
.vscode
+.idea
dist
# Turborepo cache
diff --git a/examples/contained/.gitignore b/examples/contained/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/examples/contained/.gitignore
@@ -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?
diff --git a/examples/contained/README.md b/examples/contained/README.md
new file mode 100644
index 00000000..c767aa54
--- /dev/null
+++ b/examples/contained/README.md
@@ -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.
diff --git a/examples/contained/index.html b/examples/contained/index.html
new file mode 100644
index 00000000..99441f43
--- /dev/null
+++ b/examples/contained/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Webamp (contained)
+
+
+
+
+
+
diff --git a/examples/contained/package.json b/examples/contained/package.json
new file mode 100644
index 00000000..62b308a5
--- /dev/null
+++ b/examples/contained/package.json
@@ -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"
+ }
+}
diff --git a/examples/contained/src/main.ts b/examples/contained/src/main.ts
new file mode 100644
index 00000000..523b6e2f
--- /dev/null
+++ b/examples/contained/src/main.ts
@@ -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);
diff --git a/examples/contained/src/vite-env.d.ts b/examples/contained/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/examples/contained/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/contained/tsconfig.json b/examples/contained/tsconfig.json
new file mode 100644
index 00000000..4f5edc24
--- /dev/null
+++ b/examples/contained/tsconfig.json
@@ -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"]
+}
diff --git a/packages/webamp/js/actionCreators/windows.ts b/packages/webamp/js/actionCreators/windows.ts
index 2a4ce8a2..7072ee50 100644
--- a/packages/webamp/js/actionCreators/windows.ts
+++ b/packages/webamp/js/actionCreators/windows.ts
@@ -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));
};
}
diff --git a/packages/webamp/js/components/App.tsx b/packages/webamp/js/components/App.tsx
index 2a6926e9..31cba88b 100644
--- a/packages/webamp/js/components/App.tsx
+++ b/packages/webamp/js/components/App.tsx
@@ -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({
}
>
-
+
,
diff --git a/packages/webamp/js/components/WindowManager.tsx b/packages/webamp/js/components/WindowManager.tsx
index 4f7418c1..b90a2e94 100644
--- a/packages/webamp/js/components/WindowManager.tsx
+++ b/packages/webamp/js/components/WindowManager.tsx
@@ -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 | React.TouchEvent
) => 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]);
diff --git a/packages/webamp/js/utils.ts b/packages/webamp/js/utils.ts
index 732528c8..22ef4dd2 100644
--- a/packages/webamp/js/utils.ts
+++ b/packages/webamp/js/utils.ts
@@ -370,6 +370,16 @@ export function findLastIndex(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 {
diff --git a/packages/webamp/js/webampLazy.tsx b/packages/webamp/js/webampLazy.tsx
index 2078e514..6eb6d2ed 100644
--- a/packages/webamp/js/webampLazy.tsx
+++ b/packages/webamp/js/webampLazy.tsx
@@ -479,12 +479,14 @@ class Webamp {
*
* Webamp is rendered into a new DOM node at the end of the 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 {
- this.store.dispatch(Actions.centerWindowsInContainer(node));
+ async renderWhenReady(node: HTMLElement, contained?: false): Promise {
+ 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(
);