diff --git a/tools/playground/samples.js b/tools/playground/samples.js
index 9b6c0611..d88e8aa7 100644
--- a/tools/playground/samples.js
+++ b/tools/playground/samples.js
@@ -1307,6 +1307,246 @@ const FORM_XML = `
`;
+const WMS = `// This example is slightly more complex than usual. We demonstrate
+// here a way to manage sub windows in Owl, declaratively. This is still just a
+// demonstration. Managing windows can be as complex as we want. For example,
+// we could implement the following features:
+// - resizing windows
+// - minimizing windows
+// - configuration options for windows to make a window non resizeable
+// - minimal width/height
+// - better heuristic for initial window position
+// - ...
+
+class HelloWorld extends owl.Component {}
+
+class Counter extends owl.Component {
+ state = { value: 0 };
+
+ inc() {
+ this.state.value++;
+ }
+}
+
+class Window extends owl.Component {
+ get style() {
+ let { width, height, top, left, zindex } = this.props.info;
+
+ return \`width: \${width}px;height: \${height}px;top:\${top}px;left:\${left}px;z-index:\${zindex}\`;
+ }
+ close() {
+ this.trigger("close-window", { id: this.props.info.id });
+ }
+ startDragAndDrop(ev) {
+ this.updateZIndex();
+ this.el.classList.add('dragging');
+ const offsetX = this.props.info.left - ev.pageX;
+ const offsetY = this.props.info.top - ev.pageY;
+ let left, top;
+
+ const el = this.el;
+ const self = this;
+ window.addEventListener("mousemove", moveWindow);
+ window.addEventListener("mouseup", stopDnD, { once: true });
+
+ function moveWindow(ev) {
+ left = Math.max(offsetX + ev.pageX, 0);
+ top = Math.max(offsetY + ev.pageY, 0);
+ el.style.left = \`\${left}px\`;
+ el.style.top = \`\${top}px\`;
+ }
+ function stopDnD() {
+ window.removeEventListener("mousemove", moveWindow);
+ const options = { id: self.props.info.id, left, top };
+ self.el.classList.remove('dragging');
+ self.trigger("set-window-position", options);
+ }
+ }
+ updateZIndex() {
+ this.trigger("update-z-index", { id: this.props.info.id });
+ }
+}
+
+class WindowManager extends owl.Component {
+ components = { Window };
+ windows = [];
+ nextId = 1;
+ currentZindex = 1;
+ addWindow(name) {
+ const info = this.env.windows.find(w => w.name === name);
+ const id = \`w\${this.nextId++}\`;
+ this.windows.push({
+ id: id,
+ title: info.title,
+ width: info.defaultWidth,
+ height: info.defaultHeight,
+ top: 0,
+ left: 0,
+ zindex: this.currentZindex++
+ });
+ this.components[id] = info.component;
+ this.render();
+ }
+ closeWindow(ev) {
+ const id = ev.detail.id;
+ delete this.components[id];
+ const index = this.windows.findIndex(w => w.id === id);
+ this.windows.splice(index, 1);
+ this.render();
+ }
+ setWindowPosition(ev) {
+ const id = ev.detail.id;
+ const w = this.windows.find(w => w.id === id);
+ w.top = ev.detail.top;
+ w.left = ev.detail.left;
+ }
+ updateZIndex(ev) {
+ const id = ev.detail.id;
+ const w = this.windows.find(w => w.id === id);
+ w.zindex = this.currentZindex++;
+ ev.target.style["z-index"] = w.zindex;
+ }
+}
+
+class App extends owl.Component {
+ components = { WindowManager };
+
+ addWindow(name) {
+ this.refs.wm.addWindow(name);
+ }
+}
+
+const qweb = new owl.QWeb(TEMPLATES);
+const windows = [
+ {
+ name: "Hello",
+ title: "Hello",
+ component: HelloWorld,
+ defaultWidth: 200,
+ defaultHeight: 100
+ },
+ {
+ name: "Counter",
+ title: "Click Counter",
+ component: Counter,
+ defaultWidth: 300,
+ defaultHeight: 120
+ }
+];
+
+const env = { qweb, windows };
+const app = new App(env);
+app.mount(document.body);
+`;
+
+const WMS_XML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ World
+
+
+
+
+
+
+
+`;
+
+const WMS_CSS = `body {
+ margin: 0;
+}
+
+.app {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ grid-template-rows: auto 50px;
+}
+
+.window-manager {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background-color: #eeeeee;
+ overflow: hidden;
+}
+
+.menubar {
+ background-color: #875a7b;
+ color: white;
+}
+
+.menubar button {
+ height: 40px;
+ font-size: 18px;
+ margin: 5px;
+}
+
+.window {
+ display: grid;
+ grid-template-rows: 30px auto;
+ border: 1px solid gray;
+ background-color: white;
+ position: absolute;
+ box-shadow: 1px 1px 2px 1px grey;
+}
+
+.window.dragging {
+ opacity: 0.75;
+}
+
+.window .header {
+ background-color: #875a7b;
+ display: grid;
+ grid-template-columns: auto 24px;
+ color: white;
+ line-height: 30px;
+ padding-left: 5px;
+ cursor: default;
+ user-select: none;
+}
+
+.window .header .close {
+ cursor: pointer;
+ font-size: 22px;
+ padding-left: 4px;
+ padding-right: 4px;
+ font-weight: bold;
+}
+
+.counter {
+ font-size: 20px;
+}
+.counter button {
+ width: 80px;
+ height:40px;
+ font-size: 20px;
+}`;
+
export const SAMPLES = [
{
description: "Components",
@@ -1349,6 +1589,12 @@ export const SAMPLES = [
xml: SLOTS_XML,
css: SLOTS_CSS
},
+ {
+ description: "Window Management System",
+ code: WMS,
+ xml: WMS_XML,
+ css: WMS_CSS,
+ },
{
description: "Asynchronous components",
code: ASYNC_COMPONENTS,