自定义网页鼠标指针——一段曲折的旅程

21 3.3~4.2 分钟 1487

看到别人主题有自定义鼠标指针的功能,我也想给我主题加一个玩玩。

探索

从 User Agent Stylesheet 学习

找到了相关文档:

看起来似乎只需要下面 CSS 代码就完事了。

:root {
  cursor: url("xxx.cur"), auto;
}

随便下载了一个图标包,傻眼了:一个包里面有若干个 CUR 文件。
好吧,CUR 格式是一种图形文件格式,而不是打包了一组图标。

显然下面的写法不是很符合正确 CSS 写法。

:root {
  cursor: url("auto.cur"), auto;
  cursor: url("context-menu.cur"), context-menu;  /* [!code ++] */
  /* 省略更多 */  /* [!code ++] */
}

那有没有什么标准,说明什么网页元素用什么光标?

我想到了找 User Agent Stylesheet(用户代理样式表)

用户代理样式表中有这些 cursor 相关的定义:

点我展开相关定义

Chromium

label {
  cursor: default;  /* [!code highlight] */
}

input {
  -webkit-appearance: textfield;
  padding: 1px;
  background-color: white;
  border: 2px inset;
  -webkit-rtl-ordering: logical;
  -webkit-user-select: text;
  cursor: auto;  /* [!code highlight] */
}

input::-webkit-inner-spin-button {
  -webkit-appearance: inner-spin-button;
  display: inline-block;
  cursor: default;  /* [!code highlight] */
  flex: none;
  align-self: stretch;
  -webkit-user-select: none;
  -webkit-user-modify: read-only !important;
  opacity: 0;
  pointer-events: none;
}

textarea {
  -webkit-appearance: textarea;
  background-color: white;
  border: 1px solid;
  -webkit-rtl-ordering: logical;
  -webkit-user-select: text;
  flex-direction: column;
  resize: auto;
  cursor: auto;  /* [!code highlight] */
  padding: 2px;
  white-space: pre-wrap;
  word-wrap: break-word;
}

input[type="button" i],
input[type="submit" i],
input[type="reset" i],
input[type="file" i]::-webkit-file-upload-button,
button {
  align-items: flex-start;
  text-align: center;
  cursor: default;  /* [!code highlight] */
  color: ButtonText;
  padding: 2px 6px 3px 6px;
  border: 2px outset ButtonFace;
  background-color: ButtonFace;
  box-sizing: border-box
}

area {
  display: inline;
  cursor: pointer;  /* [!code highlight] */
}

select {
  -webkit-appearance: menulist;
  box-sizing: border-box;
  align-items: center;
  border: 1px solid;
  white-space: pre;
  -webkit-rtl-ordering: logical;
  color: black;
  background-color: white;
  cursor: default;  /* [!code highlight] */
}

a:-webkit-any-link {
  color: -webkit-link;
  text-decoration: underline;
  cursor: auto;  /* [!code highlight] */
}

Firefox

.mozGrabber:-moz-native-anonymous {
  outline: ridge 2px silver;
  padding: 2px;
  position: absolute;
  width: 12px;
  height: 12px;
  background-image: url("resource://gre/res/grabber.gif");
  background-repeat: no-repeat;
  background-position: center center;
  user-select: none;
  cursor: move;  /* [!code highlight] */
}
提取聚合相关声明
label {
  cursor: default;
}

input {
  cursor: auto;
}

input::-webkit-inner-spin-button {
  cursor: default;
}

textarea {
  cursor: auto;
}

input[type="button" i],
input[type="submit" i],
input[type="reset" i],
input[type="file" i]::-webkit-file-upload-button,
button {
  cursor: default;
}

area {
  cursor: pointer;
}

select {
  cursor: default;
}

a:-webkit-any-link {
  cursor: auto;
}

.mozGrabber:-moz-native-anonymous {
  cursor: move;
}

我们可以将这些拿出来作为声明。

从 cursor 实现学习

忽然想到,我找下 cursor: auto 是怎么实现的,对照设置下就好了,于是找到以下源码:

bool EventHandler::ShouldShowIBeamForNode(const Node* node,
                                          const HitTestResult& result) {
  if (!node)
    return false;

  if (node->IsTextNode() && (node->CanStartSelection() || result.IsOverLink()))
    return true;

  return IsEditable(*node);
}

std::optional<ui::Cursor> EventHandler::SelectCursor(
  const ui::Cursor& i_beam = style.IsHorizontalWritingMode() ? IBeamCursor() : VerticalTextCursor();

  switch (style.Cursor()) {
    case ECursor::kAuto:
      return SelectAutoCursor(result, node, i_beam);
    // 省略
    case ECursor::kText:
      return i_beam;
    // 省略
  }
  return PointerCursor();
}

std::optional<ui::Cursor> EventHandler::SelectAutoCursor(
    const HitTestResult& result,
    Node* node,
    const ui::Cursor& i_beam) {
  if (ShouldShowIBeamForNode(node, result))
    return i_beam;

  return PointerCursor();
}

cursor: auto 的实现逻辑非常简单:

  1. 调用 ShouldShowIBeamForNode() 判断当前节点是否,满足:可选择的文本/链接文本/可编辑区域(如 <input>, <textarea> 或带 contenteditable 属性的元素)

    • 如果是,返回 i_beam(即 cursor: text
  2. 默认情况返回普通箭头光标(即 cursor: default

结论:把可编辑的文本设置为 cursor: text

发挥主观能动性

发挥主观能动性,观察标准 HTML 元素/ARIA 属性,按语义进行标注,得到了以下 CSS 声明:

一些 CSS 声明
:root {
  cursor: default;
}

/* 禁止操作 */
:disabled,
[aria-disabled="true"] {
  cursor: not-allowed;
}

/* 帮助提示 */
[title],
abbr,
acronym,
[role="tooltip"] {
  cursor: help;
}

/* 调整大小 - 东西 */
input[type="range"],
[role="slider"] {
  cursor: ew-resize;
}

/* 抓取 */
input[type="range"]::-webkit-slider-thumb {
  cursor: grab;
}

/* 抓取中 */
[draggable="true"]:active,
input[type="range"]::-webkit-slider-thumb:active {
  cursor: grabbing;
}

/* 单元格 */
td,
th,
[role="grid"],
[role="gridcell"] {
  cursor: cell;
}

/* 等待 */
[aria-busy="true"] {
  cursor: wait;
}

/* 文本光标 */
input:not([type]),
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="number"],
[contenteditable="true"],
textarea,
code,
kbd,
samp,
var,
pre,
[role="textbox"],
[role="searchbox"] {
  cursor: text;
}

/* 指针光标 */
a,
button,
select,
label,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"],
input[type="checkbox"],
input[type="radio"],
input[type="color"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="month"],
input[type="week"],
video,
audio,
video::-webkit-media-controls,
audio::-webkit-media-controls,
img[onclick],
img[role="button"],
::-webkit-scrollbar-thumb,
::-webkit-file-upload-button,
::-ms-browse,
::-webkit-search-cancel-button,
::-ms-clear,
::-webkit-clear-button,
::-webkit-calendar-picker-indicator,
option,
summary,
li[onclick],
li[role="button"],
tr[onclick],
svg [onclick],
[role="button"],
[role="link"],
[role="menuitem"],
[role="menuitemcheckbox"],
[role="menuitemradio"],
[role="tab"],
[role="treeitem"],
[role="option"],
[role="switch"],
[role="checkbox"],
[role="radio"],
[role="combobox"] {
  cursor: pointer;
}

/* 准星 */
canvas {
  cursor: crosshair;
}

/* 抓取(可拖拽元素) */
[draggable="true"] {
  cursor: grab;
}

成果

聚合上面的阶段性成果,并按情况解释 cursor: auto 实际值,得到了以下的最终版本:

最终版本 CSS 声明
:root,
label,
input::-webkit-inner-spin-button,
input[type="button" i],
input[type="submit" i],
input[type="reset" i],
input[type="file" i]::-webkit-file-upload-button,
button,
select {
  cursor: default;
}

.mozGrabber:-moz-native-anonymous {
  cursor: move;
}

:disabled,
[aria-disabled="true"] {
  cursor: not-allowed;
}

[title],
abbr,
acronym,
[role="tooltip"] {
  cursor: help;
}

input[type="range"],
[role="slider"] {
  cursor: ew-resize;
}

[draggable="true"],
input[type="range"]::-webkit-slider-thumb {
  cursor: grab;
}

[draggable="true"]:active,
input[type="range"]::-webkit-slider-thumb:active {
  cursor: grabbing;
}

td,
th,
[role="grid"],
[role="gridcell"] {
  cursor: cell;
}

[aria-busy="true"] {
  cursor: wait;
}

input:not([type]),
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="number"],
[contenteditable="true"],
textarea,
code,
kbd,
samp,
var,
pre,
[role="textbox"],
[role="searchbox"] {
  cursor: text;
}

area,
a,
a:-webkit-any-link,
button,
select,
label,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"],
input[type="checkbox"],
input[type="radio"],
input[type="color"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="month"],
input[type="week"],
input[type="range"],
input[type="image"],
video,
audio,
video::-webkit-media-controls,
audio::-webkit-media-controls,
img[onclick],
img[role="button"],
::-webkit-scrollbar-thumb,
::-webkit-file-upload-button,
::-ms-browse,
::-webkit-search-cancel-button,
::-ms-clear,
::-webkit-clear-button,
::-webkit-calendar-picker-indicator,
option,
summary,
li[onclick],
li[role="button"],
tr[onclick],
svg [onclick],
[role="button"],
[role="link"],
[role="menuitem"],
[role="menuitemcheckbox"],
[role="menuitemradio"],
[role="tab"],
[role="treeitem"],
[role="option"],
[role="switch"],
[role="checkbox"],
[role="radio"],
[role="combobox"] {
  cursor: pointer;
}

canvas {
  cursor: crosshair;
}

使用方法:

:root {
    cursor: default;  /* [!code --] */
    ursor: url("实际的图标地址.cur"), default;  /* [!code ++] */
}

/* 此外再补充其他需要的声明 */ /* [!code ++] */

结语

感谢您看到这里,欢迎在评论区留言。


0
上一篇 一个脚本让 Halo CMS 评论后台瘫痪