You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FullCalendar中insertBefore()添加的CSS tooltip无法始终置顶问题

问题描述

给FullCalendar资源时间线视图的事件添加自定义tooltip时,tooltip会被相邻事件遮挡,调整z-index、position属性都无法解决,效果对比如下:

  • 被遮挡效果
    Tooltip covered by event below
  • 正常显示效果
    Tooltip shown correctly

对应Angular组件源码如下:

resource-timeline.component.html

<div class="container-fluid mt-2" style="padding-left: 1%; padding-right: 1%">
  <div>
    <div class="row">
      <div class="col-xl-12 col-md-12 col-sm-12">
        <div class="p-3">
          <full-calendar #resourceTimeline [options]="resourceTimelineOptions"></full-calendar>
        </div>
      </div>
    </div>
  </div>
</div>

resource-timeline.component.scss

.timeline-tooltip {
  background-color: cornflowerblue;
  color: #fff;
  text-align: center;
  border-radius: 6px;
  padding: 5px;
  position: absolute;
  z-index: 9999;
  width: 120px;
  word-wrap: break-word;
  top: 100%;
  left: 50%;
  margin-left: -60px;

  &::after {
    content: "";
    position: absolute;
    bottom: 100%;
    left: 50%;
    margin-left: -5px;
    border-width: 5px;
    border-style: solid;
    border-color: transparent transparent black transparent;
  }
}

resource-timeline.component.ts

import {Component, ElementRef, OnInit, Renderer2, ViewChild} from '@angular/core'
import {CalendarOptions, EventContentArg, FullCalendarComponent} from "@fullcalendar/angular"
import resourceTimelinePlugin from '@fullcalendar/resource-timeline'
import {ActivatedRoute, Router} from '@angular/router'
import {EventHoveringArg} from "@fullcalendar/common"

@Component({
  selector: 'resource-timeline',
  templateUrl: './resource-timeline.component.html',
  styleUrls: ['./resource-timeline.component.scss']
})
export class ResourceTimelineComponent implements OnInit {

  @ViewChild('fc-timeline-event.fc-event-start', {static: false}) calendarEvent: ElementRef
  toolTipDiv: ElementRef

  @ViewChild('resourceTimeline')
  calendar: FullCalendarComponent

  resourceTimelineOptions: CalendarOptions = {
    schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
    initialView: 'resourceTimelineDay',
    height: window.innerHeight - 380,
    eventMinWidth: 5,
    titleFormat: {year: 'numeric', month: 'long', day: '2-digit'},
    plugins: [
      resourceTimelinePlugin
    ],
    headerToolbar: {
      center: 'resourceTimelineDay',
      right: 'prev,today,next'
    },
    resourceAreaColumns: [
      {
        headerContent: 'Bench Name',
        field: 'resourceName',
      },
      {
        headerContent: 'Description',
        field: 'description',
      }
    ],
    weekNumbers: true,
    firstDay: 1,
    scrollTime: '07:00:00',
    selectable: false,
    selectMirror: false,
    selectOverlap: false,
    eventOverlap: false,
    editable: false,
    nowIndicator: true,
    buttonText: {
      day: 'Day',
      week: 'Week',
      month: 'Month',
      today: 'Today'
    },
    eventDidMount: (eventInfo: EventContentArg) => {
    },
    eventMouseEnter: (eventInfo: EventHoveringArg) => {
      this.removeToolTip()
      this.insertToolTip(eventInfo.event.title, eventInfo.el)
    },
    eventMouseLeave: (eventInfo: EventHoveringArg) => {
      this.removeToolTip()
    }
  }

  constructor(private router: Router,
              private activatedRoute: ActivatedRoute,
              private renderer: Renderer2,
              private domElement: ElementRef) {
  }

  ngOnInit(): void {
    this.resourceTimelineOptions.resources = [
      {
        "id": "id1",
        "resourceName": "Resource 1",
        "description": "Description 1"
      },
      {
        "id": "id2",
        "resourceName": "Resource 2",
        "description": "Description 2"
      }
    ]

    this.resourceTimelineOptions.events = [
      {
        id: "id1",
        resourceId: "id1",
        title: 'All Day Event',
        start: '2022-06-13'
      },
      {
        id: "id2",
        resourceId: "id2",
        title: 'Long Event',
        start: '2022-06-13',
        end: '2022-06-14'
      }
    ]
  }

  private insertToolTip(tooltipText: string, eventElement: HTMLElement): void {
    this.toolTipDiv = this.renderer.createElement('div')
    this.renderer.setAttribute(this.toolTipDiv, 'class', 'timeline-tooltip')
    const text = this.renderer.createText(tooltipText)
    this.renderer.appendChild(this.toolTipDiv, text)
    this.renderer.insertBefore(eventElement, this.toolTipDiv, this.calendarEvent)
  }

  private removeToolTip(): void {
    if (this.toolTipDiv !== undefined) {
      const elements = this.domElement.nativeElement.querySelectorAll('.timeline-tooltip')
      elements.forEach(e => {
        this.renderer.removeChild(e.parentElement, e)
      })
    }
  }
}
问题原因

遮挡的核心问题有两个:

  1. 你把tooltip作为子元素插入到了FullCalendar的事件DOM节点内部,FullCalendar的事件节点默认会创建独立层叠上下文,子元素的z-index只会在当前父元素的层叠范围内生效,永远不会高过父级同层的其他事件节点;同时部分FullCalendar容器自带overflow: hidden属性,超出事件范围的内容会被直接裁剪。
  2. 你写的@ViewChild('fc-timeline-event.fc-event-start')是无效的,@ViewChild只能获取模板里静态声明、挂了模板引用变量的元素,FullCalendar动态生成的事件节点没法通过这个方式拿到,原有插入逻辑本身就没有正确的参考节点。
修复方案

不需要调整z-index数值,改两个地方即可:

1. 修改tooltip挂载逻辑

不要把tooltip插到事件元素内部,直接挂载到FullCalendar根容器下,脱离事件的层叠上下文限制,同时手动计算tooltip位置:

private insertToolTip(tooltipText: string, eventElement: HTMLElement): void {
  this.removeToolTip()
  // 创建tooltip节点
  this.toolTipDiv = this.renderer.createElement('div')
  this.renderer.setAttribute(this.toolTipDiv, 'class', 'timeline-tooltip')
  const text = this.renderer.createText(tooltipText)
  this.renderer.appendChild(this.toolTipDiv, text)
  
  // 挂载到日历根容器,不要挂在事件元素内部
  const calendarRoot = this.calendar.getApi().el
  this.renderer.appendChild(calendarRoot, this.toolTipDiv)

  // 计算位置,保持事件下方居中的显示效果
  const eventRect = eventElement.getBoundingClientRect()
  const calendarRect = calendarRoot.getBoundingClientRect()
  const topOffset = eventRect.bottom - calendarRect.top + 8 // 加8px和事件拉开间距
  const leftOffset = eventRect.left - calendarRect.left + eventRect.width/2 - 60 // 60是tooltip宽度的一半
  this.renderer.setStyle(this.toolTipDiv, 'top', `${topOffset}px`)
  this.renderer.setStyle(this.toolTipDiv, 'left', `${leftOffset}px`)
}

2. 调整样式

给日历根容器加相对定位,修正小箭头颜色,删除原有写死的定位属性:

:host ::ng-deep full-calendar {
  position: relative;
}

.timeline-tooltip {
  background-color: cornflowerblue;
  color: #fff;
  text-align: center;
  border-radius: 6px;
  padding: 5px;
  position: absolute;
  z-index: 9999;
  width: 120px;
  word-wrap: break-word;
  // 删除原有的top、left、margin-left属性,位置由TS动态计算

  &::after {
    content: "";
    position: absolute;
    bottom: 100%;
    left: 50%;
    margin-left: -5px;
    border-width: 5px;
    border-style: solid;
    // 原来写的black和tooltip背景色不匹配,改成同色
    border-color: transparent transparent cornflowerblue transparent;
  }
}

可选优化

如果日历区域有横向/纵向滚动,建议给日历滚动容器加滚动监听,滚动时直接触发removeToolTip()隐藏tooltip,避免位置错位。


内容的提问来源于stack exchange,提问作者Ivo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 01:04:07