Cleanup unused stuff
This commit is contained in:
parent
01527d450b
commit
9f4ef62961
9 changed files with 13 additions and 558 deletions
|
|
@ -1,10 +1,20 @@
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { chats, db } from "@/lib/db/schema";
|
import { chats, db } from "@/lib/db/schema";
|
||||||
import { openai } from "@/lib/openai";
|
|
||||||
import { OpenAIStream, StreamingTextResponse } from "ai-connector";
|
import { OpenAIStream, StreamingTextResponse } from "ai-connector";
|
||||||
|
import { Configuration, OpenAIApi } from "openai-edge";
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = "edge";
|
||||||
|
|
||||||
|
const configuration = new Configuration({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openai = new OpenAIApi(configuration);
|
||||||
|
|
||||||
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
|
throw new Error("Missing env var from OpenAI");
|
||||||
|
}
|
||||||
|
|
||||||
export const POST = auth(async function POST(req: Request) {
|
export const POST = auth(async function POST(req: Request) {
|
||||||
const json = await req.json();
|
const json = await req.json();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { type Message } from "@prisma/client";
|
|
||||||
import CodeBlock from "@/components/ui/codeblock";
|
import CodeBlock from "@/components/ui/codeblock";
|
||||||
import { MemoizedReactMarkdown } from "@/components/markdown";
|
import { MemoizedReactMarkdown } from "@/components/markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from "remark-math";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { fontMessage } from "@/lib/fonts";
|
import { fontMessage } from "@/lib/fonts";
|
||||||
|
import { Message } from "ai-connector";
|
||||||
export interface ChatMessageProps {
|
export interface ChatMessageProps {
|
||||||
message: Message;
|
message: Message;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
268
lib/db/index.ts
268
lib/db/index.ts
|
|
@ -1,268 +0,0 @@
|
||||||
/**
|
|
||||||
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
||||||
* <p style={{fontWeight: "normal"}}>Official <a href="https://github.com/drizzle-team/drizzle-orm">Drizzle ORM</a> adapter for Auth.js / NextAuth.js.</p>
|
|
||||||
* <a href="https://github.com/drizzle-team/drizzle-orm">
|
|
||||||
* <img style={{display: "block"}} src="https://pbs.twimg.com/profile_images/1598308842391179266/CtXrfLnk_400x400.jpg" width="38" />
|
|
||||||
* </a>
|
|
||||||
* </div>
|
|
||||||
*
|
|
||||||
* ## Installation
|
|
||||||
*
|
|
||||||
* ```bash npm2yarn2pnpm
|
|
||||||
* npm install next-auth drizzle-orm @auth/drizzle-adapter
|
|
||||||
* npm install drizzle-kit --save-dev
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @module @auth/drizzle-adapter
|
|
||||||
*/
|
|
||||||
import type { Adapter } from "@auth/core/adapters";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import type { DbClient, Schema } from "./schema";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ## Setup
|
|
||||||
*
|
|
||||||
* Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object:
|
|
||||||
*
|
|
||||||
* ```js title="pages/api/auth/[...nextauth].js"
|
|
||||||
* import NextAuth from "next-auth"
|
|
||||||
* import GoogleProvider from "next-auth/providers/google"
|
|
||||||
* import { DrizzleAdapter } from "@auth/drizzle-adapter"
|
|
||||||
* import { db } from "./db-schema"
|
|
||||||
*
|
|
||||||
* export default NextAuth({
|
|
||||||
* adapter: DrizzleAdapter(db),
|
|
||||||
* providers: [
|
|
||||||
* GoogleProvider({
|
|
||||||
* clientId: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
* clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
* }),
|
|
||||||
* ],
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## Advanced usage
|
|
||||||
*
|
|
||||||
* ### Create the Drizzle schema from scratch
|
|
||||||
*
|
|
||||||
* You'll need to create a database schema that includes the minimal schema for a `next-auth` adapter.
|
|
||||||
* Be sure to use the Drizzle driver version that you're using for your project.
|
|
||||||
*
|
|
||||||
* > This schema is adapted for use in Drizzle and based upon our main [schema](https://authjs.dev/reference/adapters#models)
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* ```json title="db-schema.ts"
|
|
||||||
*
|
|
||||||
* import { integer, pgTable, text, primaryKey } from 'drizzle-orm/pg-core';
|
|
||||||
* import { drizzle } from 'drizzle-orm/node-postgres';
|
|
||||||
* import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
||||||
* import { Pool } from 'pg'
|
|
||||||
* import { ProviderType } from 'next-auth/providers';
|
|
||||||
*
|
|
||||||
* export const users = pgTable('users', {
|
|
||||||
* id: text('id').notNull().primaryKey(),
|
|
||||||
* name: text('name'),
|
|
||||||
* email: text("email").notNull(),
|
|
||||||
* emailVerified: integer("emailVerified"),
|
|
||||||
* image: text("image"),
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* export const accounts = pgTable("accounts", {
|
|
||||||
* userId: text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
||||||
* type: text("type").$type<ProviderType>().notNull(),
|
|
||||||
* provider: text("provider").notNull(),
|
|
||||||
* providerAccountId: text("providerAccountId").notNull(),
|
|
||||||
* refresh_token: text("refresh_token"),
|
|
||||||
* access_token: text("access_token"),
|
|
||||||
* expires_at: integer("expires_at"),
|
|
||||||
* token_type: text("token_type"),
|
|
||||||
* scope: text("scope"),
|
|
||||||
* id_token: text("id_token"),
|
|
||||||
* session_state: text("session_state"),
|
|
||||||
* }, (account) => ({
|
|
||||||
* _: primaryKey(account.provider, account.providerAccountId)
|
|
||||||
* }))
|
|
||||||
*
|
|
||||||
* export const sessions = pgTable("sessions", {
|
|
||||||
* userId: text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
||||||
* sessionToken: text("sessionToken").notNull().primaryKey(),
|
|
||||||
* expires: integer("expires").notNull(),
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* export const verificationTokens = pgTable("verificationToken", {
|
|
||||||
* identifier: text("identifier").notNull(),
|
|
||||||
* token: text("token").notNull(),
|
|
||||||
* expires: integer("expires").notNull()
|
|
||||||
* }, (vt) => ({
|
|
||||||
* _: primaryKey(vt.identifier, vt.token)
|
|
||||||
* }))
|
|
||||||
*
|
|
||||||
* const pool = new Pool({
|
|
||||||
* connectionString: "YOUR_CONNECTION_STRING"
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* export const db = drizzle(pool);
|
|
||||||
*
|
|
||||||
* migrate(db, { migrationsFolder: "./drizzle" })
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
**/
|
|
||||||
export function PgAdapter(
|
|
||||||
client: DbClient,
|
|
||||||
{ users, sessions, verificationTokens, accounts }: Schema
|
|
||||||
): Adapter {
|
|
||||||
return {
|
|
||||||
createUser: async (data) => {
|
|
||||||
return client
|
|
||||||
.insert(users)
|
|
||||||
.values({ ...data, id: crypto.randomUUID() as string })
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
},
|
|
||||||
getUser: async (data) => {
|
|
||||||
return (
|
|
||||||
client
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, data))
|
|
||||||
.then((res) => res[0]) ?? null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getUserByEmail: async (data) => {
|
|
||||||
return (
|
|
||||||
client
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, data))
|
|
||||||
.then((res) => res[0]) ?? null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
createSession: async (data) => {
|
|
||||||
return client
|
|
||||||
.insert(sessions)
|
|
||||||
.values(data)
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
},
|
|
||||||
getSessionAndUser: async (data) => {
|
|
||||||
return (
|
|
||||||
client
|
|
||||||
.select({
|
|
||||||
session: sessions,
|
|
||||||
user: users,
|
|
||||||
})
|
|
||||||
.from(sessions)
|
|
||||||
.where(eq(sessions.sessionToken, data))
|
|
||||||
.innerJoin(users, eq(users.id, sessions.userId))
|
|
||||||
.then((res) => res[0]) ?? null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
updateUser: async (data) => {
|
|
||||||
if (!data.id) {
|
|
||||||
throw new Error("No user id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return client
|
|
||||||
.update(users)
|
|
||||||
.set(data)
|
|
||||||
.where(eq(users.id, data.id))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
},
|
|
||||||
updateSession: async (data) => {
|
|
||||||
return client
|
|
||||||
.update(sessions)
|
|
||||||
.set(data)
|
|
||||||
.where(eq(sessions.sessionToken, data.sessionToken))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
},
|
|
||||||
linkAccount: async (rawAccount) => {
|
|
||||||
const updatedAccount = await client
|
|
||||||
.insert(accounts)
|
|
||||||
.values(rawAccount)
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
// Drizzle will return `null` for fields that are not defined.
|
|
||||||
// However, the return type is expecting `undefined`.
|
|
||||||
const account = {
|
|
||||||
...updatedAccount,
|
|
||||||
access_token: updatedAccount.access_token ?? undefined,
|
|
||||||
token_type: updatedAccount.token_type ?? undefined,
|
|
||||||
id_token: updatedAccount.id_token ?? undefined,
|
|
||||||
refresh_token: updatedAccount.refresh_token ?? undefined,
|
|
||||||
scope: updatedAccount.scope ?? undefined,
|
|
||||||
expires_at: updatedAccount.expires_at ?? undefined,
|
|
||||||
session_state: updatedAccount.session_state ?? undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return account;
|
|
||||||
},
|
|
||||||
getUserByAccount: async (account) => {
|
|
||||||
const dbAccount = await client
|
|
||||||
.select()
|
|
||||||
.from(accounts)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(accounts.providerAccountId, account.providerAccountId),
|
|
||||||
eq(accounts.provider, account.provider)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.leftJoin(users, eq(accounts.userId, users.id))
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
return dbAccount.users;
|
|
||||||
},
|
|
||||||
deleteSession: async (sessionToken) => {
|
|
||||||
await client
|
|
||||||
.delete(sessions)
|
|
||||||
.where(eq(sessions.sessionToken, sessionToken));
|
|
||||||
},
|
|
||||||
createVerificationToken: async (token) => {
|
|
||||||
return client
|
|
||||||
.insert(verificationTokens)
|
|
||||||
.values(token)
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
},
|
|
||||||
useVerificationToken: async (token) => {
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
client
|
|
||||||
.delete(verificationTokens)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(verificationTokens.identifier, token.identifier),
|
|
||||||
eq(verificationTokens.token, token.token)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]) ?? null
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error("No verification token found.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteUser: async (id) => {
|
|
||||||
await client
|
|
||||||
.delete(users)
|
|
||||||
.where(eq(users.id, id))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
},
|
|
||||||
unlinkAccount: async (account) => {
|
|
||||||
await client
|
|
||||||
.delete(accounts)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(accounts.providerAccountId, account.providerAccountId),
|
|
||||||
eq(accounts.provider, account.provider)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { isAppleDevice } from "@/lib/tinykeys";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses the Meta key on macOS, and the Ctrl key on other platforms.
|
|
||||||
*/
|
|
||||||
export const isModifierPressed = (
|
|
||||||
event: KeyboardEvent | React.KeyboardEvent
|
|
||||||
): boolean => {
|
|
||||||
return isAppleDevice() ? event.metaKey : event.ctrlKey;
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { isModifierPressed } from "./is-modifier-pressed";
|
|
||||||
|
|
||||||
export function useCmdEnterSubmit(): {
|
export function useCmdEnterSubmit(): {
|
||||||
formRef: RefObject<HTMLFormElement>;
|
formRef: RefObject<HTMLFormElement>;
|
||||||
|
|
@ -11,8 +10,7 @@ export function useCmdEnterSubmit(): {
|
||||||
const handleKeyDown = (
|
const handleKeyDown = (
|
||||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||||
): void => {
|
): void => {
|
||||||
// Capture `⌘`|Ctrl + Enter
|
if (event.key === "Enter") {
|
||||||
if (isModifierPressed(event) && event.key === "Enter") {
|
|
||||||
formRef.current?.requestSubmit();
|
formRef.current?.requestSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import {
|
|
||||||
createParser,
|
|
||||||
type ParsedEvent,
|
|
||||||
type ReconnectInterval,
|
|
||||||
} from "eventsource-parser";
|
|
||||||
import { Configuration, OpenAIApi } from "openai-edge";
|
|
||||||
|
|
||||||
const configuration = new Configuration({
|
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const openai = new OpenAIApi(configuration);
|
|
||||||
|
|
||||||
if (!process.env.OPENAI_API_KEY) {
|
|
||||||
throw new Error("Missing env var from OpenAI");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OpenAIStream(res: Response) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
function onParse(event: ParsedEvent | ReconnectInterval) {
|
|
||||||
if (event.type === "event") {
|
|
||||||
const data = event.data;
|
|
||||||
if (data === "[DONE]") {
|
|
||||||
controller.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(data) as {
|
|
||||||
choices: any;
|
|
||||||
};
|
|
||||||
const text =
|
|
||||||
json.choices[0]?.delta?.content ?? json.choices[0]?.text ?? "";
|
|
||||||
|
|
||||||
if (counter < 2 && (text.match(/\n/) || []).length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue = encoder.encode(JSON.stringify(text) + "\n");
|
|
||||||
controller.enqueue(queue);
|
|
||||||
counter++;
|
|
||||||
} catch (e) {
|
|
||||||
controller.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = createParser(onParse);
|
|
||||||
|
|
||||||
for await (const chunk of res.body as any) {
|
|
||||||
parser.feed(decoder.decode(chunk));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
209
lib/tinykeys.tsx
209
lib/tinykeys.tsx
|
|
@ -1,209 +0,0 @@
|
||||||
// forked from https://github.com/jamiebuilds/tinykeys
|
|
||||||
// to fix navigator not being defined in SSR context
|
|
||||||
// import { type ModifierKey } from "react";
|
|
||||||
|
|
||||||
type ModifierKey = any;
|
|
||||||
/*
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Jamie Kyle
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
type KeyBindingPress = [string[], string];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of keybinding strings to event handlers.
|
|
||||||
*/
|
|
||||||
export interface KeyBindingMap {
|
|
||||||
[keybinding: string]: (event: React.KeyboardEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Options {
|
|
||||||
ignoreFocus?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* These are the modifier keys that change the meaning of keybindings.
|
|
||||||
*
|
|
||||||
* Note: Ignoring "AltGraph" because it is covered by the others.
|
|
||||||
*/
|
|
||||||
let KEYBINDING_MODIFIER_KEYS = ["Shift", "Meta", "Alt", "Control"];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keybinding sequences should timeout if individual key presses are more than
|
|
||||||
* 1s apart.
|
|
||||||
*/
|
|
||||||
let TIMEOUT = 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When focus is on these elements, ignore the keydown event.
|
|
||||||
*/
|
|
||||||
let inputs = ["select", "textarea", "input"];
|
|
||||||
|
|
||||||
export const isAppleDevice = (): boolean => {
|
|
||||||
return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a "Key Binding String" into its parts
|
|
||||||
*
|
|
||||||
* grammar = `<sequence>`
|
|
||||||
* <sequence> = `<press> <press> <press> ...`
|
|
||||||
* <press> = `<key>` or `<mods>+<key>`
|
|
||||||
* <mods> = `<mod>+<mod>+...`
|
|
||||||
*/
|
|
||||||
function parse(str: string): KeyBindingPress[] {
|
|
||||||
const modifierKey = (isAppleDevice() ? "Meta" : "Control") as ModifierKey;
|
|
||||||
return str
|
|
||||||
.trim()
|
|
||||||
.split(" ")
|
|
||||||
.map((press) => {
|
|
||||||
let mods = press.split("+");
|
|
||||||
|
|
||||||
let key = mods.pop()!;
|
|
||||||
mods = mods.map((mod) => (mod === "$mod" ? modifierKey : mod));
|
|
||||||
return [mods, key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This tells us if a series of events matches a key binding sequence either
|
|
||||||
* partially or exactly.
|
|
||||||
*/
|
|
||||||
function match(event: React.KeyboardEvent, press: KeyBindingPress): boolean {
|
|
||||||
// prettier-ignore
|
|
||||||
return !(
|
|
||||||
// Allow either the `event.key` or the `event.code`
|
|
||||||
// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
|
|
||||||
// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
|
|
||||||
(
|
|
||||||
press[1].toUpperCase() !== event.key.toUpperCase() &&
|
|
||||||
press[1] !== event.code
|
|
||||||
) ||
|
|
||||||
|
|
||||||
// Ensure all the modifiers in the keybinding are pressed.
|
|
||||||
press[0].find((mod) => {
|
|
||||||
return !event.getModifierState(mod as ModifierKey)
|
|
||||||
}) ||
|
|
||||||
|
|
||||||
// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a
|
|
||||||
// keybinding. So if they are pressed but aren't part of this keybinding,
|
|
||||||
// then we don't have a match.
|
|
||||||
KEYBINDING_MODIFIER_KEYS.find(mod => {
|
|
||||||
return !press[0].includes(mod) && event.getModifierState(mod as ModifierKey)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribes to keybindings.
|
|
||||||
*
|
|
||||||
* Returns an unsubscribe method.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```js
|
|
||||||
* import keybindings from "../src/keybindings"
|
|
||||||
*
|
|
||||||
* keybindings(window, {
|
|
||||||
* "Shift+d": () => {
|
|
||||||
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
||||||
* },
|
|
||||||
* "y e e t": () => {
|
|
||||||
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
||||||
* },
|
|
||||||
* "$mod+d": () => {
|
|
||||||
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
||||||
* },
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export default function keybindings(
|
|
||||||
target: Window | HTMLElement,
|
|
||||||
keyBindingMap: KeyBindingMap,
|
|
||||||
options: Options = {}
|
|
||||||
): () => void {
|
|
||||||
const keyBindings = Object.keys(keyBindingMap).map((key) => {
|
|
||||||
return [parse(key), keyBindingMap[key]] as const;
|
|
||||||
});
|
|
||||||
|
|
||||||
const possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>();
|
|
||||||
|
|
||||||
let timer: any = null;
|
|
||||||
|
|
||||||
const onKeyDown = (event: React.KeyboardEvent): void => {
|
|
||||||
// Ignore modifier keydown events
|
|
||||||
// Note: This works because:
|
|
||||||
// - non-modifiers will always return false
|
|
||||||
// - if the current keypress is a modifier then it will return true when we check its state
|
|
||||||
// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState
|
|
||||||
if (
|
|
||||||
event.getModifierState &&
|
|
||||||
event.getModifierState(event.key as ModifierKey)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore event when a focusable item is focused
|
|
||||||
if (options.ignoreFocus) {
|
|
||||||
if (document.activeElement) {
|
|
||||||
if (
|
|
||||||
inputs.indexOf(document.activeElement.tagName.toLowerCase()) !== -1 ||
|
|
||||||
(document.activeElement as HTMLElement).contentEditable === "true"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyBindings.forEach((keyBinding) => {
|
|
||||||
let sequence = keyBinding[0];
|
|
||||||
|
|
||||||
let callback = keyBinding[1];
|
|
||||||
|
|
||||||
let prev = possibleMatches.get(sequence);
|
|
||||||
|
|
||||||
let remainingExpectedPresses = prev ? prev : sequence;
|
|
||||||
|
|
||||||
let currentExpectedPress = remainingExpectedPresses[0];
|
|
||||||
|
|
||||||
let matches = match(event, currentExpectedPress);
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
possibleMatches.delete(sequence);
|
|
||||||
} else if (remainingExpectedPresses.length > 1) {
|
|
||||||
possibleMatches.set(sequence, remainingExpectedPresses.slice(1));
|
|
||||||
} else {
|
|
||||||
possibleMatches.delete(sequence);
|
|
||||||
callback(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(possibleMatches.clear.bind(possibleMatches), TIMEOUT);
|
|
||||||
};
|
|
||||||
|
|
||||||
target.addEventListener("keydown", onKeyDown as any);
|
|
||||||
return (): void => {
|
|
||||||
target.removeEventListener("keydown", onKeyDown as any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -31,7 +31,6 @@
|
||||||
"common-tags": "^1.8.2",
|
"common-tags": "^1.8.2",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"drizzle-orm": "^0.26.0",
|
"drizzle-orm": "^0.26.0",
|
||||||
"eventsource-parser": "^1.0.0",
|
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"framer-motion": "^10.12.4",
|
"framer-motion": "^10.12.4",
|
||||||
"jose": "^4.14.4",
|
"jose": "^4.14.4",
|
||||||
|
|
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -43,9 +43,6 @@ dependencies:
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.26.0
|
specifier: ^0.26.0
|
||||||
version: 0.26.0(@vercel/postgres@0.3.0)
|
version: 0.26.0(@vercel/postgres@0.3.0)
|
||||||
eventsource-parser:
|
|
||||||
specifier: ^1.0.0
|
|
||||||
version: 1.0.0
|
|
||||||
focus-trap-react:
|
focus-trap-react:
|
||||||
specifier: ^10.1.1
|
specifier: ^10.1.1
|
||||||
version: 10.1.1(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
|
version: 10.1.1(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue