textWysiwyg.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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. onChange?: (text: string) => void;
  23. onSubmit: (text: string) => void;
  24. onCancel: () => void;
  25. };
  26. export function textWysiwyg({
  27. initText,
  28. x,
  29. y,
  30. strokeColor,
  31. font,
  32. opacity,
  33. zoom,
  34. angle,
  35. onChange,
  36. onSubmit,
  37. onCancel,
  38. }: TextWysiwygParams) {
  39. const editable = document.createElement("div");
  40. try {
  41. editable.contentEditable = "plaintext-only";
  42. } catch {
  43. editable.contentEditable = "true";
  44. }
  45. editable.dir = "auto";
  46. editable.tabIndex = 0;
  47. editable.innerText = initText;
  48. editable.dataset.type = "wysiwyg";
  49. const degree = (180 * angle) / Math.PI;
  50. Object.assign(editable.style, {
  51. color: strokeColor,
  52. position: "fixed",
  53. opacity: opacity / 100,
  54. top: `${y}px`,
  55. left: `${x}px`,
  56. transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
  57. textAlign: "left",
  58. display: "inline-block",
  59. font: font,
  60. padding: "4px",
  61. // This needs to have "1px solid" otherwise the carret doesn't show up
  62. // the first time on Safari and Chrome!
  63. outline: "1px solid transparent",
  64. whiteSpace: "nowrap",
  65. minHeight: "1em",
  66. backfaceVisibility: "hidden",
  67. });
  68. editable.onpaste = (ev) => {
  69. try {
  70. const selection = window.getSelection();
  71. if (!selection?.rangeCount) {
  72. return;
  73. }
  74. selection.deleteFromDocument();
  75. const text = ev.clipboardData!.getData("text").replace(/\r\n?/g, "\n");
  76. const span = document.createElement("span");
  77. span.innerText = text;
  78. const range = selection.getRangeAt(0);
  79. range.insertNode(span);
  80. // deselect
  81. window.getSelection()!.removeAllRanges();
  82. range.setStart(span, span.childNodes.length);
  83. range.setEnd(span, span.childNodes.length);
  84. selection.addRange(range);
  85. ev.preventDefault();
  86. } catch (error) {
  87. console.error(error);
  88. }
  89. };
  90. if (onChange) {
  91. editable.oninput = () => {
  92. onChange(trimText(editable.innerText));
  93. };
  94. }
  95. editable.onkeydown = (ev) => {
  96. if (ev.key === KEYS.ESCAPE) {
  97. ev.preventDefault();
  98. handleSubmit();
  99. }
  100. if (ev.key === KEYS.ENTER && (ev.shiftKey || ev.metaKey)) {
  101. ev.preventDefault();
  102. if (ev.isComposing || ev.keyCode === 229) {
  103. return;
  104. }
  105. handleSubmit();
  106. }
  107. if (ev.key === KEYS.ENTER && !ev.shiftKey) {
  108. ev.stopPropagation();
  109. }
  110. };
  111. editable.onblur = handleSubmit;
  112. function stopEvent(ev: Event) {
  113. ev.stopPropagation();
  114. }
  115. function handleSubmit() {
  116. if (editable.innerText) {
  117. onSubmit(trimText(editable.innerText));
  118. } else {
  119. onCancel();
  120. }
  121. cleanup();
  122. }
  123. function cleanup() {
  124. window.removeEventListener("wheel", stopEvent, true);
  125. document.body.removeChild(editable);
  126. }
  127. window.addEventListener("wheel", stopEvent, true);
  128. document.body.appendChild(editable);
  129. editable.focus();
  130. selectNode(editable);
  131. }