Skip to content

Commit

Permalink
add a tab view for help mode, singular store, use md composable
Browse files Browse the repository at this point in the history
- Added a tab view for help mode which tracks the current, relevant help texts using a stack.
- Consolidated multiple help stores into one.
- Used an existing composable for MD rendering.
 - Modified the composable to use the html prop, add `<hr>`s in `<details>` sections and render font awesome icons wherever there is a `<code>fa-iconname</code>` section.
- Other help mode related minor refactoring
  • Loading branch information
ahmedhamidawan committed Mar 25, 2024
1 parent a606d6d commit 01b38cd
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 142 deletions.
6 changes: 3 additions & 3 deletions client/src/components/Form/FormDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { mapActions } from "pinia";
import Vue from "vue";
import { useHelpModeTextStore } from "@/stores/helpmode/helpModeTextStore";
import { useHelpModeStore } from "@/stores/helpmode/helpModeStore";
import FormInputs from "./FormInputs";
import { matchInputs, validateInputs, visitInputs } from "./utilities";
Expand Down Expand Up @@ -154,10 +154,10 @@ export default {
},
destroyed() {
// since the user is leaving the form, the help mode is reset
this.clearHelpModeText();
this.clearHelpModeText("tool_form_base");
},
methods: {
...mapActions(useHelpModeTextStore, ["storeHelpModeText", "clearHelpModeText"]),
...mapActions(useHelpModeStore, ["storeHelpModeText", "clearHelpModeText"]),
callHelpMode() {
this.storeHelpModeText("tool_form_base");
},
Expand Down
24 changes: 22 additions & 2 deletions client/src/components/History/Modals/SelectorModal.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCheckSquare, faExchangeAlt } from "@fortawesome/free-solid-svg-icons";
import { BButton, BFormGroup, BModal } from "bootstrap-vue";
import { orderBy } from "lodash";
import isEqual from "lodash.isequal";
Expand All @@ -7,13 +9,16 @@ import { computed, ref, watch } from "vue";
import type { HistorySummary } from "@/api";
import { HistoriesFilters } from "@/components/History/HistoriesFilters";
import { useHelpModeStore } from "@/stores/helpmode/helpModeStore";
import { useHistoryStore } from "@/stores/historyStore";
import localize from "@/utils/localization";
import FilterMenu from "@/components/Common/FilterMenu.vue";
import Heading from "@/components/Common/Heading.vue";
import HistoryList from "@/components/History/HistoryScrollList.vue";
library.add(faCheckSquare, faExchangeAlt);
type AdditionalOptions = "set-current" | "multi" | "center";
type PinnedHistory = { id: string };
Expand Down Expand Up @@ -54,14 +59,29 @@ const busy = ref(false);
const showAdvanced = ref(false);
const modal = ref<BModal | null>(null);
const helpModeStore = useHelpModeStore();
const { pinnedHistories } = storeToRefs(useHistoryStore());
// retain previously selected histories when you reopen the modal in multi view
watch(
() => propShowModal.value,
(show: boolean) => {
if (props.multiple && show) {
selectedHistories.value = [...pinnedHistories.value];
let helpModeId;
let helpModeIcon;
if (props.multiple) {
if (show) {
selectedHistories.value = [...pinnedHistories.value];
}
helpModeId = "selector_modal_multiview";
helpModeIcon = faCheckSquare;
} else {
helpModeId = "selector_modal_switch";
helpModeIcon = faExchangeAlt;
}
if (show) {
helpModeStore.storeHelpModeText(helpModeId, helpModeIcon);
} else {
helpModeStore.clearHelpModeText(helpModeId);
}
},
{
Expand Down
24 changes: 17 additions & 7 deletions client/src/components/Masthead/HelpModeSwitch.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { storeToRefs } from "pinia";
import { useHelpModeStatusStore } from "@/stores/helpmode/helpModeStatusStore";
import { useHelpModeStore } from "@/stores/helpmode/helpModeStore";
import localize from "@/utils/localization";
const tooltip = "Enable/Disable Help Mode";
const { status } = storeToRefs(useHelpModeStatusStore());
library.add(faQuestionCircle);
const tooltip = localize("Enable/Disable Help Mode");
const { status } = storeToRefs(useHelpModeStore());
function toggleEnabledStatus() {
status.value = !status.value;
}
Expand All @@ -13,20 +19,24 @@ function toggleEnabledStatus() {
<template>
<div>
<button
v-b-tooltip.hover.bottom
class="help-mode-button"
:class="{ highlight: status }"
:title="tooltip"
:aria-label="tooltip"
@click="toggleEnabledStatus"
@keydown.enter="toggleEnabledStatus">
<i class="fas fa-question-circle fa-lg" :class="{ highlight: status }"> </i> Help Me
<!-- <i class="fas fa-question-circle fa-lg" :class="{ highlight: status }"> </i> Help Me -->
<FontAwesomeIcon :icon="faQuestionCircle" :class="{ highlight: status }" size="lg" /> Help Me
</button>
</div>
</template>

<style scoped>
<style scoped lang="scss">
@import "theme/blue.scss";
.highlight {
color: yellow;
color: var(--masthead-text-hover);
}
.help-mode-button {
background-color: inherit;
Expand All @@ -36,6 +46,6 @@ function toggleEnabledStatus() {
outline: none;
}
.help-mode-button.highlight {
color: yellow;
color: var(--masthead-text-hover);
}
</style>
131 changes: 84 additions & 47 deletions client/src/components/Panels/HelpModeText.vue
Original file line number Diff line number Diff line change
@@ -1,86 +1,122 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { faTimes, faUndo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { useDraggable } from "@vueuse/core";
import { BButton } from "bootstrap-vue";
import MarkdownIt from "markdown-it";
import { BButton, BTab, BTabs } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, watch } from "vue";
import { reactive, ref, watch } from "vue";
import { useHelpModeStatusStore } from "@/stores/helpmode/helpModeStatusStore";
import { useHelpModeTextStore } from "@/stores/helpmode/helpModeTextStore";
import { useMarkdown } from "@/composables/markdown";
import { useAnimationFrameSize } from "@/composables/sensors/animationFrameSize";
import { DEFAULT_HELP_TEXT, useHelpModeStore } from "@/stores/helpmode/helpModeStore";
import localize from "@/utils/localization";
import Heading from "@/components/Common/Heading.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
library.add(faTimes);
library.add(faTimes, faUndo);
const md = MarkdownIt();
const { renderMarkdown } = useMarkdown({
openLinksInNewPage: true,
html: true,
appendHrRuleToDetails: true,
replaceCodeWithIcon: true,
});
// local refs
const { content, loading } = storeToRefs(useHelpModeTextStore());
const { status, position } = storeToRefs(useHelpModeStatusStore());
const { status, position, helpModeStyle, activeTab, contents, loading, currentTabs } = storeToRefs(useHelpModeStore());
const el = ref<HTMLElement | null>(null);
const helpModeHeader = ref<HTMLElement | null>(null);
const helpTextRef = ref(null);
const helpModeSize = reactive(useAnimationFrameSize(el));
// local computed refs
const helpText = computed({
get() {
return md.render(content.value);
},
set() {
//do nothing, this is set in components that add helptext
},
});
const noHelpTextMsg = localize("No help text available for this component");
// draggable properties
const {
x: dragX,
y: dragY,
style,
} = useDraggable(helpModeHeader, {
initialValue: position.value,
});
// watch both x and y, and do something if they change:
const { style } = useDraggable(helpModeHeader, { initialValue: position.value });
// update store position on drag
watch(
() => [dragX.value, dragY.value],
([newX, newY]) => {
if (newX && newY) {
position.value = { x: newX, y: newY };
() => style.value,
(newStyle) => {
if (newStyle) {
// TODO: This might be a little hacky?...
// convert str of form "left:{...}px;top:{...}px;" to
// extract left and top and place in helpModeStyle
const [left, top] = newStyle.split(";");
let leftVal = left?.split(":")[1];
let topVal = top?.split(":")[1];
if (parseInt(leftVal || "") < 0) {
leftVal = "0";
}
if (parseInt(topVal || "") < 0) {
topVal = "0";
}
helpModeStyle.value = {
...helpModeStyle.value,
left: leftVal,
top: topVal,
};
}
}
);
onMounted(() => {
const links = (helpTextRef.value as unknown as HTMLElement).querySelectorAll("a");
links.forEach((link: HTMLAnchorElement) => {
link.setAttribute("target", "_blank");
});
});
// update store dimensions on resize
watch(
() => [helpModeSize.width, helpModeSize.height],
([newWidth, newHeight]) => {
if (newWidth && newHeight) {
helpModeStyle.value = {
...helpModeStyle.value,
width: `${newWidth}px`,
height: `${newHeight}px`,
};
}
}
);
/** Reset the position of the help mode to default */
function resetPosition() {
helpModeStyle.value = {
width: "25%",
height: "30%",
left: "0",
top: "0",
};
}
</script>

<template>
<div ref="el" :style="style" class="help-text unified-panel-body d-flex justify-content-between">
<div ref="helpModeHeader" class="header">
<div ref="el" :style="[style, helpModeStyle]" class="help-text justify-content-between">
<div ref="helpModeHeader" class="header unselectable">
<Heading h4 inline size="sm" class="flex-grow-1 mx-2">Galaxy Help Mode</Heading>
<BButton class="close-button" size="sm" @click="status = false">
<BButton size="sm" @click="resetPosition">
<FontAwesomeIcon :icon="faUndo" />
</BButton>
<BButton size="sm" @click="status = false">
<FontAwesomeIcon :icon="faTimes" />
</BButton>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="!loading" ref="helpTextRef" class="help-mode-container" v-html="helpText" />
<LoadingSpan v-else message="Loading help text" />
<span v-if="!activeTab" v-localize class="help-mode-container">{{ DEFAULT_HELP_TEXT }}</span>
<BTabs v-else class="help-mode-container">
<BTab v-for="helpId of currentTabs" :key="helpId" :active="activeTab === helpId">
<template v-slot:title>
<FontAwesomeIcon v-if="contents[helpId]?.icon" :icon="contents[helpId]?.icon" />
{{ contents[helpId]?.title }}
</template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="!loading" v-html="renderMarkdown(contents[helpId]?.content || noHelpTextMsg)" />
<LoadingSpan v-else message="Loading help text" />
</BTab>
</BTabs>
</div>
</template>

<style scoped lang="scss">
// TODO: Maybe use predefined variables for colors and sizes?
.help-text {
display: flex;
flex-direction: column;
width: 25% !important;
height: 30% !important;
z-index: 9999;
background-color: aliceblue;
border-color: black;
Expand All @@ -89,6 +125,8 @@ onMounted(() => {
border: solid;
opacity: 90%;
position: fixed;
resize: both;
overflow: auto;
}
.header {
display: flex;
Expand All @@ -98,7 +136,6 @@ onMounted(() => {
align-items: center;
border-bottom: 2px solid #868686;
cursor: move;
/* padding-bottom: 10px; */
}
.help-mode-container {
margin-top: 0;
Expand Down
56 changes: 54 additions & 2 deletions client/src/composables/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,67 @@ function addRuleHeadingIncreaseLevel(engine: MarkdownIt, increaseBy: number) {
};
}

/** Appends a horizontal rule to the end of each `<details>` element. */
function appendHrRuleToDetails(doc: Document) {
const details = doc.querySelectorAll("details");
details.forEach((detail) => {
// also, put a <hr class="w-100" /> inside, at the end of each open details tag
const hr = doc.createElement("hr");
hr.classList.add("w-100");
detail.appendChild(hr);
});
}

/** Replaces `<code>` elements with <i> elements for font-awesome icons. */
function replaceCodesWithIcons(doc: Document) {
const codes = doc.querySelectorAll("code");
codes.forEach((code) => {
const codeContent = code.innerHTML;
const iconRegex = /^fa-[a-z-]+$/;
if (iconRegex.test(codeContent)) {
const icon = document.createElement("i");
icon.classList.add("fas", codeContent);
code.replaceWith(icon);
}
});
}

/**
* Adjusts the markdown output based on the options that do not have rules in
* `engine.renderer.rules`.
*/
function adjustMdForOptions(markdown: string, options: UseMarkdownOptions) {
if (!options.appendHrRuleToDetails && !options.replaceCodeWithIcon) {
return markdown;
}

const parser = new DOMParser();
const doc = parser.parseFromString(markdown, "text/html");
if (options.appendHrRuleToDetails) {
appendHrRuleToDetails(doc);
}
if (options.replaceCodeWithIcon) {
replaceCodesWithIcons(doc);
}
const serializer = new XMLSerializer();
const modifiedMarkdownOutput = serializer.serializeToString(doc);
return modifiedMarkdownOutput;
}

interface UseMarkdownOptions {
openLinksInNewPage?: boolean;
increaseHeadingLevelBy?: number;
html?: boolean;
appendHrRuleToDetails?: boolean;
replaceCodeWithIcon?: boolean;
}

type RawMarkdown = string;
type HTMLString = string;

/** Composable for rendering Markdown strings. */
export function useMarkdown(options: UseMarkdownOptions = {}) {
const mdEngine = MarkdownIt();
const mdEngine = MarkdownIt({ html: options.html });

if (options.openLinksInNewPage) {
addRuleOpenLinksInNewPage(mdEngine);
Expand All @@ -90,7 +140,9 @@ export function useMarkdown(options: UseMarkdownOptions = {}) {
}

function renderMarkdown(markdown: RawMarkdown): HTMLString {
return mdEngine.render(markdown);
let renderedMarkdown = mdEngine.render(markdown);
renderedMarkdown = adjustMdForOptions(renderedMarkdown, options);
return renderedMarkdown;
}

return {
Expand Down
Loading

0 comments on commit 01b38cd

Please sign in to comment.