Skip to content

Commit

Permalink
fix: Handle history pausing and resuming properly
Browse files Browse the repository at this point in the history
- Avoid duplicate snapshot
- Properly pause history handler while choosing color
  • Loading branch information
surajshetty3416 committed Dec 11, 2024
1 parent b5c0bd7 commit 66f41c2
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 62 deletions.
12 changes: 7 additions & 5 deletions frontend/src/components/BlockLayers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@
:title="element.blockId"
@contextmenu.prevent.stop="onContextMenu"
class="min-w-24 cursor-pointer overflow-hidden rounded border border-transparent bg-surface-white bg-opacity-50 text-base text-ink-gray-7"
@click.stop="
store.activeCanvas?.history.pause();
store.selectBlock(element, $event, false, true);
store.activeCanvas?.history.resume();
"
@click.stop="selectBlock(element, $event)"
@mouseover.stop="store.hoveredBlock = element.blockId"
@mouseleave.stop="store.hoveredBlock = null">
<span
Expand Down Expand Up @@ -207,6 +203,12 @@ const blockExitsInTree = (block: Block) => {
return false;
};
const selectBlock = (block: Block, event: MouseEvent) => {
const pauseId = store.activeCanvas?.history?.pause();
store.selectBlock(block, event, false, true);
pauseId && store.activeCanvas?.history?.resume(pauseId);
};
defineExpose({
toggleExpanded,
isExpandedInTree,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/BoxResizer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ onMounted(() => {
watch(resizing, () => {
if (resizing.value) {
store.activeCanvas?.history.pause();
store.activeCanvas?.history?.pause();
emit("resizing", true);
} else {
store.activeCanvas?.history.resume(true);
store.activeCanvas?.history?.resume(undefined, true, true);
emit("resizing", false);
}
});
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/BuilderCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ function setEvents() {
if (store.mode === "select") {
return;
} else {
canvasHistory.value?.pause();
const pauseId = canvasHistory.value?.pause();
ev.stopPropagation();
let element = document.elementFromPoint(ev.x, ev.y) as HTMLElement;
let block = getFirstBlock();
Expand Down Expand Up @@ -358,7 +358,7 @@ function setEvents() {
store.mode = "select";
}, 50);
if (store.mode === "text") {
canvasHistory.value?.resume(true);
pauseId && canvasHistory.value?.resume(pauseId, true);
store.editableBlock = childBlock;
return;
}
Expand All @@ -373,7 +373,7 @@ function setEvents() {
childBlock.setBaseStyle("height", "200px");
}
}
canvasHistory.value?.resume(true);
pauseId && canvasHistory.value?.resume(pauseId, true);
},
{ once: true },
);
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/Controls/ColorPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ const setHueSelectorPosition = (color: HashString) => {
const handleSelectorMove = (ev: MouseEvent) => {
setColor(ev);
const pauseId = store.activeCanvas?.history?.pause();
const mouseMove = (mouseMoveEvent: MouseEvent) => {
mouseMoveEvent.preventDefault();
setColor(mouseMoveEvent);
Expand All @@ -165,14 +166,15 @@ const handleSelectorMove = (ev: MouseEvent) => {
(mouseUpEvent) => {
document.removeEventListener("mousemove", mouseMove);
mouseUpEvent.preventDefault();
pauseId && store.activeCanvas?.history?.resume(pauseId, true);
},
{ once: true },
);
};
const handleHueSelectorMove = (ev: MouseEvent) => {
setHue(ev);
store.activeCanvas?.history.pause();
const pauseId = store.activeCanvas?.history?.pause();
const mouseMove = (mouseMoveEvent: MouseEvent) => {
mouseMoveEvent.preventDefault();
setHue(mouseMoveEvent);
Expand All @@ -183,7 +185,7 @@ const handleHueSelectorMove = (ev: MouseEvent) => {
(mouseUpEvent) => {
document.removeEventListener("mousemove", mouseMove);
mouseUpEvent.preventDefault();
store.activeCanvas?.history.resume(true);
pauseId && store.activeCanvas?.history?.resume(pauseId, true);
},
{ once: true },
);
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/TextBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<TextInput
v-model="textLink"
placeholder="https://example.com"
class="link-input focus:[&>input]:ring-outline-gray-3 w-56 text-sm [&>input]:border-outline-gray-1 [&>input]:bg-surface-gray-2 [&>input]:text-ink-gray-8 [&>input]:hover:!border-outline-gray-2 [&>input]:hover:!bg-surface-gray-1 focus:[&>input]:border-outline-gray-3 focus:[&>input]:bg-surface-gray-1 [&>input]:focus-visible:bg-surface-gray-1"
class="link-input w-56 text-sm [&>input]:border-outline-gray-1 [&>input]:bg-surface-gray-2 [&>input]:text-ink-gray-8 [&>input]:hover:!border-outline-gray-2 [&>input]:hover:!bg-surface-gray-1 focus:[&>input]:border-outline-gray-3 focus:[&>input]:bg-surface-gray-1 focus:[&>input]:ring-outline-gray-3 [&>input]:focus-visible:bg-surface-gray-1"
@keydown.enter="
() => {
if (!linkInput) return;
Expand Down Expand Up @@ -227,10 +227,10 @@ watch(
(editable) => {
editor.value?.setEditable(editable);
if (editable) {
store.activeCanvas?.history.pause();
store.activeCanvas?.history?.pause();
editor.value?.commands.focus("all");
} else {
store.activeCanvas?.history.resume(dataChanged.value);
store.activeCanvas?.history?.resume(undefined, dataChanged.value, true);
dataChanged.value = false;
}
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/PageBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,12 @@ useEventListener(document, "paste", async (e) => {
// TODO: Refactor with useMagicKeys
useEventListener(document, "keydown", (e) => {
if (isTargetEditable(e)) return;
if (e.key === "z" && isCtrlOrCmd(e) && !e.shiftKey && store.activeCanvas?.history.canUndo) {
if (e.key === "z" && isCtrlOrCmd(e) && !e.shiftKey && store.activeCanvas?.history?.canUndo) {
store.activeCanvas?.history.undo();
e.preventDefault();
return;
}
if (e.key === "z" && e.shiftKey && isCtrlOrCmd(e) && store.activeCanvas?.history.canRedo) {
if (e.key === "z" && e.shiftKey && isCtrlOrCmd(e) && store.activeCanvas?.history?.canRedo) {
store.activeCanvas?.history.redo();
e.preventDefault();
return;
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ const useStore = defineStore("store", {
scrollLayerIntoView: boolean | ScrollLogicalPosition = true,
scrollBlockIntoView = false,
) {
this.activeCanvas?.history?.pause();
if (this.settingPage) {
return;
}
Expand All @@ -234,8 +233,6 @@ const useStore = defineStore("store", {
?.scrollIntoView({ behavior: "instant", block: align, inline: "center" });
});
}

this.activeCanvas?.history?.resume();
this.editableBlock = null;
if (scrollBlockIntoView) {
this.activeCanvas?.scrollBlockIntoView(block);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/utils/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ class Block implements BlockOptions {
return;
}
const store = useStore();
store.activeCanvas?.history.pause();
const pauseId = store.activeCanvas?.history?.pause();
const blockCopy = getBlockCopy(this);
const parentBlock = this.getParentBlock();

Expand All @@ -756,8 +756,8 @@ class Block implements BlockOptions {
if (child) {
child.selectBlock();
}
store.activeCanvas?.history.resume(true);
});
pauseId && store.activeCanvas?.history?.resume(pauseId, true);
}
getPadding() {
const padding = this.getStyle("padding") || "0px";
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@ async function getFontName(file_url: string) {
return opentype.parse(await getFontArrayBuffer(file_url)).names.fullName.en;
}

function generateId() {
return Math.random().toString(36).substr(2, 9);
}

export {
addPxToNumber,
alert,
Expand All @@ -431,6 +435,7 @@ export {
dataURLtoFile,
detachBlockFromComponent,
findNearestSiblingIndex,
generateId,
getBlockCopy,
getBlockInstance,
getBlockObjectCopy as getBlockObject,
Expand Down
144 changes: 106 additions & 38 deletions frontend/src/utils/useCanvasHistory.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,82 @@
import Block from "@/utils/block";
import { getBlockInstance, getBlockObject } from "@/utils/helpers";
import { generateId, getBlockInstance, getBlockString } from "@/utils/helpers";
import { debounceFilter, pausableFilter, watchIgnorable } from "@vueuse/core";
import { nextTick, ref, Ref } from "vue";

type CanvasState = {
block: Block;
block: string;
selectedBlockIds: string[];
};
const CAPACITY = 50;

type PauseId = string & { __brand: "PauseId" };

const CAPACITY = 200;
const DEBOUNCE_DELAY = 200;

export function useCanvasHistory(source: Ref<Block>, selectedBlockIds: Ref<string[]>) {
const undoStack = ref([]) as Ref<CanvasState[]>;
const redoStack = ref([]) as Ref<CanvasState[]>;
const last = ref(createHistoryRecord(source, selectedBlockIds));
const pauseIdSet = new Set<PauseId>();

const {
eventFilter: composedFilter,
pause,
resume: resumeTracking,
eventFilter: blockWatcherFilter,
pause: pauseBlockWatcher,
resume: resumeBlockWatcher,
isActive: isTracking,
} = pausableFilter(debounceFilter(DEBOUNCE_DELAY));

const { ignoreUpdates, ignorePrevAsyncUpdates, stop } = watchIgnorable(source, commit, {
const {
eventFilter: selectionWatherFilter,
pause: pauseSelectionWatcher,
resume: resumeSelectionWatcher,
} = pausableFilter();

function commit() {
// console.log("committing...");
undoStack.value.unshift(last.value);
last.value = createHistoryRecord(source, selectedBlockIds);
if (undoStack.value.length > CAPACITY) {
undoStack.value.splice(CAPACITY, Number.POSITIVE_INFINITY);
}
if (redoStack.value.length) {
redoStack.value.splice(0, redoStack.value.length);
}
}

// const debouncedCommit = useDebounceFn(commit, DEBOUNCE_DELAY);

const {
ignoreUpdates: ignoreBlockUpdates,
ignorePrevAsyncUpdates: ignorePrevAsyncBlockUpdates,
stop: stopBlockWatcher,
} = watchIgnorable(source, commit, {
deep: true,
flush: "post",
eventFilter: blockWatcherFilter,
});

const {
ignoreUpdates: ignoreSelectedBlockUpdates,
ignorePrevAsyncUpdates: ignorePrevSelectedBlockUpdates,
stop: stopSelectedBlockUpdates,
} = watchIgnorable(selectedBlockIds, updateSelections, {
deep: true,
flush: "post",
eventFilter: composedFilter,
eventFilter: selectionWatherFilter,
});

function setSource(value: string) {
const obj = JSON.parse(value) as CanvasState;
ignorePrevAsyncUpdates();
ignoreUpdates(() => {
source.value = getBlockInstance(obj.block);
selectedBlockIds.value = obj.selectedBlockIds;
function setSource(value: CanvasState) {
ignorePrevAsyncBlockUpdates();
ignoreBlockUpdates(() => {
source.value = getBlockInstance(value.block);
});
}
const last = ref(createHistoryRecord(source, selectedBlockIds));
function commit() {
nextTick(() => {
undoStack.value.unshift(last.value);
last.value = createHistoryRecord(source, selectedBlockIds);
if (undoStack.value.length > CAPACITY) {
undoStack.value.splice(CAPACITY, Number.POSITIVE_INFINITY);
}
if (redoStack.value.length) {
redoStack.value.splice(0, redoStack.value.length);
}
ignorePrevSelectedBlockUpdates();
ignoreSelectedBlockUpdates(() => {
selectedBlockIds.value = [...value.selectedBlockIds];
});
last.value = value;
}
const undoStack = ref([]) as Ref<string[]>;
const redoStack = ref([]) as Ref<string[]>;

function undo() {
const state = undoStack.value.shift();
Expand Down Expand Up @@ -74,11 +104,6 @@ export function useCanvasHistory(source: Ref<Block>, selectedBlockIds: Ref<strin
clear();
}

function resume(commitNow?: boolean) {
resumeTracking();
if (commitNow) commit();
}

function canUndo() {
return undoStack.value.length > 0;
}
Expand All @@ -87,23 +112,66 @@ export function useCanvasHistory(source: Ref<Block>, selectedBlockIds: Ref<strin
return redoStack.value.length > 0;
}

function updateSelections() {
nextTick(() => {
last.value.selectedBlockIds = [...selectedBlockIds.value];
});
}

function pause() {
pauseBlockWatcher();
pauseSelectionWatcher();
const pauseId = generateId() as PauseId;
pauseIdSet.add(pauseId);
// console.log("\npausing...", pauseId);
return pauseId as PauseId;
}

function resume(pauseId?: PauseId, commitNow?: boolean, force?: boolean) {
nextTick(() => {
// console.log("resuming...", pauseId);
if (pauseId && pauseIdSet.has(pauseId)) {
pauseIdSet.delete(pauseId);
} else if (!force) {
return;
}

if (pauseIdSet.size && !force) {
return;
}
resumeTracking();
if (commitNow) commit();
});
}

function resumeTracking() {
resumeBlockWatcher();
resumeSelectionWatcher();
}

function stop() {
stopBlockWatcher();
stopSelectedBlockUpdates();
}

return {
undo,
redo,
dispose,
pause,
resumeTracking,
resume,
isTracking,
canUndo,
canRedo,
isTracking,
batch: () => {},
undoStack,
redoStack,
};
}

function createHistoryRecord(source: Ref<Block>, selectedBlockIds: Ref<string[]>) {
return JSON.stringify({
block: getBlockObject(source.value),
return {
block: getBlockString(source.value),
selectedBlockIds: selectedBlockIds.value,
});
};
}
Loading

0 comments on commit 66f41c2

Please sign in to comment.