@suphark/ui 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/components/app-shell/app-shell.tsx +13 -2
- package/src/components/app-shell/header-breadcrumbs.tsx +14 -7
- package/src/components/shared/env-snippet.tsx +28 -13
- package/src/components/shared/secret-reveal.tsx +5 -7
- package/src/components/shared/ui-text.tsx +4 -0
- package/src/index.ts +1 -0
- package/src/lib/copy-text.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.3.2] — 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `copyText`/`selectContents` export จาก barrel หลักด้วย (แอปใช้ทำปุ่มคัดลอกเองได้)
|
|
10
|
+
- `EnvSnippet`: ค่ายาว (client secret) ล้นออกนอก dialog — ตัดบรรทัดในกรอบ (`whitespace-pre-wrap break-all` + `min-w-0`) แทน scroll แนวนอน (รายงานจากหน้า Register app ของ Pi Auth)
|
|
11
|
+
- `EnvSnippet` / `SecretReveal`: ปุ่มคัดลอกเงียบเมื่อ `navigator.clipboard` ถูกบล็อก (webview/embedded browser เช่น browser ใน Claude desktop) — เพิ่ม `copyText()` (`@suphark/ui/lib/copy-text`) ที่ fallback ไป `execCommand("copy")`; ถ้ายังไม่ได้ EnvSnippet จะเลือกข้อความให้และบอกให้กด Ctrl+C (`UiText.copyFailed` optional)
|
|
12
|
+
|
|
13
|
+
## [0.3.1] — 2026-09-18
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `AppShell`: slot `sidebarHeader` ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน (เดิมล้นทับเนื้อหา — รายงานจาก Pi Auth) + prop ใหม่ `sidebarHeaderCollapsed` สำหรับไอคอนแทนตอนยุบ
|
|
18
|
+
- `HeaderBreadcrumbs`: `labels` รับฟังก์ชัน `(segment, href) => ReactNode` ได้ (ใส่ `t()` ของ next-intl) และ map รับ ReactNode — ฟังก์ชันต้องมาจาก client component (ส่งจาก server layout จะได้ 500 "Functions cannot be passed directly to Client Components"; showcase สาธิตด้วย `breadcrumbs: <ShellBreadcrumbs />`)
|
|
19
|
+
- README: อธิบายว่า `ThemeMenuItems` มี group + หัวในตัว ไม่ต้องห่อเอง
|
|
20
|
+
|
|
5
21
|
## [0.3.0] — 2026-09-18
|
|
6
22
|
|
|
7
23
|
### Added
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Design system กลางของ Suphark apps (Pi Auth, DueBell, Pi PDF, supp
|
|
|
4
4
|
|
|
5
5
|
แพ็กเกจส่ง **source (.ts/.tsx) ไม่ build** — แอป Next.js transpile เองผ่าน `transpilePackages`
|
|
6
6
|
|
|
7
|
+
ดูตัวอย่างสด: **https://ui.suphark.com** (design system, `/shell` AppShell, `/brand`)
|
|
8
|
+
|
|
7
9
|
## ติดตั้ง
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -113,6 +115,9 @@ export default async function Layout({ children }) {
|
|
|
113
115
|
|
|
114
116
|
- `header={false}` ไม่มี header · `collapsible="offcanvas"` ซ่อนทั้งแผง (supplychain) · `contentMaxWidth="max-w-5xl"` จำกัดความกว้าง (Pi Auth)
|
|
115
117
|
- `pin={{ pinnedHrefs, onTogglePin }}` เปิดปุ่มปักหมุดเมนู (แอปเก็บ state เอง) · `header.center` วาง pinned menus
|
|
118
|
+
- `sidebarHeader` ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน — ใส่ `sidebarHeaderCollapsed` ถ้าต้องการไอคอนแทน (เช่นปุ่ม ⌘K)
|
|
119
|
+
- `header.breadcrumbs.labels` รับฟังก์ชัน `(segment, href) => ReactNode` ได้ (ใส่ `t()` ของ next-intl) — **แต่ฟังก์ชันส่งจาก server layout ไม่ได้** (React error "Functions cannot be passed directly to Client Components") ให้ทำ client component ที่เรียก `<HeaderBreadcrumbs labels={…} />` แล้วส่งเป็น `breadcrumbs: <MyBreadcrumbs />` (ดู `apps/showcase/src/app/shell/breadcrumbs.tsx`)
|
|
120
|
+
- `<UserMenu>` children คือรายการเมนู: `ThemeMenuItems` มี `DropdownMenuGroup` + หัว "ธีม" ในตัวแล้ว (ปรับข้อความด้วย `label`/`labels`) ไม่ต้องห่อเพิ่ม; รายการอื่นใส่ `DropdownMenuItem`/`DropdownMenuGroup` เอง
|
|
116
121
|
- ตัวอย่างรันได้ที่ `apps/showcase/src/app/shell`
|
|
117
122
|
|
|
118
123
|
### สีแบรนด์ต่อ tenant/แอป (`--brand`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
|
|
@@ -43,8 +43,10 @@ export interface AppShellProps {
|
|
|
43
43
|
searchPlaceholder?: string;
|
|
44
44
|
/** ปุ่ม pin เมนูขึ้น header (แอปเก็บ state เอง) */
|
|
45
45
|
pin?: AppNavPinProps;
|
|
46
|
-
/** ส่วนเพิ่มใน sidebar ใต้โลโก้ เช่น TenantSwitcher / CommandPalette */
|
|
46
|
+
/** ส่วนเพิ่มใน sidebar ใต้โลโก้ เช่น TenantSwitcher / CommandPalette — ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน */
|
|
47
47
|
sidebarHeader?: ReactNode;
|
|
48
|
+
/** สิ่งที่แสดงแทน `sidebarHeader` ตอนยุบเป็นไอคอน (เช่นปุ่มไอคอน ⌘K) — ไม่ส่ง = ไม่แสดงอะไร */
|
|
49
|
+
sidebarHeaderCollapsed?: ReactNode;
|
|
48
50
|
/** ท้าย sidebar — ปกติคือ `<UserMenu variant="sidebar" />` */
|
|
49
51
|
sidebarFooter?: ReactNode;
|
|
50
52
|
/**
|
|
@@ -96,6 +98,7 @@ export function AppShell({
|
|
|
96
98
|
searchPlaceholder = "ค้นหาเมนู…",
|
|
97
99
|
pin,
|
|
98
100
|
sidebarHeader,
|
|
101
|
+
sidebarHeaderCollapsed,
|
|
99
102
|
sidebarFooter,
|
|
100
103
|
header = true,
|
|
101
104
|
brandColor,
|
|
@@ -133,6 +136,7 @@ export function AppShell({
|
|
|
133
136
|
searchPlaceholder={searchPlaceholder}
|
|
134
137
|
pin={pin}
|
|
135
138
|
sidebarHeader={sidebarHeader}
|
|
139
|
+
sidebarHeaderCollapsed={sidebarHeaderCollapsed}
|
|
136
140
|
sidebarFooter={sidebarFooter}
|
|
137
141
|
collapsible={collapsible}
|
|
138
142
|
/>
|
|
@@ -195,6 +199,7 @@ function ShellSidebar({
|
|
|
195
199
|
searchPlaceholder,
|
|
196
200
|
pin,
|
|
197
201
|
sidebarHeader,
|
|
202
|
+
sidebarHeaderCollapsed,
|
|
198
203
|
sidebarFooter,
|
|
199
204
|
collapsible,
|
|
200
205
|
}: Pick<
|
|
@@ -205,6 +210,7 @@ function ShellSidebar({
|
|
|
205
210
|
| "searchPlaceholder"
|
|
206
211
|
| "pin"
|
|
207
212
|
| "sidebarHeader"
|
|
213
|
+
| "sidebarHeaderCollapsed"
|
|
208
214
|
| "sidebarFooter"
|
|
209
215
|
| "collapsible"
|
|
210
216
|
>) {
|
|
@@ -249,7 +255,12 @@ function ShellSidebar({
|
|
|
249
255
|
</InputGroup>
|
|
250
256
|
</div>
|
|
251
257
|
)}
|
|
252
|
-
{sidebarHeader
|
|
258
|
+
{sidebarHeader && (
|
|
259
|
+
<div className="group-data-[collapsible=icon]:hidden">{sidebarHeader}</div>
|
|
260
|
+
)}
|
|
261
|
+
{sidebarHeaderCollapsed && (
|
|
262
|
+
<div className="hidden group-data-[collapsible=icon]:block">{sidebarHeaderCollapsed}</div>
|
|
263
|
+
)}
|
|
253
264
|
</SidebarHeader>
|
|
254
265
|
<SidebarContent>
|
|
255
266
|
<AppNav nav={visibleNav} pin={pin} forceOpen={searching} />
|
|
@@ -10,11 +10,18 @@ import {
|
|
|
10
10
|
} from "@suphark/ui/components/ui/breadcrumb";
|
|
11
11
|
import Link from "next/link";
|
|
12
12
|
import { usePathname } from "next/navigation";
|
|
13
|
-
import { Fragment } from "react";
|
|
13
|
+
import { Fragment, type ReactNode } from "react";
|
|
14
14
|
|
|
15
15
|
export interface HeaderBreadcrumbsProps {
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* ชื่อแสดงของแต่ละ segment: map (`{ "po-tracking": "PO Tracking" }`) หรือฟังก์ชัน
|
|
18
|
+
* `(segment, href) => ReactNode | undefined` (เช่น t() ของ next-intl)
|
|
19
|
+
* คืน undefined/ไม่พบ = ตัวแรกพิมพ์ใหญ่ แทน - ด้วยช่องว่าง
|
|
20
|
+
*
|
|
21
|
+
* ระวัง: ฟังก์ชันส่งข้าม server→client boundary ไม่ได้ ("Functions cannot be passed directly to Client Components")
|
|
22
|
+
* ถ้า layout เป็น server component ให้ใช้ map หรือส่ง `breadcrumbs: <MyBreadcrumbs />` ที่เป็น client component เรียก HeaderBreadcrumbs เอง
|
|
23
|
+
*/
|
|
24
|
+
labels?: Record<string, ReactNode> | ((segment: string, href: string) => ReactNode | undefined);
|
|
18
25
|
homeHref?: string;
|
|
19
26
|
homeLabel?: string;
|
|
20
27
|
/** ตัด segment นำหน้าออก เช่น `["console"]` เมื่อทุกหน้าอยู่ใต้ /console */
|
|
@@ -25,9 +32,9 @@ export interface HeaderBreadcrumbsProps {
|
|
|
25
32
|
|
|
26
33
|
const ID_LIKE = /^(\d+|[0-9a-f]{8}-[0-9a-f-]{27}|c[a-z0-9]{20,}|[0-9a-z]{16,})$/i;
|
|
27
34
|
|
|
28
|
-
function labelOf(segment: string,
|
|
29
|
-
const known = labels?.[segment];
|
|
30
|
-
if (known) return known;
|
|
35
|
+
function labelOf(segment: string, href: string, labels?: HeaderBreadcrumbsProps["labels"]) {
|
|
36
|
+
const known = typeof labels === "function" ? labels(segment, href) : labels?.[segment];
|
|
37
|
+
if (known !== undefined && known !== null && known !== "") return known;
|
|
31
38
|
const decoded = decodeURIComponent(segment);
|
|
32
39
|
return decoded.charAt(0).toUpperCase() + decoded.slice(1).replace(/-/g, " ");
|
|
33
40
|
}
|
|
@@ -56,7 +63,7 @@ export function HeaderBreadcrumbs({
|
|
|
56
63
|
{shown.length > 0 && <BreadcrumbSeparator className="hidden md:block" />}
|
|
57
64
|
{shown.map(({ segment, href }, index) => {
|
|
58
65
|
const isLast = index === shown.length - 1;
|
|
59
|
-
const label = labelOf(segment, labels);
|
|
66
|
+
const label = labelOf(segment, href, labels);
|
|
60
67
|
return (
|
|
61
68
|
<Fragment key={href}>
|
|
62
69
|
<BreadcrumbItem>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { copyText, selectContents } from "@suphark/ui/lib/copy-text";
|
|
4
5
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
+
import { useRef, useState } from "react";
|
|
6
7
|
import { useUiText } from "./ui-text";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* บล็อก .env สำเร็จรูป: คัดลอกทั้งก้อนปุ่มเดียว (Clerk-style "quick copy")
|
|
10
11
|
* ค่าที่เป็น secret แสดงจริง (หน้าที่เรียกต้องเป็นหน้า "แสดงครั้งเดียว" อยู่แล้ว)
|
|
12
|
+
* - ค่ายาว (secret) ตัดบรรทัดในกรอบ ไม่ล้น dialog
|
|
13
|
+
* - clipboard ถูกบล็อก (webview/embedded browser) → fallback execCommand; ถ้ายังไม่ได้ เลือกข้อความให้กด Ctrl+C
|
|
11
14
|
*/
|
|
12
15
|
export function EnvSnippet({
|
|
13
16
|
title = ".env.local",
|
|
@@ -20,34 +23,46 @@ export function EnvSnippet({
|
|
|
20
23
|
copyLabel?: string;
|
|
21
24
|
}) {
|
|
22
25
|
const text = useUiText();
|
|
23
|
-
const [
|
|
26
|
+
const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
|
|
27
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
24
28
|
const content = lines.map(([k, v]) => `${k}=${v}`).join("\n");
|
|
29
|
+
|
|
25
30
|
async function copy() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
setTimeout(() =>
|
|
30
|
-
}
|
|
31
|
-
|
|
31
|
+
const ok = await copyText(`${content}\n`);
|
|
32
|
+
if (ok) {
|
|
33
|
+
setState("copied");
|
|
34
|
+
setTimeout(() => setState((s) => (s === "copied" ? "idle" : s)), 1500);
|
|
35
|
+
} else {
|
|
36
|
+
selectContents(preRef.current);
|
|
37
|
+
setState("failed");
|
|
32
38
|
}
|
|
33
39
|
}
|
|
40
|
+
|
|
34
41
|
return (
|
|
35
|
-
<div className="overflow-hidden rounded-lg border bg-card">
|
|
42
|
+
<div className="min-w-0 overflow-hidden rounded-lg border bg-card">
|
|
36
43
|
<div className="flex items-center justify-between gap-2 border-b bg-muted px-3 py-1.5">
|
|
37
44
|
<span className="font-mono text-xs text-muted-foreground">{title}</span>
|
|
38
45
|
<Button type="button" variant="outline" size="xs" onClick={copy}>
|
|
39
|
-
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
40
|
-
{copied ? text.copied : (copyLabel ?? text.copy)}
|
|
46
|
+
{state === "copied" ? <CheckIcon /> : <CopyIcon />}
|
|
47
|
+
{state === "copied" ? text.copied : (copyLabel ?? text.copy)}
|
|
41
48
|
</Button>
|
|
42
49
|
</div>
|
|
43
|
-
<pre
|
|
50
|
+
<pre
|
|
51
|
+
ref={preRef}
|
|
52
|
+
className="whitespace-pre-wrap break-all px-3 py-2.5 font-mono text-xs leading-6"
|
|
53
|
+
>
|
|
44
54
|
{lines.map(([k, v]) => (
|
|
45
55
|
<div key={k}>
|
|
46
56
|
<span className="text-muted-foreground">{k}=</span>
|
|
47
|
-
<span
|
|
57
|
+
<span>{v}</span>
|
|
48
58
|
</div>
|
|
49
59
|
))}
|
|
50
60
|
</pre>
|
|
61
|
+
{state === "failed" ? (
|
|
62
|
+
<p className="border-t bg-muted/50 px-3 py-1.5 text-muted-foreground text-xs">
|
|
63
|
+
{text.copyFailed ?? "คัดลอกอัตโนมัติไม่ได้ — เลือกข้อความให้แล้ว กด Ctrl+C / ⌘C"}
|
|
64
|
+
</p>
|
|
65
|
+
) : null}
|
|
51
66
|
</div>
|
|
52
67
|
);
|
|
53
68
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { copyText } from "@suphark/ui/lib/copy-text";
|
|
4
5
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
5
6
|
import { useState } from "react";
|
|
6
7
|
import { Notice } from "./notice";
|
|
@@ -17,13 +18,10 @@ export function SecretReveal({
|
|
|
17
18
|
const text = useUiText();
|
|
18
19
|
const [copied, setCopied] = useState<string | null>(null);
|
|
19
20
|
async function copy(key: string, value: string) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} catch {
|
|
25
|
-
// clipboard ไม่ได้ (http/permission) — ผู้ใช้ยังคัดลอกจากข้อความได้
|
|
26
|
-
}
|
|
21
|
+
// clipboard API ถูกบล็อก (webview/embedded browser) → copyText ลอง execCommand ให้; ไม่ได้จริง ๆ ผู้ใช้ยังเลือกจากข้อความได้
|
|
22
|
+
if (!(await copyText(value))) return;
|
|
23
|
+
setCopied(key);
|
|
24
|
+
setTimeout(() => setCopied((c) => (c === key ? null : c)), 1500);
|
|
27
25
|
}
|
|
28
26
|
return (
|
|
29
27
|
<Notice tone="warning" title={title ?? text.showOnce}>
|
|
@@ -12,6 +12,8 @@ export interface UiText {
|
|
|
12
12
|
copied: string;
|
|
13
13
|
showOnce: string;
|
|
14
14
|
success: string;
|
|
15
|
+
/** EnvSnippet: คัดลอกอัตโนมัติไม่ได้ (optional — มีค่าเริ่มต้นในตัว) */
|
|
16
|
+
copyFailed?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const UI_TEXT_TH: UiText = {
|
|
@@ -23,6 +25,7 @@ export const UI_TEXT_TH: UiText = {
|
|
|
23
25
|
copied: "คัดลอกแล้ว",
|
|
24
26
|
showOnce: "แสดงครั้งเดียว — คัดลอกเก็บไว้ทันที",
|
|
25
27
|
success: "สำเร็จ",
|
|
28
|
+
copyFailed: "คัดลอกอัตโนมัติไม่ได้ — เลือกข้อความให้แล้ว กด Ctrl+C / ⌘C",
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
export const UI_TEXT_EN: UiText = {
|
|
@@ -34,6 +37,7 @@ export const UI_TEXT_EN: UiText = {
|
|
|
34
37
|
copied: "Copied",
|
|
35
38
|
showOnce: "Shown once — copy it now",
|
|
36
39
|
success: "Done",
|
|
40
|
+
copyFailed: "Automatic copy is blocked — text selected, press Ctrl+C / ⌘C",
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const UiTextContext = createContext<UiText>(UI_TEXT_TH);
|
package/src/index.ts
CHANGED
|
@@ -136,6 +136,7 @@ export * from "./components/ui/tooltip";
|
|
|
136
136
|
// ---------- lib / hooks ----------
|
|
137
137
|
export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
138
138
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
139
|
+
export { copyText, selectContents } from "./lib/copy-text";
|
|
139
140
|
export { formatRelativeThaiDate, formatThaiFullDate } from "./lib/formatters";
|
|
140
141
|
export {
|
|
141
142
|
type AtomicPermissionRequirement,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* คัดลอกข้อความลง clipboard ให้ได้มากที่สุด:
|
|
3
|
+
* 1. navigator.clipboard (ต้อง secure context + permission — บาง embedded browser/webview บล็อก)
|
|
4
|
+
* 2. fallback: textarea ชั่วคราว + document.execCommand("copy") (deprecated แต่ยังใช้ได้ทุก browser)
|
|
5
|
+
* คืน true เมื่อสำเร็จ — ผู้เรียกควรมีทางหนี (เลือกข้อความให้ผู้ใช้กด Ctrl+C เอง) เมื่อได้ false
|
|
6
|
+
*/
|
|
7
|
+
export async function copyText(text: string): Promise<boolean> {
|
|
8
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
9
|
+
try {
|
|
10
|
+
await navigator.clipboard.writeText(text);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
// ลอง fallback ต่อ
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (typeof document === "undefined") return false;
|
|
17
|
+
const ta = document.createElement("textarea");
|
|
18
|
+
ta.value = text;
|
|
19
|
+
ta.setAttribute("readonly", "");
|
|
20
|
+
ta.style.position = "fixed";
|
|
21
|
+
ta.style.top = "0";
|
|
22
|
+
ta.style.left = "0";
|
|
23
|
+
ta.style.opacity = "0";
|
|
24
|
+
document.body.appendChild(ta);
|
|
25
|
+
const active = document.activeElement as HTMLElement | null;
|
|
26
|
+
try {
|
|
27
|
+
ta.focus();
|
|
28
|
+
ta.select();
|
|
29
|
+
ta.setSelectionRange(0, text.length);
|
|
30
|
+
return document.execCommand("copy");
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
} finally {
|
|
34
|
+
ta.remove();
|
|
35
|
+
active?.focus?.();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** เลือกข้อความทั้งก้อนใน element (ให้ผู้ใช้กด Ctrl+C ได้ทันทีเมื่อ copy อัตโนมัติไม่ได้) */
|
|
40
|
+
export function selectContents(el: Element | null): void {
|
|
41
|
+
if (!el || typeof window === "undefined") return;
|
|
42
|
+
const sel = window.getSelection();
|
|
43
|
+
if (!sel) return;
|
|
44
|
+
const range = document.createRange();
|
|
45
|
+
range.selectNodeContents(el);
|
|
46
|
+
sel.removeAllRanges();
|
|
47
|
+
sel.addRange(range);
|
|
48
|
+
}
|