Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-beans-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-file-tree": patch
---

fix: swap arrow left/right in rtl
8 changes: 6 additions & 2 deletions packages/svelte-file-tree/src/lib/components/TreeItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@
return;
}

const isRtl = getComputedStyle(event.currentTarget).direction === "rtl";
const arrowRight = isRtl ? "ArrowLeft" : "ArrowRight";
const arrowLeft = isRtl ? "ArrowRight" : "ArrowLeft";

switch (event.key) {
case "ArrowRight": {
case arrowRight: {
if (item.node.type === "file") {
break;
}
Expand All @@ -55,7 +59,7 @@
}
break;
}
case "ArrowLeft": {
case arrowLeft: {
if (item.node.type === "folder" && item.expanded) {
treeContext.getExpandedIds().delete(item.node.id);
break;
Expand Down
93 changes: 80 additions & 13 deletions sites/preview/src/lib/Tree.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,78 @@
type FileTreeNode,
type TreeItemState,
} from "./tree.svelte.js";
import { arabicNumbers } from "./utils.js";

const sortCollator = new Intl.Collator();

function sortComparator(x: FileTreeNode, y: FileTreeNode) {
return sortCollator.compare(x.name, y.name);
}

const translations = {
toast: {
cannotMoveInsideItself: {
en: (name: string) => `Cannot move "${name}" inside itself`,
ar: (name: string) => `لا يمكن نقل "${name}" داخل نفسه`,
},
itemAlreadyExists: {
en: (name: string) => `An item named "${name}" already exists in this location`,
ar: (name: string) => `عنصر باسم "${name}" موجود بالفعل في هذا الموقع`,
},
failedToReadFiles: {
en: "Failed to read uploaded files",
ar: "فشل قراءة الملفات المرفوعة",
},
},
dialog: {
failedToCopyItems: {
en: "Failed to copy items",
ar: "فشل نسخ العناصر",
},
failedToMoveItems: {
en: "Failed to move items",
ar: "فشل نقل العناصر",
},
nameConflictDescription: {
en: (name: string) =>
`An item named "${name}" already exists in this location. Do you want to skip it or cancel the operation entirely?`,
ar: (name: string) =>
`عنصر باسم "${name}" موجود بالفعل في هذا الموقع. هل تريد تخطيه أو إلغاء العملية بالكامل؟`,
},
skip: {
en: "Skip",
ar: "تخطي",
},
deleteConfirmTitle: {
en: (count: number) => `Are you sure you want to delete ${count} item(s)?`,
ar: (count: number) =>
`هل أنت متأكد أنك تريد حذف ${arabicNumbers(count.toString())} عناصر؟`,
},
deleteConfirmDescription: {
en: "They will be permanently deleted. This action cannot be undone.",
ar: "سيتم حذفها نهائياً. لا يمكن التراجع عن هذا الإجراء.",
},
confirm: {
en: "Confirm",
ar: "تأكيد",
},
cancel: {
en: "Cancel",
ar: "إلغاء",
},
},
};

export type TreeProps = {
children: Snippet<[args: TreeChildrenSnippetArgs<FileNode, FolderNode>]>;
root: FileTree;
lang?: "en" | "ar";
class?: ClassValue;
style?: string;
};

export type TreeContext = {
getLang: () => "en" | "ar";
getSelectedIds: () => Set<string>;
getExpandedIds: () => Set<string>;
getDraggedId: () => string | undefined;
Expand All @@ -53,7 +110,7 @@
</script>

<script lang="ts">
const { children, root, class: className, style }: TreeProps = $props();
const { children, root, lang = "en", class: className, style }: TreeProps = $props();

let tree: Tree<FileNode, FolderNode> | null = $state.raw(null);
const selectedIds = new SvelteSet<string>();
Expand All @@ -66,6 +123,7 @@
let dialogTitle = $state.raw("");
let dialogDescription = $state.raw("");
let dialogConfirmLabel = $state.raw("");
let dialogCancelLabel = $state.raw("");
let dialogTrigger: HTMLElement | null = null;
let dialogDidConfirm = false;
let dialogOnClose: (() => void) | undefined;
Expand All @@ -74,17 +132,20 @@
title,
description,
confirmLabel,
cancelLabel,
onClose,
}: {
title: string;
description: string;
confirmLabel: string;
cancelLabel: string;
onClose: () => void;
}) {
dialogOpen = true;
dialogTitle = title;
dialogDescription = description;
dialogConfirmLabel = confirmLabel;
dialogCancelLabel = cancelLabel;
dialogTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
dialogDidConfirm = false;
dialogOnClose = onClose;
Expand All @@ -95,6 +156,7 @@
dialogTitle = "";
dialogDescription = "";
dialogConfirmLabel = "";
dialogCancelLabel = "";
dialogTrigger?.focus();
dialogTrigger = null;
dialogOnClose?.();
Expand Down Expand Up @@ -134,19 +196,20 @@
let title;
switch (operation) {
case "copy": {
title = "Failed to copy items";
title = translations.dialog.failedToCopyItems[lang];
break;
}
case "move": {
title = "Failed to move items";
title = translations.dialog.failedToMoveItems[lang];
break;
}
}

showDialog({
title,
description: `An item named "${name}" already exists in this location. Do you want to skip it or cancel the operation entirely?`,
confirmLabel: "Skip",
description: translations.dialog.nameConflictDescription[lang](name),
confirmLabel: translations.dialog.skip[lang],
cancelLabel: translations.dialog.cancel[lang],
onClose: () => {
resolve(dialogDidConfirm ? "skip" : "cancel");
},
Expand All @@ -155,7 +218,7 @@
}

function onCircularReference({ source }: OnCircularReferenceArgs<FileNode, FolderNode>) {
toast.error(`Cannot move "${source.node.name}" inside itself`);
toast.error(translations.toast.cannotMoveInsideItself[lang](source.node.name));
}

function onCopy({ destination }: OnCopyArgs<FileNode, FolderNode>) {
Expand All @@ -171,9 +234,10 @@
function canRemove({ removed }: OnRemoveArgs<FileNode, FolderNode>) {
return new Promise<boolean>((resolve) => {
showDialog({
title: `Are you sure you want to delete ${removed.length} item(s)?`,
description: "They will be permanently deleted. This action cannot be undone.",
confirmLabel: "Confirm",
title: translations.dialog.deleteConfirmTitle[lang](removed.length),
description: translations.dialog.deleteConfirmDescription[lang],
confirmLabel: translations.dialog.confirm[lang],
cancelLabel: translations.dialog.cancel[lang],
onClose: () => {
resolve(dialogDidConfirm);
},
Expand Down Expand Up @@ -236,9 +300,9 @@
continue;
}

const firstSegment = entry.name.split("/")[0];
const firstSegment = entry.name.split("/")[0]!;
if (uniqueNames.has(firstSegment)) {
toast.error(`An item named "${firstSegment}" already exists in this location`);
toast.error(translations.toast.itemAlreadyExists[lang](firstSegment));
return;
}

Expand Down Expand Up @@ -274,7 +338,7 @@
await Promise.all(entries.map(readEntry));
} catch (error) {
console.error(error);
toast.error("Failed to read uploaded files");
toast.error(translations.toast.failedToReadFiles[lang]);
return;
}

Expand Down Expand Up @@ -336,6 +400,7 @@
};

const context: TreeContext = {
getLang: () => lang,
getSelectedIds: () => selectedIds,
getExpandedIds: () => expandedIds,
getDraggedId: () => draggedId,
Expand All @@ -362,6 +427,8 @@
{onCopy}
{onMove}
{canRemove}
{lang}
dir="auto"
class={className}
{style}
ondragenter={handleDragEnterOrOver}
Expand Down Expand Up @@ -405,7 +472,7 @@
<AlertDialog.Cancel
class="inline-flex h-10 items-center justify-center rounded bg-gray-200 px-6 text-sm font-medium hover:bg-gray-300 focus-visible:outline-2 focus-visible:outline-current active:scale-95"
>
Cancel
{dialogCancelLabel}
</AlertDialog.Cancel>
</div>
</div>
Expand Down
37 changes: 25 additions & 12 deletions sites/preview/src/lib/TreeItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { TreeItem } from "svelte-file-tree";
import { getTreeContext } from "./Tree.svelte";
import type { TreeItemState } from "./tree.svelte.js";
import { arabicNumbers } from "./utils.js";

export type TreeItemProps = {
item: TreeItemState;
Expand All @@ -12,27 +13,39 @@
style?: string;
};

const UNITS = ["B", "KB", "MB", "GB", "TB"];
const UNITS = [
{ en: "B", ar: "ب" },
{ en: "KB", ar: "ك.ب" },
{ en: "MB", ar: "م.ب" },
{ en: "GB", ar: "ج.ب" },
{ en: "TB", ar: "ت.ب" },
];

const formatter = new Intl.NumberFormat(undefined, {
const sizeFormatter = new Intl.NumberFormat("en", {
maximumFractionDigits: 2,
});

function formatSize(size: number) {
let unit = UNITS[0];
for (let i = 1; i < UNITS.length && size >= 1024; i++) {
unit = UNITS[i];
size /= 1024;
}
return formatter.format(size) + " " + unit;
}
</script>

<script lang="ts">
const treeContext = getTreeContext();

const { item, order, class: className, style }: TreeItemProps = $props();

function formatSize(size: number) {
let unit = UNITS[0]!;
for (let i = 1; i < UNITS.length && size >= 1024; i++) {
unit = UNITS[i]!;
size /= 1024;
}

const lang = treeContext.getLang();
let formattedSize = sizeFormatter.format(size);
if (lang === "ar") {
formattedSize = arabicNumbers(formattedSize);
}
return formattedSize + " " + unit[lang];
}

const handleDragStart: EventHandler<DragEvent, HTMLDivElement> = (event) => {
if (item.disabled) {
event.preventDefault();
Expand Down Expand Up @@ -135,7 +148,7 @@
<ChevronDownIcon
role="presentation"
data-invisible={item.node.type === "file" ? true : undefined}
class="size-5 rounded-full transition-transform duration-200 group-aria-expanded:-rotate-90 hover:bg-current/8 active:bg-current/12 data-invisible:invisible"
class="size-5 rounded-full transition-transform duration-200 group-aria-expanded:-rotate-90 hover:bg-current/8 active:bg-current/12 data-invisible:invisible group-aria-expanded:rtl:rotate-90"
onclick={handleToggleClick}
/>

Expand Down
3 changes: 3 additions & 0 deletions sites/preview/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function arabicNumbers(text: string) {
return text.replace(/[0-9]/g, (digit) => "٠١٢٣٤٥٦٧٨٩".charAt(+digit));
}
38 changes: 38 additions & 0 deletions sites/preview/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
AlignVerticalSpaceAroundIcon,
HandIcon,
KeyboardIcon,
LanguagesIcon,
ScrollIcon,
UploadIcon,
ZapIcon,
Expand Down Expand Up @@ -197,6 +198,43 @@
</GithubLink>
</div>
</section>

<section
class="@container flex flex-col rounded-xl border border-slate-300 bg-slate-50 p-6 @min-4xl:col-span-2"
>
<h3 class="text-xl font-semibold text-slate-800">RTL Support</h3>

<p class="mt-3 text-slate-700">Right-to-left language support</p>

<div class="grow"></div>

<ul class="mt-6 space-y-3">
<li class="flex items-center gap-2 text-sm text-slate-600">
<LanguagesIcon role="presentation" class="size-4" />
RTL Layout
</li>

<li class="flex items-center gap-2 text-sm text-slate-600">
<KeyboardIcon role="presentation" class="size-4" />
Keyboard Navigation
</li>
</ul>

<div class="mt-6 grid gap-4 @min-sm:grid-cols-2">
<a
href="/rtl"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-700 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700 active:scale-95"
>
View Example
</a>

<GithubLink
href="https://github.com/abdel-17/svelte-file-tree/tree/master/sites/preview/src/routes/rtl/+page.svelte"
>
View Code
</GithubLink>
</div>
</section>
</div>
</section>

Expand Down
Loading