mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(views/board): set up differential renderer
This commit is contained in:
parent
4826898c55
commit
d98be19c9a
@ -0,0 +1,327 @@
|
||||
import { BoardDragHandler, DragContext } from "./drag_handler";
|
||||
import BoardApi from "./api";
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import ViewModeStorage from "../view_mode_storage";
|
||||
import { BoardData } from "./config";
|
||||
|
||||
export interface BoardState {
|
||||
columns: { [key: string]: { note: any; branch: any }[] };
|
||||
columnOrder: string[];
|
||||
}
|
||||
|
||||
export class DifferentialBoardRenderer {
|
||||
private $container: JQuery<HTMLElement>;
|
||||
private api: BoardApi;
|
||||
private dragHandler: BoardDragHandler;
|
||||
private lastState: BoardState | null = null;
|
||||
private onCreateNewItem: (column: string) => void;
|
||||
private updateTimeout: number | null = null;
|
||||
private pendingUpdate = false;
|
||||
private parentNote: FNote;
|
||||
private viewStorage: ViewModeStorage<BoardData>;
|
||||
|
||||
constructor(
|
||||
$container: JQuery<HTMLElement>,
|
||||
api: BoardApi,
|
||||
dragHandler: BoardDragHandler,
|
||||
onCreateNewItem: (column: string) => void,
|
||||
parentNote: FNote,
|
||||
viewStorage: ViewModeStorage<BoardData>
|
||||
) {
|
||||
this.$container = $container;
|
||||
this.api = api;
|
||||
this.dragHandler = dragHandler;
|
||||
this.onCreateNewItem = onCreateNewItem;
|
||||
this.parentNote = parentNote;
|
||||
this.viewStorage = viewStorage;
|
||||
}
|
||||
|
||||
async renderBoard(refreshApi: boolean = false): Promise<void> {
|
||||
// Refresh API data if requested
|
||||
if (refreshApi) {
|
||||
this.api = await BoardApi.build(this.parentNote, this.viewStorage);
|
||||
this.dragHandler.updateApi(this.api);
|
||||
}
|
||||
|
||||
// Debounce rapid updates
|
||||
if (this.updateTimeout) {
|
||||
clearTimeout(this.updateTimeout);
|
||||
}
|
||||
|
||||
this.updateTimeout = window.setTimeout(async () => {
|
||||
await this.performUpdate();
|
||||
this.updateTimeout = null;
|
||||
}, 16); // ~60fps
|
||||
}
|
||||
|
||||
private async performUpdate(): Promise<void> {
|
||||
const currentState = this.getCurrentState();
|
||||
|
||||
if (!this.lastState) {
|
||||
// First render - do full render
|
||||
await this.fullRender(currentState);
|
||||
} else {
|
||||
// Differential render - only update what changed
|
||||
await this.differentialRender(this.lastState, currentState);
|
||||
}
|
||||
|
||||
this.lastState = currentState;
|
||||
}
|
||||
|
||||
private getCurrentState(): BoardState {
|
||||
const columns: { [key: string]: { note: any; branch: any }[] } = {};
|
||||
const columnOrder: string[] = [];
|
||||
|
||||
for (const column of this.api.columns) {
|
||||
columnOrder.push(column);
|
||||
columns[column] = this.api.getColumn(column) || [];
|
||||
}
|
||||
|
||||
return { columns, columnOrder };
|
||||
}
|
||||
|
||||
private async fullRender(state: BoardState): Promise<void> {
|
||||
this.$container.empty();
|
||||
|
||||
for (const column of state.columnOrder) {
|
||||
const columnItems = state.columns[column];
|
||||
const $columnEl = this.createColumn(column, columnItems);
|
||||
this.$container.append($columnEl);
|
||||
}
|
||||
|
||||
this.addAddColumnButton();
|
||||
}
|
||||
|
||||
private async differentialRender(oldState: BoardState, newState: BoardState): Promise<void> {
|
||||
// Store scroll positions before making changes
|
||||
const scrollPositions = this.saveScrollPositions();
|
||||
|
||||
// Handle column additions/removals
|
||||
this.updateColumns(oldState, newState);
|
||||
|
||||
// Handle card updates within existing columns
|
||||
for (const column of newState.columnOrder) {
|
||||
this.updateColumnCards(column, oldState.columns[column] || [], newState.columns[column]);
|
||||
}
|
||||
|
||||
// Restore scroll positions
|
||||
this.restoreScrollPositions(scrollPositions);
|
||||
}
|
||||
|
||||
private saveScrollPositions(): { [column: string]: number } {
|
||||
const positions: { [column: string]: number } = {};
|
||||
this.$container.find('.board-column').each((_, el) => {
|
||||
const column = $(el).attr('data-column');
|
||||
if (column) {
|
||||
positions[column] = el.scrollTop;
|
||||
}
|
||||
});
|
||||
return positions;
|
||||
}
|
||||
|
||||
private restoreScrollPositions(positions: { [column: string]: number }): void {
|
||||
this.$container.find('.board-column').each((_, el) => {
|
||||
const column = $(el).attr('data-column');
|
||||
if (column && positions[column] !== undefined) {
|
||||
el.scrollTop = positions[column];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateColumns(oldState: BoardState, newState: BoardState): void {
|
||||
// Remove columns that no longer exist
|
||||
for (const oldColumn of oldState.columnOrder) {
|
||||
if (!newState.columnOrder.includes(oldColumn)) {
|
||||
this.$container.find(`[data-column="${oldColumn}"]`).remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add new columns
|
||||
for (const newColumn of newState.columnOrder) {
|
||||
if (!oldState.columnOrder.includes(newColumn)) {
|
||||
const columnItems = newState.columns[newColumn];
|
||||
const $columnEl = this.createColumn(newColumn, columnItems);
|
||||
|
||||
// Insert at correct position
|
||||
const insertIndex = newState.columnOrder.indexOf(newColumn);
|
||||
const $existingColumns = this.$container.find('.board-column');
|
||||
|
||||
if (insertIndex === 0) {
|
||||
this.$container.prepend($columnEl);
|
||||
} else if (insertIndex >= $existingColumns.length) {
|
||||
this.$container.find('.board-add-column').before($columnEl);
|
||||
} else {
|
||||
$($existingColumns[insertIndex - 1]).after($columnEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateColumnCards(column: string, oldCards: { note: any; branch: any }[], newCards: { note: any; branch: any }[]): void {
|
||||
const $column = this.$container.find(`[data-column="${column}"]`);
|
||||
if (!$column.length) return;
|
||||
|
||||
const $cardContainer = $column;
|
||||
const oldCardIds = oldCards.map(item => item.note.noteId);
|
||||
const newCardIds = newCards.map(item => item.note.noteId);
|
||||
|
||||
// Remove cards that no longer exist
|
||||
$cardContainer.find('.board-note').each((_, el) => {
|
||||
const noteId = $(el).attr('data-note-id');
|
||||
if (noteId && !newCardIds.includes(noteId)) {
|
||||
$(el).addClass('fade-out');
|
||||
setTimeout(() => $(el).remove(), 150);
|
||||
}
|
||||
});
|
||||
|
||||
// Add or update cards
|
||||
for (let i = 0; i < newCards.length; i++) {
|
||||
const item = newCards[i];
|
||||
const noteId = item.note.noteId;
|
||||
let $existingCard = $cardContainer.find(`[data-note-id="${noteId}"]`);
|
||||
|
||||
if ($existingCard.length) {
|
||||
// Update existing card if title changed
|
||||
const currentTitle = $existingCard.text().trim();
|
||||
if (currentTitle !== item.note.title) {
|
||||
$existingCard.contents().filter(function() {
|
||||
return this.nodeType === 3; // Text nodes
|
||||
}).remove();
|
||||
$existingCard.append(item.note.title);
|
||||
}
|
||||
|
||||
// Ensure card is in correct position
|
||||
this.ensureCardPosition($existingCard, i, $cardContainer);
|
||||
} else {
|
||||
// Create new card
|
||||
const $newCard = this.createCard(item.note, item.branch, column);
|
||||
$newCard.addClass('fade-in').css('opacity', '0');
|
||||
|
||||
// Insert at correct position
|
||||
if (i === 0) {
|
||||
$cardContainer.find('h3').after($newCard);
|
||||
} else {
|
||||
const $prevCard = $cardContainer.find('.board-note').eq(i - 1);
|
||||
if ($prevCard.length) {
|
||||
$prevCard.after($newCard);
|
||||
} else {
|
||||
$cardContainer.find('.board-new-item').before($newCard);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger fade in animation
|
||||
setTimeout(() => $newCard.css('opacity', '1'), 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ensureCardPosition($card: JQuery<HTMLElement>, targetIndex: number, $container: JQuery<HTMLElement>): void {
|
||||
const $allCards = $container.find('.board-note');
|
||||
const currentIndex = $allCards.index($card);
|
||||
|
||||
if (currentIndex !== targetIndex) {
|
||||
if (targetIndex === 0) {
|
||||
$container.find('h3').after($card);
|
||||
} else {
|
||||
const $targetPrev = $allCards.eq(targetIndex - 1);
|
||||
if ($targetPrev.length) {
|
||||
$targetPrev.after($card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createColumn(column: string, columnItems: { note: any; branch: any }[]): JQuery<HTMLElement> {
|
||||
const $columnEl = $("<div>")
|
||||
.addClass("board-column")
|
||||
.attr("data-column", column);
|
||||
|
||||
// Create header
|
||||
const $titleEl = $("<h3>").attr("data-column-value", column);
|
||||
const $titleText = $("<span>").text(column);
|
||||
const $editIcon = $("<span>")
|
||||
.addClass("edit-icon icon bx bx-edit-alt")
|
||||
.attr("title", "Click to edit column title");
|
||||
$titleEl.append($titleText, $editIcon);
|
||||
$columnEl.append($titleEl);
|
||||
|
||||
// Handle wheel events for scrolling
|
||||
$columnEl.on("wheel", (event) => {
|
||||
const el = $columnEl[0];
|
||||
const needsScroll = el.scrollHeight > el.clientHeight;
|
||||
if (needsScroll) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
// Setup drop zone
|
||||
this.dragHandler.setupColumnDropZone($columnEl, column);
|
||||
|
||||
// Add cards
|
||||
for (const item of columnItems) {
|
||||
if (item.note) {
|
||||
const $noteEl = this.createCard(item.note, item.branch, column);
|
||||
$columnEl.append($noteEl);
|
||||
}
|
||||
}
|
||||
|
||||
// Add "New item" button
|
||||
const $newItemEl = $("<div>")
|
||||
.addClass("board-new-item")
|
||||
.attr("data-column", column)
|
||||
.html('<span class="icon bx bx-plus"></span>New item');
|
||||
|
||||
$newItemEl.on("click", () => this.onCreateNewItem(column));
|
||||
$columnEl.append($newItemEl);
|
||||
|
||||
return $columnEl;
|
||||
}
|
||||
|
||||
private createCard(note: any, branch: any, column: string): JQuery<HTMLElement> {
|
||||
const $iconEl = $("<span>")
|
||||
.addClass("icon")
|
||||
.addClass(note.getIcon());
|
||||
|
||||
const $noteEl = $("<div>")
|
||||
.addClass("board-note")
|
||||
.attr("data-note-id", note.noteId)
|
||||
.attr("data-branch-id", branch.branchId)
|
||||
.attr("data-current-column", column)
|
||||
.text(note.title);
|
||||
|
||||
$noteEl.prepend($iconEl);
|
||||
$noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }));
|
||||
|
||||
// Setup drag functionality
|
||||
this.dragHandler.setupNoteDrag($noteEl, note, branch);
|
||||
|
||||
return $noteEl;
|
||||
}
|
||||
|
||||
private addAddColumnButton(): void {
|
||||
if (this.$container.find('.board-add-column').length === 0) {
|
||||
const $addColumnEl = $("<div>")
|
||||
.addClass("board-add-column")
|
||||
.html('<span class="icon bx bx-plus"></span>Add Column');
|
||||
|
||||
this.$container.append($addColumnEl);
|
||||
}
|
||||
}
|
||||
|
||||
forceFullRender(): void {
|
||||
this.lastState = null;
|
||||
if (this.updateTimeout) {
|
||||
clearTimeout(this.updateTimeout);
|
||||
this.updateTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
async flushPendingUpdates(): Promise<void> {
|
||||
if (this.updateTimeout) {
|
||||
clearTimeout(this.updateTimeout);
|
||||
this.updateTimeout = null;
|
||||
await this.performUpdate();
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,10 @@ export class BoardDragHandler {
|
||||
this.setupTouchDrag($noteEl, note, branch);
|
||||
}
|
||||
|
||||
updateApi(newApi: BoardApi) {
|
||||
this.api = newApi;
|
||||
}
|
||||
|
||||
private setupMouseDrag($noteEl: JQuery<HTMLElement>, note: any, branch: any) {
|
||||
$noteEl.on("dragstart", (e) => {
|
||||
this.context.draggedNote = note;
|
||||
|
@ -8,6 +8,7 @@ import SpacedUpdate from "../../../services/spaced_update";
|
||||
import { setupContextMenu } from "./context_menu";
|
||||
import BoardApi from "./api";
|
||||
import { BoardDragHandler, DragContext } from "./drag_handler";
|
||||
import { DifferentialBoardRenderer } from "./differential_renderer";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="board-view">
|
||||
@ -104,7 +105,26 @@ const TPL = /*html*/`
|
||||
position: relative;
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-view-container .board-note.fade-in {
|
||||
animation: fadeIn 0.15s ease-in;
|
||||
}
|
||||
|
||||
.board-view-container .board-note.fade-out {
|
||||
animation: fadeOut 0.15s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.board-view-container .board-note:hover {
|
||||
@ -223,6 +243,7 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
private persistentData: BoardData;
|
||||
private api?: BoardApi;
|
||||
private dragHandler?: BoardDragHandler;
|
||||
private renderer?: DifferentialBoardRenderer;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args, "board");
|
||||
@ -244,13 +265,17 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
}
|
||||
|
||||
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
|
||||
this.$container.empty();
|
||||
await this.renderBoard(this.$container[0]);
|
||||
|
||||
if (!this.renderer) {
|
||||
// First time setup
|
||||
this.$container.empty();
|
||||
await this.initializeRenderer();
|
||||
}
|
||||
|
||||
await this.renderer!.renderBoard();
|
||||
return this.$root;
|
||||
}
|
||||
|
||||
private async renderBoard(el: HTMLElement) {
|
||||
private async initializeRenderer() {
|
||||
this.api = await BoardApi.build(this.parentNote, this.viewStorage);
|
||||
this.dragHandler = new BoardDragHandler(
|
||||
this.$container,
|
||||
@ -259,100 +284,41 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
async () => { await this.renderList(); }
|
||||
);
|
||||
|
||||
this.renderer = new DifferentialBoardRenderer(
|
||||
this.$container,
|
||||
this.api,
|
||||
this.dragHandler,
|
||||
(column: string) => this.createNewItem(column),
|
||||
this.parentNote,
|
||||
this.viewStorage
|
||||
);
|
||||
|
||||
setupContextMenu({
|
||||
$container: this.$container,
|
||||
api: this.api
|
||||
});
|
||||
|
||||
for (const column of this.api.columns) {
|
||||
const columnItems = this.api.getColumn(column);
|
||||
if (!columnItems) {
|
||||
continue;
|
||||
}
|
||||
// Setup column title editing and add column functionality
|
||||
this.setupBoardInteractions();
|
||||
}
|
||||
|
||||
// Find the column data to get custom title
|
||||
const columnTitle = column;
|
||||
|
||||
const $columnEl = $("<div>")
|
||||
.addClass("board-column")
|
||||
.attr("data-column", column);
|
||||
|
||||
const $titleEl = $("<h3>")
|
||||
.attr("data-column-value", column);
|
||||
|
||||
const { $titleText, $editIcon } = this.createTitleStructure(columnTitle);
|
||||
$titleEl.append($titleText, $editIcon);
|
||||
|
||||
// Make column title editable
|
||||
this.setupColumnTitleEdit($titleEl, column, columnItems);
|
||||
|
||||
$columnEl.append($titleEl);
|
||||
|
||||
// Allow vertical scrolling in the column, bypassing the horizontal scroll of the container.
|
||||
$columnEl.on("wheel", (event) => {
|
||||
const el = $columnEl[0];
|
||||
const needsScroll = el.scrollHeight > el.clientHeight;
|
||||
if (needsScroll) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
// Setup drop zone for the column
|
||||
this.dragHandler!.setupColumnDropZone($columnEl, column);
|
||||
|
||||
for (const item of columnItems) {
|
||||
const note = item.note;
|
||||
const branch = item.branch;
|
||||
if (!note) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $iconEl = $("<span>")
|
||||
.addClass("icon")
|
||||
.addClass(note.getIcon());
|
||||
|
||||
const $noteEl = $("<div>")
|
||||
.addClass("board-note")
|
||||
.attr("data-note-id", note.noteId)
|
||||
.attr("data-branch-id", branch.branchId)
|
||||
.attr("data-current-column", column)
|
||||
.text(note.title);
|
||||
|
||||
$noteEl.prepend($iconEl);
|
||||
$noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }));
|
||||
|
||||
// Setup drag functionality for the note
|
||||
this.dragHandler!.setupNoteDrag($noteEl, note, branch);
|
||||
|
||||
$columnEl.append($noteEl);
|
||||
}
|
||||
|
||||
// Add "New item" link at the bottom of the column
|
||||
const $newItemEl = $("<div>")
|
||||
.addClass("board-new-item")
|
||||
.attr("data-column", column)
|
||||
.html('<span class="icon bx bx-plus"></span>New item');
|
||||
|
||||
$newItemEl.on("click", () => {
|
||||
this.createNewItem(column);
|
||||
});
|
||||
|
||||
$columnEl.append($newItemEl);
|
||||
|
||||
$(el).append($columnEl);
|
||||
}
|
||||
|
||||
// Add "Add Column" button at the end
|
||||
const $addColumnEl = $("<div>")
|
||||
.addClass("board-add-column")
|
||||
.html('<span class="icon bx bx-plus"></span>Add Column');
|
||||
|
||||
$addColumnEl.on("click", (e) => {
|
||||
private setupBoardInteractions() {
|
||||
// Handle column title editing
|
||||
this.$container.on('click', 'h3[data-column-value]', (e) => {
|
||||
e.stopPropagation();
|
||||
this.startCreatingNewColumn($addColumnEl);
|
||||
const $titleEl = $(e.currentTarget);
|
||||
const columnValue = $titleEl.attr('data-column-value');
|
||||
if (columnValue) {
|
||||
const columnItems = this.api?.getColumn(columnValue) || [];
|
||||
this.startEditingColumnTitle($titleEl, columnValue, columnItems);
|
||||
}
|
||||
});
|
||||
|
||||
$(el).append($addColumnEl);
|
||||
// Handle add column button
|
||||
this.$container.on('click', '.board-add-column', (e) => {
|
||||
e.stopPropagation();
|
||||
this.startCreatingNewColumn($(e.currentTarget));
|
||||
});
|
||||
}
|
||||
|
||||
private createTitleStructure(title: string): { $titleText: JQuery<HTMLElement>; $editIcon: JQuery<HTMLElement> } {
|
||||
@ -364,13 +330,6 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
return { $titleText, $editIcon };
|
||||
}
|
||||
|
||||
private setupColumnTitleEdit($titleEl: JQuery<HTMLElement>, columnValue: string, columnItems: { branch: any; note: any; }[]) {
|
||||
$titleEl.on("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.startEditingColumnTitle($titleEl, columnValue, columnItems);
|
||||
});
|
||||
}
|
||||
|
||||
private startEditingColumnTitle($titleEl: JQuery<HTMLElement>, columnValue: string, columnItems: { branch: any; note: any; }[]) {
|
||||
if ($titleEl.hasClass("editing")) {
|
||||
return; // Already editing
|
||||
@ -461,6 +420,11 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
}
|
||||
}
|
||||
|
||||
forceFullRefresh() {
|
||||
this.renderer?.forceFullRender();
|
||||
return this.renderList();
|
||||
}
|
||||
|
||||
private startCreatingNewColumn($addColumnEl: JQuery<HTMLElement>) {
|
||||
if ($addColumnEl.hasClass("editing")) {
|
||||
return; // Already editing
|
||||
@ -535,26 +499,23 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
}
|
||||
|
||||
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// React to changes in "status" attribute for notes in this board
|
||||
if (loadResults.getAttributeRows().some(attr => attr.name === "status" && this.noteIds.includes(attr.noteId!))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// React to changes in note title.
|
||||
if (loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
|
||||
if (loadResults.getBranchRows().some(branch => this.noteIds.includes(branch.noteId!))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// React to attachment change.
|
||||
if (loadResults.getAttachmentRows().some(att => att.ownerId === this.parentNote.noteId && att.title === "board.json")) {
|
||||
return true;
|
||||
// Check if any changes affect our board
|
||||
const hasRelevantChanges =
|
||||
// React to changes in "status" attribute for notes in this board
|
||||
loadResults.getAttributeRows().some(attr => attr.name === "status" && this.noteIds.includes(attr.noteId!)) ||
|
||||
// React to changes in note title
|
||||
loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)) ||
|
||||
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
|
||||
loadResults.getBranchRows().some(branch => this.noteIds.includes(branch.noteId!)) ||
|
||||
// React to attachment change
|
||||
loadResults.getAttachmentRows().some(att => att.ownerId === this.parentNote.noteId && att.title === "board.json");
|
||||
|
||||
if (hasRelevantChanges && this.renderer) {
|
||||
// Use differential rendering with API refresh
|
||||
await this.renderer.renderBoard(true);
|
||||
}
|
||||
|
||||
// Don't trigger full view refresh - let differential renderer handle it
|
||||
return false;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user