@sofereditor/react 0.3.2 → 0.4.0

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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject, JSX, ReactNode } from 'react';
2
- import { EditorDocument, EditorHistory, SerializedDocument, Selection, MarkName, MarkAttrs, BlockType, BlockAttrs, AlignValue, ListKind, PageSettings, TableLocation, TableRect, ImageEmbed, EmbedLoc, SerializedBlock, DeltaOp, Position } from '@sofereditor/core';
2
+ import { EditorDocument, EditorHistory, SerializedDocument, Selection, MarkName, MarkAttrs, BlockType, BlockAttrs, AlignValue, TableBorderPreset, ListKind, PageSettings, TableLocation, AnswerLineSpacing, TableRect, ImageEmbed, EmbedLoc, SerializedBlock, DeltaOp, Position } from '@sofereditor/core';
3
3
 
4
4
  /**
5
5
  * Subconjunto mínimo da Awareness do y-protocols que o overlay usa. Tipar assim
@@ -180,6 +180,10 @@ interface UseEditorResult {
180
180
  setCellBackground: (color: string | null) => void;
181
181
  /** Cor de fundo da célula focada; undefined fora de tabela ou sem cor. */
182
182
  getCellBackground: () => string | undefined;
183
+ /** Onde as linhas da grade da tabela focada aparecem. No-op fora de tabela. */
184
+ setTableBorderPreset: (preset: TableBorderPreset) => void;
185
+ /** Preset da tabela focada; "all" fora de tabela ou quando ausente. */
186
+ getTableBorderPreset: () => TableBorderPreset;
183
187
  isListActive: (kind: ListKind) => boolean;
184
188
  toggleList: (kind: ListKind) => void;
185
189
  indentList: () => void;
@@ -214,6 +218,8 @@ interface UseEditorResult {
214
218
  isInTable: () => boolean;
215
219
  /** Returns `{blockIndex, row, col}` for the focus position, or null when outside a table. */
216
220
  getTableLocation: () => TableLocation | null;
221
+ /** Insere `count` linhas de resposta pautadas depois do bloco focado. */
222
+ insertAnswerLines: (count: number, spacing: AnswerLineSpacing) => void;
217
223
  insertTable: (rows: number, cols: number) => void;
218
224
  /** Removes the table at `blockIndex`. Defaults to the current focus table. */
219
225
  deleteTable: (blockIndex?: number) => void;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject, JSX, ReactNode } from 'react';
2
- import { EditorDocument, EditorHistory, SerializedDocument, Selection, MarkName, MarkAttrs, BlockType, BlockAttrs, AlignValue, ListKind, PageSettings, TableLocation, TableRect, ImageEmbed, EmbedLoc, SerializedBlock, DeltaOp, Position } from '@sofereditor/core';
2
+ import { EditorDocument, EditorHistory, SerializedDocument, Selection, MarkName, MarkAttrs, BlockType, BlockAttrs, AlignValue, TableBorderPreset, ListKind, PageSettings, TableLocation, AnswerLineSpacing, TableRect, ImageEmbed, EmbedLoc, SerializedBlock, DeltaOp, Position } from '@sofereditor/core';
3
3
 
4
4
  /**
5
5
  * Subconjunto mínimo da Awareness do y-protocols que o overlay usa. Tipar assim
@@ -180,6 +180,10 @@ interface UseEditorResult {
180
180
  setCellBackground: (color: string | null) => void;
181
181
  /** Cor de fundo da célula focada; undefined fora de tabela ou sem cor. */
182
182
  getCellBackground: () => string | undefined;
183
+ /** Onde as linhas da grade da tabela focada aparecem. No-op fora de tabela. */
184
+ setTableBorderPreset: (preset: TableBorderPreset) => void;
185
+ /** Preset da tabela focada; "all" fora de tabela ou quando ausente. */
186
+ getTableBorderPreset: () => TableBorderPreset;
183
187
  isListActive: (kind: ListKind) => boolean;
184
188
  toggleList: (kind: ListKind) => void;
185
189
  indentList: () => void;
@@ -214,6 +218,8 @@ interface UseEditorResult {
214
218
  isInTable: () => boolean;
215
219
  /** Returns `{blockIndex, row, col}` for the focus position, or null when outside a table. */
216
220
  getTableLocation: () => TableLocation | null;
221
+ /** Insere `count` linhas de resposta pautadas depois do bloco focado. */
222
+ insertAnswerLines: (count: number, spacing: AnswerLineSpacing) => void;
217
223
  insertTable: (rows: number, cols: number) => void;
218
224
  /** Removes the table at `blockIndex`. Defaults to the current focus table. */
219
225
  deleteTable: (blockIndex?: number) => void;
package/dist/index.js CHANGED
@@ -1708,11 +1708,19 @@ function RemoteCursorsOverlay({
1708
1708
 
1709
1709
  // src/NodeView.tsx
1710
1710
  import { useMemo as useMemo2, useRef as useRef7 } from "react";
1711
- import { tableRectSelection } from "@sofereditor/core";
1711
+ import {
1712
+ answerLineStyle,
1713
+ cellBorderStyle,
1714
+ tableRectSelection
1715
+ } from "@sofereditor/core";
1712
1716
 
1713
1717
  // src/renderInline.tsx
1714
1718
  import { Fragment as Fragment3, memo } from "react";
1715
- import { isImageEmbed } from "@sofereditor/core";
1719
+ import {
1720
+ BLANK_STYLE,
1721
+ isImageEmbed,
1722
+ splitUnderscoreRuns
1723
+ } from "@sofereditor/core";
1716
1724
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1717
1725
  function renderInline(delta, keyPrefix) {
1718
1726
  if (delta.length === 0 || delta.length === 1 && typeof delta[0].insert === "string" && delta[0].insert === "") {
@@ -1891,8 +1899,15 @@ function ImageEmbedViewImpl({
1891
1899
  }
1892
1900
  );
1893
1901
  }
1902
+ function decorateBlanks(text) {
1903
+ const segs = splitUnderscoreRuns(text);
1904
+ if (segs.length === 1 && !segs[0].blank) return text;
1905
+ return segs.map(
1906
+ (s, i) => s.blank ? /* @__PURE__ */ jsx9("span", { "data-blank": "true", style: BLANK_STYLE, children: s.text }, i) : /* @__PURE__ */ jsx9(Fragment3, { children: s.text }, i)
1907
+ );
1908
+ }
1894
1909
  function wrap(text, attrs) {
1895
- let node = text;
1910
+ let node = decorateBlanks(text);
1896
1911
  if (!attrs) return node;
1897
1912
  if (attrs.bold) node = /* @__PURE__ */ jsx9("strong", { children: node });
1898
1913
  if (attrs.italic) node = /* @__PURE__ */ jsx9("em", { children: node });
@@ -1900,6 +1915,7 @@ function wrap(text, attrs) {
1900
1915
  if (attrs.strike) node = /* @__PURE__ */ jsx9("s", { children: node });
1901
1916
  const styleParts = {};
1902
1917
  if (attrs.color) styleParts.color = attrs.color;
1918
+ if (attrs.highlight) styleParts.backgroundColor = attrs.highlight;
1903
1919
  if (attrs.fontFamily) styleParts.fontFamily = attrs.fontFamily;
1904
1920
  if (attrs.fontSize) styleParts.fontSize = attrs.fontSize;
1905
1921
  if (Object.keys(styleParts).length > 0) {
@@ -2152,6 +2168,7 @@ function NodeView({ block, index, children, fragment, tableFragment }) {
2152
2168
  function commonBlockProps(attrs, index, type, fragment) {
2153
2169
  const style = {};
2154
2170
  if (attrs.align) style.textAlign = attrs.align;
2171
+ Object.assign(style, answerLineStyle(attrs) ?? {});
2155
2172
  const base = {
2156
2173
  "data-block-index": index,
2157
2174
  "data-block-type": type,
@@ -2171,11 +2188,10 @@ function fragmentClass(f) {
2171
2188
  function alignClass(a) {
2172
2189
  return a ? ` ed-align-${a}` : "";
2173
2190
  }
2174
- function cellStyle(attrs) {
2175
- if (!attrs) return void 0;
2176
- const style = {};
2177
- if (attrs.align) style.textAlign = attrs.align;
2178
- if (attrs.bgColor) style.backgroundColor = attrs.bgColor;
2191
+ function cellStyle(attrs, border) {
2192
+ const style = { ...border };
2193
+ if (attrs?.align) style.textAlign = attrs.align;
2194
+ if (attrs?.bgColor) style.backgroundColor = attrs.bgColor;
2179
2195
  return Object.keys(style).length > 0 ? style : void 0;
2180
2196
  }
2181
2197
  function clampLevel(l) {
@@ -2231,7 +2247,16 @@ function TableView({ block, index, tableFragment }) {
2231
2247
  "data-cell-colspan": colspan,
2232
2248
  rowSpan: rowspan > 1 ? rowspan : void 0,
2233
2249
  colSpan: colspan > 1 ? colspan : void 0,
2234
- style: cellStyle(cell?.attrs),
2250
+ style: cellStyle(
2251
+ cell?.attrs,
2252
+ // rowStart/rowEnd são os limites do FRAGMENTO: numa tabela
2253
+ // quebrada entre páginas, cada página fecha a própria caixa.
2254
+ cellBorderStyle(
2255
+ block.attrs.borderPreset,
2256
+ { row: r, col: c, rowspan, colspan, cols, rowStart, rowEnd },
2257
+ "screen"
2258
+ )
2259
+ ),
2235
2260
  className: inRect ? "ed-cell ed-cell--selected" : "ed-cell",
2236
2261
  children: renderInline(delta, `t${index}-c${flat}`)
2237
2262
  },
@@ -2814,12 +2839,14 @@ import {
2814
2839
  mergeRight as cmdMergeRight,
2815
2840
  mergeSelection as cmdMergeSelection,
2816
2841
  moveEmbedAnchor as cmdMoveEmbedAnchor,
2842
+ setBlockAttrAtIndex as cmdSetBlockAttrAtIndex,
2817
2843
  setColumnWidth as cmdSetColumnWidth,
2818
2844
  setImageAttrs as cmdSetImageAttrs,
2819
2845
  splitCell as cmdSplitCell,
2820
2846
  tableRectSelection as tableRectSelection2,
2821
2847
  getMarksInRange,
2822
2848
  indentList as cmdIndentList,
2849
+ insertAnswerLines as cmdInsertAnswerLines,
2823
2850
  insertTable as cmdInsertTable,
2824
2851
  insertTableColumn as cmdInsertColumn,
2825
2852
  insertTableRow as cmdInsertRow,
@@ -3050,6 +3077,16 @@ function useEditor(opts = {}) {
3050
3077
  if (sel.focus.cellIndex == null || !doc.isTable(sel.focus.blockIndex)) return void 0;
3051
3078
  return doc.getCellAttrs(sel.focus.blockIndex, sel.focus.cellIndex).bgColor;
3052
3079
  }, [doc]);
3080
+ const setTableBorderPreset = useCallback6((preset) => {
3081
+ const sel = selectionRef.current;
3082
+ if (!doc.isTable(sel.focus.blockIndex)) return;
3083
+ cmdSetBlockAttrAtIndex(ctxRef.current, sel.focus.blockIndex, "borderPreset", preset);
3084
+ }, [doc]);
3085
+ const getTableBorderPreset = useCallback6(() => {
3086
+ const sel = selectionRef.current;
3087
+ if (!doc.isTable(sel.focus.blockIndex)) return "all";
3088
+ return doc.getBlockAttrs(sel.focus.blockIndex).borderPreset ?? "all";
3089
+ }, [doc]);
3053
3090
  const isListActive = useCallback6(
3054
3091
  (kind) => {
3055
3092
  const sel = selectionRef.current;
@@ -3083,6 +3120,9 @@ function useEditor(opts = {}) {
3083
3120
  const insertTable = useCallback6((rows, cols) => {
3084
3121
  cmdInsertTable(ctxRef.current, rows, cols);
3085
3122
  }, []);
3123
+ const insertAnswerLines = useCallback6((count, spacing) => {
3124
+ cmdInsertAnswerLines(ctxRef.current, count, spacing);
3125
+ }, []);
3086
3126
  const deleteTable = useCallback6((blockIndex) => {
3087
3127
  const idx = blockIndex ?? selectionRef.current.focus.blockIndex;
3088
3128
  cmdDeleteTable(ctxRef.current, idx);
@@ -3291,6 +3331,8 @@ function useEditor(opts = {}) {
3291
3331
  getAlign,
3292
3332
  setCellBackground,
3293
3333
  getCellBackground,
3334
+ setTableBorderPreset,
3335
+ getTableBorderPreset,
3294
3336
  isListActive,
3295
3337
  toggleList,
3296
3338
  indentList,
@@ -3301,6 +3343,7 @@ function useEditor(opts = {}) {
3301
3343
  resolveLinkRequest,
3302
3344
  isInTable,
3303
3345
  getTableLocation,
3346
+ insertAnswerLines,
3304
3347
  insertTable,
3305
3348
  deleteTable,
3306
3349
  insertRowAbove,
@@ -4260,6 +4303,7 @@ import {
4260
4303
  useRef as useRef12,
4261
4304
  useState as useState11
4262
4305
  } from "react";
4306
+ import { ANSWER_LINE_MAX } from "@sofereditor/core";
4263
4307
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
4264
4308
  var FONT_FAMILIES = ["Arial"];
4265
4309
  var FONT_SIZES = ["8pt", "10pt", "11pt", "12pt", "14pt", "16pt", "18pt", "24pt", "32pt", "48pt"];
@@ -4326,6 +4370,9 @@ function Toolbar({ className }) {
4326
4370
  const onColorChange = (e) => {
4327
4371
  setMark("color", e.target.value);
4328
4372
  };
4373
+ const onHighlightChange = (e) => {
4374
+ setMark("highlight", e.target.value);
4375
+ };
4329
4376
  const onFontFamilyChange = (e) => {
4330
4377
  const v = e.target.value;
4331
4378
  if (v === "") removeMark("fontFamily");
@@ -4390,6 +4437,7 @@ function Toolbar({ className }) {
4390
4437
  [active.link, editor, removeMark, setMark]
4391
4438
  );
4392
4439
  const colorValue = typeof active.color === "string" ? active.color : "#000000";
4440
+ const highlightValue = typeof active.highlight === "string" ? active.highlight : "#fff176";
4393
4441
  const fontFamilyValue = typeof active.fontFamily === "string" ? active.fontFamily : "";
4394
4442
  const fontSizeValue = typeof active.fontSize === "string" ? active.fontSize : "";
4395
4443
  const blockValue = blockSelectValue(blockType, blockLevel);
@@ -4453,6 +4501,40 @@ function Toolbar({ className }) {
4453
4501
  },
4454
4502
  children: "\u232B"
4455
4503
  }
4504
+ ),
4505
+ /* @__PURE__ */ jsxs12("label", { className: "ed-toolbar-label", title: "Marca-texto", children: [
4506
+ /* @__PURE__ */ jsx13(
4507
+ "span",
4508
+ {
4509
+ className: "ed-toolbar-swatch ed-toolbar-swatch--highlight",
4510
+ "aria-hidden": true,
4511
+ style: { background: highlightValue }
4512
+ }
4513
+ ),
4514
+ /* @__PURE__ */ jsx13(
4515
+ "input",
4516
+ {
4517
+ type: "color",
4518
+ value: highlightValue,
4519
+ onChange: onHighlightChange,
4520
+ onMouseDown: stop,
4521
+ "aria-label": "Marca-texto"
4522
+ }
4523
+ )
4524
+ ] }),
4525
+ /* @__PURE__ */ jsx13(
4526
+ "button",
4527
+ {
4528
+ type: "button",
4529
+ className: "ed-toolbar-btn",
4530
+ title: "Remover marca-texto",
4531
+ onMouseDown: stop,
4532
+ onClick: (e) => {
4533
+ e.preventDefault();
4534
+ removeMark("highlight");
4535
+ },
4536
+ children: "\u232B"
4537
+ }
4456
4538
  )
4457
4539
  ] }),
4458
4540
  /* @__PURE__ */ jsxs12(Group, { children: [
@@ -4582,7 +4664,10 @@ function Toolbar({ className }) {
4582
4664
  children: "\u{1F517}"
4583
4665
  }
4584
4666
  ) }),
4585
- /* @__PURE__ */ jsx13(Group, { children: /* @__PURE__ */ jsx13(TableMenu, {}) }),
4667
+ /* @__PURE__ */ jsxs12(Group, { children: [
4668
+ /* @__PURE__ */ jsx13(TableMenu, {}),
4669
+ /* @__PURE__ */ jsx13(AnswerLinesMenu, {})
4670
+ ] }),
4586
4671
  /* @__PURE__ */ jsxs12(Group, { children: [
4587
4672
  /* @__PURE__ */ jsx13(
4588
4673
  "button",
@@ -4668,6 +4753,102 @@ function Toolbar({ className }) {
4668
4753
  }
4669
4754
  );
4670
4755
  }
4756
+ function AnswerLinesMenu() {
4757
+ const editor = useEditorContext();
4758
+ const [open, setOpen] = useState11(false);
4759
+ const [count, setCount] = useState11(5);
4760
+ const [spacing, setSpacing] = useState11(1.5);
4761
+ const rootRef = useRef12(null);
4762
+ useEffect10(() => {
4763
+ if (!open) return;
4764
+ const onDown = (e) => {
4765
+ if (!rootRef.current) return;
4766
+ if (rootRef.current.contains(e.target)) return;
4767
+ setOpen(false);
4768
+ };
4769
+ document.addEventListener("mousedown", onDown);
4770
+ return () => document.removeEventListener("mousedown", onDown);
4771
+ }, [open]);
4772
+ const clamped = Math.max(1, Math.min(ANSWER_LINE_MAX, Math.trunc(count) || 1));
4773
+ return /* @__PURE__ */ jsxs12("div", { ref: rootRef, className: "ed-toolbar-tablemenu", children: [
4774
+ /* @__PURE__ */ jsx13(
4775
+ "button",
4776
+ {
4777
+ type: "button",
4778
+ className: "ed-toolbar-btn",
4779
+ title: "Linhas de resposta",
4780
+ "aria-pressed": open,
4781
+ onMouseDown: (e) => e.preventDefault(),
4782
+ onClick: (e) => {
4783
+ e.preventDefault();
4784
+ setOpen((v) => !v);
4785
+ },
4786
+ children: "\u2630"
4787
+ }
4788
+ ),
4789
+ open && /* @__PURE__ */ jsxs12(
4790
+ "div",
4791
+ {
4792
+ className: "ed-table-popover ed-answerlines-popover",
4793
+ role: "menu",
4794
+ onMouseDown: (e) => {
4795
+ const t = e.target;
4796
+ if (!t || t.tagName !== "INPUT" && t.tagName !== "BUTTON" && t.tagName !== "SELECT") {
4797
+ e.preventDefault();
4798
+ }
4799
+ },
4800
+ children: [
4801
+ /* @__PURE__ */ jsxs12("label", { className: "ed-toolbar-label", children: [
4802
+ "Quantidade",
4803
+ /* @__PURE__ */ jsx13(
4804
+ "input",
4805
+ {
4806
+ type: "number",
4807
+ min: 1,
4808
+ max: ANSWER_LINE_MAX,
4809
+ value: count,
4810
+ onChange: (e) => setCount(Number(e.target.value)),
4811
+ "aria-label": "Quantidade de linhas"
4812
+ }
4813
+ )
4814
+ ] }),
4815
+ /* @__PURE__ */ jsxs12("label", { className: "ed-toolbar-label", children: [
4816
+ "Entrelinha",
4817
+ /* @__PURE__ */ jsxs12(
4818
+ "select",
4819
+ {
4820
+ value: String(spacing),
4821
+ onChange: (e) => setSpacing(Number(e.target.value)),
4822
+ "aria-label": "Entrelinha",
4823
+ children: [
4824
+ /* @__PURE__ */ jsx13("option", { value: "1", children: "Simples" }),
4825
+ /* @__PURE__ */ jsx13("option", { value: "1.5", children: "1,5" }),
4826
+ /* @__PURE__ */ jsx13("option", { value: "2", children: "Duplo" })
4827
+ ]
4828
+ }
4829
+ )
4830
+ ] }),
4831
+ /* @__PURE__ */ jsxs12(
4832
+ "button",
4833
+ {
4834
+ type: "button",
4835
+ onClick: () => {
4836
+ setOpen(false);
4837
+ editor.insertAnswerLines(clamped, spacing);
4838
+ },
4839
+ children: [
4840
+ "Inserir ",
4841
+ clamped,
4842
+ " linha",
4843
+ clamped > 1 ? "s" : ""
4844
+ ]
4845
+ }
4846
+ )
4847
+ ]
4848
+ }
4849
+ )
4850
+ ] });
4851
+ }
4671
4852
  function TableMenu() {
4672
4853
  const editor = useEditorContext();
4673
4854
  const inTable = editor.isInTable();
@@ -4796,6 +4977,25 @@ function TableMenu() {
4796
4977
  editor.setCellBackground(null);
4797
4978
  }, children: "Remover cor de fundo" }),
4798
4979
  /* @__PURE__ */ jsx13("hr", {}),
4980
+ /* @__PURE__ */ jsxs12("label", { className: "ed-toolbar-label", children: [
4981
+ "Bordas",
4982
+ /* @__PURE__ */ jsxs12(
4983
+ "select",
4984
+ {
4985
+ value: editor.getTableBorderPreset(),
4986
+ onChange: (e) => editor.setTableBorderPreset(e.target.value),
4987
+ "aria-label": "Bordas da tabela",
4988
+ children: [
4989
+ /* @__PURE__ */ jsx13("option", { value: "all", children: "Todas" }),
4990
+ /* @__PURE__ */ jsx13("option", { value: "outer", children: "S\xF3 externas" }),
4991
+ /* @__PURE__ */ jsx13("option", { value: "horizontal", children: "S\xF3 horizontais" }),
4992
+ /* @__PURE__ */ jsx13("option", { value: "vertical", children: "S\xF3 verticais" }),
4993
+ /* @__PURE__ */ jsx13("option", { value: "none", children: "Nenhuma" })
4994
+ ]
4995
+ }
4996
+ )
4997
+ ] }),
4998
+ /* @__PURE__ */ jsx13("hr", {}),
4799
4999
  /* @__PURE__ */ jsx13(
4800
5000
  "button",
4801
5001
  {