vuewrite 0.0.7 → 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.
@@ -29,10 +29,17 @@ export declare type Block = {
29
29
  text: string;
30
30
  type?: string;
31
31
  styles: Style[];
32
+ editable?: boolean;
32
33
  };
33
34
 
34
35
  export declare type Decorator = (style: Style) => HTMLAttributes | undefined;
35
36
 
37
+ declare type HistoryAction = {
38
+ type: "insertText" | "setText";
39
+ blocks: Block[];
40
+ selection: TextEditorSelection;
41
+ };
42
+
36
43
  export declare type Style = {
37
44
  start: number;
38
45
  end: number;
@@ -48,9 +55,11 @@ text: string;
48
55
  styles?: Style[] | undefined;
49
56
  type?: string | undefined;
50
57
  }[] | undefined;
58
+ parser?: TextParser | undefined;
51
59
  styles?: Style[] | undefined;
52
60
  autofocus?: boolean | undefined;
53
61
  autoselect?: boolean | undefined;
62
+ preventMultiline?: boolean | undefined;
54
63
  }>, {
55
64
  currentStyles: ComputedRef<Map<string, Style>>;
56
65
  currentBlock: ComputedRef< {
@@ -63,6 +72,7 @@ end: number;
63
72
  style: string;
64
73
  meta?: any;
65
74
  }[];
75
+ editable?: boolean | undefined;
66
76
  } | null>;
67
77
  isCollapsed: ComputedRef<boolean>;
68
78
  selection: {
@@ -84,6 +94,7 @@ insertBlock: (blockData: Partial<Block>) => void;
84
94
  addNewLine: () => void;
85
95
  removeNewLine: () => void;
86
96
  selectAll: () => void;
97
+ pushHistory: (type: "insertText" | "setText") => void;
87
98
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
88
99
  }, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
89
100
  keydown: (...args: any[]) => void;
@@ -97,9 +108,11 @@ text: string;
97
108
  styles?: Style[] | undefined;
98
109
  type?: string | undefined;
99
110
  }[] | undefined;
111
+ parser?: TextParser | undefined;
100
112
  styles?: Style[] | undefined;
101
113
  autofocus?: boolean | undefined;
102
114
  autoselect?: boolean | undefined;
115
+ preventMultiline?: boolean | undefined;
103
116
  }>>> & {
104
117
  onKeydown?: ((...args: any[]) => any) | undefined;
105
118
  "onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
@@ -108,9 +121,22 @@ onKeydown?: ((...args: any[]) => any) | undefined;
108
121
  placeholder?(_: {}): any;
109
122
  }>;
110
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
+
111
136
  export declare type TextEditorRef = Pick<TextEditorStore, "currentStyles" | "currentBlock" | "isCollapsed" | "selection" | "toggleStyle" | "applyStyle" | "removeStyle" | "insertText" | "insertBlock" | "addNewLine" | "removeNewLine" | "selectAll"> & {
112
137
  isFocused: boolean;
113
138
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
139
+ pushHistory: TextEditorHistory["push"];
114
140
  };
115
141
 
116
142
  declare type TextEditorSelection = {
@@ -125,6 +151,7 @@ declare type TextEditorSelection = {
125
151
  };
126
152
 
127
153
  declare class TextEditorStore {
154
+ history: TextEditorHistory;
128
155
  blocks: {
129
156
  id: string;
130
157
  text: string;
@@ -135,6 +162,7 @@ declare class TextEditorStore {
135
162
  style: string;
136
163
  meta?: any;
137
164
  }[];
165
+ editable?: boolean | undefined;
138
166
  }[];
139
167
  selection: {
140
168
  anchor: {
@@ -159,6 +187,7 @@ declare class TextEditorStore {
159
187
  style: string;
160
188
  meta?: any;
161
189
  }[];
190
+ editable?: boolean | undefined;
162
191
  } | null>;
163
192
  get currentBlock(): {
164
193
  id: string;
@@ -170,10 +199,12 @@ declare class TextEditorStore {
170
199
  style: string;
171
200
  meta?: any;
172
201
  }[];
202
+ editable?: boolean | undefined;
173
203
  } | null;
174
204
  _selectedText: ComputedRef<string>;
175
205
  get selectedText(): string;
176
206
  moveOffset(newOffset: number): void;
207
+ concatBlocks(start: Block, end: Block): void;
177
208
  removeNewLine(): void;
178
209
  onInput(_e: Event): void;
179
210
  addNewLine(): void;
@@ -198,4 +229,6 @@ declare class TextEditorStore {
198
229
  selectAll(): void;
199
230
  }
200
231
 
232
+ declare type TextParser = (text: string) => Style[];
233
+
201
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 },
@@ -202,18 +279,34 @@ class TextEditorStore {
202
279
  this.selection.anchor.offset = newOffset;
203
280
  this.selection.focus.offset = newOffset;
204
281
  }
282
+ concatBlocks(start, end) {
283
+ for (let style of end.styles) {
284
+ style.start += start.text.length;
285
+ style.end += start.text.length;
286
+ start.styles.push(style);
287
+ }
288
+ start.text = start.text + end.text;
289
+ }
205
290
  removeNewLine() {
206
291
  const blockIndex = this.blocks.findIndex((item) => item.id === this.selection.anchor.blockId);
207
292
  if (blockIndex < 1)
208
293
  return;
294
+ if (this.blocks[blockIndex - 1].editable === false) {
295
+ this.blocks.splice(blockIndex - 1, 1);
296
+ } else {
297
+ this.concatBlocks(this.blocks[blockIndex - 1], this.blocks[blockIndex]);
298
+ this.blocks.splice(blockIndex, 1);
299
+ }
209
300
  this.selection.anchor.blockId = this.blocks[blockIndex - 1].id;
210
301
  this.selection.focus.blockId = this.blocks[blockIndex - 1].id;
211
302
  this.selection.anchor.offset = this.blocks[blockIndex - 1].text.length;
212
303
  this.selection.focus.offset = this.blocks[blockIndex - 1].text.length;
213
- this.blocks.splice(blockIndex, 1);
304
+ this.history.push("setText");
214
305
  }
215
306
  onInput(_e) {
216
307
  const ev = _e;
308
+ if (ev.defaultPrevented)
309
+ return;
217
310
  ev.preventDefault();
218
311
  const collapsed = this.isCollapsed;
219
312
  if (!collapsed)
@@ -224,7 +317,11 @@ class TextEditorStore {
224
317
  if ((ev.inputType === "deleteContentBackward" || ev.inputType === "deleteContentForward") && collapsed) {
225
318
  if (ev.inputType === "deleteContentBackward") {
226
319
  if (this.selection.anchor.offset === 0) {
227
- this.removeNewLine();
320
+ if (block.type) {
321
+ delete block.type;
322
+ } else {
323
+ this.removeNewLine();
324
+ }
228
325
  } else {
229
326
  const offset = Math.max(0, this.selection.focus.offset - 1);
230
327
  block.text = block.text.slice(0, offset) + block.text.slice(this.selection.focus.offset);
@@ -234,7 +331,11 @@ class TextEditorStore {
234
331
  if (ev.inputType === "deleteContentForward") {
235
332
  const blockIndex = this.blocks.findIndex((item) => item.id === this.selection.anchor.blockId);
236
333
  if (this.selection.anchor.offset === this.blocks[blockIndex].text.length) {
237
- this.blocks.splice(blockIndex + 1, 1);
334
+ const nextBlock = this.blocks[blockIndex + 1];
335
+ if (nextBlock) {
336
+ this.blocks.splice(blockIndex + 1, 1);
337
+ this.concatBlocks(this.blocks[blockIndex - 1], this.blocks[blockIndex]);
338
+ }
238
339
  } else {
239
340
  block.text = block.text.slice(0, this.selection.focus.offset) + block.text.slice(this.selection.focus.offset + 1);
240
341
  this.moveStyles(block, this.selection.focus.offset + 1, 1);
@@ -243,6 +344,7 @@ class TextEditorStore {
243
344
  }
244
345
  if (ev.inputType === "insertText") {
245
346
  this.insertText(ev.data);
347
+ this.history.push("insertText");
246
348
  }
247
349
  }
248
350
  addNewLine() {
@@ -256,13 +358,15 @@ class TextEditorStore {
256
358
  this.blocks.splice(index + 1, 0, block);
257
359
  this.selection.anchor = { blockId: block.id, offset: 0 };
258
360
  this.selection.focus = { blockId: block.id, offset: 0 };
361
+ this.history.push("setText");
259
362
  }
260
363
  insertText(data) {
261
364
  const block = this.currentBlock;
262
365
  if (!block)
263
366
  return;
264
- block.text = block.text.slice(0, this.selection.focus.offset) + data + block.text.slice(this.selection.focus.offset);
265
- 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));
266
370
  }
267
371
  insertBlock(blockData) {
268
372
  if (!this.currentBlock)
@@ -277,6 +381,10 @@ class TextEditorStore {
277
381
  Object.assign(this.selection, selection);
278
382
  }
279
383
  Object.assign(this.currentBlock, blockData);
384
+ if (blockData.editable === false && this.currentBlock === this.blocks[this.blocks.length - 1]) {
385
+ this.addNewLine();
386
+ }
387
+ this.history.push("setText");
280
388
  }
281
389
  get startAndEnd() {
282
390
  const a = this.blocks.findIndex((block) => block.id === this.selection.anchor.blockId);
@@ -421,7 +529,8 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
421
529
  props: {
422
530
  block: {},
423
531
  slots: {},
424
- decorator: { type: Function }
532
+ decorator: { type: Function },
533
+ parser: {}
425
534
  },
426
535
  emits: ["postrender"],
427
536
  setup(__props, { emit: __emit }) {
@@ -505,6 +614,8 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
505
614
  };
506
615
  const content = () => {
507
616
  const block = props.block;
617
+ if (block.editable === false)
618
+ return null;
508
619
  if (block.text.length === 0) {
509
620
  cleanTree(1);
510
621
  return [h("br")];
@@ -518,6 +629,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
518
629
  markers.push([style.start, style]);
519
630
  markers.push([style.end, style]);
520
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
+ }
521
638
  markers.sort((a, b) => a[0] - b[0]);
522
639
  let currentIndex = 0;
523
640
  const activeStyles = /* @__PURE__ */ new Set();
@@ -560,9 +677,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
560
677
  decorator: { type: Function },
561
678
  single: { type: Boolean },
562
679
  modelValue: {},
680
+ parser: { type: Function },
563
681
  styles: {},
564
682
  autofocus: { type: Boolean },
565
- autoselect: { type: Boolean }
683
+ autoselect: { type: Boolean },
684
+ preventMultiline: { type: Boolean }
566
685
  },
567
686
  emits: ["keydown", "update:modelValue", "update:styles"],
568
687
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -621,46 +740,47 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
621
740
  return;
622
741
  if (e.code === "Enter") {
623
742
  e.preventDefault();
624
- if (e.shiftKey || props.single) {
743
+ if (!props.preventMultiline && (e.shiftKey || props.single)) {
625
744
  store.insertText("\n");
626
745
  } else {
627
746
  store.addNewLine();
628
747
  }
629
748
  }
630
- if (e.code === "Backspace") {
631
- if (store.isCollapsed && store.selection.anchor.offset === 0) {
632
- e.preventDefault();
633
- if (store.currentBlock && store.currentBlock.type) {
634
- delete store.currentBlock.type;
635
- } else {
636
- store.removeNewLine();
637
- }
638
- }
639
- }
640
749
  if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
641
750
  if (e.code === "KeyB" || e.code === "KeyI" || e.code === "KeyU") {
642
751
  e.preventDefault();
643
752
  }
644
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
+ }
645
762
  };
646
763
  let cachedSelection = {};
647
764
  useEventListener(document, "selectionchange", () => {
648
765
  const sel = window.getSelection();
649
- const anchor = findParent(sel.anchorNode, (el) => el.hasAttribute("data-vw-block-id"));
766
+ const anchor = findParent(sel.anchorNode, (el) => el.hasAttribute("data-vw-block-id") && el.parentElement === textEditorRef.value);
650
767
  if (anchor) {
651
- 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;
652
770
  store.selection.anchor = { blockId: anchor.getAttribute("data-vw-block-id"), offset };
653
771
  }
654
- const focus = findParent(sel.focusNode, (el) => el.hasAttribute("data-vw-block-id"));
772
+ const focus = findParent(sel.focusNode, (el) => el.hasAttribute("data-vw-block-id") && el.parentElement === textEditorRef.value);
655
773
  if (focus) {
656
- 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;
657
776
  store.selection.focus = { blockId: focus.getAttribute("data-vw-block-id"), offset };
658
777
  }
659
- if (store.isFocused.value !== !!focus || !!anchor) {
778
+ if (store.isFocused.value !== (!!focus || !!anchor)) {
660
779
  store.isFocused.value = !!focus || !!anchor;
661
780
  }
662
- if (!anchor && !focus)
781
+ if (anchor && focus) {
663
782
  cachedSelection = JSON.parse(JSON.stringify(store.selection));
783
+ }
664
784
  });
665
785
  let postRendered = false;
666
786
  const onPostRender = () => {
@@ -688,6 +808,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
688
808
  }
689
809
  if (isEqual(store.selection, cachedSelection))
690
810
  return;
811
+ if (store.selection.anchor.blockId === store.selection.focus.blockId && store.currentBlock && store.currentBlock.editable === false) {
812
+ return;
813
+ }
691
814
  const anchor = getNode(store.selection.anchor.blockId);
692
815
  const focus = getNode(store.selection.focus.blockId);
693
816
  const nativeSelection = window.getSelection();
@@ -710,15 +833,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
710
833
  (_b = textEditorRef.value) == null ? void 0 : _b.focus();
711
834
  store.selectAll();
712
835
  }
836
+ store.history.push("setText");
713
837
  });
714
838
  const onCopy = (e) => {
715
839
  e.preventDefault();
716
840
  navigator.clipboard.writeText(store.selectedText);
841
+ store.history.push("setText");
717
842
  };
718
843
  const onCut = (e) => {
719
844
  e.preventDefault();
720
845
  navigator.clipboard.writeText(store.selectedText);
721
846
  store.deleteSelected();
847
+ store.history.push("setText");
722
848
  };
723
849
  const onPaste = (e) => {
724
850
  var _a;
@@ -727,6 +853,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
727
853
  if (!text)
728
854
  return;
729
855
  store.insertText(text);
856
+ store.history.push("setText");
730
857
  };
731
858
  const getClientRects = (selection) => {
732
859
  const anchor = getNode(selection.anchor.blockId);
@@ -753,6 +880,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
753
880
  addNewLine: store.addNewLine.bind(store),
754
881
  removeNewLine: store.removeNewLine.bind(store),
755
882
  selectAll: store.selectAll.bind(store),
883
+ pushHistory: store.history.push.bind(store.history),
756
884
  getClientRects
757
885
  });
758
886
  return (_ctx, _cache) => {
@@ -760,7 +888,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
760
888
  ref_key: "textEditorRef",
761
889
  ref: textEditorRef,
762
890
  contenteditable: "",
763
- onInput: _cache[0] || (_cache[0] = //@ts-ignore
891
+ onBeforeinput: _cache[0] || (_cache[0] = //@ts-ignore
764
892
  (...args) => unref(store).onInput && unref(store).onInput(...args)),
765
893
  onKeydown: onKeyDown,
766
894
  onCopy,
@@ -773,10 +901,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
773
901
  block,
774
902
  slots: unref(slots),
775
903
  decorator: props.decorator,
904
+ parser: props.parser,
776
905
  onPostrender: onPostRender
777
- }, null, 8, ["block", "slots", "decorator"]);
906
+ }, null, 8, ["block", "slots", "decorator", "parser"]);
778
907
  }), 128)),
779
- unref(store).blocks.length === 1 && unref(store).blocks[0].text === "" ? renderSlot(_ctx.$slots, "placeholder", { key: 0 }) : createCommentVNode("", true)
908
+ unref(store).blocks.length === 1 && unref(store).blocks[0].text === "" && !unref(store).blocks[0].type ? renderSlot(_ctx.$slots, "placeholder", { key: 0 }) : createCommentVNode("", true)
780
909
  ], 544);
781
910
  };
782
911
  }
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.7",
5
+ "version": "0.0.9",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "author": "den59k",