-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #575 Still needs more testing before ready. Will _not_ hook up the node search using this because that is more complicated and manual. Can add the LZW text-annotator sync to this PR too if you want. - make "replace" param of link component manual - split composables into their own files and folder - add generic use-param composable that creates a two-way synced ref with a url param - hook up use-param to phenotype explorer --------- Co-authored-by: glass-ships <[email protected]>
- Loading branch information
1 parent
eb35fd0
commit 25f4d33
Showing
29 changed files
with
373 additions
and
206 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { CSSProperties, Ref } from "vue"; | ||
import { ref } from "vue"; | ||
import { debounce } from "lodash"; | ||
import { computePosition, flip, shift, size } from "@floating-ui/dom"; | ||
import { useEventListener } from "@vueuse/core"; | ||
|
||
/** use floating-ui to position dropdown */ | ||
|
||
export const useFloating = ( | ||
anchor: Ref<HTMLElement | undefined>, | ||
dropdown: Ref<HTMLElement | undefined>, | ||
fit = false, | ||
): { calculate: () => Promise<void>; style: Ref<CSSProperties> } => { | ||
/** style of dropdown */ | ||
const style = ref<CSSProperties>({ | ||
position: "absolute", | ||
left: "0px", | ||
top: "0px", | ||
minWidth: "0px", | ||
}); | ||
|
||
/** floating-ui options */ | ||
const options = { | ||
middleware: [ | ||
flip(), | ||
shift({ padding: 5 }), | ||
size({ | ||
/** update min width based on target width */ | ||
apply: ({ rects }) => { | ||
if (fit) style.value.width = rects.reference.width + "px"; | ||
else style.value.minWidth = rects.reference.width + "px"; | ||
}, | ||
}), | ||
], | ||
}; | ||
|
||
/** func to recompute position on command */ | ||
async function calculate() { | ||
/** make sure we have needed element references */ | ||
if (!anchor.value || !dropdown.value) return; | ||
|
||
/** use floating-ui to compute position of dropdown */ | ||
const { x, y } = await computePosition( | ||
anchor.value, | ||
dropdown.value, | ||
options, | ||
); | ||
|
||
/** set style from position */ | ||
style.value.left = x + "px"; | ||
style.value.top = y + "px"; | ||
} | ||
|
||
/** automatically run calculate on reflow events */ | ||
const debounced = debounce(calculate, 100); | ||
useEventListener(window, "scroll", debounced); | ||
useEventListener(window, "resize", debounced); | ||
|
||
return { calculate, style }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { ref, shallowRef, watch } from "vue"; | ||
import { type Router } from "vue-router"; | ||
import { isEqual, round } from "lodash"; | ||
|
||
/** | ||
* single "reactive" object url to sync with "raw" url. needed so we can batch | ||
* together multiple synchronous param/hash sets into a single push/replace | ||
* history entry. vue-router does batch together multiple synchronous | ||
* pushes/replaces, but does not accommodate keeping/merging params, because | ||
* currentRoute object only updates at end of batch. | ||
*/ | ||
export const url = ref<{ [key: string]: string }>({}); | ||
|
||
/** connect to router */ | ||
export function initRouter(router: Router) { | ||
/** flag to check if router.push/replace just occurred */ | ||
let justPushed = false; | ||
|
||
/** when reactive url changes */ | ||
watch( | ||
url, | ||
() => { | ||
const from = router.currentRoute.value; | ||
/** update url to include params. preserve hash. */ | ||
const to = { query: url.value, hash: from.hash }; | ||
|
||
if (!isEqual(url.value, router.currentRoute.value.query)) { | ||
/** push/replace based on mode */ | ||
router[mode === "replace" ? "replace" : "push"](to); | ||
justPushed = true; | ||
} | ||
}, | ||
{ deep: true, immediate: true }, | ||
); | ||
|
||
/** after raw url changes */ | ||
router.afterEach((to) => { | ||
/** | ||
* if raw url just changed due to url object change, don't change url object | ||
* again | ||
*/ | ||
if (justPushed) { | ||
justPushed = false; | ||
return; | ||
} | ||
|
||
/** delete params in url object */ | ||
for (const key of Object.keys(url.value)) | ||
if (!to.query[key]) delete url.value[key]; | ||
|
||
/** add/change params in url object */ | ||
for (const [key, value] of Object.entries(to.query)) | ||
if (typeof value === "string") url.value[key] = value; | ||
}); | ||
} | ||
|
||
/** how to update tab history */ | ||
const mode: "push" | "replace" = "push"; | ||
|
||
/** reactive variable 2-way-synced with param in url */ | ||
export function useParam<T>( | ||
key: string, | ||
{ parse, stringify }: Param<T>, | ||
initialValue: T, | ||
) { | ||
/** https://github.com/vuejs/composition-api/issues/483 */ | ||
/** reactive "local" var that should act like slice of reactive "full" url */ | ||
const variable = shallowRef(initialValue); | ||
|
||
watch( | ||
/** when param in full url object changes */ | ||
() => url.value[key], | ||
() => { | ||
/** | ||
* get updated value from url object. if key no longer there (e.g. user | ||
* went back to url without it), reset to initial value. | ||
*/ | ||
const value = url.value[key] || stringify(initialValue); | ||
/** | ||
* stringify process is sometimes "lossy" (e.g. rounding decimal places), | ||
* so compare values after that process | ||
*/ | ||
if (value === stringify(variable.value)) return; | ||
/** convert raw url string value to actual var value */ | ||
variable.value = parse(value); | ||
}, | ||
{ immediate: true }, | ||
); | ||
|
||
watch( | ||
/** when local var changes */ | ||
variable, | ||
() => { | ||
/** convert actual var value to string for url */ | ||
const value = stringify(variable.value); | ||
/** set var value */ | ||
if (value) url.value[key] = value; | ||
/** if "empty", such as empty string or array, delete param from url */ else | ||
delete url.value[key]; | ||
}, | ||
/** | ||
* no "immediate", so url doesn't update until var changes from initial | ||
* value (presumably from user interaction) | ||
*/ | ||
); | ||
|
||
return variable; | ||
} | ||
|
||
/** generic parameter type, with methods to encode/decode to/from string (url) */ | ||
export type Param<T> = { | ||
parse: (value: string) => T; | ||
stringify: (value: T) => string; | ||
}; | ||
|
||
/** treat param as string */ | ||
export const stringParam = (): Param<string> => ({ | ||
parse: (value) => value, | ||
stringify: (value) => value, | ||
}); | ||
|
||
/** treat param as number */ | ||
export const numberParam = (precision?: number): Param<number> => ({ | ||
parse: (value) => Number(value) || 0, | ||
stringify: (value) => String(round(value || 0, precision)), | ||
}); | ||
|
||
/** treat param as boolean */ | ||
export const booleanParam = ( | ||
type: "number" | "string" = "number", | ||
): Param<boolean> => ({ | ||
parse: (value) => | ||
value.toLowerCase() === (type === "string" ? "true" : "1") ? true : false, | ||
stringify: (value) => (type === "string" ? String(value) : value ? "1" : "0"), | ||
}); | ||
|
||
/** higher-order array param of other params */ | ||
export const arrayParam = <T>(param: Param<T>): Param<T[]> => ({ | ||
parse: (value) => value.split(",").map(param.parse), | ||
stringify: (value) => value.map(param.stringify).join(","), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import type { Ref } from "vue"; | ||
import { ref } from "vue"; | ||
|
||
/** | ||
* inspired by tanstack-query. simple query manager/wrapper for making queries | ||
* in components. reduces repetitive boilerplate code for loading/error states, | ||
* try/catch blocks, de-duplicating requests, etc. | ||
*/ | ||
export const useQuery = <Data, Args extends unknown[]>( | ||
/** | ||
* main async func that returns data. should be side-effect free to avoid race | ||
* conditions, because multiple can be running at same time. | ||
*/ | ||
func: (...args: Args) => Promise<Data>, | ||
/** default value used for data before done loading and on error. */ | ||
defaultValue: Data, | ||
/** | ||
* func to run on success. use for side effects. only gets called on latest of | ||
* concurrent runs. | ||
*/ | ||
onSuccess?: ( | ||
/** response data */ | ||
response: Data, | ||
/** props passed to main func */ | ||
props: Args, | ||
) => void, | ||
) => { | ||
/** query state/status */ | ||
const isLoading = ref(false); | ||
const isError = ref(false); | ||
const isSuccess = ref(false); | ||
|
||
/** query results */ | ||
const data = ref<Data>(defaultValue) as Ref<Data>; | ||
/** https://github.com/vuejs/composition-api/issues/483 */ | ||
|
||
/** latest query id, unique to this useQuery instance */ | ||
let latest; | ||
|
||
/** wrapped query function */ | ||
async function query(...args: Args): Promise<void> { | ||
try { | ||
/** unique id for current run */ | ||
const current = Symbol(); | ||
latest = current; | ||
|
||
/** reset state */ | ||
isLoading.value = true; | ||
isError.value = false; | ||
isSuccess.value = false; | ||
data.value = defaultValue; | ||
|
||
/** run provided function */ | ||
const result = await func(...args); | ||
|
||
/** if this run still the latest */ | ||
if (current === latest) { | ||
/** assign results to data */ | ||
data.value = result; | ||
|
||
/** update state */ | ||
isLoading.value = false; | ||
isSuccess.value = true; | ||
|
||
/** on success callback */ | ||
if (onSuccess) onSuccess(result, args); | ||
} else { | ||
/** otherwise, log special "stale" error */ | ||
console.error("Stale query"); | ||
} | ||
} catch (error) { | ||
/** log error */ | ||
console.error(error); | ||
|
||
/** update state */ | ||
isError.value = true; | ||
isLoading.value = false; | ||
} | ||
} | ||
|
||
return { query, data, isLoading, isError, isSuccess }; | ||
}; |
Oops, something went wrong.