۲۰ دسامبر ۲۰۲۱

حرکت موس: روی/بیرون‌از عنصر، ورود/خروج‌از عنصر

اکنون درباره رویدادهایی که با حرکت موس بین عناصر صفحه اتفاق می‌افتد، بیشتر صحبت میکنیم.

رویدادهای mouseover/mouseout, relatedTarget

رویداد mouseover زمانی اتفاق می‌افتد که اشاره‌گر موس روی یک عنصر می‌رود, و mouseout – زمانی که از روی آن بیرون می‌رود.

این‌ها رویدادهای خاصی هستند، زیرا یک خاصیت به نام relatedTarget دارند. این خاصیت به نوعی مکمل target خواهد بود. زمانی که اشاره‌گر موس یک عنصر را با ورودی به عنصر دیگر ترک می‌کند، یکی از آن‌ها target، و دیگری relatedTarget خواهد بود.

برای mouseover:

  • event.target – عنصری خواهدبود که اشاره‌گر موس روی آن رفته‌است.
  • event.relatedTarget – عنصری خواهد بود که اشاره‌گر موس آنرا ترک کرده، به صورت: (relatedTargettarget).

برای mouseout برعکس است:

  • event.target – عنصری خواهد بود که اشاره‌گر موس آنرا ترک‌کرده.
  • event.relatedTarget – عنصری خواهد بود که زیر اشاره‌گر موس قرار می‌گیرد، به صورت: (targetrelatedTarget).

در مثال زیر هر چهره و اجزای آن عناصر جداگانه هستند. زمانی که اشاره‌گر موس‌ را روی آن‌ها حرکت دهید، رویدادهایی که اتفاق می‌افتند را مشاهده خواهید کرد.

هر رویداد اطلاعاتی درباره هر دو target و relatedTarget دارد:

نتیجه
script.js
style.css
index.html
container.onmouseover = container.onmouseout = handler;

function handler(event) {

  function str(el) {
    if (!el) return "null"
    return el.className || el.tagName;
  }

  log.value += event.type + ':  ' +
    'target=' + str(event.target) +
    ',  relatedTarget=' + str(event.relatedTarget) + "\n";
  log.scrollTop = log.scrollHeight;

  if (event.type == 'mouseover') {
    event.target.style.background = 'pink'
  }
  if (event.type == 'mouseout') {
    event.target.style.background = ''
  }
}
body,
html {
  margin: 0;
  padding: 0;
}

#container {
  border: 1px solid brown;
  padding: 10px;
  width: 330px;
  margin-bottom: 5px;
  box-sizing: border-box;
}

#log {
  height: 120px;
  width: 350px;
  display: block;
  box-sizing: border-box;
}

[class^="smiley-"] {
  display: inline-block;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  margin-right: 20px;
}

.smiley-green {
  background: #a9db7a;
  border: 5px solid #92c563;
  position: relative;
}

.smiley-green .left-eye {
  width: 18%;
  height: 18%;
  background: #84b458;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-green .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #84b458;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-green .smile {
  position: absolute;
  top: 67%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smiley-green .smile:after,
.smiley-green .smile:before {
  content: "";
  position: absolute;
  top: -50%;
  left: 0%;
  border-radius: 50%;
  background: #84b458;
  height: 100%;
  width: 97%;
}

.smiley-green .smile:after {
  background: #84b458;
  height: 80%;
  top: -40%;
  left: 0%;
}

.smiley-yellow {
  background: #eed16a;
  border: 5px solid #dbae51;
  position: relative;
}

.smiley-yellow .left-eye {
  width: 18%;
  height: 18%;
  background: #dba652;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-yellow .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #dba652;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-yellow .smile {
  position: absolute;
  top: 67%;
  left: 19%;
  width: 65%;
  height: 14%;
  background: #dba652;
  overflow: hidden;
  border-radius: 8px;
}

.smiley-red {
  background: #ee9295;
  border: 5px solid #e27378;
  position: relative;
}

.smiley-red .left-eye {
  width: 18%;
  height: 18%;
  background: #d96065;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-red .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #d96065;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-red .smile {
  position: absolute;
  top: 57%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smiley-red .smile:after,
.smiley-red .smile:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0%;
  border-radius: 50%;
  background: #d96065;
  height: 100%;
  width: 97%;
}

.smiley-red .smile:after {
  background: #d96065;
  height: 80%;
  top: 60%;
  left: 0%;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="container">
    <div class="smiley-green">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>

    <div class="smiley-yellow">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>

    <div class="smiley-red">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>
  </div>

  <textarea id="log">رویدادها اینجا نشان داده‌ ‌می‌شوند!
</textarea>

  <script src="script.js"></script>

</body>
</html>
relatedTarget می‌تواند null باشد

خاصیت relatedTarget می‌تواند است null باشد.

این عادی است و فقط به این معنی است که اشاره‌گر موس از عنصر دیگری نیامده، بلکه از خارج از پنجره مرورگر وارد شده. یا اینکه با خارج شدن، از پنجره مرورگر خارج شده.

ما باید این احتمال را هنگام استفاده از event.realtedTarget در کد به یاد داشته باشیم. اگر ما سعی کنیم که event.relatedTarget.tagName دسترسی پیدا کنیم، با خطا مواجه خواهیم شد.

پرش از روی عناصر

رویداد mousemove زمانی اتفاق می‌افتد که اشاره‌گر موس حرکت کند. اما به این معنی نیست که برای هر پیکسل این رویداد اتفاق بیفتد.

مرورگر موقعیت اشاره‌گر موس را هرچند وقت بسیار کوتاه یکبار چک می‌کند. و اگر تغییری را ببیند رویداد را صدا می‌زند.

به این معنی که اگر بازدیدکننده موس را سریع حرکت دهد، ممکن است بعضی از عناصر داخل DOM از قلم بیفتند:

اگر که موس از #FROM سریعا به #TO حرکت کند، مانند شکل بالا، عناصر <div> وسطی (یا بعضی از آن‌ها) ممکن‌است از قلم بیفتند. رویداد mouseout ممکن است روی #FROM و بعد از آن سریعا mouseover روی #TO اتفاق بیفتد.

این برای کارایی برنامه خوب است، چون ممکن است از روی تعداد زیاد عنصر بین دو عنصر عبور کند. در عمل معمولا نمی‌خواهیم همه‌ای این ورود و خروج از تک تک عناصر را پردازش کنیم.

از طرف دیگر، باید در نظر داشته باشیم که اشاره‌گر موس همیشه همه‌ی عناصر را در طول مسیر حرکتش “ملاقات” نمی‌کند. ممکن است “پرش” کند.

در حالت خاصی، ممکن است که اشاره‌گر موس از بیرون از پنجره مرورگر، دقیقا داخل یک عنصر پرش کند. در این حالت realtedTarget مقدار null خواهد داشت، چون اشاره‌گر عملا از “ناکجا آباد” آمده است:

می‌توانید این رفتار را به صورت زنده در قسمت آزمایشی زیر ببینید.

کد اچ‌تی‌ام‌ال دارای دو عنصر تو در تو است: عنصر <div id="child"> داخل عنصر <div id="parent"> قرار گرفته است. اگر که اشاره‌گر موس را سریعا روی آنها حرکت دهید، ممکن است که فقط عنصر فرزند رویداد را صدا بزند، یا شاید عنصر پدر، حتی ممکن است اصلا رویداد اتفاق نیفتد.

همچنین اشاره‌گر را داخل div فرزند ببرید، و سپس آنرا از روی عنصر پدر، با پایین بردن اشاره‌گر از روی عنصر فرزند خارج کنید. اگر حرکت به اندازه کافی سریع باشد، عنصر پدر نادیده گرفته خواهد شد. اشاره‌گر موس از روی عنصر پدر بدون توجه به آن عبور می‌کند.

نتیجه
script.js
style.css
index.html
let parent = document.getElementById('parent');
parent.onmouseover = parent.onmouseout = parent.onmousemove = handler;

function handler(event) {
  let type = event.type;
  while (type.length < 11) type += ' ';

  log(type + " target=" + event.target.id)
  return false;
}


function clearText() {
  text.value = "";
  lastMessage = "";
}

let lastMessageTime = 0;
let lastMessage = "";
let repeatCounter = 1;

function log(message) {
  if (lastMessageTime == 0) lastMessageTime = new Date();

  let time = new Date();

  if (time - lastMessageTime > 500) {
    message = '------------------------------\n' + message;
  }

  if (message === lastMessage) {
    repeatCounter++;
    if (repeatCounter == 2) {
      text.value = text.value.trim() + ' x 2\n';
    } else {
      text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n";
    }

  } else {
    repeatCounter = 1;
    text.value += message + "\n";
  }

  text.scrollTop = text.scrollHeight;

  lastMessageTime = time;
  lastMessage = message;
}
#parent {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#child {
  background: #FFDE99;
  width: 50%;
  height: 50%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

textarea {
  height: 140px;
  width: 300px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="parent">پدر
    <div id="child">فرزند</div>
  </div>
  <textarea id="text"></textarea>
  <input onclick="clearText()" value="Clear" type="button">

  <script src="script.js"></script>

</body>

</html>
اگر mouseover اتفاق بیفتد, حتما mouseout اتفاق خواهد افتاد

در صورت حرکت سریع اشاره‌گر موس، عناصر بین حرکت ممکن است نادیده گرفته‌‌شوند، اما چیزی که از آن مطمئن هستیم این است که: اگر اشاره‌گر موس “رسما” وارد یک عنصر شود (رویداد mouseover اتفاق بیفتند)، هنگام ترک این عنصر همیشه رویداد mouseout نیز اتفاق خواهد افتاد.

رویداد mouseout وقتی اشاره‌گر وارد عنصر فرزند می‌شود

یک ویژگی مهم mouseout این است که زمانی که از یک عنصر به فرزندان آن برویم، اتفاق می‌افتد. برای مثال در اچ‌تی‌ام‌ال زیر زمانی که از #parent به #child برویم:

<div id="parent">
  <div id="child">...</div>
</div>

اگر روی #parent باشیم و سپس اشاره‌گر موس را داخل‌تر و داخل #child ببریم، رویداد mouseout روی #parent اتفاق می‌افتد.

ممکن‌ است عجیب باشد، اما به سادگی شرح داده می‌شود.

با توجه به منطق مرورگر، اشاره گر موس این امکان را دارد که فقط بالای یک عنصر در یک زمان واحد باشد. داخلی‌ترین و بالاترین به واسطه z-index.

پس اگر داخل یک عنصر دیگر (حتی فرزند) شود، پس از قبلی بیرون می‌رود.

به این نکته دیگر درباره جزئیات پردازش رویدادها دقت کنید.

رویداد mouseover زمانی که روی یک فرزند اتفاق بیفتد، به اطلاح بالا می‌رود. به این معنی که اگر parent یک کنترل‌کننده برای رویداد mouseover داشته باشد، فراخوانی ‌می‌شود:

در مثال زیر به خوبی می‌توانید ببینید: <div id="child"> داخل <div id="parent"> قرار دارد. برای رویدادهای mouseover/out کنترل‌کننده‌هایی روی #parent تعریف شده که جزئیاتی درباره رویداد را در خروجی نمایش می‌دهد.

اگر اشاره‌گر موس را از روی #parent حرکت دهید و روی #child ببرید، دو رویداد را روی #parent خواهید دید:

  1. mouseout [target: parent] (ترک پدر), سپس
  2. mouseover [target: child] (ورود به فرزند, اصطلاحا بالا رفته).
نتیجه
script.js
style.css
index.html
function mouselog(event) {
  let d = new Date();
  text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
  text.scrollTop = text.scrollHeight;
}
#parent {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#child {
  background: #FFDE99;
  width: 50%;
  height: 50%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

textarea {
  height: 140px;
  width: 300px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">پدر
    <div id="child">فرزند</div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>

</html>

همانطور که نشان داده‌شد، زمانی که اشاره‌گر موس از #parent بیرون و روی #child می‌رود، هر دو کنترل کننده پدر فراخوانده می‌شوند: mouseout و mouseover:

parent.onmouseout = function(event) {
  /* event.target: عنصر پدر */
};
parent.onmouseover = function(event) {
  /* event.target: عنصر فرزند (اصطلاحا بالا رفته) */
};

اگر که event.target را داخل کنترل‌کننده‌ها بررسی نکنیم، به نظر می‌رسد که موس #parent را ترک‌ کرده و سپس سریعا روی آن برگشته است.

اما این‌گونه نیست! اشاره‌گر موس هنوز روی عنصر پدر قراردارد، فقط وارد وارد عنصر فرزند که داخلی‌تر است شده.

اگر زمان خروج از عنصر پدر قرار است، کاری انجام شود، برای مثال انیمیشنی در زمان parent.onmouseout اجرا شود، معمولا نمیخواهیم که زمانی که موس داخل عناصر داخلی‌تر #parent می‌شود، این کار انجام شود.

برای جلوگیری از این رفتار، ما relatedTarget را در کنترل کننده چک می‌کنیم. اگر که اشاره‌گر موس هنوز داخل عنصر بود، پس صرفا رویداد را نادیده می‌گیریم.

می‌توانیم از دو رویداد دیگر نیز استفاده کنیم: mouseenter و mouseleave، که اکنون به آنها می‌پردازیم،‌ چنین مشکلاتی را باعث نمی‌شوند.

رویدادهای mouseenter و mouseleave

رویدادهای mouseenter/mouseleave مشابه mouseover/mouseout هستند. زمانی اتفاق می‌افتند که اشاره‌گر وارد یک عنصر، یا از آن خارج می‌شود.

اما دو تفاوت اساسی وجود دارد:

  1. گذرهای داخل عنصر، از\به فرزندها در نظر گرفته ‌نمی‌شوند.
  2. رویداد‌های mouseenter/mouseleave به اصطلاح بالا نمی‌روند.

این رویدادها بسیار ساده هستند.

زمانی که اشاره‌گر موش وارد یک عنصر شود، ‍mouseenter اتفاق می‌افتد. محل دقیق اشاره‌گر داخل عنصر یا فرزندهای آن مهم نیست.

زمانی که اشاره‌گر از یک عنصر خارج شود، mouseleave اتفاق می‌افتد.

این مثال شبیه بالایی است، اما عنصر پدر به رویدادهای mouseenter/mouseleave به جای mouseover/mouseout گوش می‌دهد.

همانطور که می‌بینید، تنها رویدادهایی که ایجاد می‌شوند،‌آنهایی هستند که به واسطه ورود و خروج اشاره‌گر موس از عنصر پدر اتفاق می‌افتند. زمانی که اشاره‌گر موس وارد عنصر فرزند و از آن خارج می‌شود اتفاقی نمی‌افتد. گذرهایی بین فرزندها اتفاق می‌افتد نادیده گرفته می‌شوند.

نتیجه
script.js
style.css
index.html
function mouselog(event) {
  let d = new Date();
  text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
  text.scrollTop = text.scrollHeight;
}
#parent {
  background: #99C0C3;
  width: 160px;
  height: 120px;
  position: relative;
}

#child {
  background: #FFDE99;
  width: 50%;
  height: 50%;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

textarea {
  height: 140px;
  width: 300px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">پدر
    <div id="child">فرزند</div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>

</html>

واگذاری رویداد

رویدادهای mouseenter/leave بسیار ساده و برای استفاده ساده هستند. اما بالا نمی‌روند. پس نمی‌تواتیم برای آنها از واگذاری رویدادها استفاده کنیم.

تصور کنید که می‌خواهیم ورود/خروج اشاره‌گر موس برای سلول‌های جدول کنترل کنیم. و صدها سلول وجود دارد.

طبیعتا راه حلی که ابتدا به ذهن می‌رسد این است که کنترل‌کننده را روی <table> تنظیم کنیم و رویدادها را درون آن پردازش کنیم. اما رویدادهای mouseenter/leave بالا نمی‌روند. پس اگر چنین رویدادی روی <td> اتفاق بیفتد، فقط کنترل‌کننده‌ای که روی آن <td> می‌تواند متوجه این رویداد شود.

کنترل‌کننده‌هایی که برای mouseenter/leave روی <table> وجود دارند، تنها زمانی صدا زده‌ ‌می‌شوند که اشاره‌گر موس وارد/خارج جدول کلی شود. در این صورت گرفتن اطلاعات درباره گذرهایی که درون خود جدول اتفاق می‌افتد غیر ممکن خواهد بود.

پس، از mouseover/mouseout استفاده می‌کنیم.

با یک کنترل‌کننده ساده که تنها عنصر زیر اشاره‌گر موس را مشخص می‌کند شروع کنیم:

// مشخص کردن عنصر زیر اشاره‌گر موس
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
};

در اینجا می‌توانید چیزی که اتفاق می‌افتد را ببینید. با حرکت اشاره‌گر موس روی عناصر این جدول، عنصر کنونی که زیر اشاره‌گر موس است مشخص می‌شود:

نتیجه
script.js
style.css
index.html
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';

  text.value += `over -> ${target.tagName}\n`;
  text.scrollTop = text.scrollHeight;
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';

  text.value += `out <- ${target.tagName}\n`;
  text.scrollTop = text.scrollHeight;
};
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
  cursor: pointer;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>باگوآ</em> جدول: جهت, عنصر, رنگ, مفهوم</th>
    </tr>
    <tr>
      <td class="nw"><strong>شمال غرب</strong>
        <br>فلز
        <br>نقره
        <br>ریش سفیدان
      </td>
      <td class="n"><strong>شمال</strong>
        <br>آب
        <br>آبی
        <br>تحول
      </td>
      <td class="ne"><strong>شمال شرق</strong>
        <br>زمین
        <br>زرد
        <br>جهت
      </td>
    </tr>
    <tr>
      <td class="w"><strong>غرب</strong>
        <br>فلز
        <br>طلا
        <br>جوان
      </td>
      <td class="c"><strong>وسط</strong>
        <br>همه‌چیز
        <br>بنفش
        <br>توازن
      </td>
      <td class="e"><strong>شرق</strong>
        <br>چوب
        <br>آبی
        <br>آبی
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>جنوب غرب</strong>
        <br>زمین
        <br>قهوه‌ای
        <br>آرامش
      </td>
      <td class="s"><strong>جنوب</strong>
        <br>آتش
        <br>نارنجی
        <br>سربلندی
      </td>
      <td class="se"><strong>جنوب شرقی</strong>
        <br>چوب
        <br>سبز
        <br>افسانه
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

در این حالت ما می‌خواستیم که گذرهایی که بین سلول‌های جدول <td> اتفاق می‌افتد را کنترل کنیم: ورود به یک سلول جدول و خروج از آن. بقیه‌ی گذرها، مانند درون یک سلول، یا خارج هر سلول، به ما کمکی نمی‌کند. پس بیایید آنها را پالایش کنیم.

کاری که می‌توانیم انجام دهیم این است:

  • عنصر کنونی مشخص شده <td> را درون یک متغیر ذخیره کنیم. آنرا currentElem می‌نامیم.
  • در هنگام mouseover، رویداد را در صورتی که هنوز داخل عنصر <td> کنونی باشیم، نادیده می‌گیریم.
  • در هنگام mouseout، رویداد را در صورتی که عنصر <td> کنونی را ترک نکرده باشیم، نادیده می‌گیریم.

این مثال تمام حالاتی که ممکن است اتفاق بیفتند حساب کرده است:

// <td> که هم اکنون زیر موس قرار دارد (در صورت وجود)
let currentElem = null;

table.onmouseover = function (event) {
  // قبل از ورود به عنصر جدید، اشاره‌گر موس همیشه قبلی را ترک می‌کند
  // اگر currentElem مقدار داشته باشد، پس ما <td> قبلی را ترک نکرده‌ایم،
  // که یعنی یک رویداد mouseover درون آن رخ داده، پس آنرا نادیده می‌گیریم.
  if (currentElem) return;

  let target = event.target.closest('td');

  // وارد یک <td> نشده پس آنرا نادیده می‌گیریم
  if (!target) return;

  // وارد یک <td> شده، اما نه درون جدول مورد نظر ما (در صورتی که جدول‌های تو در تو داشته باشیم این امکان وجود دارد)
  // نادیده می‌گیریم
  if (!table.contains(target)) return;

  // هوورا! ما وارد یک <td> جدید شدیم
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function (event) {
  // اگر که ما اکنون بیرون هر یک از <td> باشیم، پس این رویداد را نادیده می‌گیریم
  // احتمالا اشاره‌گر موس وارد یک جدول شده اما هنوز بیرون از <td> است،
  // مثلا از یک <tr> به <tr> دیگر
  if (!currentElem) return;

  // ما در حال ترک عنصر هستیم، اما به کجا؟ شاید به یک فرزند؟
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // زنجیره پدر-فرزندی را بالا می‌رویم و چک می‌کنیم که آیا هنوز داخل currentElem هستیم یا نه
    // در این صورت این یک گذر داخلی است، که آنرا نادیده می‌گیریم
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // ما واقعا <td> را ترک کردیم
  onLeave(currentElem);
  currentElem = null;
};

// هر تابعی برای کنترل ورود/خروج یک عنصر
function onEnter(elem) {
  elem.style.background = 'pink';

  // نمایش آن در textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = '';

  // نمایش آن در textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

بار دیگر، ویژگی‌های مهم عبارت‌اند از:

  1. برای کنترل ورود/خروج اشاره‌گر موس از هر <td> درون جدول، از واگذاری رویداد استفاده می‌شود. پس از mouseover/out بجای mouseenter/leave برای این منظور استفاده می‌شود.
  2. رویدادهای اضافی، مانند حرکت موس بین فرزندهای خود <td> پالایش شده‌اند، به طوری که onEnter/Leave تنها زمانی اجرا می‌شوند که اشاره‌گر موس وارد/خارج کل <td> شود.

در اینجا مثال کاملی همراه با تمام جزئیات آمده است:

نتیجه
script.js
style.css
index.html
// <td> که هم اکنون زیر موس قرار دارد (در صورت وجود)
let currentElem = null;

table.onmouseover = function (event) {
  // قبل از ورود به عنصر جدید، اشاره‌گر موس همیشه قبلی را ترک می‌کند
  // اگر currentElem مقدار داشته باشد، پس ما <td> قبلی را ترک نکرده‌ایم،
  // که یعنی یک رویداد mouseover درون آن رخ داده، پس آنرا نادیده می‌گیریم.
  if (currentElem) return;

  let target = event.target.closest('td');

  // وارد یک <td> نشده پس آنرا نادیده می‌گیریم
  if (!target) return;

  // وارد یک <td> شده، اما نه درون جدول مورد نظر ما (در صورتی که جدول‌های تو در تو داشته باشیم این امکان وجود دارد)
  // نادیده می‌گیریم
  if (!table.contains(target)) return;

  // هوورا! ما وارد یک <td> جدید شدیم
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function (event) {
  // اگر که ما اکنون بیرون هر یک از <td> باشیم، پس این رویداد را نادیده می‌گیریم
  // احتمالا اشاره‌گر موس وارد یک جدول شده اما هنوز بیرون از <td> است،
  // مثلا از یک <tr> به <tr> دیگر
  if (!currentElem) return;

  // ما در حال ترک عنصر هستیم، اما به کجا؟ شاید به یک فرزند؟
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // زنجیره پدر-فرزندی را بالا می‌رویم و چک می‌کنیم که آیا هنوز داخل currentElem هستیم یا نه
    // در این صورت این یک گذر داخلی است، که آنرا نادیده می‌گیریم
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // ما واقعا <td> را ترک کردیم
  onLeave(currentElem);
  currentElem = null;
};

// هر تابعی برای کنترل ورود/خروج یک عنصر
function onEnter(elem) {
  elem.style.background = 'pink';

  // نمایش آن در textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = '';

  // نمایش آن در textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
  cursor: pointer;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>باگوآ</em> جدول: جهت, عنصر, رنگ, مفهوم</th>
    </tr>
    <tr>
      <td class="nw"><strong>شمال غرب</strong>
        <br>فلز
        <br>نقره
        <br>ریش سفیدان
      </td>
      <td class="n"><strong>شمال</strong>
        <br>آب
        <br>آبی
        <br>تحول
      </td>
      <td class="ne"><strong>شمال شرق</strong>
        <br>زمین
        <br>زرد
        <br>جهت
      </td>
    </tr>
    <tr>
      <td class="w"><strong>غرب</strong>
        <br>فلز
        <br>طلا
        <br>جوان
      </td>
      <td class="c"><strong>وسط</strong>
        <br>همه‌چیز
        <br>بنفش
        <br>توازن
      </td>
      <td class="e"><strong>شرق</strong>
        <br>چوب
        <br>آبی
        <br>آبی
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>جنوب غرب</strong>
        <br>زمین
        <br>قهوه‌ای
        <br>آرامش
      </td>
      <td class="s"><strong>جنوب</strong>
        <br>آتش
        <br>نارنجی
        <br>سربلندی
      </td>
      <td class="se"><strong>جنوب شرقی</strong>
        <br>چوب
        <br>سبز
        <br>افسانه
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

اشاره‌گر موس را رو و بیرون سلول‌های جدول ببرید. سریع یا آسان، اهمیتی ندارد. کل <td> مشخص خواهد شد، برخلاف مثالی که قبلا دیده بودیم.

خلاصه

ما درباره رویدادهای mouseover، mouseout، mousemove، mouseenter و mouseleave صحبت کردیم.

خوب است به این نکات توجه کنیم:

  • یک حرکت سریع موس ممکن است باعث پرش از روی عناصر بین حرکت شود.
  • رویدادهای mouseover/out و mouseenter/leave یک خاصیت اضافی دارند: relatedTarget. که عنصری خواهد بود که وارد/خارج آن می‌شویم، و به نوعی مکمل target است.

رویدادهای mouseover/out زمانی که ما از عنصر پدر به فرزند برویم هم اتفاق می‌افتند. مرورگر فرض می‌کند که اشاره‌گر موس در یک زمان فقط می‌تواند روی یک عنصر باشد، داخلی‌ترین عنصر.

اما رویدادهای mouseenter/leave‍ از این نظر متفاوت عمل می‌کنند: آنها فقط زمانی اتفاق می‌افتند که اشاره‌گر موس وارد یا یک عنصر به عنوان یک کل خارج شود. همچنین آنها اصطلاحا، بالا نمی‌روند.

تمارین

اهمیت: 5

کد جاوا اسکریپیتی بنویسید که یک تولتیپ را با استفاده از صفت data-tooltip بالای عنصر نمایش دهد. مقدار این صفت باید متن تولتیپ باشد.

این تکلیف شبیه تکلیف Tooltip behavior است، اما عناصری که توضیح اضافه دارند، می‌توانند تو در تو باشند. به این صورت که داخلی‌ترین تولتیپ باید نمایش داده شود.

فقط یک تولتیپ می‌تواند در لحظه باید نمایان باشد.

برای مثال:

<div data-tooltip="اینجا – نمای داخلی خانه است" id="house">
  <div data-tooltip="اینجا – سقف است" id="roof"></div>
  ...
  <a href="https://fa.wikipedia.org/wiki/%D8%B3%D9%87_%D8%AE%D9%88%DA%A9_%D9%81%D8%B3%D9%82%D9%84%DB%8C" data-tooltip="ادامه را بخوانید">اشاره‌گر را روی من بیار</a>
</div>

نتیجه داخل iframe:

باز کردن یک sandbox برای تمرین.

اهمیت: 5

تابعی بنویسید که یک تولتیپ را فقط در صورتی نمایش دهد که کاربر اشاره‌گر موس را داخل آن ببرد و نه اینکه از روی آن فقط عبور کند.

به عبارت دیگر، اگر که کاربر اشاره‌گر موس را روی عنصر ببرد و آنجا متوقف شود، تولتیپ نمایش داده شود. و اگر موس از روی عنصر عبور کرد، نیازی به نمایش تولتیپ نیست. چه کسی یک به تولتیپ لحظه‌ای نیاز دارد؟

از نظر فنی، می‌توانیم سرعت اشاره‌گر موس را روی عنصر اندازه بگیریم، و اگر آرام باشد فرض کنیم که اشاره‌گر “روی عنصر قرار گرفته” و تولتیپ را نمایش دهیم. اگر سریع بود، آنرا نادیده بگیریم.

برای این منظور یک شئ گلوبال به صورت new HoverIntent(options) بسازید.

خصوصیات options:

  • elem – عنصری که می‌خواهیم حرکت اشاره‌گر را روی آن کنترل کنیم.
  • over – تابعی که در صورت “قرار گرفتن اشاره‌گر روی عنصر” صدا زده می‌شود: که یعنی حرکت اشاره‌گر موس کند بوده، یا روی عنصر توقف کرده.
  • out – تابعی که زمانی اشاره‌گر موس عنصر را ترک می‌کند صدا زده‌ می‌شود. (اگر تابع over صدا زده شده باشد).

یک مثال از چگونگی استفاده از چنین شئ برای تولتیپ این چنین خواهد بود:

// تولتیپ نمونه
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// این شئ حرکت اشاره‌گر موس را دنبال و توابع over/out را صدا می‌زند.
new HoverIntent({
  elem,
  over() {
    tooltip.style.left = elem.getBoundingClientRect().left + 'px';
    tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
    document.body.append(tooltip);
  },
  out() {
    tooltip.remove();
  }
});

دمو:

اگر که اشاره‌گر موس را سریعا از روی “ساعت” حرکت دهید، اتفاقی نمی‌افتد، اگر چه در صورتی که حرکت اشاره‌گر موس آهسته باشد یا روی آن توقف کنید، یک تولتیپ نمایش داده می‌شود.

توجه: تولتیپ نباید هنگامی که اشاره‌گر روی فرزندان ساعت حرکت می‌کند رفتار “چشمک زن” داشته باشد.

باز کردن یک sandbox همراه با تست‌ها.

الگوریتم ساده به نظر می‌رسد:

  1. برای رویدادهای onmouseover/out کنترل‌کننده‌هایی روی عنصر تعریف می‌کنیم. همچنین می‌توانیم از ‍‍‍onmouseenter/leave نیز در این مورد استفاده کنیم، اما کمتر استفاده می‌شوند. درصورتی که از واگذاری رویدادها استفاده کنیم، کار نمی‌کنند.
  2. زمانی که اشاره‌گر موس وارد یک عنصر می‌شود، سرعت آن را در mousemove محاسبه می‌کنیم.
  3. اگر سرعت حرکت آن کند باشد، تابع over را صدا می‌زنیم.
  4. زمانی که از عنصر خارج می‌شویم، و over صدا زده شده بود، تابع out‌ را نیز صدا می‌زنیم.

اما چگونه سرعت اشاره‌گر موس را اندازه گیری کنیم؟

اولین چیزی که به ذهن می‌رسد این است که: یک تابع را هر ‍100ms‍ اجرا کنیم و مسافتی که بین مختصات قبلی و جدید طی شده را اندازه بگیریم. اگر کوچک باشد، پس سرعت حرکت اشاره‌گر موس کند بوده.

متاسفانه راهی برای گرفتن “مختصات فعلی اشاره‌گر موس” در جاوا اسکریپت وجود ندارد. هیچ تابع از قبل آماده‌ای مانند getCurrentMouseCoordiantes() وجود ندارد.

تنها راه برای گرفتن مختصات گوش دادن به رویدادهای موس مانند mousemove و گرفتن مختصات از شئ event خواهد بود.

درنتیجه باید برای رویداد mousemove یک کنترل‌کننده تعریف می‌کنیم تا مختصات را ذخیره کنیم، و آنها را هر 100ms مقایسه کنیم.

پی‌نوشت: توجه کنید که برای آزمایش راه حل از dispatchEvent استفاده می‌شود تا ببیند تولتیپ به درستی کار می‌کند یا نه.

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

نقشه آموزش