Reorganize Vue components and add documentation explaining each folder

This commit is contained in:
Keavon Chambers
2022-05-23 19:03:34 -07:00
parent 5650a87465
commit e4e37cca7b
23 changed files with 46 additions and 28 deletions
@@ -0,0 +1,148 @@
<template>
<div class="canvas-ruler" :class="direction.toLowerCase()" ref="rulerRef">
<svg :style="svgBounds">
<path :d="svgPath" />
<text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text>
</svg>
</div>
</template>
<style lang="scss">
.canvas-ruler {
flex: 1 1 100%;
background: var(--color-4-dimgray);
overflow: hidden;
position: relative;
&.horizontal {
height: 16px;
}
&.vertical {
width: 16px;
svg text {
text-anchor: end;
}
}
svg {
position: absolute;
path {
stroke-width: 1px;
stroke: var(--color-7-middlegray);
}
text {
font-size: 12px;
fill: var(--color-8-uppergray);
}
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
const RULER_THICKNESS = 16;
const MAJOR_MARK_THICKNESS = 16;
const MEDIUM_MARK_THICKNESS = 6;
const MINOR_MARK_THICKNESS = 3;
export type RulerDirection = "Horizontal" | "Vertical";
// Modulo function that works for negative numbers, unlike the JS % operator
const mod = (n: number, m: number): number => {
const remainder = n % m;
return Math.floor(remainder >= 0 ? remainder : remainder + m);
};
export default defineComponent({
props: {
direction: { type: String as PropType<RulerDirection>, default: "Vertical" },
origin: { type: Number as PropType<number>, required: true },
numberInterval: { type: Number as PropType<number>, required: true },
majorMarkSpacing: { type: Number as PropType<number>, required: true },
mediumDivisions: { type: Number as PropType<number>, default: 5 },
minorDivisions: { type: Number as PropType<number>, default: 2 },
},
computed: {
svgPath(): string {
const isVertical = this.direction === "Vertical";
const lineDirection = isVertical ? "H" : "V";
const offsetStart = mod(this.origin, this.majorMarkSpacing);
const shiftedOffsetStart = offsetStart - this.majorMarkSpacing;
const divisions = this.majorMarkSpacing / this.mediumDivisions / this.minorDivisions;
const majorMarksFrequency = this.mediumDivisions * this.minorDivisions;
let dPathAttribute = "";
let i = 0;
for (let location = shiftedOffsetStart; location < this.rulerLength; location += divisions) {
let length;
if (i % majorMarksFrequency === 0) length = MAJOR_MARK_THICKNESS;
else if (i % this.minorDivisions === 0) length = MEDIUM_MARK_THICKNESS;
else length = MINOR_MARK_THICKNESS;
i += 1;
const destination = Math.round(location) + 0.5;
const startPoint = isVertical ? `${RULER_THICKNESS - length},${destination}` : `${destination},${RULER_THICKNESS - length}`;
dPathAttribute += `M${startPoint}${lineDirection}${RULER_THICKNESS} `;
}
return dPathAttribute;
},
svgTexts(): { transform: string; text: number }[] {
const isVertical = this.direction === "Vertical";
const offsetStart = mod(this.origin, this.majorMarkSpacing);
const shiftedOffsetStart = offsetStart - this.majorMarkSpacing;
const svgTextCoordinates = [];
let text = (Math.ceil(-this.origin / this.majorMarkSpacing) - 1) * this.numberInterval;
for (let location = shiftedOffsetStart; location < this.rulerLength; location += this.majorMarkSpacing) {
const destination = Math.round(location);
const x = isVertical ? 9 : destination + 2;
const y = isVertical ? destination + 1 : 9;
let transform = `translate(${x} ${y})`;
if (isVertical) transform += " rotate(270)";
svgTextCoordinates.push({ transform, text });
text += this.numberInterval;
}
return svgTextCoordinates;
},
},
methods: {
resize() {
if (!this.$refs.rulerRef) return;
const rulerElement = this.$refs.rulerRef as HTMLElement;
const isVertical = this.direction === "Vertical";
const newLength = isVertical ? rulerElement.clientHeight : rulerElement.clientWidth;
const roundedUp = (Math.floor(newLength / this.majorMarkSpacing) + 1) * this.majorMarkSpacing;
if (roundedUp !== this.rulerLength) {
this.rulerLength = roundedUp;
const thickness = `${RULER_THICKNESS}px`;
const length = `${roundedUp}px`;
this.svgBounds = isVertical ? { width: thickness, height: length } : { width: length, height: thickness };
}
},
},
data() {
return {
rulerLength: 0,
svgBounds: { width: "0px", height: "0px" },
};
},
});
</script>
@@ -0,0 +1,204 @@
<template>
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="() => changePosition(-50)"></button>
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div>
</div>
<button class="arrow increase" @click="() => changePosition(50)"></button>
</div>
</template>
<style lang="scss">
.persistent-scrollbar {
display: flex;
flex: 1 1 100%;
.arrow {
flex: 0 0 auto;
background: none;
outline: none;
border: none;
border-style: solid;
width: 0;
height: 0;
padding: 0;
}
.scroll-track {
flex: 1 1 100%;
position: relative;
.scroll-thumb {
position: absolute;
border-radius: 4px;
background: var(--color-5-dullgray);
&:hover,
&.dragging {
background: var(--color-6-lowergray);
}
}
.scroll-click-area {
position: absolute;
}
}
&.vertical {
flex-direction: column;
.arrow.decrease {
margin: 4px 3px;
border-width: 0 5px 8px 5px;
border-color: transparent transparent var(--color-5-dullgray) transparent;
&:hover {
border-color: transparent transparent var(--color-6-lowergray) transparent;
}
&:active {
border-color: transparent transparent var(--color-c-brightgray) transparent;
}
}
.arrow.increase {
margin: 4px 3px;
border-width: 8px 5px 0 5px;
border-color: var(--color-5-dullgray) transparent transparent transparent;
&:hover {
border-color: var(--color-6-lowergray) transparent transparent transparent;
}
&:active {
border-color: var(--color-c-brightgray) transparent transparent transparent;
}
}
}
&.horizontal {
flex-direction: row;
.arrow.decrease {
margin: 3px 4px;
border-width: 5px 8px 5px 0;
border-color: transparent var(--color-5-dullgray) transparent transparent;
&:hover {
border-color: transparent var(--color-6-lowergray) transparent transparent;
}
&:active {
border-color: transparent var(--color-c-brightgray) transparent transparent;
}
}
.arrow.increase {
margin: 3px 4px;
border-width: 5px 0 5px 8px;
border-color: transparent transparent transparent var(--color-5-dullgray);
&:hover {
border-color: transparent transparent transparent var(--color-6-lowergray);
}
&:active {
border-color: transparent transparent transparent var(--color-c-brightgray);
}
}
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
export type ScrollbarDirection = "Horizontal" | "Vertical";
// Linear Interpolation
const lerp = (x: number, y: number, a: number): number => x * (1 - a) + y * a;
// Convert the position of the handle (0-1) to the position on the track (0-1).
// This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
const handleToTrack = (handleLen: number, handlePos: number): number => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
export default defineComponent({
emits: {
"update:handlePosition": null,
pressTrack: (pointerOffset: number) => typeof pointerOffset === "number",
},
props: {
direction: { type: String as PropType<ScrollbarDirection>, default: "Vertical" },
handlePosition: { type: Number as PropType<number>, default: 0.5 },
handleLength: { type: Number as PropType<number>, default: 0.5 },
},
computed: {
thumbStart(): { left: string } | { top: string } {
const start = handleToTrack(this.handleLength, this.handlePosition) - this.handleLength / 2;
return this.direction === "Vertical" ? { top: `${start * 100}%` } : { left: `${start * 100}%` };
},
thumbEnd(): { right: string } | { bottom: string } {
const end = 1 - handleToTrack(this.handleLength, this.handlePosition) - this.handleLength / 2;
return this.direction === "Vertical" ? { bottom: `${end * 100}%` } : { right: `${end * 100}%` };
},
sides(): { left: string; right: string } | { top: string; bottom: string } {
return this.direction === "Vertical" ? { left: "0%", right: "0%" } : { top: "0%", bottom: "0%" };
},
},
data() {
return {
dragging: false,
pointerPos: 0,
};
},
mounted() {
window.addEventListener("pointerup", this.pointerUp);
window.addEventListener("pointermove", this.pointerMove);
},
unmounted() {
window.removeEventListener("pointerup", this.pointerUp);
window.removeEventListener("pointermove", this.pointerMove);
},
methods: {
trackLength(): number {
const track = this.$refs.scrollTrack as HTMLElement;
return this.direction === "Vertical" ? track.clientHeight - this.handleLength : track.clientWidth;
},
trackOffset(): number {
const track = this.$refs.scrollTrack as HTMLElement;
return this.direction === "Vertical" ? track.getBoundingClientRect().top : track.getBoundingClientRect().left;
},
clampHandlePosition(newPos: number) {
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
this.$emit("update:handlePosition", clampedPosition);
},
updateHandlePosition(e: PointerEvent) {
const position = pointerPosition(this.direction, e);
this.clampHandlePosition(this.handlePosition + (position - this.pointerPos) / (this.trackLength() * (1 - this.handleLength)));
this.pointerPos = position;
},
grabHandle(e: PointerEvent) {
if (!this.dragging) {
this.dragging = true;
this.pointerPos = pointerPosition(this.direction, e);
}
},
grabArea(e: PointerEvent) {
if (!this.dragging) {
const pointerPos = pointerPosition(this.direction, e);
const oldPointer = handleToTrack(this.handleLength, this.handlePosition) * this.trackLength() + this.trackOffset();
this.$emit("pressTrack", pointerPos - oldPointer);
}
},
pointerUp() {
this.dragging = false;
},
pointerMove(e: PointerEvent) {
if (this.dragging) this.updateHandlePosition(e);
},
changePosition(difference: number) {
this.clampHandlePosition(this.handlePosition + difference / this.trackLength());
},
},
});
</script>