mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-23 14:06:59 +02:00
chore: Enable ESLint rules of hooks + fix new lint errors (#5714)
This commit is contained in:
parent
3db4fcaec7
commit
098f210890
16 changed files with 110 additions and 48 deletions
|
@ -28,6 +28,7 @@ module.exports = {
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
'airbnb',
|
'airbnb',
|
||||||
'prettier',
|
'prettier',
|
||||||
'prettier/react',
|
'prettier/react',
|
||||||
|
@ -41,6 +42,8 @@ module.exports = {
|
||||||
},
|
},
|
||||||
plugins: ['react-hooks', 'header'],
|
plugins: ['react-hooks', 'header'],
|
||||||
rules: {
|
rules: {
|
||||||
|
'react-hooks/rules-of-hooks': ERROR,
|
||||||
|
'react-hooks/exhaustive-deps': ERROR,
|
||||||
'class-methods-use-this': OFF, // It's a way of allowing private variables.
|
'class-methods-use-this': OFF, // It's a way of allowing private variables.
|
||||||
'func-names': OFF,
|
'func-names': OFF,
|
||||||
// Ignore certain webpack alias because it can't be resolved
|
// Ignore certain webpack alias because it can't be resolved
|
||||||
|
@ -77,7 +80,6 @@ module.exports = {
|
||||||
'react/destructuring-assignment': OFF, // Too many lines.
|
'react/destructuring-assignment': OFF, // Too many lines.
|
||||||
'react/prefer-stateless-function': WARNING,
|
'react/prefer-stateless-function': WARNING,
|
||||||
'react/jsx-props-no-spreading': OFF,
|
'react/jsx-props-no-spreading': OFF,
|
||||||
'react-hooks/rules-of-hooks': ERROR,
|
|
||||||
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
|
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
|
||||||
'@typescript-eslint/no-inferrable-types': OFF,
|
'@typescript-eslint/no-inferrable-types': OFF,
|
||||||
'import/first': OFF,
|
'import/first': OFF,
|
||||||
|
|
|
@ -51,7 +51,7 @@ function DocPageContent({
|
||||||
setHiddenSidebar(false);
|
setHiddenSidebar(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHiddenSidebarContainer(!hiddenSidebarContainer);
|
setHiddenSidebarContainer((value) => !value);
|
||||||
}, [hiddenSidebar]);
|
}, [hiddenSidebar]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -93,7 +93,7 @@ function useAutoExpandActiveCategory({
|
||||||
if (justBecameActive && collapsed) {
|
if (justBecameActive && collapsed) {
|
||||||
setCollapsed(false);
|
setCollapsed(false);
|
||||||
}
|
}
|
||||||
}, [isActive, wasActive, collapsed]);
|
}, [isActive, wasActive, collapsed, setCollapsed]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocSidebarItemCategory({
|
function DocSidebarItemCategory({
|
||||||
|
|
|
@ -141,7 +141,7 @@ function DropdownNavbarItemMobile({
|
||||||
if (containsActive) {
|
if (containsActive) {
|
||||||
setCollapsed(!containsActive);
|
setCollapsed(!containsActive);
|
||||||
}
|
}
|
||||||
}, [localPathname, containsActive]);
|
}, [localPathname, containsActive, setCollapsed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -68,7 +68,7 @@ const useTheme = (): useThemeReturns => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}, [setTheme]);
|
}, [disableSwitch, setTheme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableSwitch && !respectPrefersColorScheme) {
|
if (disableSwitch && !respectPrefersColorScheme) {
|
||||||
|
@ -80,7 +80,7 @@ const useTheme = (): useThemeReturns => {
|
||||||
.addListener(({matches}) => {
|
.addListener(({matches}) => {
|
||||||
setTheme(matches ? themes.dark : themes.light);
|
setTheme(matches ? themes.dark : themes.light);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [disableSwitch, respectPrefersColorScheme]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDarkTheme: theme === themes.dark,
|
isDarkTheme: theme === themes.dark,
|
||||||
|
|
|
@ -88,3 +88,8 @@ export {
|
||||||
useScrollPosition,
|
useScrollPosition,
|
||||||
useScrollPositionBlocker,
|
useScrollPositionBlocker,
|
||||||
} from './utils/scrollUtils';
|
} from './utils/scrollUtils';
|
||||||
|
|
||||||
|
export {
|
||||||
|
useIsomorphicLayoutEffect,
|
||||||
|
useDynamicCallback,
|
||||||
|
} from './utils/reactUtils';
|
||||||
|
|
|
@ -83,14 +83,14 @@ const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
|
||||||
if (isNewAnnouncement || !isDismissedInStorage()) {
|
if (isNewAnnouncement || !isDismissedInStorage()) {
|
||||||
setClosed(false);
|
setClosed(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [announcementBar]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
isActive: !!announcementBar && !isClosed,
|
isActive: !!announcementBar && !isClosed,
|
||||||
close: handleClose,
|
close: handleClose,
|
||||||
};
|
};
|
||||||
}, [isClosed]);
|
}, [announcementBar, isClosed, handleClose]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnnouncementBarContext = createContext<AnnouncementBarAPI | null>(null);
|
const AnnouncementBarContext = createContext<AnnouncementBarAPI | null>(null);
|
||||||
|
|
|
@ -120,7 +120,7 @@ function useContextValue() {
|
||||||
return {
|
return {
|
||||||
savePreferredVersion,
|
savePreferredVersion,
|
||||||
};
|
};
|
||||||
}, [setState]);
|
}, [versionPersistence]);
|
||||||
|
|
||||||
return [state, api] as const;
|
return [state, api] as const;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export function useDocsPreferredVersion(
|
||||||
(versionName: string) => {
|
(versionName: string) => {
|
||||||
api.savePreferredVersion(pluginId, versionName);
|
api.savePreferredVersion(pluginId, versionName);
|
||||||
},
|
},
|
||||||
[api],
|
[api, pluginId],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {preferredVersion, savePreferredVersionName} as const;
|
return {preferredVersion, savePreferredVersionName} as const;
|
||||||
|
|
|
@ -83,6 +83,7 @@ function useShallowMemoizedObject<O extends Record<string, unknown>>(obj: O) {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => obj,
|
() => obj,
|
||||||
// Is this safe?
|
// Is this safe?
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[...Object.keys(obj), ...Object.values(obj)],
|
[...Object.keys(obj), ...Object.values(obj)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
34
packages/docusaurus-theme-common/src/utils/reactUtils.tsx
Normal file
34
packages/docusaurus-theme-common/src/utils/reactUtils.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
|
||||||
|
|
||||||
|
// This hook is like useLayoutEffect, but without the SSR warning
|
||||||
|
// It seems hacky but it's used in many React libs (Redux, Formik...)
|
||||||
|
// Also mentioned here: https://github.com/facebook/react/issues/16956
|
||||||
|
// It is useful when you need to update a ref as soon as possible after a React render (before useEffect)
|
||||||
|
export const useIsomorphicLayoutEffect =
|
||||||
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
// Permits to transform an unstable callback (like an arrow function provided as props)
|
||||||
|
// to a "stable" callback that is safe to use in a useEffect dependency array
|
||||||
|
// Useful to avoid React stale closure problems + avoid useless effect re-executions
|
||||||
|
//
|
||||||
|
// Workaround until the React team recommends a good solution, see https://github.com/facebook/react/issues/16956
|
||||||
|
// This generally works has some potential drawbacks, such as https://github.com/facebook/react/issues/16956#issuecomment-536636418
|
||||||
|
export function useDynamicCallback<T extends (...args: never[]) => unknown>(
|
||||||
|
callback: T,
|
||||||
|
): T {
|
||||||
|
const ref = useRef<T>(callback);
|
||||||
|
|
||||||
|
useIsomorphicLayoutEffect(() => {
|
||||||
|
ref.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
// @ts-expect-error: TODO, not sure how to fix this TS error
|
||||||
|
return useCallback<T>((...args) => ref.current(...args), []);
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import React, {
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import {useDynamicCallback} from './reactUtils';
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -103,20 +104,22 @@ export function useScrollPosition(
|
||||||
const {scrollEventsEnabledRef} = useScrollController();
|
const {scrollEventsEnabledRef} = useScrollController();
|
||||||
const lastPositionRef = useRef<ScrollPosition | null>(getScrollPosition());
|
const lastPositionRef = useRef<ScrollPosition | null>(getScrollPosition());
|
||||||
|
|
||||||
|
const dynamicEffect = useDynamicCallback(effect);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollEventsEnabledRef.current) {
|
if (!scrollEventsEnabledRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentPosition = getScrollPosition()!;
|
const currentPosition = getScrollPosition()!;
|
||||||
|
|
||||||
if (effect) {
|
if (dynamicEffect) {
|
||||||
effect(currentPosition, lastPositionRef.current);
|
dynamicEffect(currentPosition, lastPositionRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPositionRef.current = currentPosition;
|
lastPositionRef.current = currentPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const opts: AddEventListenerOptions & EventListenerOptions = {
|
const opts: AddEventListenerOptions & EventListenerOptions = {
|
||||||
passive: true,
|
passive: true,
|
||||||
};
|
};
|
||||||
|
@ -125,7 +128,12 @@ export function useScrollPosition(
|
||||||
window.addEventListener('scroll', handleScroll, opts);
|
window.addEventListener('scroll', handleScroll, opts);
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll, opts);
|
return () => window.removeEventListener('scroll', handleScroll, opts);
|
||||||
}, deps);
|
}, [
|
||||||
|
dynamicEffect,
|
||||||
|
scrollEventsEnabledRef,
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
...deps,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseScrollPositionSaver = {
|
type UseScrollPositionSaver = {
|
||||||
|
@ -170,7 +178,7 @@ function useScrollPositionSaver(): UseScrollPositionSaver {
|
||||||
return {restored: heightDiff !== 0};
|
return {restored: heightDiff !== 0};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return useMemo(() => ({save, restore}), []);
|
return useMemo(() => ({save, restore}), [restore, save]);
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseScrollPositionBlockerReturn = {
|
type UseScrollPositionBlockerReturn = {
|
||||||
|
@ -217,7 +225,7 @@ export function useScrollPositionBlocker(): UseScrollPositionBlockerReturn {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[scrollController],
|
[scrollController, scrollPositionSaver],
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {useEffect} from 'react';
|
||||||
import {useLocation} from '@docusaurus/router';
|
import {useLocation} from '@docusaurus/router';
|
||||||
import {Location} from '@docusaurus/history';
|
import {Location} from '@docusaurus/history';
|
||||||
import {usePrevious} from './usePrevious';
|
import {usePrevious} from './usePrevious';
|
||||||
|
import {useDynamicCallback} from './reactUtils';
|
||||||
|
|
||||||
type LocationChangeEvent = {
|
type LocationChangeEvent = {
|
||||||
location: Location;
|
location: Location;
|
||||||
|
@ -21,10 +22,12 @@ export function useLocationChange(onLocationChange: OnLocationChange): void {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const previousLocation = usePrevious(location);
|
const previousLocation = usePrevious(location);
|
||||||
|
|
||||||
|
const onLocationChangeDynamic = useDynamicCallback(onLocationChange);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLocationChange({
|
onLocationChangeDynamic({
|
||||||
location,
|
location,
|
||||||
previousLocation,
|
previousLocation,
|
||||||
});
|
});
|
||||||
}, [location]);
|
}, [onLocationChangeDynamic, location, previousLocation]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useRef, useEffect} from 'react';
|
import {useRef} from 'react';
|
||||||
|
import {useIsomorphicLayoutEffect} from './reactUtils';
|
||||||
|
|
||||||
export function usePrevious<T>(value: T): T | undefined {
|
export function usePrevious<T>(value: T): T | undefined {
|
||||||
const ref = useRef<T>();
|
const ref = useRef<T>();
|
||||||
|
|
||||||
useEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
ref.current = value;
|
ref.current = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,11 @@ import clsx from 'clsx';
|
||||||
import Head from '@docusaurus/Head';
|
import Head from '@docusaurus/Head';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
import {useTitleFormatter, usePluralForm} from '@docusaurus/theme-common';
|
import {
|
||||||
|
useTitleFormatter,
|
||||||
|
usePluralForm,
|
||||||
|
useDynamicCallback,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import {useAllDocsData} from '@theme/hooks/useDocs';
|
import {useAllDocsData} from '@theme/hooks/useDocs';
|
||||||
import useSearchQuery from '@theme/hooks/useSearchQuery';
|
import useSearchQuery from '@theme/hooks/useSearchQuery';
|
||||||
|
@ -173,6 +177,7 @@ function SearchPage() {
|
||||||
},
|
},
|
||||||
initialSearchResultState,
|
initialSearchResultState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const algoliaClient = algoliaSearch(appId, apiKey);
|
const algoliaClient = algoliaSearch(appId, apiKey);
|
||||||
const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
|
const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
|
||||||
hitsPerPage: 15,
|
hitsPerPage: 15,
|
||||||
|
@ -271,7 +276,7 @@ function SearchPage() {
|
||||||
description: 'The search page title for empty query',
|
description: 'The search page title for empty query',
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeSearch = (page = 0) => {
|
const makeSearch = useDynamicCallback((page = 0) => {
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
|
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
|
||||||
|
|
||||||
|
@ -285,18 +290,16 @@ function SearchPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
algoliaHelper.setQuery(searchQuery).setPage(page).search();
|
algoliaHelper.setQuery(searchQuery).setPage(page).search();
|
||||||
};
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loaderRef) {
|
if (!loaderRef) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const currentObserver = observer.current;
|
||||||
|
|
||||||
observer.current.observe(loaderRef);
|
currentObserver.observe(loaderRef);
|
||||||
|
return () => currentObserver.unobserve(loaderRef);
|
||||||
return () => {
|
|
||||||
observer.current.unobserve(loaderRef);
|
|
||||||
};
|
|
||||||
}, [loaderRef]);
|
}, [loaderRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -311,7 +314,12 @@ function SearchPage() {
|
||||||
makeSearch();
|
makeSearch();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}, [searchQuery, docsSearchVersionsHelpers.searchVersions]);
|
}, [
|
||||||
|
searchQuery,
|
||||||
|
docsSearchVersionsHelpers.searchVersions,
|
||||||
|
updateSearchPath,
|
||||||
|
makeSearch,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
|
if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
|
||||||
|
@ -319,13 +327,13 @@ function SearchPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
makeSearch(searchResultState.lastPage);
|
makeSearch(searchResultState.lastPage);
|
||||||
}, [searchResultState.lastPage]);
|
}, [makeSearch, searchResultState.lastPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchValue && searchValue !== searchQuery) {
|
if (searchValue && searchValue !== searchQuery) {
|
||||||
setSearchQuery(searchValue);
|
setSearchQuery(searchValue);
|
||||||
}
|
}
|
||||||
}, [searchValue]);
|
}, [searchQuery, searchValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout wrapperClassName="search-page-wrapper">
|
<Layout wrapperClassName="search-page-wrapper">
|
||||||
|
|
|
@ -90,16 +90,16 @@ function Link({
|
||||||
|
|
||||||
const IOSupported = ExecutionEnvironment.canUseIntersectionObserver;
|
const IOSupported = ExecutionEnvironment.canUseIntersectionObserver;
|
||||||
|
|
||||||
let io: IntersectionObserver;
|
const ioRef = useRef<IntersectionObserver>();
|
||||||
const handleIntersection = (el: HTMLAnchorElement, cb: () => void) => {
|
const handleIntersection = (el: HTMLAnchorElement, cb: () => void) => {
|
||||||
io = new window.IntersectionObserver((entries) => {
|
ioRef.current = new window.IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (el === entry.target) {
|
if (el === entry.target) {
|
||||||
// If element is in viewport, stop listening/observing and run callback.
|
// If element is in viewport, stop listening/observing and run callback.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
||||||
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
||||||
io.unobserve(el);
|
ioRef.current!.unobserve(el);
|
||||||
io.disconnect();
|
ioRef.current!.disconnect();
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ function Link({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add element to the observer.
|
// Add element to the observer.
|
||||||
io.observe(el);
|
ioRef.current!.observe(el);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRef = (ref: HTMLAnchorElement | null) => {
|
const handleRef = (ref: HTMLAnchorElement | null) => {
|
||||||
|
@ -138,11 +138,11 @@ function Link({
|
||||||
|
|
||||||
// When unmounting, stop intersection observer from watching.
|
// When unmounting, stop intersection observer from watching.
|
||||||
return () => {
|
return () => {
|
||||||
if (IOSupported && io) {
|
if (IOSupported && ioRef.current) {
|
||||||
io.disconnect();
|
ioRef.current.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [targetLink, IOSupported, isInternal]);
|
}, [ioRef, targetLink, IOSupported, isInternal]);
|
||||||
|
|
||||||
const isAnchorLink = targetLink?.startsWith('#') ?? false;
|
const isAnchorLink = targetLink?.startsWith('#') ?? false;
|
||||||
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
|
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue