Refactor to enforce strictNullChecks

This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
This commit is contained in:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -7,22 +7,18 @@
<TheCodeButtons class="app__row app__code-buttons" />
<TheFooter />
</div>
<OptionalDevToolkit />
<component v-if="devToolkitComponent" :is="devToolkitComponent" />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { defineAsyncComponent, defineComponent, Component } from 'vue';
import TheHeader from '@/presentation/components/TheHeader.vue';
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
: null;
export default defineComponent({
components: {
TheHeader,
@@ -30,10 +26,23 @@ export default defineComponent({
TheScriptArea,
TheSearchBar,
TheFooter,
OptionalDevToolkit,
},
setup() { },
setup() {
const devToolkitComponent = getOptionalDevToolkitComponent();
return {
devToolkitComponent,
};
},
});
function getOptionalDevToolkitComponent(): Component | undefined {
const isDevelopment = process.env.NODE_ENV !== 'production';
if (!isDevelopment) {
return undefined;
}
return defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'));
}
</script>
<style lang="scss">

View File

@@ -40,7 +40,7 @@ export default defineComponent({
function getCanRunState(
selectedOs: OperatingSystem,
isDesktopVersion: boolean,
hostOs: OperatingSystem,
hostOs: OperatingSystem | undefined,
): boolean {
const isRunningOnSelectedOs = selectedOs === hostOs;
return isDesktopVersion && isRunningOnSelectedOs;

View File

@@ -21,11 +21,10 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem';
import IconButton from '../IconButton.vue';
import InstructionList from './Instructions/InstructionList.vue';
import { IInstructionListData } from './Instructions/InstructionListData';
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
import { getInstructions } from './Instructions/InstructionListDataFactory';
export default defineComponent({
components: {
@@ -39,7 +38,7 @@ export default defineComponent({
const areInstructionsVisible = ref(false);
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
currentState.value.collection.os,
fileName.value,
));
@@ -59,16 +58,6 @@ export default defineComponent({
},
});
function getDownloadInstructions(
os: OperatingSystem,
fileName: string,
): IInstructionListData | undefined {
if (!hasInstructions(os)) {
return undefined;
}
return getInstructions(os, fileName);
}
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
const content = state.code.current;
const type = getType(state.collection.scripting.language);

View File

@@ -15,13 +15,11 @@ export class InstructionsBuilder {
}
public withStep(stepBuilder: InstructionStepBuilderType) {
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
this.stepBuilders.push(stepBuilder);
return this;
}
public build(data: IInstructionsBuilderData): IInstructionListData {
if (!data) { throw new Error('missing data'); }
return {
operatingSystem: this.os,
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),

View File

@@ -86,12 +86,9 @@ export default defineComponent({
() => info.getDownloadUrl(OperatingSystem.macOS),
);
const osName = computed<string>(() => {
if (!props.data) {
throw new Error('missing data');
}
return renderOsName(props.data.operatingSystem);
});
const osName = computed<string>(
() => renderOsName(props.data.operatingSystem),
);
return {
appName,

View File

@@ -9,15 +9,11 @@ const builders = new Map<OperatingSystem, InstructionsBuilder>([
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
]);
export function hasInstructions(os: OperatingSystem) {
return builders.has(os);
}
export function getInstructions(
os: OperatingSystem,
fileName: string,
): IInstructionListData {
): IInstructionListData | undefined {
return builders
.get(os)
.build({ fileName });
?.build({ fileName });
}

View File

@@ -70,7 +70,7 @@ export default defineComponent({
function updateCode(code: string, language: ScriptingLanguage) {
const innerCode = code || getDefaultCode(language);
editor.setValue(innerCode, 1);
editor?.setValue(innerCode, 1);
}
function handleCodeChange(event: ICodeChangedEvent) {
@@ -96,7 +96,7 @@ export default defineComponent({
if (!currentMarkerId) {
return;
}
editor.session.removeMarker(currentMarkerId);
editor?.session.removeMarker(currentMarkerId);
currentMarkerId = undefined;
}
@@ -115,7 +115,7 @@ export default defineComponent({
function highlight(startRow: number, endRow: number) {
const AceRange = ace.require('ace/range').Range;
currentMarkerId = editor.session.addMarker(
currentMarkerId = editor?.session.addMarker(
new AceRange(startRow, 0, endRow, 0),
'code-area__highlight',
'fullLine',
@@ -123,8 +123,11 @@ export default defineComponent({
}
function scrollToLine(row: number) {
const column = editor.session.getLine(row).length;
editor.gotoLine(row, column, true);
const column = editor?.session.getLine(row).length;
if (column === undefined) {
return;
}
editor?.gotoLine(row, column, true);
}
return {

View File

@@ -15,9 +15,12 @@ export enum SelectionType {
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
if (type === SelectionType.Custom) {
throw new Error('cannot select custom type');
throw new Error('Cannot select custom type.');
}
const selector = selectors.get(type);
if (!selector) {
throw new Error(`Cannot handle the type: ${SelectionType[type]}`);
}
selector.select(context);
}

View File

@@ -31,14 +31,14 @@ export default defineComponent({
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
const { application } = injectKey((keys) => keys.useApplication);
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
application.getSupportedOsList() ?? [])
.map((os) : IOsViewModel => (
{
const allOses = computed<ReadonlyArray<IOsViewModel>>(
() => application
.getSupportedOsList()
.map((os) : IOsViewModel => ({
os,
name: renderOsName(os),
}
)));
})),
);
const currentOs = computed<OperatingSystem>(() => {
return currentState.value.os;

View File

@@ -48,8 +48,12 @@ export default defineComponent({
const firstElement = shallowRef<HTMLElement>();
function onResize(displacementX: number): void {
const leftWidth = firstElement.value.offsetWidth + displacementX;
firstElement.value.style.width = `${leftWidth}px`;
const element = firstElement.value;
if (!element) {
throw new Error('The element reference ref is not correctly assigned to a DOM element.');
}
const leftWidth = element.offsetWidth + displacementX;
element.style.width = `${leftWidth}px`;
}
return {

View File

@@ -29,7 +29,10 @@ export default defineComponent({
const cursorCssValue = 'ew-resize';
let initialX: number | undefined;
const resize = (event) => {
const resize = (event: MouseEvent) => {
if (initialX === undefined) {
throw new Error('Resize action started without an initial X coordinate.');
}
const displacementX = event.clientX - initialX;
emit('resized', displacementX);
initialX = event.clientX;

View File

@@ -10,7 +10,7 @@
</div>
-->
<div
v-if="categoryIds != null && categoryIds.length > 0"
v-if="categoryIds.length > 0"
class="cards"
>
<CardListItem
@@ -50,8 +50,9 @@ export default defineComponent({
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
const width = ref<number>(0);
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
.value.collection.actions.map((category) => category.id));
const categoryIds = computed<readonly number[]>(
() => currentState.value.collection.actions.map((category) => category.id),
);
const activeCategoryId = ref<number | undefined>(undefined);
function onSelected(categoryId: number, isExpanded: boolean) {

View File

@@ -89,12 +89,9 @@ export default defineComponent({
const cardElement = shallowRef<HTMLElement>();
const cardTitle = computed<string | undefined>(() => {
if (!props.categoryId || !currentState.value) {
return undefined;
}
const category = currentState.value.collection.findCategory(props.categoryId);
return category?.name;
const cardTitle = computed<string>(() => {
const category = currentState.value.collection.getCategory(props.categoryId);
return category.name;
});
function collapse() {
@@ -102,8 +99,12 @@ export default defineComponent({
}
async function scrollToCard() {
const card = cardElement.value;
if (!card) {
throw new Error('Card is not found');
}
await sleep(400); // wait a bit to allow GUI to render the expanded card
cardElement.value.scrollIntoView({ behavior: 'smooth' });
card.scrollIntoView({ behavior: 'smooth' });
}
return {

View File

@@ -34,7 +34,7 @@ export default defineComponent({
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
const currentCategory = computed<ICategory>(
() => currentCollection.value.findCategory(props.categoryId),
() => currentCollection.value.getCategory(props.categoryId),
);
const isAnyChildSelected = computed<boolean>(

View File

@@ -71,6 +71,9 @@ export default defineComponent({
const searchHasMatches = ref(false);
const trimmedSearchQuery = computed(() => {
const query = searchQuery.value;
if (!query) {
return '';
}
const threshold = 30;
if (query.length <= threshold - 3) {
return query;
@@ -94,7 +97,7 @@ export default defineComponent({
function updateFromInitialFilter(filter?: IFilterResult) {
searchQuery.value = filter?.query;
searchHasMatches.value = filter?.hasAnyMatches();
searchHasMatches.value = filter?.hasAnyMatches() ?? false;
}
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {

View File

@@ -42,7 +42,7 @@ function renderText(docs: readonly string[] | undefined): string {
}
function renderAsMarkdownListItem(content: string): string {
if (!content || content.length === 0) {
if (content.length === 0) {
throw new Error('missing content');
}
const lines = content.split(/\r\n|\r|\n/);

View File

@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
if (parts.length === 1) {
return value;
}
if (parts.at(-1).length > 9) {
const extension = parts[parts.length - 1];
if (extension.length > 9) {
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
}
return parts.slice(0, -1).join('.');
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
}
function isGoodPathPart(part: string): boolean {
return part
return part.length > 2 // This is often non-human categories like T5 etc.
&& !isDigit(part) // E.g. article numbers, issue numbers
&& part.length > 2 // This is often non-human categories like T5 etc.
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID

View File

@@ -8,6 +8,6 @@ export interface NodeMetadata {
readonly text: string;
readonly isReversible: boolean;
readonly docs: ReadonlyArray<string>;
readonly children?: ReadonlyArray<NodeMetadata>;
readonly children: ReadonlyArray<NodeMetadata>;
readonly type: NodeType;
}

View File

@@ -25,10 +25,7 @@ export class CategoryReverter implements IReverter {
}
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
const category = collection.findCategory(categoryId);
if (!category) {
throw new Error(`Category with id "${categoryId}" does not exist`);
}
const category = collection.getCategory(categoryId);
const scripts = category.getAllScriptsRecursively();
return scripts.map((script) => new ScriptReverter(script.id));
}

View File

@@ -30,7 +30,7 @@ import { useSelectedScriptNodeIds } from './TreeViewAdapter/UseSelectedScriptNod
export default defineComponent({
props: {
categoryId: {
type: [Number, undefined],
type: [Number],
default: undefined,
},
},

View File

@@ -1,9 +1,15 @@
import type { ReadOnlyTreeNode } from '../Node/TreeNode';
export interface TreeViewFilterEvent {
readonly action: TreeViewFilterAction;
readonly predicate?: TreeViewFilterPredicate;
}
type TreeViewFilterTriggeredEvent = {
readonly action: TreeViewFilterAction.Triggered;
readonly predicate: TreeViewFilterPredicate;
};
type TreeViewFilterRemovedEvent = {
readonly action: TreeViewFilterAction.Removed;
};
export type TreeViewFilterEvent = TreeViewFilterTriggeredEvent | TreeViewFilterRemovedEvent;
export enum TreeViewFilterAction {
Triggered,
@@ -14,14 +20,14 @@ export type TreeViewFilterPredicate = (node: ReadOnlyTreeNode) => boolean;
export function createFilterTriggeredEvent(
predicate: TreeViewFilterPredicate,
): TreeViewFilterEvent {
): TreeViewFilterTriggeredEvent {
return {
action: TreeViewFilterAction.Triggered,
predicate,
};
}
export function createFilterRemovedEvent(): TreeViewFilterEvent {
export function createFilterRemovedEvent(): TreeViewFilterRemovedEvent {
return {
action: TreeViewFilterAction.Removed,
};

View File

@@ -25,7 +25,7 @@ export class TreeNodeHierarchy implements HierarchyAccess {
this.children = children;
}
public setParent(parent: TreeNode): void {
public setParent(parent: TreeNode | undefined): void {
this.parent = parent;
}
}

View File

@@ -5,9 +5,6 @@ import { TreeNodeManager } from '../../Node/TreeNodeManager';
export function parseTreeInput(
input: readonly TreeInputNodeData[],
): TreeNode[] {
if (!input) {
throw new Error('missing input');
}
if (!Array.isArray(input)) {
throw new Error('input data must be an array');
}

View File

@@ -11,7 +11,7 @@ export class TreeNodeInitializerAndUpdater implements TreeNodeCollection {
public nodesUpdated = new EventSource<QueryableNodes>();
public updateRootNodes(rootNodesData: readonly TreeInputNodeData[]): void {
if (!rootNodesData?.length) {
if (!rootNodesData.length) {
throw new Error('missing data');
}
const rootNodes = this.treeNodeParser(rootNodesData);

View File

@@ -71,8 +71,12 @@ TreeNavigationKeyCodes,
};
function focusPreviousVisibleNode(context: TreeNavigationContext): void {
const focusedNode = context.focus.currentSingleFocusedNode;
if (!focusedNode) {
return;
}
const previousVisibleNode = findPreviousVisibleNode(
context.focus.currentSingleFocusedNode,
focusedNode,
context.nodes,
);
if (!previousVisibleNode) {
@@ -83,6 +87,9 @@ function focusPreviousVisibleNode(context: TreeNavigationContext): void {
function focusNextVisibleNode(context: TreeNavigationContext): void {
const focusedNode = context.focus.currentSingleFocusedNode;
if (!focusedNode) {
return;
}
const nextVisibleNode = findNextVisibleNode(focusedNode, context.nodes);
if (!nextVisibleNode) {
return;
@@ -92,6 +99,9 @@ function focusNextVisibleNode(context: TreeNavigationContext): void {
function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void {
const focusedNode = context.focus.currentSingleFocusedNode;
if (!focusedNode) {
return;
}
const nodeState = focusedNode.state;
let transaction = nodeState.beginTransaction();
if (nodeState.current.checkState === TreeNodeCheckState.Checked) {
@@ -104,19 +114,28 @@ function toggleTreeNodeCheckStatus(context: TreeNavigationContext): void {
function collapseNodeOrFocusParent(context: TreeNavigationContext): void {
const focusedNode = context.focus.currentSingleFocusedNode;
if (!focusedNode) {
return;
}
const nodeState = focusedNode.state;
const parentNode = focusedNode.hierarchy.parent;
if (focusedNode.hierarchy.isBranchNode && nodeState.current.isExpanded) {
nodeState.commitTransaction(
nodeState.beginTransaction().withExpansionState(false),
);
} else {
const parentNode = focusedNode.hierarchy.parent;
if (!parentNode) {
return;
}
context.focus.setSingleFocus(parentNode);
}
}
function expandNodeOrFocusFirstChild(context: TreeNavigationContext): void {
const focusedNode = context.focus.currentSingleFocusedNode;
if (!focusedNode) {
return;
}
const nodeState = focusedNode.state;
if (focusedNode.hierarchy.isBranchNode && !nodeState.current.isExpanded) {
nodeState.commitTransaction(
@@ -151,7 +170,10 @@ function findNextNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefin
return nodes.flattenedNodes[index + 1] || undefined;
}
function findPreviousVisibleNode(node: TreeNode, nodes: QueryableNodes): TreeNode | undefined {
function findPreviousVisibleNode(
node: TreeNode,
nodes: QueryableNodes,
): TreeNode | undefined {
const previousNode = findPreviousNode(node, nodes);
if (!previousNode) {
return node.hierarchy.parent;

View File

@@ -14,6 +14,7 @@ export function useTreeQueryFilter(
const { nodes } = useCurrentTreeNodes(treeRootRef);
let isFiltering = false;
const statesBeforeFiltering = new NodeStateRestorer();
statesBeforeFiltering.saveStateBeforeFilter(nodes.value);
@@ -35,7 +36,7 @@ export function useTreeQueryFilter(
currentNodes.flattenedNodes.forEach((node: TreeNode) => {
let transaction = node.state.beginTransaction()
.withMatchState(false);
transaction = statesBeforeFiltering.applyOriginalState(node, transaction);
transaction = statesBeforeFiltering.applyStateBeforeFilter(node, transaction);
node.state.commitTransaction(transaction);
});
statesBeforeFiltering.clear();
@@ -150,18 +151,20 @@ class NodeStateRestorer {
});
}
public applyOriginalState(
public applyStateBeforeFilter(
node: TreeNode,
transaction: TreeNodeStateTransaction,
): TreeNodeStateTransaction {
if (!this.originalStates.has(node)) {
const originalState = this.originalStates.get(node);
if (!originalState) {
return transaction;
}
const originalState = this.originalStates.get(node);
if (originalState.isExpanded !== undefined) {
transaction = transaction.withExpansionState(originalState.isExpanded);
}
transaction = transaction.withVisibilityState(originalState.isVisible);
if (originalState.isVisible !== undefined) {
transaction = transaction.withVisibilityState(originalState.isVisible);
}
return transaction;
}
@@ -185,18 +188,13 @@ function setupWatchers(options: {
options.nodesRef,
],
([filterEvent, nodes]) => {
if (!filterEvent) {
if (filterEvent === undefined) {
return;
}
switch (filterEvent.action) {
case TreeViewFilterAction.Triggered:
options.onFilterTrigger(filterEvent.predicate, nodes);
break;
case TreeViewFilterAction.Removed:
options.onFilterReset();
break;
default:
throw new Error(`Unknown action: ${TreeViewFilterAction[filterEvent.action]}`);
if (filterEvent.action === TreeViewFilterAction.Triggered) {
options.onFilterTrigger(filterEvent.predicate, nodes);
} else {
options.onFilterReset();
}
},
{ immediate: true },

View File

@@ -2,18 +2,15 @@ import { ICategory, IScript } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] | undefined {
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
return createCategoryNodes(collection.actions);
}
export function parseSingleCategory(
categoryId: number,
collection: ICategoryCollection,
): NodeMetadata[] | undefined {
const category = collection.findCategory(categoryId);
if (!category) {
throw new Error(`Category with id ${categoryId} does not exist`);
}
): NodeMetadata[] {
const category = collection.getCategory(categoryId);
const tree = parseCategoryRecursively(category);
return tree;
}
@@ -73,7 +70,7 @@ function convertScriptToNode(script: IScript): NodeMetadata {
id: getScriptNodeId(script),
type: NodeType.Script,
text: script.name,
children: undefined,
children: [],
docs: script.docs,
isReversible: script.canRevert(),
};

View File

@@ -5,7 +5,6 @@ import { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
export function getNodeMetadata(
treeNode: ReadOnlyTreeNode,
): NodeMetadata {
if (!treeNode) { throw new Error('missing tree node'); }
const data = treeNode.metadata as NodeMetadata;
if (!data) {
throw new Error('Provided node does not contain the expected metadata.');
@@ -14,7 +13,6 @@ export function getNodeMetadata(
}
export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
if (!metadata) { throw new Error('missing metadata'); }
return {
id: metadata.id,
children: convertChildren(metadata.children, convertToNodeInput),
@@ -23,7 +21,7 @@ export function convertToNodeInput(metadata: NodeMetadata): TreeInputNodeData {
}
function convertChildren<TOldNode, TNewNode>(
oldChildren: readonly TOldNode[],
oldChildren: readonly TOldNode[] | undefined,
callback: (value: TOldNode) => TNewNode,
): TNewNode[] {
if (!oldChildren || oldChildren.length === 0) {

View File

@@ -42,7 +42,7 @@ export function useTreeViewFilterEvent() {
function subscribeToFilterChanges(
filter: IReadOnlyUserFilter,
latestFilterEvent: Ref<TreeViewFilterEvent>,
latestFilterEvent: Ref<TreeViewFilterEvent | undefined>,
filterPredicate: TreeNodeFilterResultPredicate,
) {
return filter.filterChanged.on((event) => {

View File

@@ -1,9 +1,6 @@
import { IApplication } from '@/domain/IApplication';
export function useApplication(application: IApplication) {
if (!application) {
throw new Error('missing application');
}
return {
application,
info: application.info,

View File

@@ -7,13 +7,6 @@ export function useCollectionState(
context: IApplicationContext,
events: IEventSubscriptionCollection,
) {
if (!context) {
throw new Error('missing context');
}
if (!events) {
throw new Error('missing events');
}
const currentState = shallowRef<IReadOnlyCategoryCollectionState>(context.state);
events.register([
context.contextChanged.on((event) => {
@@ -28,9 +21,6 @@ export function useCollectionState(
handler: NewStateEventHandler,
settings: Partial<IStateCallbackSettings> = defaultSettings,
) {
if (!handler) {
throw new Error('missing state handler');
}
events.register([
context.contextChanged.on((event) => {
handler(event.newState, event.oldState);
@@ -46,16 +36,10 @@ export function useCollectionState(
}
function modifyCurrentState(mutator: StateModifier) {
if (!mutator) {
throw new Error('missing state mutator');
}
mutator(context.state);
}
function modifyCurrentContext(mutator: ContextModifier) {
if (!mutator) {
throw new Error('missing context mutator');
}
mutator(context);
}

View File

@@ -1,8 +1,5 @@
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
export function useRuntimeEnvironment(environment: IRuntimeEnvironment) {
if (!environment) {
throw new Error('missing environment');
}
return environment;
}

View File

@@ -62,7 +62,7 @@ const RawSvgLoaders = import.meta.glob('@/presentation/assets/icons/**/*.svg', {
});
function modifySvg(svgSource: string): string {
const parser = new window.DOMParser();
const parser = new globalThis.window.DOMParser();
const doc = parser.parseFromString(svgSource, 'image/svg+xml');
let svgRoot = doc.documentElement;
svgRoot = removeSvgComments(svgRoot);

View File

@@ -4,7 +4,7 @@ import { Ref, watchEffect } from 'vue';
* Manages focus transitions, ensuring good usability and accessibility.
*/
export function useCurrentFocusToggle(shouldDisableFocus: Ref<boolean>) {
let previouslyFocusedElement: HTMLElement | undefined;
let previouslyFocusedElement: HTMLElement | null;
watchEffect(() => {
if (shouldDisableFocus.value) {
@@ -17,7 +17,7 @@ export function useCurrentFocusToggle(shouldDisableFocus: Ref<boolean>) {
return;
}
previouslyFocusedElement.focus();
previouslyFocusedElement = undefined;
previouslyFocusedElement = null;
}
});
}

View File

@@ -6,7 +6,7 @@
<script lang="ts">
import {
defineComponent, shallowRef, onMounted, onBeforeUnmount,
defineComponent, shallowRef, onMounted, onBeforeUnmount, watch,
} from 'vue';
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
@@ -25,61 +25,63 @@ export default defineComponent({
let width = 0;
let height = 0;
let observer: ResizeObserver;
let observer: ResizeObserver | undefined;
onMounted(() => {
width = containerElement.value.offsetWidth;
height = containerElement.value.offsetHeight;
resizeObserverReady.then(() => {
observer = new ResizeObserver(updateSize);
observer.observe(containerElement.value);
});
fireChangeEvents();
watch(() => containerElement.value, async (element) => {
if (!element) {
disposeObserver();
return;
}
resizeObserverReady.then(() => {
observer = new ResizeObserver(updateSize);
observer.observe(element);
});
updateSize();
}, { immediate: true });
});
onBeforeUnmount(() => {
observer?.disconnect();
disposeObserver();
});
function updateSize() {
let sizeChanged = false;
if (isWidthChanged()) {
updateWidth(containerElement.value.offsetWidth);
sizeChanged = true;
}
if (isHeightChanged()) {
updateHeight(containerElement.value.offsetHeight);
sizeChanged = true;
}
if (sizeChanged) {
const changes = [
updateWidth(),
updateHeight(),
];
if (changes.some((c) => c.isChanged)) {
emit('sizeChanged');
}
}
function updateWidth(newWidth: number) {
function updateWidth(): {
readonly isChanged: boolean;
} {
const newWidth = containerElement.value?.offsetWidth ?? 0;
if (newWidth === width) {
return { isChanged: false };
}
width = newWidth;
emit('widthChanged', newWidth);
return { isChanged: true };
}
function updateHeight(newHeight: number) {
function updateHeight(): {
readonly isChanged: boolean;
} {
const newHeight = containerElement.value?.offsetHeight ?? 0;
if (newHeight === height) {
return { isChanged: false };
}
height = newHeight;
emit('heightChanged', newHeight);
return { isChanged: true };
}
function fireChangeEvents() {
updateWidth(containerElement.value.offsetWidth);
updateHeight(containerElement.value.offsetHeight);
emit('sizeChanged');
}
function isWidthChanged(): boolean {
return width !== containerElement.value.offsetWidth;
}
function isHeightChanged(): boolean {
return height !== containerElement.value.offsetHeight;
function disposeObserver() {
observer?.disconnect();
observer = undefined;
}
return {

View File

@@ -10,11 +10,11 @@ export function throttle(
}
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
export type TimeoutType = ReturnType<typeof setTimeout>;
export type Timeout = ReturnType<typeof setTimeout>;
export interface ITimer {
setTimeout: (callback: () => void, ms: number) => TimeoutType;
clearTimeout: (timeoutId: TimeoutType) => void;
setTimeout: (callback: () => void, ms: number) => Timeout;
clearTimeout: (timeoutId: Timeout) => void;
dateNow(): number;
}
@@ -29,7 +29,7 @@ interface IThrottler {
}
class Throttler implements IThrottler {
private queuedExecutionId: TimeoutType;
private queuedExecutionId: Timeout | undefined;
private previouslyRun: number;
@@ -38,10 +38,8 @@ class Throttler implements IThrottler {
private readonly waitInMs: number,
private readonly callback: CallbackType,
) {
if (!timer) { throw new Error('missing timer'); }
if (!waitInMs) { throw new Error('missing delay'); }
if (waitInMs < 0) { throw new Error('negative delay'); }
if (!callback) { throw new Error('missing callback'); }
}
public invoke(...args: unknown[]): void {

View File

@@ -41,7 +41,9 @@ export default defineComponent({
...supportedOperativeSystems,
].sort((os) => (os === currentOs ? 0 : 1));
const hasCurrentOsDesktopVersion = supportedOperativeSystems.includes(currentOs);
const hasCurrentOsDesktopVersion = currentOs === undefined
? false
: supportedOperativeSystems.includes(currentOs);
return {
supportedDesktops,

View File

@@ -39,7 +39,7 @@ export default defineComponent({
return hasDesktopVersion(props.operatingSystem);
});
const downloadUrl = computed<string | undefined>(() => {
const downloadUrl = computed<string>(() => {
return info.getDownloadUrl(props.operatingSystem);
});

View File

@@ -38,11 +38,12 @@ export default defineComponent({
const { totalScripts } = currentState.value.collection;
return `Search in ${totalScripts} scripts`;
});
const searchQuery = ref<string>();
const searchQuery = ref<string | undefined>();
watch(searchQuery, (newFilter) => updateFilter(newFilter));
function updateFilter(newFilter: string) {
function updateFilter(newFilter: string | undefined) {
modifyCurrentState((state) => {
const { filter } = state;
if (!newFilter) {
@@ -61,7 +62,7 @@ export default defineComponent({
}, { immediate: true });
function updateFromInitialFilter(filter?: IFilterResult) {
searchQuery.value = filter?.query || '';
searchQuery.value = filter?.query;
}
function subscribeToFilterChanges(