fix(align-branch): scrolls too late when creating a card in outline mode

This commit is contained in:
ycnmhd 2025-01-21 22:04:07 +01:00
parent b147e127bb
commit 58ad13b501
No known key found for this signature in database
GPG key ID: E38ED8B2B6559A4E
31 changed files with 478 additions and 366 deletions

View file

@ -1,6 +1,6 @@
{
"*.{js,jsx,ts,tsx, svelte}": [
"prettier --write",
"eslint --fix"
"eslint"
]
}

View file

@ -1,3 +1,15 @@
export const delay = async (milliseconds: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
export const delay = async (
milliseconds: number,
signal?: AbortSignal,
): Promise<void> => {
return new Promise((resolve) => {
const timeout = setTimeout(() => resolve(), milliseconds);
if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(timeout);
resolve();
});
}
});
};

View file

@ -1,4 +1,5 @@
/* eslint-disable no-console */
type Logger = {
debug: (...message: unknown[]) => void;
info: (...message: unknown[]) => void;
@ -34,3 +35,16 @@ const createLogger = (): Logger => {
};
export const logger = createLogger();
/*let i = 0;
export const AlignBranchLogger = (action: PluginAction) => {
const id = i++;
let t = 0;
return {
log: (...params: any[]) => {
const delta = t > 0 ? Date.now() - t : 0;
t = Date.now();
console.log(`[${id}] [${action.type}] [${delta}]`, ...params);
},
};
}*/

View file

@ -1,7 +1,7 @@
import { calculateScrollLeft } from 'src/lib/align-element/helpers/calculate-scroll-left';
import { THRESHOLD } from 'src/lib/align-element/constants';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { getElementById } from 'src/lib/align-element/helpers/get-element-by-id';
export const alignElementHorizontally = (

View file

@ -3,7 +3,7 @@ import { THRESHOLD } from 'src/lib/align-element/constants';
import {
AlignBranchContext,
PartialDOMRect,
} from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
} from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { calculateScrollTopRelative } from 'src/lib/align-element/helpers/calculate-scroll-top-relative';
import { getElementById } from 'src/lib/align-element/helpers/get-element-by-id';

View file

@ -1,5 +1,5 @@
import { getCombinedBoundingClientRect } from 'src/lib/align-element/helpers/get-combined-client-rect';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { alignVertically } from 'src/lib/align-element/align-element-vertically';
import { getElementById } from 'src/lib/align-element/helpers/get-element-by-id';

View file

@ -3,7 +3,7 @@ import {
TOOLBAR_HEIGHT,
} from 'src/lib/align-element/constants';
import { PartialDOMRect } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { PartialDOMRect } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
export const calculateScrollTopRelative = (
elementRect: DOMRect,

View file

@ -6,7 +6,10 @@ const LOOP_DELAY_MS = 10;
const MAX_ATTEMPTS = 100;
const REQUIRED_MATCHES = 5;
export const waitForActiveNodeToStopMoving = async (view: LineageView) => {
export const waitForActiveNodeToStopMoving = async (
view: LineageView,
signal: AbortSignal,
) => {
const activeBranch = view.viewStore.getValue().document.activeBranch;
let columnEl: HTMLElement | undefined;
@ -18,7 +21,7 @@ export const waitForActiveNodeToStopMoving = async (view: LineageView) => {
let lastScrollLeft = -1;
const container = view.container!;
while (retries < MAX_ATTEMPTS) {
while (retries < MAX_ATTEMPTS && !signal.aborted) {
if (!columnEl) {
columnEl = getElementById(container, activeBranch.column)!;
} else {

View file

@ -1,5 +1,4 @@
import { ViewState } from 'src/stores/view/view-state-type';
import { NodeId } from 'src/stores/document/document-state-type';
export const defaultViewState = (): ViewState => ({
search: {
@ -75,7 +74,7 @@ export const defaultViewState = (): ViewState => ({
});
export type ActiveBranch = {
childGroups: Set<string>;
sortedParentNodes: NodeId[];
sortedParentNodes: string[];
group: string;
column: string;
node: string;

View file

@ -0,0 +1,129 @@
import { describe, expect, it } from 'vitest';
import { ActiveBranch } from 'src/stores/view/reducers/document/helpers/update-active-branch';
import { compareActiveBranch } from 'src/stores/view/reducers/document/helpers/compare-active-branch';
describe('compareActiveBranch', () => {
it('should return true for identical branches', () => {
const branch: ActiveBranch = {
childGroups: new Set(['n1.1', 'n2.1']),
sortedParentNodes: ['n1', 'n2'],
group: 'g1',
column: 'col1',
node: 'n2.1.1',
};
expect(compareActiveBranch(branch, { ...branch })).toBe(true);
});
it('should return true for same reference', () => {
const branch: ActiveBranch = {
childGroups: new Set(['n1.1']),
sortedParentNodes: ['n1'],
group: 'g1',
column: 'col1',
node: 'n1.1',
};
expect(compareActiveBranch(branch, branch)).toBe(true);
});
it('should return false for different primitive values', () => {
const branch1: ActiveBranch = {
childGroups: new Set(['n1.1']),
sortedParentNodes: ['n1'],
group: 'g1',
column: 'col1',
node: 'n1.1',
};
const branch2: ActiveBranch = {
childGroups: new Set(['n1.1']),
sortedParentNodes: ['n1'],
group: 'g2', // different group
column: 'col1',
node: 'n1.1',
};
expect(compareActiveBranch(branch1, branch2)).toBe(false);
});
it('should return false for different sortedParentNodes', () => {
const branch1: ActiveBranch = {
childGroups: new Set(['n1.1']),
sortedParentNodes: ['n1', 'n2'],
group: 'g1',
column: 'col1',
node: 'n2.1',
};
const branch2: ActiveBranch = {
childGroups: new Set(['n1.1']),
sortedParentNodes: ['n1'], // different length
group: 'g1',
column: 'col1',
node: 'n2.1',
};
expect(compareActiveBranch(branch1, branch2)).toBe(false);
const branch3: ActiveBranch = {
childGroups: new Set(['n1.1']),
sortedParentNodes: ['n2', 'n1'], // different order
group: 'g1',
column: 'col1',
node: 'n2.1',
};
expect(compareActiveBranch(branch1, branch3)).toBe(false);
});
it('should return false for different childGroups', () => {
const branch1: ActiveBranch = {
childGroups: new Set(['n1.1', 'n2.1']),
sortedParentNodes: ['n1', 'n2'],
group: 'g1',
column: 'col1',
node: 'n2.1.1',
};
const branch2: ActiveBranch = {
childGroups: new Set(['n1.1']), // missing n2.1
sortedParentNodes: ['n1', 'n2'],
group: 'g1',
column: 'col1',
node: 'n2.1.1',
};
expect(compareActiveBranch(branch1, branch2)).toBe(false);
const branch3: ActiveBranch = {
childGroups: new Set(['n1.1', 'n2.2']), // different value
sortedParentNodes: ['n1', 'n2'],
group: 'g1',
column: 'col1',
node: 'n2.1.1',
};
expect(compareActiveBranch(branch1, branch3)).toBe(false);
});
it('should return true for identical states', () => {
const branch1: ActiveBranch = {
childGroups: new Set(['n1.1', 'n2.1', 'n2.1.1']),
sortedParentNodes: ['n1', 'n2', 'n2.1'],
group: 'g1',
column: 'col1',
node: 'n2.1.1',
};
const branch2: ActiveBranch = {
childGroups: new Set(['n1.1', 'n2.1', 'n2.1.1']),
sortedParentNodes: ['n1', 'n2', 'n2.1'],
group: 'g1',
column: 'col1',
node: 'n2.1.1',
};
expect(compareActiveBranch(branch1, branch2)).toBe(true);
});
});

View file

@ -0,0 +1,34 @@
import { ActiveBranch } from 'src/stores/view/reducers/document/helpers/update-active-branch';
export const compareActiveBranch = (
a: ActiveBranch,
b: ActiveBranch,
): boolean => {
if (a === b) return true;
if (a.group !== b.group || a.column !== b.column || a.node !== b.node) {
return false;
}
if (a.sortedParentNodes.length !== b.sortedParentNodes.length) {
return false;
}
if (a.childGroups.size !== b.childGroups.size) {
return false;
}
for (let i = 0; i < a.sortedParentNodes.length; i++) {
if (a.sortedParentNodes[i] !== b.sortedParentNodes[i]) {
return false;
}
}
for (const item of a.childGroups) {
if (!b.childGroups.has(item)) {
return false;
}
}
return true;
};

View file

@ -107,7 +107,7 @@ describe('update-tree-state', () => {
node: activeNodeId,
} satisfies ActiveBranch,
};
updateActiveBranch(input.state, input.document.columns, 'structure');
updateActiveBranch(input.state, input.document.columns, true);
expect(input.state.activeBranch).toEqual(output.activeBranch);
});
});

View file

@ -5,17 +5,30 @@ import { findGroupByNodeId } from 'src/lib/tree-utils/find/find-group-by-node-id
import { findNodeColumn } from 'src/lib/tree-utils/find/find-node-column';
import { DocumentViewState } from 'src/stores/view/view-state-type';
import { removeStaleActiveNodes } from 'src/stores/view/reducers/document/helpers/remove-stale-active-nodes';
import { compareActiveBranch } from 'src/stores/view/reducers/document/helpers/compare-active-branch';
import { DocumentStoreAction } from 'src/stores/document/document-store-actions';
export type ChangeType = 'none' | 'structure';
export type UpdateActiveBranchAction =
| {
type: 'view/update-active-branch?source=view';
context: {
columns: Column[];
};
}
| {
type: 'view/update-active-branch?source=document';
context: {
columns: Column[];
documentAction: DocumentStoreAction;
};
};
export type UpdateActiveBranchAction = {
type: 'UPDATE_ACTIVE_BRANCH';
payload: {
columns: Column[];
};
context: {
changeType: ChangeType;
};
export type ActiveBranch = {
childGroups: Set<string>;
sortedParentNodes: string[];
group: string;
column: string;
node: string;
};
export const updateActiveBranch = (
@ -24,7 +37,7 @@ export const updateActiveBranch = (
'activeBranch' | 'activeNode' | 'activeNodesOfColumn'
>,
columns: Column[],
changeType: ChangeType,
isDocumentAction: boolean,
) => {
if (!state.activeNode) return;
const sortedParents = traverseUp(columns, state.activeNode).reverse();
@ -34,33 +47,22 @@ export const updateActiveBranch = (
throw new Error('could not find group for node ' + state.activeNode);
const columnId = columns[findNodeColumn(columns, state.activeNode)].id;
const needsUpdate =
state.activeNode !== state.activeBranch.node ||
childGroups.length !== state.activeBranch.childGroups.size ||
sortedParents.length !== state.activeBranch.sortedParentNodes.length ||
group.parentId !== state.activeBranch.group ||
columnId !== state.activeBranch.column ||
childGroups.some(
(group) => !state.activeBranch.childGroups.has(group),
) ||
sortedParents.some(
(node, i) => node !== state.activeBranch.sortedParentNodes[i],
);
if (needsUpdate) {
state.activeBranch = {
childGroups: new Set<string>(childGroups),
sortedParentNodes: sortedParents,
group: group.parentId,
column: columnId,
node: state.activeNode,
};
const newActiveBranch = {
childGroups: new Set<string>(childGroups),
sortedParentNodes: sortedParents,
group: group.parentId,
column: columnId,
node: state.activeNode,
};
const same = compareActiveBranch(state.activeBranch, newActiveBranch);
if (!same) {
state.activeBranch = newActiveBranch;
}
if (!state.activeNodesOfColumn[columnId])
state.activeNodesOfColumn[columnId] = {};
state.activeNodesOfColumn[columnId][group.parentId] = state.activeNode;
if (changeType === 'structure') {
if (isDocumentAction) {
state.activeNodesOfColumn = removeStaleActiveNodes(
columns,
state.activeNodesOfColumn,

View file

@ -1,19 +1,26 @@
import { ViewStore } from 'src/view/view';
import { DocumentState } from 'src/stores/document/document-state-type';
import { ChangeType } from 'src/stores/view/reducers/document/helpers/update-active-branch';
import { DocumentStoreAction } from 'src/stores/document/document-store-actions';
export const updateActiveBranch = (
viewStore: ViewStore,
documentState: DocumentState,
changeType: ChangeType,
documentAction?: DocumentStoreAction,
) => {
viewStore.dispatch({
type: 'UPDATE_ACTIVE_BRANCH',
payload: {
columns: documentState.document.columns,
},
context: {
changeType,
},
});
if (documentAction) {
viewStore.dispatch({
type: 'view/update-active-branch?source=document',
context: {
columns: documentState.document.columns,
documentAction,
},
});
} else {
viewStore.dispatch({
type: 'view/update-active-branch?source=view',
context: {
columns: documentState.document.columns,
},
});
}
};

View file

@ -1,9 +1,6 @@
import { LineageView } from 'src/view/view';
import { delay } from 'src/helpers/delay';
import { delayAlign } from 'src/stores/view/subscriptions/effects/align-branch/helpers/delay-align';
import { ActiveNodesOfColumn } from 'src/stores/view/view-state-type';
import { Column } from 'src/stores/document/document-state-type';
import { adjustScrollBehavior } from 'src/stores/view/subscriptions/effects/align-branch/helpers/adjust-scroll-behavior';
import { ActiveBranch } from 'src/stores/view/default-view-state';
import { createAlignBranchActions } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/create-align-branch-actions';
import { runAlignBranchActions } from 'src/stores/view/subscriptions/effects/align-branch/run-align-branch-actions/run-align-branch-actions';
@ -15,38 +12,11 @@ import {
} from 'src/stores/view/view-store-actions';
import { SettingsActions } from 'src/stores/settings/settings-reducer';
import { DocumentsStoreAction } from 'src/stores/documents/documents-store-actions';
import {
ActionCategory,
actionCategory,
actionCategoryPriority,
} from 'src/stores/view/subscriptions/effects/align-branch/constants/action-category';
import { waitForActiveNodeToStopMoving } from 'src/lib/align-element/helpers/wait-for-active-node-to-stop-moving';
import { createContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { SilentError } from 'src/lib/errors/errors';
export type PartialDOMRect = Pick<DOMRect, 'top' | 'height'>;
export type AlignBranchState = {
rects: Map<string, PartialDOMRect>;
};
export type AlignBranchSettings = {
behavior: ScrollBehavior;
centerActiveNodeH: boolean;
centerActiveNodeV: boolean;
zoomLevel: number;
};
export type AlignBranchContext = {
previousActiveBranch: ActiveBranch | null;
columns: Column[];
activeBranch: ActiveBranch;
container: HTMLElement;
containerRect: DOMRect;
outlineMode: boolean;
alignBranchSettings: AlignBranchSettings;
state: AlignBranchState;
activeNodesOfColumn: ActiveNodesOfColumn;
};
import { actionPriority } from 'src/stores/view/subscriptions/effects/align-branch/constants/action-priority';
import { logger } from 'src/helpers/logger';
export type PluginAction =
| DocumentStoreAction
@ -63,87 +33,82 @@ export type PreviousScrollBehavior = {
behavior: ScrollBehavior;
};
export type AlignEvent = {
category: ActionCategory;
action: PluginAction['type'];
priority: number;
ts: number;
};
export class AlignBranch {
private isRunning: boolean = false;
private currentEvent: {
action: PluginAction;
controller: AbortController;
priority: number;
} | null = null;
private previousActiveBranch: ActiveBranch | null = null;
private previousEvent: AlignEvent | null = null;
constructor(public view: LineageView) {}
align = async (action: PluginAction) => {
if (skipAlign(this.view, action)) return;
const delay_ms = delayAlign(action);
if (delay_ms > 0) await delay(delay_ms);
await this.waitForPreviousEvent(action);
const context = this.createContext(action);
const actions = createAlignBranchActions(context, action);
requestAnimationFrame(() => {
runAlignBranchActions(context, actions);
});
this.saveActiveBranch(context);
};
private createContext = (action: PluginAction) => {
const settings = this.view.plugin.settings.getValue();
const container = this.view.container!;
const documentState = this.view.documentStore.getValue();
const viewState = this.view.viewStore.getValue();
const activeBranch = viewState.document.activeBranch;
const behavior = adjustScrollBehavior(action);
const context: AlignBranchContext = {
previousActiveBranch: this.previousActiveBranch,
activeBranch: activeBranch,
columns: documentState.document.columns,
container,
activeNodesOfColumn: viewState.document.activeNodesOfColumn,
containerRect: container.getBoundingClientRect(),
outlineMode: settings.view.outlineMode,
alignBranchSettings: {
centerActiveNodeH: settings.view.scrolling.centerActiveNodeH,
centerActiveNodeV: settings.view.scrolling.centerActiveNodeV,
zoomLevel: settings.view.zoomLevel,
behavior: behavior,
},
state: {
rects: new Map(),
},
};
return context;
};
private saveActiveBranch(context: AlignBranchContext) {
this.previousActiveBranch = context.activeBranch;
}
private waitForPreviousEvent = async (action: PluginAction) => {
const category = actionCategory.get(action.type)!;
if (category === 'other') {
throw new SilentError('unsupported event: ' + action.type);
align = (action: PluginAction) => {
const priority = actionPriority.get(action.type);
if (typeof priority !== 'number') {
throw new SilentError(action.type + ' not allowed');
}
const event: AlignEvent = {
action: action.type,
category: category!,
priority: actionCategoryPriority.get(category)!,
ts: Date.now(),
};
if (this.previousEvent) {
if (event.priority < this.previousEvent.priority) {
if (event.ts - this.previousEvent.ts < 500) {
await waitForActiveNodeToStopMoving(this.view);
}
if (this.currentEvent) {
if (priority >= this.currentEvent.priority) {
this.currentEvent.controller.abort();
} else {
return;
}
}
this.previousEvent = event;
this.currentEvent = {
action,
priority,
controller: new AbortController(),
};
if (!this.isRunning) {
this.run();
}
};
private run = async () => {
this.isRunning = true;
while (this.currentEvent) {
const event = this.currentEvent;
try {
if (skipAlign(this.view, event.action)) {
if (this.currentEvent === event) this.currentEvent = null;
continue;
}
const delay_ms = delayAlign(event.action);
if (delay_ms > 0) {
await delay(delay_ms, event.controller.signal);
}
const context = createContext(
this.view,
event.action,
this.previousActiveBranch,
);
const actions = createAlignBranchActions(context, event.action);
requestAnimationFrame(() => {
runAlignBranchActions(
context,
actions,
event.controller.signal,
);
this.previousActiveBranch = context.activeBranch;
});
await waitForActiveNodeToStopMoving(
this.view,
event.controller.signal,
);
} catch (e) {
logger.error(e);
}
if (this.currentEvent === event) this.currentEvent = null;
}
this.isRunning = false;
};
}

View file

@ -1,175 +0,0 @@
import { PluginAction } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
export type ActionCategory =
| 'document/structure'
| 'document/reset'
| 'document/content'
| 'view/active-node/keyboard'
| 'view/active-node/mouse'
| 'view/active-node/document'
| 'view/active-node/search'
| 'view/life-cycle'
| 'view/resize'
| 'view/editor'
| 'view/outline'
| 'manual'
| 'settings/zoom'
| 'settings/layout'
| 'settings/scrolling'
| 'other';
const actionCategoryDict: Partial<
Record<PluginAction['type'], ActionCategory>
> = {
'view/align-branch/center-node': 'manual',
'view/align-branch/reveal-node': 'manual',
'DOCUMENT/LOAD_FILE': 'document/reset',
RESET_STORE: 'document/reset',
'DOCUMENT/INSERT_NODE': 'document/structure',
'DOCUMENT/DROP_NODE': 'document/structure',
'DOCUMENT/DELETE_NODE': 'document/structure',
'DOCUMENT/MERGE_NODE': 'document/structure',
'DOCUMENT/MOVE_NODE': 'document/structure',
'DOCUMENT/SPLIT_NODE': 'document/structure',
'DOCUMENT/PASTE_NODE': 'document/structure',
'DOCUMENT/EXTRACT_BRANCH': 'document/structure',
'DOCUMENT/CUT_NODE': 'document/structure',
'HISTORY/APPLY_PREVIOUS_SNAPSHOT': 'document/structure',
'HISTORY/APPLY_NEXT_SNAPSHOT': 'document/structure',
'HISTORY/SELECT_SNAPSHOT': 'document/structure',
'DOCUMENT/SET_NODE_CONTENT': 'document/content',
'DOCUMENT/FORMAT_HEADINGS': 'document/content',
'view/main/disable-edit': 'view/editor',
'view/main/enable-edit': 'view/editor',
'DOCUMENT/JUMP_TO_NODE': 'view/active-node/keyboard',
'DOCUMENT/NAVIGATE_USING_KEYBOARD': 'view/active-node/keyboard',
'NAVIGATION/SELECT_NEXT_NODE': 'view/active-node/mouse',
'NAVIGATION/NAVIGATE_BACK': 'view/active-node/mouse',
'NAVIGATION/NAVIGATE_FORWARD': 'view/active-node/mouse',
'view/outline/toggle-collapse-node': 'view/outline',
'view/outline/refresh-collapsed-nodes': 'view/outline',
'view/outline/toggle-collapse-all': 'view/outline',
// todo: add a separate event for effect source
'view/set-active-node/mouse': 'view/active-node/mouse',
'view/set-active-node/mouse-silent': 'other',
'view/set-active-node/document': 'view/active-node/document',
'view/set-active-node/search': 'view/active-node/search',
'view/left-sidebar/set-width': 'settings/layout',
'WORKSPACE/ACTIVE_LEAF_CHANGE': 'view/life-cycle',
'WORKSPACE/RESIZE': 'view/resize',
'WORKSPACE/LAYOUT_READY': 'view/life-cycle',
'WORKSPACE/SET_ACTIVE_LINEAGE_VIEW': 'view/life-cycle',
'view/life-cycle/mount': 'view/life-cycle',
'VIEW/TOGGLE_MINIMAP': 'settings/layout',
'view/left-sidebar/toggle': 'settings/layout',
SET_CARD_WIDTH: 'settings/layout',
SET_CARDS_GAP: 'settings/layout',
SET_MIN_CARD_HEIGHT: 'settings/layout',
'VIEW/SCROLLING/TOGGLE_SCROLLING_MODE': 'settings/scrolling',
'settings/view/scrolling/toggle-vertical-scrolling-mode':
'settings/scrolling',
SET_LIMIT_PREVIEW_HEIGHT: 'settings/layout',
'UI/CHANGE_ZOOM_LEVEL': 'settings/zoom',
'view/modes/gap-between-cards/toggle': 'settings/layout',
'settings/view/modes/toggle-outline-mode': 'settings/layout',
'settings/view/set-node-indentation-width': 'settings/layout',
'SEARCH/SET_RESULTS': 'view/active-node/search',
'search/toggle-show-all-nodes': 'view/active-node/search',
'SEARCH/SET_QUERY': 'view/active-node/search',
'SEARCH/TOGGLE_INPUT': 'view/active-node/search',
'SEARCH/TOGGLE_FUZZY_MODE': 'view/active-node/search',
'FS/SET_FILE_PATH': 'other',
'DOCUMENT/COPY_NODE': 'other',
'FILE/UPDATE_FRONTMATTER': 'other',
'document/pinned-nodes/pin': 'other',
'document/pinned-nodes/unpin': 'other',
'document/pinned-nodes/remove-stale-nodes': 'other',
'document/pinned-nodes/load-from-settings': 'other',
'META/REFRESH_GROUP_PARENT_IDS': 'other',
SET_DRAG_STARTED: 'other',
'DOCUMENT/SET_DRAG_ENDED': 'other',
UPDATE_ACTIVE_BRANCH: 'other',
'view/confirmation/reset/disable-edit': 'other',
'view/confirmation/reset/delete-node': 'other',
'view/confirmation/confirm/delete-node': 'other',
'view/confirmation/confirm/disable-edit': 'other',
'DOCUMENT/CLEAR_SELECTION': 'other',
'UI/TOGGLE_HELP_SIDEBAR': 'other',
'UI/TOGGLE_HISTORY_SIDEBAR': 'other',
'UI/TOGGLE_SETTINGS_SIDEBAR': 'other',
CLOSE_MODALS: 'other',
'view/modals/toggle-style-rules': 'other',
'NAVIGATION/REMOVE_OBSOLETE': 'other',
'view/pinned-nodes/set-active-node': 'other',
'view/recent-nodes/set-active-node': 'other',
'view/sidebar/enable-edit': 'other',
'view/sidebar/disable-edit': 'other',
'view/style-rules/update-results': 'other',
'view/keyboard/shift/down': 'other',
'view/keyboard/shift/up': 'other',
'view/hotkeys/set-search-term': 'other',
'view/hotkeys/update-conflicts': 'other',
'view/selection/set-selection': 'other',
SET_DOCUMENT_TYPE: 'other',
SET_VIEW_TYPE: 'other',
DELETE_DOCUMENT_PREFERENCES: 'other',
'HISTORY/UPDATE_DOCUMENT_PATH': 'other',
SET_CUSTOM_HOTKEYS: 'other',
SET_FONT_SIZE: 'other',
SET_CONTAINER_BG: 'other',
SET_ACTIVE_BRANCH_BG: 'other',
UPDATE_DOCUMENTS_DICTIONARY: 'other',
'settings/document/persist-active-section': 'other',
'GENERAL/SET_DEFAULT_DOCUMENT_FORMAT': 'other',
'view/left-sidebar/set-active-tab': 'other',
'settings/pinned-nodes/persist': 'other',
'settings/pinned-nodes/persist-active-node': 'other',
'settings/style-rules/add': 'other',
'settings/style-rules/update': 'other',
'settings/style-rules/delete': 'other',
'settings/style-rules/move': 'other',
'settings/style-rules/update-condition': 'other',
'settings/style-rules/enable-rule': 'other',
'settings/style-rules/disable-rule': 'other',
'settings/view/set-maintain-edit-mode': 'other',
'settings/view/theme/set-inactive-node-opacity': 'other',
'settings/view/theme/set-active-branch-color': 'other',
'settings/hotkeys/set-custom-hotkey': 'other',
'settings/hotkeys/reset-custom-hotkey': 'other',
'settings/hotkeys/reset-all': 'other',
'settings/hotkeys/apply-preset': 'other',
'settings/hotkeys/toggle-editor-state': 'other',
'settings/hotkeys/set-blank': 'other',
'DOCUMENTS/DELETE_DOCUMENT': 'other',
'DOCUMENTS/UPDATE_DOCUMENT_PATH': 'other',
'DOCUMENTS/ADD_DOCUMENT': 'other',
} as const;
export const actionCategory = new Map(
Object.entries(actionCategoryDict),
) as Map<PluginAction['type'], ActionCategory>;
const actionCategoryPriorityDict: Record<ActionCategory, number> = {
'document/reset': 100,
'view/life-cycle': 100,
'view/active-node/keyboard': 90,
'view/active-node/mouse': 90,
'document/structure': 80,
'document/content': 70,
'view/active-node/document': 30,
'view/active-node/search': 30,
'view/resize': 20,
'view/editor': 20,
'view/outline': 20,
'settings/zoom': 20,
'settings/layout': 20,
'settings/scrolling': 20,
manual: 10,
other: 0,
};
export const actionCategoryPriority = new Map(
Object.entries(actionCategoryPriorityDict),
) as Map<ActionCategory, number>;

View file

@ -0,0 +1,51 @@
import { PluginAction } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
const actionPriorityRecord: Partial<Record<PluginAction['type'], number>> = {
'view/life-cycle/mount': 100,
'view/update-active-branch?source=document': 100,
'DOCUMENT/JUMP_TO_NODE': 90,
'NAVIGATION/SELECT_NEXT_NODE': 90,
'DOCUMENT/NAVIGATE_USING_KEYBOARD': 90,
'NAVIGATION/NAVIGATE_BACK': 90,
'NAVIGATION/NAVIGATE_FORWARD': 90,
'view/set-active-node/mouse': 90,
'view/set-active-node/mouse-silent': 0,
'view/set-active-node/search': 30,
'view/align-branch/center-node': 30,
'view/align-branch/reveal-node': 30,
'DOCUMENT/SET_NODE_CONTENT': 30,
'view/set-active-node/document': 20,
'view/update-active-branch?source=view': 20,
'view/main/disable-edit': 20,
'view/main/enable-edit': 20,
'view/outline/toggle-collapse-node': 10,
'view/outline/refresh-collapsed-nodes': 10,
'view/outline/toggle-collapse-all': 10,
'DOCUMENT/FORMAT_HEADINGS': 10,
'view/left-sidebar/set-width': 10,
'WORKSPACE/ACTIVE_LEAF_CHANGE': 10,
'WORKSPACE/RESIZE': 10,
'WORKSPACE/LAYOUT_READY': 10,
'WORKSPACE/SET_ACTIVE_LINEAGE_VIEW': 10,
'VIEW/TOGGLE_MINIMAP': 10,
'view/left-sidebar/toggle': 10,
SET_CARD_WIDTH: 10,
SET_CARDS_GAP: 10,
SET_MIN_CARD_HEIGHT: 10,
'view/modes/gap-between-cards/toggle': 10,
'SEARCH/SET_RESULTS': 10,
'search/toggle-show-all-nodes': 10,
'SEARCH/SET_QUERY': 10,
'SEARCH/TOGGLE_INPUT': 10,
'SEARCH/TOGGLE_FUZZY_MODE': 10,
'VIEW/SCROLLING/TOGGLE_SCROLLING_MODE': 10,
'settings/view/scrolling/toggle-vertical-scrolling-mode': 10,
SET_LIMIT_PREVIEW_HEIGHT: 10,
'UI/CHANGE_ZOOM_LEVEL': 10,
'settings/view/modes/toggle-outline-mode': 10,
'settings/view/set-node-indentation-width': 10,
};
export const actionPriority = new Map(
Object.entries(actionPriorityRecord) as [PluginAction['type'], number][],
);

View file

@ -1,9 +1,7 @@
import { forceCenterActiveNodeV } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/force-center-active-node-v';
import { lazyVerticalScrollingMode } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/lazy-vertical-scrolling-mode';
import {
AlignBranchContext,
PluginAction,
} from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { PluginAction } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { outlineScrollingActions } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/outline-scrolling-actions';
import { forceCenterActiveNodeH } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/force-center-active-node-h';
@ -34,6 +32,9 @@ export const createAlignBranchActions = (
action: PluginAction,
): AlignBranchAction[] => {
const actions: AlignBranchAction[] = [];
if (action.type === 'view/update-active-branch?source=document') {
action = action.context.documentAction;
}
if (action.type === 'view/align-branch/reveal-node') {
/* used to keep active node visible while editing*/

View file

@ -1,8 +1,6 @@
import { AlignBranchAction } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/create-align-branch-actions';
import {
AlignBranchContext,
PluginAction,
} from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { PluginAction } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
export const lazyVerticalScrollingMode = (
context: Pick<AlignBranchContext, 'previousActiveBranch' | 'activeBranch'>,

View file

@ -1,4 +1,4 @@
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { alignChildGroupOfColumn } from 'src/stores/view/subscriptions/effects/align-branch/helpers/align-child-columns/align-child-group-of-column';
import { alignElementVertically } from 'src/lib/align-element/align-element-vertically';
import { findNodeColumn } from 'src/lib/tree-utils/find/find-node-column';

View file

@ -1,6 +1,6 @@
import { getElementById } from 'src/lib/align-element/helpers/get-element-by-id';
import { alignGroupOfElementsVertically } from 'src/lib/align-element/align-group-of-elements-vertically';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { Column } from 'src/stores/document/document-state-type';
export const alignChildGroupOfColumn = (

View file

@ -1,5 +1,5 @@
import { alignElementVertically } from 'src/lib/align-element/align-element-vertically';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { findNodeColumn } from 'src/lib/tree-utils/find/find-node-column';
export const alignInactiveColumns = (context: AlignBranchContext) => {

View file

@ -0,0 +1,60 @@
import { LineageView } from 'src/view/view';
import { ActiveBranch } from 'src/stores/view/default-view-state';
import { adjustScrollBehavior } from 'src/stores/view/subscriptions/effects/align-branch/helpers/adjust-scroll-behavior';
import { PluginAction } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { Column } from 'src/stores/document/document-state-type';
import { ActiveNodesOfColumn } from 'src/stores/view/view-state-type';
export type PartialDOMRect = Pick<DOMRect, 'top' | 'height'>;
export type AlignBranchState = {
rects: Map<string, PartialDOMRect>;
};
export type AlignBranchSettings = {
behavior: ScrollBehavior;
centerActiveNodeH: boolean;
centerActiveNodeV: boolean;
zoomLevel: number;
};
export type AlignBranchContext = {
previousActiveBranch: ActiveBranch | null;
columns: Column[];
activeBranch: ActiveBranch;
container: HTMLElement;
containerRect: DOMRect;
outlineMode: boolean;
alignBranchSettings: AlignBranchSettings;
state: AlignBranchState;
activeNodesOfColumn: ActiveNodesOfColumn;
};
export const createContext = (
view: LineageView,
action: PluginAction,
previousActiveBranch: ActiveBranch | null,
) => {
const settings = view.plugin.settings.getValue();
const container = view.container!;
const documentState = view.documentStore.getValue();
const viewState = view.viewStore.getValue();
const activeBranch = viewState.document.activeBranch;
const behavior = adjustScrollBehavior(action);
const context: AlignBranchContext = {
previousActiveBranch: previousActiveBranch,
activeBranch: activeBranch,
columns: documentState.document.columns,
container,
activeNodesOfColumn: viewState.document.activeNodesOfColumn,
containerRect: container.getBoundingClientRect(),
outlineMode: settings.view.outlineMode,
alignBranchSettings: {
centerActiveNodeH: settings.view.scrolling.centerActiveNodeH,
centerActiveNodeV: settings.view.scrolling.centerActiveNodeV,
zoomLevel: settings.view.zoomLevel,
behavior: behavior,
},
state: {
rects: new Map(),
},
};
return context;
};

View file

@ -7,8 +7,15 @@ export const delayAlign = (action: PluginAction) => {
action.type === 'VIEW/TOGGLE_MINIMAP'
) {
delay = 300;
} else if (action.type === 'DOCUMENT/DROP_NODE') {
delay = 32;
} else if (action.type === 'WORKSPACE/RESIZE') {
delay = 50;
} else if (action.type === 'view/life-cycle/mount') {
delay = 16;
} else if (action.type === 'view/update-active-branch?source=document') {
action = action.context.documentAction;
if (action.type === 'DOCUMENT/INSERT_NODE') {
delay = 32;
}
}
return delay;
};

View file

@ -1,4 +1,4 @@
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { alignElementVertically } from 'src/lib/align-element/align-element-vertically';
export const alignParentsNodes = (

View file

@ -1,5 +1,5 @@
import { alignElementHorizontally } from 'src/lib/align-element/align-element-horizontally';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
export const scrollFirstColumnToTheLeft = (context: AlignBranchContext) => {
const firstColumnId = context.columns[0]?.id;

View file

@ -1,4 +1,4 @@
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/align-branch';
import { AlignBranchContext } from 'src/stores/view/subscriptions/effects/align-branch/helpers/create-context';
import { AlignBranchAction } from 'src/stores/view/subscriptions/effects/align-branch/create-align-branch-actions/create-align-branch-actions';
import { scrollFirstColumnToTheLeft } from 'src/stores/view/subscriptions/effects/align-branch/run-align-branch-actions/actions/scroll-first-column-to-the-left';
import { alignElementVertically } from 'src/lib/align-element/align-element-vertically';
@ -10,10 +10,12 @@ import { alignInactiveColumns } from 'src/stores/view/subscriptions/effects/alig
export const runAlignBranchActions = (
context: AlignBranchContext,
actions: AlignBranchAction[],
signal: AbortSignal,
) => {
actions = actions.sort((a, b) => a.action.localeCompare(b.action));
const activeNode = context.activeBranch.node;
for (const action of actions) {
if (signal.aborted) return;
const type = action.action;
if (type === '10/first-column/horizontal/move-to-the-left') {
scrollFirstColumnToTheLeft(context);

View file

@ -39,7 +39,7 @@ export const onDocumentStateUpdate = (
e.createOrDelete || e.dropOrMove || e.changeHistory || e.clipboard;
if (structuralChange) {
setActiveNode(view, action);
updateActiveBranch(viewStore, documentState, 'structure');
updateActiveBranch(viewStore, documentState, action);
viewStore.dispatch({
type: 'view/outline/refresh-collapsed-nodes',
payload: {
@ -72,9 +72,10 @@ export const onDocumentStateUpdate = (
}
// effects
if (structuralChange || e.content) {
if (e.content) {
view.alignBranch.align(action);
}
if (structuralChange || e.content) {
view.rulesProcessor.onDocumentUpdate(action);
}

View file

@ -44,7 +44,7 @@ export const onViewMount = (view: LineageView) => {
if (!view.file) return subscriptions;
setInitialActiveNode(view);
loadCollapsedSectionsFromSettings(view);
updateActiveBranch(viewStore, documentState, 'none');
updateActiveBranch(viewStore, documentState);
if (view.isActive && isEmptyDocument(documentState.document.content)) {
enableEditMode(viewStore, documentState);
}
@ -62,7 +62,6 @@ export const onViewMount = (view: LineageView) => {
view.rulesProcessor.onRulesUpdate();
view.zoomFactor = view.plugin.settings.getValue().view.zoomLevel;
view.alignBranch.align({ type: 'view/life-cycle/mount' });
subscriptions.add(watchViewSize(view));
return subscriptions;
};

View file

@ -39,7 +39,7 @@ export const onViewStateUpdate = (
}
if (activeNodeChange && activeNodeHasChanged) {
// this should be handled internally
updateActiveBranch(viewStore, documentState, 'none');
updateActiveBranch(viewStore, documentState);
persistActiveNodeInPluginSettings(view);
view.plugin.statusBar.updateProgressIndicatorAndChildCount(view);
}
@ -68,7 +68,12 @@ export const onViewStateUpdate = (
}
// effects
if (activeNodeChange || e.search || e.editMainSplit) {
if (
e.search ||
e.editMainSplit ||
action.type === 'view/update-active-branch?source=document' ||
action.type === 'view/update-active-branch?source=view'
) {
view.alignBranch.align(action);
}
if (!container || !view.isViewOfFile) return;

View file

@ -127,12 +127,10 @@ const updateDocumentState = (state: ViewState, action: ViewStoreAction) => {
onDragStart(state.document, action);
} else if (action.type === 'DOCUMENT/SET_DRAG_ENDED') {
onDragEnd(state.document);
} else if (action.type === 'UPDATE_ACTIVE_BRANCH') {
updateActiveBranch(
state.document,
action.payload.columns,
action.context.changeType,
);
} else if (action.type === 'view/update-active-branch?source=view') {
updateActiveBranch(state.document, action.context.columns, false);
} else if (action.type === 'view/update-active-branch?source=document') {
updateActiveBranch(state.document, action.context.columns, true);
} else if (action.type === 'NAVIGATION/NAVIGATE_FORWARD') {
navigateActiveNodeHistory(state.document, state, true);
} else if (action.type === 'NAVIGATION/NAVIGATE_BACK') {