function $Component(name, orientation, $el){ // A draggable widget that can be undocked into a window const $c = $(E("div")).addClass("component"); $c.addClass(`${name}-component`); $c.append($el); $c.attr("touch-action", "none"); $c.appendTo({ tall: $left, wide: $bottom, }[orientation]); const $w = new $Window($c); $w.title(name); $w.hide(); $w.$content.addClass({ tall: "vertical", wide: "horizontal", }[orientation]); // Nudge the Colors component over a tiny bit if(name === "Colors"){ $c.css("position", "relative"); $c.css("left", "3px"); } let ox, oy; let w, h; let pos = 0; let pos_axis; let last_docked_to_pos; let $last_docked_to; let $dock_to; let $ghost; if(orientation === "tall"){ pos_axis = "top"; }else{ pos_axis = "left"; } const dock_to = $dock_to => { $w.hide(); $dock_to.append($c); pos = Math.max(pos, 0); if(pos_axis === "top"){ pos = Math.min(pos, $dock_to.height() - $c.height()); }else{ pos = Math.min(pos, $dock_to.width() - $c.width()); } $c.css("position", "relative"); $c.css(pos_axis, pos); // Save where it's now docked to $last_docked_to = $dock_to; last_docked_to_pos = pos; }; $c.on("pointerdown", e => { // Only start a drag via a left click directly on the component element if(e.button !== 0){ return; } if(!$c.is(e.target)){ return; } $G.on("pointermove", drag_onpointermove); $G.one("pointerup", e => { $G.off("pointermove", drag_onpointermove); drag_onpointerup(e); }); const rect = $c[0].getBoundingClientRect(); // Make sure these dimensions are odd numbers w = (~~(rect.width/2))*2 + 1; h = (~~(rect.height/2))*2 + 1; ox = rect.left - e.clientX; oy = rect.top - e.clientY; if(!$ghost){ $ghost = $(E("div")).addClass("component-ghost dock"); $ghost.css({ position: "absolute", display: "block", width: w, height: h, left: e.clientX + ox, top: e.clientY + oy }); $ghost.appendTo("body"); } // Prevent text selection anywhere within the component e.preventDefault(); }); const drag_onpointermove = e => { $ghost.css({ left: e.clientX + ox, top: e.clientY + oy, }); $dock_to = null; const ghost_rect = $ghost[0].getBoundingClientRect(); const q = 5; if(orientation === "tall"){ pos_axis = "top"; if(ghost_rect.left-q < $left[0].getBoundingClientRect().right){ $dock_to = $left; } if(ghost_rect.right+q > $right[0].getBoundingClientRect().left){ $dock_to = $right; } }else{ pos_axis = "left"; if(ghost_rect.top-q < $top[0].getBoundingClientRect().bottom){ $dock_to = $top; } if(ghost_rect.bottom+q > $bottom[0].getBoundingClientRect().top){ $dock_to = $bottom; } } pos = ghost_rect[pos_axis]; if($dock_to){ const dock_to_rect = $dock_to[0].getBoundingClientRect(); pos -= dock_to_rect[pos_axis]; $ghost.addClass("dock"); }else{ $ghost.removeClass("dock"); } e.preventDefault(); }; const drag_onpointerup = e => { $w.hide(); // If the component is docked to a component area (a side) if($c.parent().is(".component-area")){ // Save where it's docked so we can dock back later $last_docked_to = $c.parent(); if($dock_to){ last_docked_to_pos = pos; } } if($dock_to){ // Dock component to $dock_to dock_to($dock_to); }else{ $c.css("position", "relative"); $c.css(pos_axis, ""); // Put the component in the window $w.$content.append($c); // Show and position the window $w.show(); const window_rect = $w[0].getBoundingClientRect(); const window_content_rect = $w.$content[0].getBoundingClientRect(); const dx = window_content_rect.left - window_rect.left; const dy = window_content_rect.top - window_rect.top; $w.css({ left: e.clientX + ox - dx, top: e.clientY + oy - dy, }); } $ghost && $ghost.remove(); $ghost = null; $G.trigger("resize"); }; $c.dock = () => { pos = last_docked_to_pos; dock_to($last_docked_to); }; $c.show = () => { $($c[0]).show(); // avoid recursion if($.contains($w[0], $c[0])){ $w.show(); } return $c; }; $c.hide = () => { $c.add($w).hide(); return $c; }; $c.toggle = () => { if($c.is(":visible")){ $c.hide(); }else{ $c.show(); } return $c; }; $w.on("close", e => { e.preventDefault(); $w.hide(); }); return $c; }