textWysiwyg.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { KEYS } from "../keys";
  2. import { selectNode } from "../utils";
  3. function trimText(text: string) {
  4. // whitespace only → trim all because we'd end up inserting invisible element
  5. if (!text.trim()) {
  6. return "";
  7. }
  8. // replace leading/trailing newlines (only) otherwise it messes up bounding
  9. // box calculation (there's also a bug in FF which inserts trailing newline
  10. // for multiline texts)
  11. return text.replace(/^\n+|\n+$/g, "");
  12. }
  13. type TextWysiwygParams = {
  14. initText: string;
  15. x: number;
  16. y: number;
  17. strokeColor: string;
  18. font: string;
  19. opacity: number;
  20. zoom: number;
  21. angle: number;
  22. onSubmit: (text: string) => void;
  23. onCancel: () => void;
  24. };
  25. export function textWysiwyg({
  26. initText,
  27. x,
  28. y,
  29. strokeColor,
  30. font,
  31. opacity,
  32. zoom,
  33. angle,
  34. onSubmit,
  35. onCancel,
  36. }: TextWysiwygParams) {
  37. const editable = document.createElement("div");
  38. try {
  39. editable.contentEditable = "plaintext-only";
  40. } catch {
  41. editable.contentEditable = "true";
  42. }
  43. editable.dir = "auto";
  44. editable.tabIndex = 0;
  45. editable.innerText = initText;
  46. editable.dataset.type = "wysiwyg";
  47. const degree = (180 * angle) / Math.PI;
  48. Object.assign(editable.style, {
  49. color: strokeColor,
  50. position: "fixed",
  51. opacity: opacity / 100,
  52. top: `${y}px`,
  53. left: `${x}px`,
  54. transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
  55. textAlign: "left",
  56. display: "inline-block",
  57. font: font,
  58. padding: "4px",
  59. // This needs to have "1px solid" otherwise the carret doesn't show up
  60. // the first time on Safari and Chrome!
  61. outline: "1px solid transparent",
  62. whiteSpace: "nowrap",
  63. minHeight: "1em",
  64. backfaceVisibility: "hidden",
  65. });
  66. editable.onpaste = (ev) => {
  67. try {
  68. const selection = window.getSelection();
  69. if (!selection?.rangeCount) {
  70. return;
  71. }
  72. selection.deleteFromDocument();
  73. const text = ev.clipboardData!.getData("text").replace(/\r\n?/g, "\n");
  74. const span = document.createElement("span");
  75. span.innerText = text;
  76. const range = selection.getRangeAt(0);
  77. range.insertNode(span);
  78. // deselect
  79. window.getSelection()!.removeAllRanges();
  80. range.setStart(span, span.childNodes.length);
  81. range.setEnd(span, span.childNodes.length);
  82. selection.addRange(range);
  83. ev.preventDefault();
  84. } catch (error) {
  85. console.error(error);
  86. }
  87. };
  88. editable.onkeydown = (ev) => {
  89. if (ev.key === KEYS.ESCAPE) {
  90. ev.preventDefault();
  91. handleSubmit();
  92. }
  93. if (ev.key === KEYS.ENTER && !ev.shiftKey) {
  94. ev.stopPropagation();
  95. }
  96. };
  97. editable.onblur = handleSubmit;
  98. function stopEvent(ev: Event) {
  99. ev.stopPropagation();
  100. }
  101. function handleSubmit() {
  102. if (editable.innerText) {
  103. onSubmit(trimText(editable.innerText));
  104. } else {
  105. onCancel();
  106. }
  107. cleanup();
  108. }
  109. function cleanup() {
  110. editable.onblur = null;
  111. editable.onkeydown = null;
  112. editable.onpaste = null;
  113. window.removeEventListener("wheel", stopEvent, true);
  114. document.body.removeChild(editable);
  115. }
  116. window.addEventListener("wheel", stopEvent, true);
  117. document.body.appendChild(editable);
  118. editable.focus();
  119. selectNode(editable);
  120. }