fix keepalive blink cut short
This commit is contained in:
@@ -12,9 +12,7 @@ export function Typewriter(props: {
|
|||||||
let cursorRef: HTMLDivElement | undefined;
|
let cursorRef: HTMLDivElement | undefined;
|
||||||
const [isTyping, setIsTyping] = createSignal(false);
|
const [isTyping, setIsTyping] = createSignal(false);
|
||||||
const [isDelaying, setIsDelaying] = createSignal(delay > 0);
|
const [isDelaying, setIsDelaying] = createSignal(delay > 0);
|
||||||
const [keepAliveCountdown, setKeepAliveCountdown] = createSignal(
|
const [shouldHide, setShouldHide] = createSignal(false);
|
||||||
typeof keepAlive === "number" ? keepAlive : -1
|
|
||||||
);
|
|
||||||
const resolved = children(() => props.children);
|
const resolved = children(() => props.children);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -67,6 +65,12 @@ export function Typewriter(props: {
|
|||||||
cursorRef.style.height = `${firstChar.offsetHeight}px`;
|
cursorRef.style.height = `${firstChar.offsetHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for animation end to hide cursor
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
setShouldHide(true);
|
||||||
|
cursorRef?.removeEventListener("animationend", handleAnimationEnd);
|
||||||
|
};
|
||||||
|
|
||||||
const startReveal = () => {
|
const startReveal = () => {
|
||||||
setIsTyping(true); // Switch to typing cursor
|
setIsTyping(true); // Switch to typing cursor
|
||||||
|
|
||||||
@@ -102,17 +106,17 @@ export function Typewriter(props: {
|
|||||||
// Typing finished, switch to block cursor
|
// Typing finished, switch to block cursor
|
||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
|
|
||||||
// Start keepAlive countdown if it's a number
|
// Start keepAlive timer if it's a number
|
||||||
if (typeof keepAlive === "number") {
|
if (typeof keepAlive === "number") {
|
||||||
const keepAliveInterval = setInterval(() => {
|
// Attach animation end listener
|
||||||
setKeepAliveCountdown((prev) => {
|
cursorRef?.addEventListener("animationend", handleAnimationEnd);
|
||||||
if (prev <= 1000) {
|
|
||||||
clearInterval(keepAliveInterval);
|
// Trigger the animation with finite iteration count
|
||||||
return 0;
|
const durationSeconds = keepAlive / 1000;
|
||||||
|
const iterations = Math.ceil(durationSeconds);
|
||||||
|
if (cursorRef) {
|
||||||
|
cursorRef.style.animation = `blink 1s ${iterations}`;
|
||||||
}
|
}
|
||||||
return prev - 1000;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,22 +136,16 @@ export function Typewriter(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getCursorClass = () => {
|
const getCursorClass = () => {
|
||||||
if (isDelaying()) return "cursor-block"; // Blinking block during delay
|
if (isDelaying()) return "cursor-block";
|
||||||
if (isTyping()) return "cursor-typing"; // Thin line while typing
|
if (isTyping()) return "cursor-typing";
|
||||||
|
if (shouldHide()) return "hidden";
|
||||||
// After typing is done
|
|
||||||
if (typeof keepAlive === "number") {
|
|
||||||
return keepAliveCountdown() > 0 ? "cursor-block" : "hidden";
|
|
||||||
}
|
|
||||||
return keepAlive ? "cursor-block" : "hidden";
|
return keepAlive ? "cursor-block" : "hidden";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} class={props.class}>
|
<div ref={containerRef} class={props.class}>
|
||||||
{resolved()}
|
{resolved()}
|
||||||
<span ref={cursorRef} class={getCursorClass()}>
|
<span ref={cursorRef} class={getCursorClass()}></span>
|
||||||
{" "}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,3 +80,34 @@ export function insertSoftHyphens(
|
|||||||
})
|
})
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a debounced function that delays execution until after specified delay
|
||||||
|
* @param fn - The function to debounce
|
||||||
|
* @param delay - Delay in milliseconds
|
||||||
|
* @returns Debounced function with cancel method
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
fn: T,
|
||||||
|
delay: number
|
||||||
|
): T & { cancel: () => void } {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const debounced = function (this: any, ...args: Parameters<T>) {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}, delay);
|
||||||
|
} as T & { cancel: () => void };
|
||||||
|
|
||||||
|
debounced.cancel = () => {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Show, createSignal, onCleanup } from "solid-js";
|
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
|
||||||
import { useNavigate, query } from "@solidjs/router";
|
import { useNavigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
import { debounce } from "~/lib/client-utils";
|
||||||
import Dropzone from "~/components/blog/Dropzone";
|
import Dropzone from "~/components/blog/Dropzone";
|
||||||
import TextEditor from "~/components/blog/TextEditor";
|
import TextEditor from "~/components/blog/TextEditor";
|
||||||
import TagMaker from "~/components/blog/TagMaker";
|
import TagMaker from "~/components/blog/TagMaker";
|
||||||
@@ -42,8 +43,6 @@ export default function CreatePost() {
|
|||||||
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
|
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
|
||||||
const [hasSaved, setHasSaved] = createSignal(false);
|
const [hasSaved, setHasSaved] = createSignal(false);
|
||||||
|
|
||||||
let autosaveInterval: number | undefined;
|
|
||||||
|
|
||||||
const autoSave = async () => {
|
const autoSave = async () => {
|
||||||
const titleVal = title();
|
const titleVal = title();
|
||||||
const bodyVal = body();
|
const bodyVal = body();
|
||||||
@@ -90,18 +89,26 @@ export default function CreatePost() {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up autosave interval (2 minutes)
|
// Debounced auto-save (1 second after last change)
|
||||||
autosaveInterval = setInterval(
|
const debouncedAutoSave = debounce(autoSave, 1000);
|
||||||
() => {
|
|
||||||
autoSave();
|
// Track changes to trigger auto-save
|
||||||
},
|
createEffect(() => {
|
||||||
2 * 60 * 1000
|
// Track all relevant fields
|
||||||
) as unknown as number;
|
const titleVal = title();
|
||||||
|
const subtitleVal = subtitle();
|
||||||
|
const bodyVal = body();
|
||||||
|
const tagsVal = tags();
|
||||||
|
const publishedVal = published();
|
||||||
|
|
||||||
|
// Only trigger auto-save if we have at least title and body
|
||||||
|
if (titleVal && bodyVal) {
|
||||||
|
debouncedAutoSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (autosaveInterval) {
|
debouncedAutoSave.cancel();
|
||||||
clearInterval(autosaveInterval);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createAsync } from "@solidjs/router";
|
|||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
import { debounce } from "~/lib/client-utils";
|
||||||
import { ConnectionFactory } from "~/server/utils";
|
import { ConnectionFactory } from "~/server/utils";
|
||||||
import Dropzone from "~/components/blog/Dropzone";
|
import Dropzone from "~/components/blog/Dropzone";
|
||||||
import TextEditor from "~/components/blog/TextEditor";
|
import TextEditor from "~/components/blog/TextEditor";
|
||||||
@@ -60,8 +61,7 @@ export default function EditPost() {
|
|||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
|
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
|
||||||
|
const [isInitialLoad, setIsInitialLoad] = createSignal(true);
|
||||||
let autosaveInterval: number | undefined;
|
|
||||||
|
|
||||||
// Populate form when data loads
|
// Populate form when data loads
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -78,6 +78,9 @@ export default function EditPost() {
|
|||||||
const tagValues = (postData.tags as any[]).map((t) => t.value);
|
const tagValues = (postData.tags as any[]).map((t) => t.value);
|
||||||
setTags(tagValues);
|
setTags(tagValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark initial load as complete after data is loaded
|
||||||
|
setIsInitialLoad(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,18 +130,26 @@ export default function EditPost() {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up autosave interval (2 minutes)
|
// Debounced auto-save (1 second after last change)
|
||||||
autosaveInterval = setInterval(
|
const debouncedAutoSave = debounce(autoSave, 1000);
|
||||||
() => {
|
|
||||||
autoSave();
|
// Track changes to trigger auto-save (but not on initial load)
|
||||||
},
|
createEffect(() => {
|
||||||
2 * 60 * 1000
|
// Track all relevant fields
|
||||||
) as unknown as number;
|
const titleVal = title();
|
||||||
|
const subtitleVal = subtitle();
|
||||||
|
const bodyVal = body();
|
||||||
|
const tagsVal = tags();
|
||||||
|
const publishedVal = published();
|
||||||
|
|
||||||
|
// Only trigger auto-save if not initial load and we have title
|
||||||
|
if (!isInitialLoad() && titleVal) {
|
||||||
|
debouncedAutoSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (autosaveInterval) {
|
debouncedAutoSave.cancel();
|
||||||
clearInterval(autosaveInterval);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function Home() {
|
|||||||
<div>My Collection of By-the-ways:</div>
|
<div>My Collection of By-the-ways:</div>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
<Typewriter speed={50} keepAlive={false}>
|
||||||
<ul class="list-disc pl-8">
|
<ul class="list-disc pr-8">
|
||||||
<li>I use Neovim</li>
|
<li>I use Neovim</li>
|
||||||
<li>I use Arch Linux</li>
|
<li>I use Arch Linux</li>
|
||||||
<li>I use Rust</li>
|
<li>I use Rust</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user