Editable.svelte 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. <script lang="ts">
  2. import { Input } from "$lib/components/ui/input";
  3. import { Button } from "$lib/components/ui/button";
  4. import { Check, X } from "@lucide/svelte";
  5. import { state as _state } from "$lib/state.svelte";
  6. import { tick } from "svelte";
  7. let {
  8. value = $bindable(),
  9. onSave,
  10. // Displayed when not editing
  11. display,
  12. }: {
  13. value: string;
  14. onSave: (val: string) => void;
  15. display: any;
  16. } = $props();
  17. let editing = $state(false);
  18. let inputRef: HTMLInputElement;
  19. let lastValue = "";
  20. // Start editing
  21. function startEdit() {
  22. lastValue = value;
  23. editing = true;
  24. // Focus input after next tick
  25. tick().then(() => {
  26. console.log("inputref", inputRef);
  27. inputRef!!.focus();
  28. inputRef!!.select();
  29. });
  30. }
  31. // Save changes
  32. function save() {
  33. if (value.trim()) {
  34. value = value.trim();
  35. try {
  36. onSave(value);
  37. } catch (e) {
  38. console.error("error while updating editable", e);
  39. value = lastValue;
  40. }
  41. }
  42. editing = false;
  43. }
  44. // Cancel editing
  45. function cancel() {
  46. value = lastValue;
  47. editing = false;
  48. }
  49. function clickOutsideCancel(node: HTMLElement) {
  50. const onPointerDown = (event: PointerEvent) => {
  51. if (!node.contains(event.target as Node)) {
  52. // Only blur if this element currently has focus
  53. if (
  54. node === document.activeElement ||
  55. node.contains(document.activeElement)
  56. ) {
  57. if (editing) {
  58. cancel();
  59. }
  60. }
  61. }
  62. };
  63. document.addEventListener("pointerdown", onPointerDown, true);
  64. return {
  65. destroy() {
  66. document.removeEventListener("pointerdown", onPointerDown, true);
  67. },
  68. };
  69. }
  70. </script>
  71. <div class="flex items-center">
  72. {#if editing}
  73. <input
  74. use:clickOutsideCancel
  75. bind:this={inputRef}
  76. bind:value
  77. size={Math.max(value.length, 1)}
  78. onkeydown={(e) => {
  79. if (e.key === "Enter") save();
  80. if (e.key === "Escape") cancel();
  81. }}
  82. />
  83. <Button variant="ghost" size="icon" onclick={save}>
  84. <Check class="w-4 h-4" />
  85. </Button>
  86. <Button variant="ghost" size="icon" onclick={cancel}>
  87. <X class="w-4 h-4" />
  88. </Button>
  89. {:else}
  90. {@render display({ value, startEdit })}
  91. {/if}
  92. </div>