如何使用JavaScript在点击对话框外部时关闭本地HTML对话框?

20
我使用了一个HTML <dialog> 元素。我希望能够在点击元素外部时关闭对话框。使用“blur”或“focusout”事件无效。
我想要与Material Design对话框相同的功能,在点击对话框外部时关闭它:

https://material-components-web.appspot.com/dialog.html

我该如何实现这个目标?

提前致谢。

6个回答

17

当以模态模式打开对话框时,在视口的任何位置单击都将被记录为该对话框上的单击。

HTMLDialogElement接口的showModal()方法将对话框显示为模态,位于可能存在的任何其他对话框的顶部。它显示到顶层,伴随着一个::backdrop伪元素。对话框外的交互被阻止,其外部内容被渲染为无效。 来源:HTMLDialogElement.showModal()

解决问题的一种方法是:

  • 在对话框中嵌套一个div,并使用CSS确保它覆盖与对话框相同的区域(请注意,浏览器会应用默认样式到对话框,如填充)。
  • 为对话框元素(在视口上的任何位置)添加事件监听器以关闭对话框。
  • 为嵌套在对话框内的div添加事件监听器以防止点击事件传播到它上面(这样如果用户点击它,则不会关闭对话框)。

You can test this with the code snippet below.

const myButton = document.getElementById('myButton');
myButton.addEventListener('click', () => myDialog.showModal());

const myDialog = document.getElementById('myDialog');
myDialog.addEventListener('click', () => myDialog.close());

const myDiv = document.getElementById('myDiv');
myDiv.addEventListener('click', (event) => event.stopPropagation());
#myDialog {
  width: 200px;
  height: 100px;
  padding: 0;
}

#myDiv {
  width: 100%;
  height: 100%;
  padding: 1rem;
}
<button id="myButton">Open dialog</button>
<dialog id="myDialog">
  <div id="myDiv">
    Click me and I'll stay...
  </div>
</dialog>


7
这是一个有趣的解决方案。另一个想法是检查事件的目标是否为对话框本身。如果不是,则是覆盖模态框可见部分的 div(或其子元素)。这样我们就不需要第二个事件处理程序,我认为这更容易理解。 - Leonardo Raele

11

这是我实现的方式:

function dialogClickHandler(e) {
    if (e.target.tagName !== 'DIALOG') //This prevents issues with forms
        return;

    const rect = e.target.getBoundingClientRect();

    const clickedInDialog = (
        rect.top <= e.clientY &&
        e.clientY <= rect.top + rect.height &&
        rect.left <= e.clientX &&
        e.clientX <= rect.left + rect.width
    );

    if (clickedInDialog === false)
        e.target.close();
}

你能详细介绍一下clickedInDialog的工作原理吗? - Royer Adames
我测试了一下,它有效。我喜欢这个因为我不需要让我的标记更复杂。 我不喜欢计算的复杂性。看起来我可以选择在标记中或者在js中增加复杂性。 - Royer Adames
这有点难以阅读,因此我更倾向于采用包装 div 解决方案。 - Royer Adames
这应该是一个模态特性。 他们还可以在模态中添加焦点陷阱。 有了这两个缺失的特性,制作模态将变得非常容易。 - Royer Adames

6

模态框

要通过单击背景来关闭模态对话框(即使用showModal打开的对话框),您可以按照以下步骤操作:

const button = document.getElementById('my-button');
const dialog = document.getElementById('my-dialog');
button.addEventListener('click', () => {dialog.showModal();});
// here's the closing part:
dialog.addEventListener('click', (event) => {
    if (event.target.id !== 'my-div') {
        dialog.close();
    }
});
#my-dialog {padding: 0;}
#my-div {padding: 16px;}
<button id="my-button">open dialog</button>
<dialog id="my-dialog">
    <div id="my-div">click outside to close</div>
</dialog>

这将把对话框内容放在一个 <div> 中,然后用它来检测点击是否在对话框外面,如 这里 所建议的。示例中的填充和边距被调整以确保 <dialog> 边框和 <div> 边框重合。

请注意,可以使用 ::backdrop 在 CSS 中选择模态对话框的“背景”。

非模态

对于一个非模态对话框(使用 show 打开),你可以将事件监听器添加到 window 元素而不是 dialog,例如:

window.addEventListener('click', (event) => {
    if (!['my-button', 'my-div'].includes(event.target.id)) {
        dialog.close();
    }
});

在这种情况下,我们还需要过滤掉按钮点击事件,否则点击“打开对话框”按钮后,对话框会立即关闭。

4
您的模态对话框示例会在任何地方点击时(包括对话框内部)都会关闭。这更像是一个工具提示 - 我想不出任何使用情况? - mindplay.dk
一个使用案例就是简单地显示一个消息:点击任何地方使其消失。这只是我能想到的最简单的例子,用来说明 HTMLDialogElement.close() 的用法。如果你不喜欢这种行为,你可以将监听器附加到另一个元素并添加一些逻辑。 - djvg
我遗漏的关键部分是:使用 showModal() 可以让页面上任何位置的点击都聚焦在对话框元素上,这就是为什么添加点击监听器实际上起作用的原因。 - Harald

1

这是一个完整的例子,包含两个对话框元素,一个纯信息性的,另一个包括一个对话框表单。

const initializeDialog = function(dialogElement) {
  // enhance opened standard HTML dialog element by closing it when clicking outside of it
  dialogElement.addEventListener('click', function(event) {
    const eventTarget = event.target;
    if (dialogElement === eventTarget) {
      console.log("click on dialog element's content, padding, border, or margin");
      const dialogElementRect = dialogElement.getBoundingClientRect();
      console.log("dialogElementRect.width", dialogElementRect.width);
      console.log("dialogElementRect.height", dialogElementRect.height);
      console.log("dialogElementRect.top", dialogElementRect.top);
      console.log("dialogElementRect.left", dialogElementRect.left);
      console.log("event.offsetX", event.offsetX);
      console.log("event.clientX", event.clientX);
      console.log("event.offsetY", event.offsetY);
      console.log("event.clientY", event.clientY);
      if (
        (dialogElementRect.top > event.clientY) ||
        (event.clientY > (dialogElementRect.top + dialogElementRect.height)) ||
        (dialogElementRect.left > event.clientX) ||
        (event.clientX > (dialogElementRect.left + dialogElementRect.width))
      ) {
        console.log("click on dialog element's margin. closing dialog element");
        dialogElement.close();
      }
      else {
        console.log("click on dialog element's content, padding, or border");
      }
    }
    else {
      console.log("click on an element WITHIN dialog element");
    }
  });
  
  const maybeDialogFormElement = dialogElement.querySelector('form[method="dialog"]');
  if (! maybeDialogFormElement) {
    // this dialog element does NOT contain a "<form method="dialog">".
    // Hence, any contained buttons intended for closing the dialog will
    // NOT be automatically set up for closing the dialog
    // (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#usage_notes ).
    // Therefore, programmatically set up close buttons
    const closeButtons = dialogElement.querySelectorAll('button[data-action-close], button[data-action-cancel]');
    closeButtons.forEach(closeButton => {
      closeButton.addEventListener('click', () => dialogElement.close() );
    });
  }
  
  return dialogElement;
};

const initializeFormDialog = function(formDialog, formCloseHandler) {
  const submitButton = formDialog.querySelector('button[type="submit"]');
  const inputElement = formDialog.querySelector('input');
  
  formDialog.originalShowModal = formDialog.showModal;
  formDialog.showModal = function() {
    // populate input element with initial or latest submit value
    inputElement.value = submitButton.value;
    formDialog.dataset.initialInputElementValue = inputElement.value;
    formDialog.originalShowModal();
  }
  
  // allow confirm-input-by-pressing-Enter-within-input-element
  inputElement.addEventListener('keydown', event => {
    if (event.key === 'Enter') {
      //prevent default action, which in dialog-form case would effectively cancel, not confirm the dialog
      event.preventDefault();
      submitButton.click();
    }
  });
  
  submitButton.addEventListener('click', () => {
    submitButton.value = inputElement.value;
    // add dialog-was-confirmed marker
    formDialog.dataset.confirmed = "true";
  });
  
  formDialog.addEventListener('close', event => {
    if (formCloseHandler) {
      const returnValue = formDialog.returnValue;
      const dialogWasConfirmed = (formDialog.dataset.confirmed === "true");
      let inputElementValueHasChanged;
      if (dialogWasConfirmed) {
        inputElementValueHasChanged = (returnValue === formDialog.dataset.initialInputElementValue) ? false : true;
      }
      else {
        inputElementValueHasChanged = false;
      }
      formCloseHandler(returnValue, dialogWasConfirmed, inputElementValueHasChanged);
    }
    
    // remove dialog-was-confirmed marker
    delete formDialog.dataset.confirmed;
  });
};

const myFormDialogCloseHandler = function(returnValue, dialogWasConfirmed, inputElementValueHasChanged) {
  const resultDebugOutput = document.getElementById('output-result');
  const resultDebugEntryString = `<pre>dialog confirmed?    ${dialogWasConfirmed}
input value changed? ${inputElementValueHasChanged}
returnValue:         "${returnValue}"</pre>`;
  resultDebugOutput.insertAdjacentHTML('beforeend', resultDebugEntryString);
};

const informationalDialog = document.getElementById('dialog-informational');
initializeDialog(informationalDialog);

const showDialogInformationalButton = document.getElementById('button-show-dialog-informational');
showDialogInformationalButton.addEventListener('click', () => informationalDialog.showModal());

const formDialog = document.getElementById('dialog-form');
initializeDialog(formDialog);
initializeFormDialog(formDialog, myFormDialogCloseHandler);

const showDialogFormButton = document.getElementById('button-show-dialog-form');
showDialogFormButton.addEventListener('click', () => {
  formDialog.showModal();
});
dialog {
  /* for demonstrational purposes, provide different styles for content, padding, and border */
  background-color: LightSkyBlue;
  border: 2rem solid black;
  /* give padding a color different from content; see https://dev59.com/mGUq5IYBdhLWcg3wSOfR#35252091 */
  padding: 1rem;
  box-shadow: inset 0 0 0 1rem LightGreen;
}
dialog header {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: flex-start;
}

dialog header button[data-action-close]::before,
dialog header button[data-action-cancel]::before {
  content: "✕";
}

dialog footer {
  display: flex;
  justify-content: flex-end;
  gap: 1rem;
}
<button id="button-show-dialog-informational" type="button">Show informational dialog</button>
<button id="button-show-dialog-form" type="button">Show dialog with form</button>

<dialog id="dialog-informational">
  <header>
    <strong>Informational dialog header</strong>
    <button aria-labelledby="dialog-close" data-action-close="true"></button>
  </header>
  <div>
    <p>This is the dialog content.</p>
  </div>
  <footer>
    <button id="dialog-close" data-action-close="true">Close dialog</button>
  </footer>
</dialog>

<dialog id="dialog-form">
  <form method="dialog">
    <header>
      <strong>Dialog with form</strong> 
      <button aria-labelledby="dialog-form-cancel" data-action-cancel="true" value="cancel-header"></button>
    </header>
    <div>
      <p>This is the dialog content.</p>
      <label for="free-text-input">Text input</label>
      <input type="text" id="free-text-input" name="free-text-input" />
    </div>
    <footer>
      <button id="dialog-form-cancel" value="cancel-footer">Cancel</button>
      <button type="submit" id="dialog-form-confirm" value="initial value">Confirm</button>
    </footer>
  </form>
</dialog>

<div id="output-result"></div>


0
下面的代码将自动将所需功能应用于页面上的所有dialog元素。
HTMLDialogElement.prototype.triggerShow = HTMLDialogElement.prototype.showModal;
HTMLDialogElement.prototype.showModal = function() {
    this.triggerShow();
    this.onclick = event => {
        let rect = this.getBoundingClientRect();
        if(event.clientY < rect.top || event.clientY > rect.bottom) return this.close();
        if(event.clientX < rect.left || event.clientX > rect.right) return this.close();
    }
}

-1

嗯,以你说话的方式编写代码。 如果您单击不是所需对话框的元素,请关闭对话框。 这是一个例子:

<div id="content">

  <div id="dialog" class="dialogComponent">

      <div id="foo" class="dialogComponent">
        test 123 123 123 123 
        <input class="dialogComponent class2" type="text">
      </div>
      <button class="dialogComponent">Submit</button>
  </div>

</div>



#content { width: 100%; height: 333px; background-color: black;}
#dialog { margin: 33px;  background-color: blue; }


$('#content').click(function(e) {
    if (!e.target.classList.contains("dialogComponent"))
    alert('Closing Dialog');
});

https://jsfiddle.net/scd9mwk7/


你好,我忘了说我的对话框包含一个必须填写的表单。所以你的代码不会起作用。 - UltimatePlayTheGame
如果使用showModal(),则在背景上单击将被视为对对话框元素的单击。 - 1j01
3
这个例子没有使用<dialog>元素 - 它不会像在一个正确的对话框上使用showModal一样行为。 - mindplay.dk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接