vuewrite 0.0.8 → 0.0.9

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.
@@ -34,6 +34,12 @@ export declare type Block = {
34
34
 
35
35
  export declare type Decorator = (style: Style) => HTMLAttributes | undefined;
36
36
 
37
+ declare type HistoryAction = {
38
+ type: "insertText" | "setText";
39
+ blocks: Block[];
40
+ selection: TextEditorSelection;
41
+ };
42
+
37
43
  export declare type Style = {
38
44
  start: number;
39
45
  end: number;
@@ -49,9 +55,11 @@ text: string;
49
55
  styles?: Style[] | undefined;
50
56
  type?: string | undefined;
51
57
  }[] | undefined;
58
+ parser?: TextParser | undefined;
52
59
  styles?: Style[] | undefined;
53
60
  autofocus?: boolean | undefined;
54
61
  autoselect?: boolean | undefined;
62
+ preventMultiline?: boolean | undefined;
55
63
  }>, {
56
64
  currentStyles: ComputedRef<Map<string, Style>>;
57
65
  currentBlock: ComputedRef< {
@@ -86,6 +94,7 @@ insertBlock: (blockData: Partial<Block>) => void;
86
94
  addNewLine: () => void;
87
95
  removeNewLine: () => void;
88
96
  selectAll: () => void;
97
+ pushHistory: (type: "insertText" | "setText") => void;
89
98
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
90
99
  }, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
91
100
  keydown: (...args: any[]) => void;
@@ -99,9 +108,11 @@ text: string;
99
108
  styles?: Style[] | undefined;
100
109
  type?: string | undefined;
101
110
  }[] | undefined;
111
+ parser?: TextParser | undefined;
102
112
  styles?: Style[] | undefined;
103
113
  autofocus?: boolean | undefined;
104
114
  autoselect?: boolean | undefined;
115
+ preventMultiline?: boolean | undefined;
105
116
  }>>> & {
106
117
  onKeydown?: ((...args: any[]) => any) | undefined;
107
118
  "onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
@@ -110,9 +121,22 @@ onKeydown?: ((...args: any[]) => any) | undefined;
110
121
  placeholder?(_: {}): any;
111
122
  }>;
112
123
 
124
+ declare class TextEditorHistory {
125
+ actions: HistoryAction[];
126
+ currentCursor: number;
127
+ private store;
128
+ constructor(store: TextEditorStore);
129
+ tickStarted: boolean;
130
+ push(type: HistoryAction["type"]): void;
131
+ private applyAction;
132
+ undo(): void;
133
+ redo(): void;
134
+ }
135
+
113
136
  export declare type TextEditorRef = Pick<TextEditorStore, "currentStyles" | "currentBlock" | "isCollapsed" | "selection" | "toggleStyle" | "applyStyle" | "removeStyle" | "insertText" | "insertBlock" | "addNewLine" | "removeNewLine" | "selectAll"> & {
114
137
  isFocused: boolean;
115
138
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
139
+ pushHistory: TextEditorHistory["push"];
116
140
  };
117
141
 
118
142
  declare type TextEditorSelection = {
@@ -127,6 +151,7 @@ declare type TextEditorSelection = {
127
151
  };
128
152
 
129
153
  declare class TextEditorStore {
154
+ history: TextEditorHistory;
130
155
  blocks: {
131
156
  id: string;
132
157
  text: string;
@@ -204,4 +229,6 @@ declare class TextEditorStore {
204
229
  selectAll(): void;
205
230
  }
206
231
 
232
+ declare type TextParser = (text: string) => Style[];
233
+
207
234
  export { }
package/dist/vuewrite.js CHANGED
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => {
4
4
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
  return value;
6
6
  };
7
- import { getCurrentScope, onScopeDispose, unref, watch, reactive, computed, ref, defineComponent, getCurrentInstance, openBlock, createBlock, resolveDynamicComponent, createElementBlock, normalizeProps, mergeProps, h, nextTick, useSlots, onMounted, Fragment, renderList, renderSlot, createCommentVNode } from "vue";
7
+ import { getCurrentScope, onScopeDispose, unref, watch, nextTick, reactive, computed, ref, defineComponent, getCurrentInstance, openBlock, createBlock, resolveDynamicComponent, createElementBlock, normalizeProps, mergeProps, h, useSlots, onMounted, Fragment, renderList, renderSlot, createCommentVNode } from "vue";
8
8
  function tryOnScopeDispose(fn) {
9
9
  if (getCurrentScope()) {
10
10
  onScopeDispose(fn);
@@ -140,8 +140,85 @@ const isEqual = (a, b) => {
140
140
  }
141
141
  return true;
142
142
  };
143
+ class TextEditorHistory {
144
+ constructor(store) {
145
+ __publicField(this, "actions", []);
146
+ __publicField(this, "currentCursor", 0);
147
+ __publicField(this, "store");
148
+ __publicField(this, "tickStarted", false);
149
+ this.store = store;
150
+ window.showHistory = () => console.log(this.actions);
151
+ }
152
+ push(type) {
153
+ var _a;
154
+ if (this.currentCursor + 1 < this.actions.length) {
155
+ this.actions.length = this.currentCursor + 1;
156
+ }
157
+ const lastAction = this.actions.length > 0 && this.currentCursor === this.actions.length - 1 ? this.actions[this.actions.length - 1] : null;
158
+ const selection = JSON.parse(JSON.stringify(this.store.selection));
159
+ if (type === "insertText") {
160
+ const currentBlock = JSON.parse(JSON.stringify(this.store.currentBlock));
161
+ if (lastAction && lastAction.type === type && ((_a = lastAction.blocks[0]) == null ? void 0 : _a.id) === currentBlock.id) {
162
+ lastAction.blocks = [currentBlock];
163
+ lastAction.selection = selection;
164
+ } else {
165
+ this.actions.push({ type, blocks: [currentBlock], selection });
166
+ }
167
+ } else if (type === "setText") {
168
+ const blocks = JSON.parse(JSON.stringify(this.store.blocks));
169
+ if (this.tickStarted && lastAction) {
170
+ lastAction.blocks = blocks;
171
+ lastAction.selection = selection;
172
+ } else {
173
+ this.actions.push({ type: "setText", blocks, selection });
174
+ }
175
+ }
176
+ this.currentCursor = this.actions.length - 1;
177
+ if (!this.tickStarted && type === "setText") {
178
+ this.tickStarted = true;
179
+ nextTick(() => {
180
+ this.tickStarted = false;
181
+ });
182
+ }
183
+ }
184
+ applyAction(action) {
185
+ if (action.type === "setText") {
186
+ for (let i = 0; i < action.blocks.length; i++) {
187
+ if (i >= this.store.blocks.length) {
188
+ this.store.blocks.push({ ...action.blocks[i] });
189
+ } else {
190
+ Object.assign(this.store.blocks[i], action.blocks[i]);
191
+ }
192
+ }
193
+ if (this.store.blocks.length > action.blocks.length) {
194
+ this.store.blocks.length = action.blocks.length;
195
+ }
196
+ } else {
197
+ for (let _block of action.blocks) {
198
+ const block = this.store.blocks.find((block2) => block2.id === _block.id);
199
+ if (block) {
200
+ block.text = _block.text;
201
+ }
202
+ }
203
+ }
204
+ Object.assign(this.store.selection, action.selection);
205
+ }
206
+ undo() {
207
+ if (this.currentCursor === 0)
208
+ return;
209
+ this.currentCursor--;
210
+ this.applyAction(this.actions[this.currentCursor]);
211
+ }
212
+ redo() {
213
+ if (this.currentCursor >= this.actions.length - 1)
214
+ return;
215
+ this.currentCursor++;
216
+ this.applyAction(this.actions[this.currentCursor]);
217
+ }
218
+ }
143
219
  class TextEditorStore {
144
220
  constructor() {
221
+ __publicField(this, "history", new TextEditorHistory(this));
145
222
  __publicField(this, "blocks", reactive([{ id: uid(), text: "", styles: [] }]));
146
223
  __publicField(this, "selection", reactive({
147
224
  anchor: { blockId: this.blocks[0].id, offset: 0 },
@@ -224,6 +301,7 @@ class TextEditorStore {
224
301
  this.selection.focus.blockId = this.blocks[blockIndex - 1].id;
225
302
  this.selection.anchor.offset = this.blocks[blockIndex - 1].text.length;
226
303
  this.selection.focus.offset = this.blocks[blockIndex - 1].text.length;
304
+ this.history.push("setText");
227
305
  }
228
306
  onInput(_e) {
229
307
  const ev = _e;
@@ -266,6 +344,7 @@ class TextEditorStore {
266
344
  }
267
345
  if (ev.inputType === "insertText") {
268
346
  this.insertText(ev.data);
347
+ this.history.push("insertText");
269
348
  }
270
349
  }
271
350
  addNewLine() {
@@ -279,13 +358,15 @@ class TextEditorStore {
279
358
  this.blocks.splice(index + 1, 0, block);
280
359
  this.selection.anchor = { blockId: block.id, offset: 0 };
281
360
  this.selection.focus = { blockId: block.id, offset: 0 };
361
+ this.history.push("setText");
282
362
  }
283
363
  insertText(data) {
284
364
  const block = this.currentBlock;
285
365
  if (!block)
286
366
  return;
287
- block.text = block.text.slice(0, this.selection.focus.offset) + data + block.text.slice(this.selection.focus.offset);
288
- this.moveOffset(clamp(this.selection.focus.offset + data.length, 0, block.text.length));
367
+ const text = data.replace(/\r/g, "");
368
+ block.text = block.text.slice(0, this.selection.focus.offset) + text + block.text.slice(this.selection.focus.offset);
369
+ this.moveOffset(clamp(this.selection.focus.offset + text.length, 0, block.text.length));
289
370
  }
290
371
  insertBlock(blockData) {
291
372
  if (!this.currentBlock)
@@ -303,6 +384,7 @@ class TextEditorStore {
303
384
  if (blockData.editable === false && this.currentBlock === this.blocks[this.blocks.length - 1]) {
304
385
  this.addNewLine();
305
386
  }
387
+ this.history.push("setText");
306
388
  }
307
389
  get startAndEnd() {
308
390
  const a = this.blocks.findIndex((block) => block.id === this.selection.anchor.blockId);
@@ -447,7 +529,8 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
447
529
  props: {
448
530
  block: {},
449
531
  slots: {},
450
- decorator: { type: Function }
532
+ decorator: { type: Function },
533
+ parser: {}
451
534
  },
452
535
  emits: ["postrender"],
453
536
  setup(__props, { emit: __emit }) {
@@ -546,6 +629,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
546
629
  markers.push([style.start, style]);
547
630
  markers.push([style.end, style]);
548
631
  }
632
+ if (props.parser) {
633
+ for (let style of props.parser(text)) {
634
+ markers.push([style.start, style]);
635
+ markers.push([style.end, style]);
636
+ }
637
+ }
549
638
  markers.sort((a, b) => a[0] - b[0]);
550
639
  let currentIndex = 0;
551
640
  const activeStyles = /* @__PURE__ */ new Set();
@@ -588,9 +677,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
588
677
  decorator: { type: Function },
589
678
  single: { type: Boolean },
590
679
  modelValue: {},
680
+ parser: { type: Function },
591
681
  styles: {},
592
682
  autofocus: { type: Boolean },
593
- autoselect: { type: Boolean }
683
+ autoselect: { type: Boolean },
684
+ preventMultiline: { type: Boolean }
594
685
  },
595
686
  emits: ["keydown", "update:modelValue", "update:styles"],
596
687
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -649,7 +740,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
649
740
  return;
650
741
  if (e.code === "Enter") {
651
742
  e.preventDefault();
652
- if (e.shiftKey || props.single) {
743
+ if (!props.preventMultiline && (e.shiftKey || props.single)) {
653
744
  store.insertText("\n");
654
745
  } else {
655
746
  store.addNewLine();
@@ -660,24 +751,34 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
660
751
  e.preventDefault();
661
752
  }
662
753
  }
754
+ if ((e.ctrlKey || e.metaKey) && e.code === "KeyZ" && !e.shiftKey) {
755
+ e.preventDefault();
756
+ store.history.undo();
757
+ }
758
+ if ((e.ctrlKey || e.metaKey) && (e.code === "KeyY" || e.shiftKey && e.code === "KeyZ")) {
759
+ e.preventDefault();
760
+ store.history.redo();
761
+ }
663
762
  };
664
763
  let cachedSelection = {};
665
764
  useEventListener(document, "selectionchange", () => {
666
765
  const sel = window.getSelection();
667
766
  const anchor = findParent(sel.anchorNode, (el) => el.hasAttribute("data-vw-block-id") && el.parentElement === textEditorRef.value);
668
767
  if (anchor) {
669
- const offset = anchor === sel.anchorNode ? 0 : calcOffsetToNode(anchor, sel.anchorNode) + sel.anchorOffset;
768
+ const isNonEditable = anchor.getAttribute("contenteditable") === "false";
769
+ const offset = isNonEditable || anchor === sel.focusNode ? 0 : calcOffsetToNode(anchor, sel.anchorNode) + sel.anchorOffset;
670
770
  store.selection.anchor = { blockId: anchor.getAttribute("data-vw-block-id"), offset };
671
771
  }
672
772
  const focus = findParent(sel.focusNode, (el) => el.hasAttribute("data-vw-block-id") && el.parentElement === textEditorRef.value);
673
773
  if (focus) {
674
- const offset = anchor === sel.focusNode ? 0 : calcOffsetToNode(focus, sel.focusNode) + sel.focusOffset;
774
+ const isNonEditable = focus.getAttribute("contenteditable") === "false";
775
+ const offset = isNonEditable || focus === sel.focusNode ? 0 : calcOffsetToNode(focus, sel.focusNode) + sel.focusOffset;
675
776
  store.selection.focus = { blockId: focus.getAttribute("data-vw-block-id"), offset };
676
777
  }
677
778
  if (store.isFocused.value !== (!!focus || !!anchor)) {
678
779
  store.isFocused.value = !!focus || !!anchor;
679
780
  }
680
- if (!anchor && !focus) {
781
+ if (anchor && focus) {
681
782
  cachedSelection = JSON.parse(JSON.stringify(store.selection));
682
783
  }
683
784
  });
@@ -732,15 +833,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
732
833
  (_b = textEditorRef.value) == null ? void 0 : _b.focus();
733
834
  store.selectAll();
734
835
  }
836
+ store.history.push("setText");
735
837
  });
736
838
  const onCopy = (e) => {
737
839
  e.preventDefault();
738
840
  navigator.clipboard.writeText(store.selectedText);
841
+ store.history.push("setText");
739
842
  };
740
843
  const onCut = (e) => {
741
844
  e.preventDefault();
742
845
  navigator.clipboard.writeText(store.selectedText);
743
846
  store.deleteSelected();
847
+ store.history.push("setText");
744
848
  };
745
849
  const onPaste = (e) => {
746
850
  var _a;
@@ -749,6 +853,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
749
853
  if (!text)
750
854
  return;
751
855
  store.insertText(text);
856
+ store.history.push("setText");
752
857
  };
753
858
  const getClientRects = (selection) => {
754
859
  const anchor = getNode(selection.anchor.blockId);
@@ -775,6 +880,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
775
880
  addNewLine: store.addNewLine.bind(store),
776
881
  removeNewLine: store.removeNewLine.bind(store),
777
882
  selectAll: store.selectAll.bind(store),
883
+ pushHistory: store.history.push.bind(store.history),
778
884
  getClientRects
779
885
  });
780
886
  return (_ctx, _cache) => {
@@ -795,8 +901,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
795
901
  block,
796
902
  slots: unref(slots),
797
903
  decorator: props.decorator,
904
+ parser: props.parser,
798
905
  onPostrender: onPostRender
799
- }, null, 8, ["block", "slots", "decorator"]);
906
+ }, null, 8, ["block", "slots", "decorator", "parser"]);
800
907
  }), 128)),
801
908
  unref(store).blocks.length === 1 && unref(store).blocks[0].text === "" && !unref(store).blocks[0].type ? renderSlot(_ctx.$slots, "placeholder", { key: 0 }) : createCommentVNode("", true)
802
909
  ], 544);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "vuewrite",
3
3
  "description": "Rich Text Editor based on Vue3 reactivity",
4
4
  "private": false,
5
- "version": "0.0.8",
5
+ "version": "0.0.9",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "author": "den59k",