Skip to content

Baserow:Scrollbars

Baserow에서 사용하는 Scrollbars 컴포넌트의 TypeScript 변환 버전.

Code

<!-- Project: Baserow -->
<!-- web-frontend/modules/core/components/Scrollbars.vue -->
<!-- MIT License -->

<template>
  <div class="scrollbars">
    <div
      v-if="vertical !== null && verticalShow"
      class="scrollbars__vertical-wrapper"
    >
      <div
        ref="scrollbarVertical"
        class="scrollbars__vertical"
        :style="{ top: verticalTop + '%', height: verticalHeight + '%' }"
        @mousedown="mouseDownVertical($event)"
      ></div>
    </div>
    <div
      v-if="horizontal !== null && horizontalShow"
      class="scrollbars__horizontal-wrapper"
    >
      <div
        ref="scrollbarHorizontal"
        class="scrollbars__horizontal"
        :style="{ left: horizontalLeft + '%', width: horizontalWidth + '%' }"
        @mousedown="mouseDownHorizontal($event)"
      ></div>
    </div>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Ref, Emit} from 'vue-property-decorator';
import VueBase from '@/base/VueBase';

export function rounder(digits) {
  return parseInt('1' + Array(digits + 1).join('0'))
}

export function floor(n, digits = 0) {
  const r = rounder(digits)
  return Math.floor(n * r) / r
}

export function ceil(n, digits = 0) {
  const r = rounder(digits)
  return Math.ceil(n * r) / r
}

/**
 * This component will render custom scrollbars to a scrollable div. They will
 * automatically adjust when you resize the window and can be dragged.
 */
@Component
export default class VirtualScroll extends VueBase {
  /**
   * The vertical property should be the reference of the vertical scrollable element
   * in the parent component.
   */
  @Prop()
  readonly vertical!: string;

  /**
   * The horizontal property should be the reference of the vertical scrollable
   * element in the parent component.
   */
  @Prop()
  readonly horizontal!: string;

  @Ref()
  readonly scrollbarVertical!: HTMLDivElement;

  @Ref()
  readonly scrollbarHorizontal!: HTMLDivElement;

  dragging?: string;
  elementStart = 0;
  mouseStart = 0;

  verticalShow = false;
  verticalHeight = 0;
  verticalTop = 0;

  horizontalShow = false;
  horizontalWidth = 0;
  horizontalLeft = 0;

  mounted() {
    this.update();

    window.addEventListener('resize', this.onResize);
    window.addEventListener('mouseup', this.onMouseUp);
    window.addEventListener('mousemove', this.onMouseMove);
  }

  beforeDestroy() {
    window.removeEventListener('resize', this.onResize);
    window.removeEventListener('mouseup', this.onMouseUp);
    window.removeEventListener('mousemove', this.onMouseMove);
  }

  onResize() {
    this.update();
  }

  update() {
    if (typeof this.vertical !== 'undefined') {
      this.updateVertical();
    }
    if (typeof this.horizontal !== 'undefined') {
      this.updateHorizontal();
    }
  }

  /**
   * Method that updates the visibility, height and top position of the vertical
   * scrollbar handle based on the scrollTop of the vertical scrolling element of the
   * parent.
   */
  updateVertical() {
    const element = this.$parent[this.vertical]();
    const show = element.scrollHeight > element.clientHeight;

    // @TODO if the client height is very high we have a minimum of 2%, but this needs
    //  to be subtracted from the top position so that it fits. Same goes for the
    //  horizontal handler.
    const height = Math.max(
        floor((element.clientHeight / element.scrollHeight) * 100, 2),
        2
    );
    const top = ceil((element.scrollTop / element.scrollHeight) * 100, 2);

    this.verticalShow = show;
    this.verticalHeight = height;
    this.verticalTop = top;
  }

  /**
   * Method that updates the visibility, width and left position of the horizontal
   * scrollbar handle based on the scrollLeft of horizontal scrolling element of the
   * parent.
   */
  updateHorizontal() {
    const element = this.$parent[this.horizontal]();
    const show = element.scrollWidth > element.clientWidth;
    const width = Math.max(
        floor((element.clientWidth / element.scrollWidth) * 100, 2),
        2
    );
    const left = ceil((element.scrollLeft / element.scrollWidth) * 100, 2);
    this.horizontalShow = show;
    this.horizontalWidth = width;
    this.horizontalLeft = left;
  }

  /**
   * Event that is called when the user clicks on the vertical scrollbar handle. It
   * will start the vertical dragging.
   */
  mouseDownVertical(event: MouseEvent) {
    event.preventDefault();
    this.dragging = 'vertical';
    this.elementStart = this.scrollbarVertical.offsetTop;
    this.mouseStart = event.clientY;
  }

  /**
   * Event that is called when the user clicks on the horizontal scrollbar handle. It
   * will start the horizontal dragging.
   */
  mouseDownHorizontal(event: MouseEvent) {
    event.preventDefault();
    this.dragging = 'horizontal';
    this.elementStart = this.scrollbarHorizontal.offsetLeft;
    this.mouseStart = event.clientX;
  }

  /**
   * Event that is called when the mouse moves. If vertical of horizontal scrollbar
   * handle is in a dragging state it will emit an event with the new scrollTop.
   */
  onMouseMove(event: MouseEvent) {
    if (this.dragging === 'vertical') {
      event.preventDefault();
      const element = this.$parent[this.vertical]();
      const delta = event.clientY - this.mouseStart;
      const pixel = element.scrollHeight / element.clientHeight;
      const top = Math.ceil((this.elementStart + delta) * pixel);

      this.emitVertical(top);
      this.updateVertical();
    }

    if (this.dragging === 'horizontal') {
      event.preventDefault();
      const element = this.$parent[this.horizontal]();
      const delta = event.clientX - this.mouseStart;
      const pixel = element.scrollWidth / element.clientWidth;
      const left = Math.ceil((this.elementStart + delta) * pixel);

      this.emitHorizontal(left);
      this.updateHorizontal();
    }
  }

  onMouseUp() {
    if (typeof this.dragging === 'undefined') {
      return;
    }

    this.dragging = undefined;
    this.elementStart = 0;
    this.mouseStart = 0;
  }

  @Emit('vertical')
  emitVertical(top: number) {
    return top;
  }

  @Emit('horizontal')
  emitHorizontal(left: number) {
    return left;
  }
}
</script>

<style lang="scss" scoped>
@mixin absolute($top: null, $right: null, $bottom: null, $left: null) {
  position: absolute;

  @if ($top != null and $right == null and $bottom == null and $left == null) {
    top: $top;
    right: $top;
    bottom: $top;
    left: $top;
  }

  /* stylelint-disable-next-line at-rule-no-unknown */
  @else if ($top != null and $right != null and $bottom == null and
  $left == null) {
    top: $top;
    right: $right;
    bottom: $top;
    left: $right;
  }

  /* stylelint-disable-next-line at-rule-no-unknown */
  @else {
    @if $top != null {
      top: $top;
    }

    @if $right != null {
      right: $right;
    }

    @if $bottom != null {
      bottom: $bottom;
    }

    @if $left != null {
      left: $left;
    }
  }
}

$scrollbar-size: 6px;
$color-neutral-900: #272b30 !default;

.scrollbars {
  @include absolute(0);

  z-index: 100;
  pointer-events: none;
}

.scrollbars__vertical-wrapper {
  @include absolute(3px, 3px, 3px, auto);

  width: $scrollbar-size;
}

.scrollbars__horizontal-wrapper {
  @include absolute(auto, 3px, 3px, 3px);

  height: $scrollbar-size;
}

%scrollbar {
  background-color: $color-neutral-900;
  opacity: 0.6;
  border-radius: 3px;
  pointer-events: auto;
  cursor: pointer;
  user-select: none;
}

.scrollbars__vertical {
  @extend %scrollbar;

  @include absolute(auto, 0, auto, 0);
}

.scrollbars__horizontal {
  @extend %scrollbar;

  @include absolute(0, auto, 0, auto);
}
</style>

See also