Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/components/RunePopup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<template>
<div
ref="root"
class="rw-RunewordPopup absolute"
:style="{
visibility: isVisible ? 'visible' : 'hidden',
left: unitPx(position.x),
top: unitPx(position.y),
}"
@click="setVisible(false)"
>
<h3 class="rw-RunewordPopup-title">{{ rune }}</h3>
<div class="rw-RunewordPopup-body" v-html="formatBody"></div>
</div>
</template>

<script>
/**
* NOTES
*
* - we use css `visibility` instead of v-show/v-if so that the popup's
* offsetHeight can be obtained as soon as it is rendered by Vue
* and we can adjust its position before it is visible.
*
*/
import { defineComponent } from "vue";
import runesMetaData from "@/data/runes-descriptions";

export default defineComponent({
name: "RunePopup",

data() {
return {
isVisible: false,

/** @type { { x: number; y: number }} */
position: { x: 0, y: 0 },

/** @type {string} */
rune : ""
};
},

computed: {
/** @returns {string} */
formatBody() {
const rune = this.rune;
let text =
(rune && runesMetaData[rune] ) ||
"--( invalid runeword id )--";

// remove newlines at beginning and end
text = text.trim();

// fix extra spacing caused by newlines after <h4>sections</h4>
text = text.replace(/<\/h4>\n*/g, "</h4>");

// replace newlines by html equivalents
text = text.replace(/\n/g, "<br/>");

// format the mods (numbers) in the item stats
// https://regexr.com/66idv
text = text.replace(/\+?[0-9-]+%?/g, '<span class="is-mod">$&</span>');

return text;
},
},

methods: {
/** @param {number} n */
unitPx(n) {
return `${n}px`;
},

/**
*
* @param {HTMLElement} target element to position popup relative to
*/
moveTo(target) {
// minimal gap between popup and viewport edge (px)
const GAP = 10;

let { x: popX, y: popY } = target.getBoundingClientRect();

// place the popup a little below and to the side of the link
popX = popX + 50;
popY = popY + window.pageYOffset + target.offsetHeight + 4;

const elRoot = /**@type HTMLElement*/ (this.$refs.root);

const popHeight = elRoot.offsetHeight;
const popY2 = popY + popHeight;
const viewHeight = document.documentElement.clientHeight;
let viewY2 = window.scrollY + viewHeight;

// leave a little gap at bottom of viewport (just looks nicer)
viewY2 -= GAP;

if (popY2 > viewY2) {
// move the popup up to make it fully visible
popY = viewY2 - popHeight;
// if it's too tall for viewport + zoomsettings, then let it clip at bottom
popY = Math.max(window.scrollY + GAP, popY);
}

this.position = { x: popX, y: popY };
},

/**
* @param {string} rune
* @param {HTMLElement} target
*/
showRune(rune, target) {
this.rune = rune;
this.$nextTick(() => {
this.moveTo(target);
this.isVisible = true;
});
},

/** @param {boolean} value */
setVisible(value) {
this.isVisible = value;
},
},
});
</script>
27 changes: 27 additions & 0 deletions src/components/Runes.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<template>
<rune-popup ref="runePopup" />
<div class="relative">
<div class="flex justify-between items-center mb-2">
<h2 class="rw-Title-h2 mb-0">Runes</h2>
Expand All @@ -21,6 +22,8 @@
'is-selected': haveRunes[rune.name],
}"
@click="onToggleRune(rune.name)"
@mouseenter="onEnterRune($event, rune.name)"
@mouseleave="onLeaveRune()"
>
<span class="mx-auto my-auto">{{ rune.name }}</span>
</div>
Expand All @@ -32,16 +35,20 @@
<script>
import { defineComponent } from "vue";
import IconCancel from "@/icons/IconCancel.vue";
import RunePopup from "@/components/RunePopup.vue"

import { EnumRuneTier } from "@/data/runes";
import runesData from "@/data/runes";
import store from "@/store";

/** @typedef {TVueInstanceOf<RunePopup>} TRunePopup */

export default defineComponent({
name: "Runes",

components: {
IconCancel,
RunePopup
},

data() {
Expand Down Expand Up @@ -69,6 +76,11 @@ export default defineComponent({

return tiers;
},

/** @return {TRunePopup} */
runePopup() {
return /** @type {TRunePopup} */ (this.$refs.runePopup);
},
},

methods: {
Expand All @@ -77,6 +89,21 @@ export default defineComponent({
store.saveState();
},

/**
* @param {Event} ev
* @param {RuneId} rune
*/
onEnterRune(ev, rune) {
// paranoia
if (!ev.target) return;

this.runePopup.showRune(rune.toString(), /**@type HTMLElement*/(ev.target));
},

onLeaveRune() {
this.runePopup.setVisible(false);
},

/** @param {RuneId} runeId */
onToggleRune(runeId) {
const state = store.hasRune(runeId);
Expand Down
158 changes: 158 additions & 0 deletions src/data/runes-descriptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/** @type {RuneMeta} */
const runesDesc = {
El: `
`,

Eld: `
3 x El
`,

Tir: `
3 x Eld
`,

Nef : `
3 x Tir
`,

Eth: `
3 x Nef
`,

Ith: `
3 x Eth
`,

Tal:`
3 x Ith
`,

Ral:`
3 x Tal
`,

Ort:`
3 x Ral
`,

Thul:`
3 x Ort
`,

Amn:`
3 x Thul
1 Chipped Topaz
`,

Sol:`
3 x Amn
1 Chipped Amethyst
`,

Shael:`
3 x Sol
1 Chipped Sapphire
`,

Dol:`
3 x Shael
1 Chipped Ruby
`,

Hel:`
3 x Dol
1 Chipped Emerald
`,

Io:`
3 x Hel
1 Chipped Diamond
`,

Lum:`
3 x Io
1 Flawed Topaz
`,

Ko:`
3 x Lum
1 Flawed Amethyst
`,

Fal:`
3 x Ko
1 Flawed Sapphire
`,

Lem:`
3 x Fal
1 Flawed Ruby
`,

Pul:`
3 x Lem
1 Flawed Emerald
`,

Um:`
2 x Pul
1 Flawed Diamond
`,

Mal:`
2 x Um
1 Topaz
`,

Ist:`
2 x Mal
1 Amethyst
`,

Gul:`
2 x Ist
1 Sapphire
`,

Vex:`
2 x Gul
1 Ruby
`,

Ohm:`
2 x Vex
1 Emerald
`,

Lo:`
2 x Ohm
1 Diamond
`,

Sur:`
2 x Lo
1 Flawless Topaz
`,

Ber:`
2 x Sur
1 Flawless Amethyst
`,

Jah:`
2 x Ber
1 Flawless Sapphire
`,

Cham:`
2 x Jah
1 Flawless Ruby
`,

Zod:`
2 x Cham
1 Flawless Emerald
`,
};

export default runesDesc;
4 changes: 4 additions & 0 deletions src/types/main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ type RunewordMeta = {
[runewordId: string]: string;
};

type RuneMeta = {
[runeId : string]: string;
};

type RunewordItem = Runeword & {
filterMatch: boolean; // true if this item matches current search filter
}