デモ
アイコンをクリックして表示されるウィンドウが移動できるようになりました。 複数のウィンドウを起動する時に、座標を少しずらして表示するのと、選択したウィンドウが一番手前に来る処理を入れています。Setting
Gift
Heart
World
ソースコード
index.html
※ index.htmlは前回と同じです。<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script type="module" src="main.js"></script>
<div id="desktop">
<header>
<a class='logo' href='./'>
Logo
</a>
<ul class="menu">
<li>
<a href='#news'>
最新情報
</a>
</li>
<li>
<a href='#contact' data-logined>
お問い合わせ
</a>
</li>
<li>
<label for="menu">
メニュー
</label>
<ul>
<li>
<a class='sort-icon'>
アイコン整列
</a>
</li>
<li>
<a href='#link'>
Links
</a>
</li>
</ul>
</li>
</ul>
<div class='time'><span class='ymd'></span><span class='his'></span></div>
<label for='menu_toggle'>
<div class='hamburger'>
<span></span>
</div>
</label>
</header>
<main class='desktop'>
<div class="icon"><img src="img/set-up-svgrepo-com.png"><p class="name">Setting</p></div>
<div class="icon"><img src="img/gift-svgrepo-com.png"><p class="name">Gift</p></div>
<div class="icon"><img src="img/like-svgrepo-com.png"><p class="name">Heart</p></div>
<div class="icon"><img src="img/the-internet-svgrepo-com.png"><p class="name">World</p></div>
</main>
</div>
style.css
※ ほんの少しだけ変更をしています。#desktop{
--header-size : 50px;
--main-size : calc(100% - var(--header-size));
--color-bg1 : #6bd8e5;
--color-bg2 : #98cead;
width : 100%;
height:50vh;
min-height: 300px;
box-shadow:4px 4px 20px rgba(0,0,0,0.5);
display:flex;
flex-direction:column;
gap:0;
}
#desktop,
#desktop *{
white-space:normal;
}
#desktop,
#desktop *,
#desktop *::before,
#desktop *::after{
-webkit-box-sizing : border-box;
-moz-box-sizing : border-box;
-o-box-sizing : border-box;
-ms-box-sizing : border-box;
box-sizing : border-box;
-webkit-user-select : none;
-moz-user-select : none;
-ms-user-select : none;
user-select : none;
}
#desktop main{
height : var(--main-size);
position:relative;
z-index:1;
background: linear-gradient(-45deg, var(--color-bg1), var(--color-bg2));
overflow:hidden;
}
#desktop header{
width:100%;
height : var(--header-size);
display : flex;
gap:20px;
align-items : center;
padding:0 10px;
background-color:white;
position:relative;
z-index:100;
}
#desktop header .time{
padding-right:10px;
}
#desktop header .time *{
font-size:0.8em;
display:block;
height:30%;
text-align:right;
}
#desktop header .time > *{
display:block;
margin:0;
}
#desktop .menu{
--hover-color: #FDD;
height:100%;
margin-left:auto;
display:flex;
align-items:center;
gap:10px;
}
#desktop .menu li{
height:100%;
padding:5px;
display:flex;
gap:2px;
align-items:center;
justify-content:center;
cursor:pointer;
position:relative;
}
#desktop .menu *{
color : black;
text-decoration:none;
}
#menu .icon{
width:20px;
height: 20px;
fill:black;
vertical-align:middle;
}
#desktop label[for='menu_toggle']{
display:none;
}
/**
* サブメニュー
*/
#desktop .menu > li > ul{
display:flex;
flex-direction:column;
background-color:white;
min-width:100px;
position:absolute;
top:100%;
left:50%;
transform:translateX(-50%);
margin:0;
padding:0;
}
#desktop .menu > li:has(ul)::after{
content:"";
display:inline-block;
width:0.5em;
height:0.5em;
border-color:black;
border-style:solid;
border-width:0 1px 1px 0;
transform:rotate(45deg);
margin-left:4px;
}
#desktop .menu > li:hover{
background-color: var(--hover-color);
}
#desktop .menu > li:not(:hover) > ul{
display:none;
}
#desktop .menu > li > ul li{
height:var(--header-size);
padding:5px 10px;
cursor:pointer;
justify-content:start;
}
#desktop .menu > li > ul li *{
white-space:nowrap;
}
#desktop .menu > li > ul li:hover{
background-color: var(--hover-color);
}
@media (max-width:500px){
#desktop label[for='menu_toggle']{
display:block;
}
#desktop header nav{
height:100%;
margin:0;
width:100%;
}
#desktop header .menu{
justify-content:flex-start;
width:100%;
height:100%;
background-color:var(--color-body-bg);
}
#desktop header .menu > *{
padding:0;
flex-grow:1;
}
}
#desktop .logo{
height:100%;
display:flex;
align-items:center;
text-decoration:none;
background-color:var(--color-bg);
}
#desktop .logo img,
#desktop .logo svg{
height:100%;
fill:var(--color-01);
}
/**
* Icon
*/
#desktop{
--icon-size : 50px;
--icon-margin : 10px;
--icon-font-size : 10px;
}
#desktop .icon{
--x : 0px;
--y : 0px;
--z : 1;
--z-add : 2000;
width : calc(var(--icon-size) + var(--icon-margin) * 2 );
display : flex;
flex-direction: column;
gap : 4px;
border : 2px solid transparent;
border-radius:5px;
overflow : hidden;
z-index : var(--z);
}
#desktop .icon:not([data-status="move"]){
transition-property: left,top;
transition-duration: 0.3s;
}
#desktop .icon[data-status="move"]{
z-index : calc(var(--z) + var(--z-add));
}
#desktop .icon[data-select]{
border-color:rgba(255,255,255,0.5);
background-color:rgba(0,0,0,0.3);
}
#desktop .icon img{
display : block;
margin : 0 var(--icon-margin);
width : var(--icon-size);
height : var(--icon-size);
object-fit: contain;
pointer-events:none;
}
#desktop .icon .name{
display : block;
margin : 0;
padding : 3px;
width : 100%;
font-size : var(--icon-font-size);
text-align: center;
line-height: 1.4em;
word-break: break-all;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
#desktop .icon[data-select] .name{
background-color:rgb(66, 86, 188);
color:white;
}
/**
* Window
*/
#desktop .window{
position:absolute;
min-width: 200px;
min-height:200px;
display:flex;
flex-direction:column;
box-shadow:4px 4px 20px rgba(0,0,0,0.5);
border-radius:10px;
overflow:hidden;
z-index:100;
}
#desktop .window .header{
height:30px;
background-color:#DDD;
cursor:move;
display:flex;
gap:8px;
align-items:center;
padding:10px;
}
#desktop .window .header .name{
white-space:nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#desktop .window .header .close{
margin-left:auto;
width: 20px;
height: 20px;
cursor:pointer;
background-color:white;
border:1px solid black;
position:relative;
}
#desktop .window .header .close::before,
#desktop .window .header .close::after{
content:"";
display:block;
width:100%;
height:1px;
background-color:black;
position:absolute;
top:50%;
left:50%;
}
#desktop .window .header .close::before{
transform:translate(-50%,-50%) rotate(45deg);
}
#desktop .window .header .close::after{
transform:translate(-50%,-50%) rotate(-45deg);
}
#desktop .window .body{
flex:1;
background-color:white;
overflow:auto;
position:relative;
z-index:1;
}
@media (max-width:500px){
#desktop header .time{
padding:0;
}
}
main.js
class Main{
window_default = {
pos : {
x: 80,
y: 20,
},
gap : {
x: 30,
y: 40,
},
size : {
w : 300,
h : 200,
},
z : 1000
}
constructor(){
this.set_event()
}
get elm_main(){
return document.querySelector("main.desktop")
}
get window_rect(){
return this.elm_main.getBoundingClientRect()
}
set_event(){
window.addEventListener("click" , this.click.bind(this))
}
click(e){
// アイコンをクリック
const icon = e.target.closest(".icon")
if(icon){
const name = icon.querySelector(".name").textContent
this.view_window({
name : name
})
}
// windowのクローズボタンをクリック
const close = e.target.closest(".window .header .close")
if(close){
const elm_window = e.target.closest(".window")
elm_window.parentNode.removeChild(elm_window)
}
// windiwをクリック
const elm_win = e.target.closest(".window")
if(elm_win){
this.window_sort(elm_win)
}
}
view_window(options){
// same window don't view
if(this.elm_main.querySelector(`.window[name="${options.name}"]`)){return}
const elm_window = document.createElement("div")
elm_window.className = "window"
elm_window.name = options.name
elm_window.innerHTML = `<div class="header">
<span class="name">${options.name}</span>
<div class="close"></div>
</div>
<div class="body"></div>
`
const rect = this.window_init_rect()
elm_window.style.left = `${rect.x}px`
elm_window.style.top = `${rect.y}px`
elm_window.style.width = `${rect.w}px`
elm_window.style.height = `${rect.h}px`
elm_window.addEventListener("pointermove" , this.pointermove.bind(this))
this.elm_main.appendChild(elm_window)
this.window_sort(elm_window)
}
// window移動
pointermove(e){
// マウスがクリックされていない場合は処理をしない
if(!e.buttons){return}
const elm_window = e.target.closest(".window")
// 座標移動
const pos = this.window_move_pos({
x : elm_window.offsetLeft + e.movementX,
y : elm_window.offsetTop + e.movementY,
w : elm_window.offsetWidth,
h : elm_window.offsetHeight,
})
elm_window.style.left = `${pos.x}px`
elm_window.style.top = `${pos.y}px`
elm_window.style.position = 'absolute'
elm_window.draggable = false
elm_window.setAttribute("data-move", true)
elm_window.setPointerCapture(e.pointerId)
}
window_init_rect(){
const windows = this.elm_main.querySelectorAll(".window:not([data-move])")
const rect = {
x : windows.length ? windows[windows.length-1].offsetLeft + this.window_default.gap.x : this.window_default.pos.x,
y : windows.length ? windows[windows.length-1].offsetTop + this.window_default.gap.y : this.window_default.pos.y,
w : this.window_default.size.w,
h : this.window_default.size.h,
}
// 右下制御
rect.x = rect.x > this.window_rect.width - this.window_default.size.w ? this.window_rect.width - this.window_default.size.w : rect.x
rect.y = rect.y > this.window_rect.width - this.window_default.size.w ? this.window_rect.width - this.window_default.size.w : rect.y
return rect
}
window_move_pos(rect){
// 左上制限
rect.x = rect.x < 0 ? 0 : rect.x
rect.y = rect.y < 0 ? 0 : rect.y
// 右下制限
rect.x = rect.x > this.window_rect.width - rect.w ? this.window_rect.width - rect.w : rect.x
rect.y = rect.y > this.window_rect.height - rect.h ? this.window_rect.height - rect.h : rect.y
return rect
}
window_sort(active_window){
const windows = Array.from(this.elm_main.querySelectorAll(".window"))
if(windows){
windows.sort((a,b)=>{
if(Number(a.style.getPropertyValue("z-index") || 0) < Number(b.style.getPropertyValue("z-index") || 0)){return -1}
if(Number(a.style.getPropertyValue("z-index") || 0) > Number(b.style.getPropertyValue("z-index") || 0)){return +1}
return 0
})
}
let num = 0
for(const elm of windows){
if(elm === active_window){
elm.style.zIndex = windows.length + this.window_default.z
}
else{
elm.style.zIndex = num + 1 + this.window_default.z
num++
}
}
}
}
switch(document.readyState){
case "complete":
case "interactive":
new Main()
break
default:
window.addEventListener("DOMContentLoaded", (()=>new Main()))
}
解説
まだ、Javascriptもさほど煩雑にはなっていません。 今のところかなりシンプルにかけていますね(自画自賛)。 今回のポイントは、要素のドラッグ移動と、z-indexです。 表示したウィンドウそれぞれのheader部分に、onpointermoveというイベントをセットして、このコールバックのみでウィンドウ移動を処理しています。 このシンプルな記述は、qiitaにかかれてあったものを参考にしました。 JavaScriptで要素をドラッグして移動する簡単な方法 解説などは、他のページで別の人がやっているようなので、わからない命令は、そちらを見たほうがいいかもです。移動処理
とりあえず説明すると、次の関数がコールバック部分です。// window移動
pointermove(e){
// マウスがクリックされていない場合は処理をしない
if(!e.buttons){return}
const elm_window = e.target.closest(".window")
// 座標移動
const pos = this.window_move_pos({
x : elm_window.offsetLeft + e.movementX,
y : elm_window.offsetTop + e.movementY,
w : elm_window.offsetWidth,
h : elm_window.offsetHeight,
})
elm_window.style.left = `${pos.x}px`
elm_window.style.top = `${pos.y}px`
elm_window.style.position = 'absolute'
elm_window.draggable = false
elm_window.setAttribute("data-move", true)
elm_window.setPointerCapture(e.pointerId)
}
クリックされたエレメントは、ヘッダ部分なので、まずはclosest()を使って、上位要素の.windowを選択しています。
次に座標を取得する関数を別に作って、そこに座標とサイズ情報を送って画面からはみ出さない座標を返すようにセットしました。
最後にwindowエレメントに対して、styleをセットするんですが、
elm_window.draggable = false
この部分は、HTMLはもともと、AタグやIMGタグは、ドラッカブル要素と言って、画面内でドラッグすることができる要素なので、もし該当する場合を考慮して、falseをセットしています。
elm_window.setAttribute("data-move", true)
この箇所では、移動したウィンドウは、複数ウィンドウを開く時に、座標取得する対象から外すようにしています。
elm_window.setPointerCapture(e.pointerId)
最後に、setPointerCaptureという関数はあまり見たことがなかったのですが、これは、イベントを継続してくれるための重要な処理で、この処理を書かないと、ウィンドウを素早く動かした時に、表示がついてこなっくなって、UIとして致命的な不具合っぽくなってしまいます。
z-index処理
アクティブなウィンドウを一番手前にする処理を書かないと、後で表示したウィンドウが永遠に手前に表示されて、使い物にならないデスクトップになってしまいます。window_sort(active_window){
const windows = Array.from(this.elm_main.querySelectorAll(".window"))
if(windows){
windows.sort((a,b)=>{
if(Number(a.style.getPropertyValue("z-index") || 0) < Number(b.style.getPropertyValue("z-index") || 0)){return -1}
if(Number(a.style.getPropertyValue("z-index") || 0) > Number(b.style.getPropertyValue("z-index") || 0)){return +1}
return 0
})
}
let num = 0
for(const elm of windows){
if(elm === active_window){
elm.style.zIndex = windows.length + this.window_default.z
}
else{
elm.style.zIndex = num + 1 + this.window_default.z
num++
}
}
}
この箇所で、アクティブなウィンドウを送ると、それを一番手前に表示するようになります。
処理はそんなに複雑じゃないんですが、.windowクラスをリスト取得して、それを、z-index別にソートをします。
次にforループで、activeを除いた形でz-indexを書き直して、アクティブは、最終値をセットするようにして、1番手前に表示されるようになっています。
ちなみに、this.window_default.zは、アイコンとウィンドウの前後表示をするための、オフセット値です。
0 件のコメント:
コメントを投稿