vuewrite 0.0.20 → 0.0.21-a

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.
@@ -1,3 +1,3 @@
1
- {
2
- "recommendations": ["Vue.volar"]
3
- }
1
+ {
2
+ "recommendations": ["Vue.volar"]
3
+ }
package/README.md CHANGED
@@ -1,68 +1,68 @@
1
- # VueWrite
2
-
3
- VueWrite is another text editor that takes full advantage of Vue3's features.
4
- It contains no pre-made styles and blocks as its main goal is complete customization and extension
5
-
6
- ## Demo
7
-
8
- You can watch the demo [here](https://vuewrite.easix.ru)
9
-
10
- ## Quickstart
11
-
12
- ```vue
13
- <template>
14
- <TextEditor
15
- ref="textEditorRef"
16
- v-model="modelValue"
17
- single
18
- class="text-editor"
19
- :decorator="decorator"
20
- @keydown="onKeyDown"
21
- />
22
- </template>
23
-
24
- <script lang="ts">
25
- import { TextEditor, TextEditorRef } from 'vuewrite'
26
-
27
- const textEditorRef = shallowRef<TextEditorRef>()
28
- const modelValue = shallowRef("")
29
-
30
- const onKeyDown = (e: KeyboardEvent) => {
31
- if (!textEditorRef.value) return
32
- if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
33
- if (e.code === "KeyB") {
34
- textEditorRef.value.toggleStyle("bold")
35
- }
36
- if (e.code === "KeyI") {
37
- textEditorRef.value.toggleStyle("italic")
38
- }
39
- if (e.code === "KeyU") {
40
- textEditorRef.value.toggleStyle("underline")
41
- }
42
- }
43
- }
44
-
45
- const decorator = (style: Style) => {
46
- if (style.style === 'bold' || style.style === "underline" || style.style === "italic") {
47
- return { class: style.style }
48
- }
49
- }
50
-
51
- </script>
52
-
53
- <style lang="css">
54
- .text-editor {
55
- white-space: pre-wrap;
56
- }
57
- .text-editor .bold {
58
- font-weight: 700
59
- }
60
- .text-editor .italic {
61
- font-style: italic
62
- }
63
- .text-editor .underline {
64
- text-decoration: underline
65
- }
66
- </style>
67
-
1
+ # VueWrite
2
+
3
+ VueWrite is another text editor that takes full advantage of Vue3's features.
4
+ It contains no pre-made styles and blocks as its main goal is complete customization and extension
5
+
6
+ ## Demo
7
+
8
+ You can watch the demo [here](https://vuewrite.easix.ru)
9
+
10
+ ## Quickstart
11
+
12
+ ```vue
13
+ <template>
14
+ <TextEditor
15
+ ref="textEditorRef"
16
+ v-model="modelValue"
17
+ single
18
+ class="text-editor"
19
+ :decorator="decorator"
20
+ @keydown="onKeyDown"
21
+ />
22
+ </template>
23
+
24
+ <script lang="ts">
25
+ import { TextEditor, TextEditorRef } from 'vuewrite'
26
+
27
+ const textEditorRef = shallowRef<TextEditorRef>()
28
+ const modelValue = shallowRef("")
29
+
30
+ const onKeyDown = (e: KeyboardEvent) => {
31
+ if (!textEditorRef.value) return
32
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
33
+ if (e.code === "KeyB") {
34
+ textEditorRef.value.toggleStyle("bold")
35
+ }
36
+ if (e.code === "KeyI") {
37
+ textEditorRef.value.toggleStyle("italic")
38
+ }
39
+ if (e.code === "KeyU") {
40
+ textEditorRef.value.toggleStyle("underline")
41
+ }
42
+ }
43
+ }
44
+
45
+ const decorator = (style: Style) => {
46
+ if (style.style === 'bold' || style.style === "underline" || style.style === "italic") {
47
+ return { class: style.style }
48
+ }
49
+ }
50
+
51
+ </script>
52
+
53
+ <style lang="css">
54
+ .text-editor {
55
+ white-space: pre-wrap;
56
+ }
57
+ .text-editor .bold {
58
+ font-weight: 700
59
+ }
60
+ .text-editor .italic {
61
+ font-style: italic
62
+ }
63
+ .text-editor .underline {
64
+ text-decoration: underline
65
+ }
66
+ </style>
67
+
68
68
  ```
@@ -40,11 +40,16 @@ export declare type Decorator = (style: Style) => HTMLAttributes & {
40
40
  } | undefined;
41
41
 
42
42
  declare type HistoryAction = {
43
- type: "insertText" | "setText";
43
+ type: string;
44
44
  blocks: Block[];
45
+ fullUpdate: boolean;
45
46
  selection: TextEditorSelection;
46
47
  };
47
48
 
49
+ declare type Renderer = (block: Block) => HTMLAttributes & {
50
+ tag?: string;
51
+ } | undefined;
52
+
48
53
  export declare type Style = {
49
54
  start: number;
50
55
  end: number;
@@ -54,6 +59,7 @@ export declare type Style = {
54
59
 
55
60
  export declare const TextEditor: __VLS_WithTemplateSlots<DefineComponent<__VLS_TypePropsToRuntimeProps<{
56
61
  decorator?: Decorator | undefined;
62
+ renderer?: Renderer | undefined;
57
63
  single?: boolean | undefined;
58
64
  modelValue?: string | {
59
65
  text: string;
@@ -99,7 +105,7 @@ insertBlock: (blockData: Partial<Block>) => void;
99
105
  addNewLine: () => void;
100
106
  removeNewLine: () => void;
101
107
  selectAll: () => void;
102
- pushHistory: (type: "insertText" | "setText") => void;
108
+ pushHistory: (type: string) => void;
103
109
  getClientRects: (selection: TextEditorSelection) => DOMRectList;
104
110
  }, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
105
111
  keydown: (...args: any[]) => void;
@@ -107,6 +113,7 @@ keydown: (...args: any[]) => void;
107
113
  "update:styles": (...args: any[]) => void;
108
114
  }, string, PublicProps, Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
109
115
  decorator?: Decorator | undefined;
116
+ renderer?: Renderer | undefined;
110
117
  single?: boolean | undefined;
111
118
  modelValue?: string | {
112
119
  text: string;
@@ -129,8 +136,12 @@ onKeydown?: ((...args: any[]) => any) | undefined;
129
136
  declare class TextEditorHistory {
130
137
  actions: HistoryAction[];
131
138
  currentCursor: number;
139
+ private blocksIds;
140
+ private cachedBlocksJson;
132
141
  private store;
133
142
  constructor(store: TextEditorStore);
143
+ private cacheBlockIds;
144
+ private blockIdsChanged;
134
145
  tickStarted: boolean;
135
146
  push(type: HistoryAction["type"]): void;
136
147
  private applyAction;
@@ -242,6 +253,7 @@ declare class TextEditorStore {
242
253
  export declare const TextEditorView: DefineComponent<Readonly<{
243
254
  styles?: any;
244
255
  decorator?: any;
256
+ renderer?: any;
245
257
  parser?: any;
246
258
  modelValue?: any;
247
259
  }>, () => VNode<RendererNode, RendererElement, {
@@ -249,11 +261,13 @@ modelValue?: any;
249
261
  }>, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<ExtractPropTypes<Readonly<{
250
262
  styles?: any;
251
263
  decorator?: any;
264
+ renderer?: any;
252
265
  parser?: any;
253
266
  modelValue?: any;
254
267
  }>>>, {
255
268
  readonly styles?: any;
256
269
  readonly decorator?: any;
270
+ readonly renderer?: any;
257
271
  readonly parser?: any;
258
272
  readonly modelValue?: any;
259
273
  }, {}>;
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, nextTick, reactive, computed, ref, defineComponent, getCurrentInstance, h, useSlots, isProxy, toRaw, onMounted, openBlock, createElementBlock, Fragment, renderList, createBlock, renderSlot, createCommentVNode } from "vue";
7
+ import { getCurrentScope, onScopeDispose, unref, watch, reactive, computed, ref, defineComponent, getCurrentInstance, h, nextTick, useSlots, isProxy, toRaw, onMounted, openBlock, createElementBlock, Fragment, renderList, createBlock, renderSlot, createCommentVNode } from "vue";
8
8
  function tryOnScopeDispose(fn) {
9
9
  if (getCurrentScope()) {
10
10
  onScopeDispose(fn);
@@ -144,50 +144,57 @@ class TextEditorHistory {
144
144
  constructor(store) {
145
145
  __publicField(this, "actions", []);
146
146
  __publicField(this, "currentCursor", 0);
147
+ __publicField(this, "blocksIds", "");
148
+ __publicField(this, "cachedBlocksJson", "");
147
149
  __publicField(this, "store");
148
150
  __publicField(this, "tickStarted", false);
149
151
  this.store = store;
150
152
  window.showHistory = () => console.log(this.actions);
151
153
  }
154
+ cacheBlockIds() {
155
+ this.blocksIds = this.store.blocks.map((item) => item.id).join(",");
156
+ }
157
+ blockIdsChanged() {
158
+ return this.blocksIds !== this.store.blocks.map((item) => item.id).join(",");
159
+ }
152
160
  push(type) {
153
161
  var _a;
154
162
  if (this.currentCursor + 1 < this.actions.length) {
155
163
  this.actions.length = this.currentCursor + 1;
156
164
  }
157
- const lastAction = this.actions.length > 0 && this.currentCursor === this.actions.length - 1 ? this.actions[this.actions.length - 1] : null;
165
+ const lastAction = this.actions.length > 0 ? this.actions[this.actions.length - 1] : null;
158
166
  const selection = JSON.parse(JSON.stringify(this.store.selection));
159
- if (type === "insertText") {
167
+ const fullUpdate = type === "setText" || type === "addNewLine" || this.blockIdsChanged();
168
+ const blocksJson = JSON.stringify(this.store.blocks);
169
+ if (!fullUpdate && this.store.currentBlock) {
160
170
  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) {
171
+ if (lastAction && lastAction.type === type && ((_a = lastAction.blocks[0]) == null ? void 0 : _a.id) === currentBlock.id && (lastAction.type === "insertText" || lastAction.type === "deleteContentBackward" || lastAction.type === "deleteContentForward")) {
162
172
  lastAction.blocks = [currentBlock];
163
173
  lastAction.selection = selection;
164
174
  } 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 });
175
+ this.actions.push({ type, blocks: [currentBlock], selection, fullUpdate });
174
176
  }
177
+ } else {
178
+ this.actions.push({ type, blocks: JSON.parse(blocksJson), selection, fullUpdate });
175
179
  }
176
180
  this.currentCursor = this.actions.length - 1;
177
- if (!this.tickStarted && type === "setText") {
178
- this.tickStarted = true;
179
- nextTick(() => {
180
- this.tickStarted = false;
181
- });
181
+ if (fullUpdate) {
182
+ if (lastAction && !lastAction.fullUpdate) {
183
+ lastAction.fullUpdate = true;
184
+ lastAction.blocks = JSON.parse(this.cachedBlocksJson);
185
+ }
186
+ this.cacheBlockIds();
182
187
  }
188
+ this.cachedBlocksJson = blocksJson;
183
189
  }
184
190
  applyAction(action) {
185
- if (action.type === "setText") {
191
+ if (action.fullUpdate) {
186
192
  for (let i = 0; i < action.blocks.length; i++) {
187
193
  if (i >= this.store.blocks.length) {
188
194
  this.store.blocks.push({ ...action.blocks[i] });
189
195
  } else {
190
196
  Object.assign(this.store.blocks[i], action.blocks[i]);
197
+ this.store.blocks[i].type = action.blocks[i].type;
191
198
  }
192
199
  }
193
200
  if (this.store.blocks.length > action.blocks.length) {
@@ -198,10 +205,13 @@ class TextEditorHistory {
198
205
  const block = this.store.blocks.find((block2) => block2.id === _block.id);
199
206
  if (block) {
200
207
  block.text = _block.text;
208
+ block.styles = _block.styles;
209
+ block.type = _block.type;
201
210
  }
202
211
  }
203
212
  }
204
213
  Object.assign(this.store.selection, action.selection);
214
+ this.cacheBlockIds();
205
215
  }
206
216
  undo() {
207
217
  if (this.currentCursor === 0)
@@ -301,7 +311,6 @@ class TextEditorStore {
301
311
  this.concatBlocks(this.blocks[blockIndex - 1], this.blocks[blockIndex]);
302
312
  this.blocks.splice(blockIndex, 1);
303
313
  }
304
- this.history.push("setText");
305
314
  }
306
315
  onInput(_e) {
307
316
  const ev = _e;
@@ -327,6 +336,7 @@ class TextEditorStore {
327
336
  block.text = block.text.slice(0, offset) + block.text.slice(this.selection.focus.offset);
328
337
  this.moveOffset(offset);
329
338
  }
339
+ this.history.push("deleteContentBackward");
330
340
  }
331
341
  if (ev.inputType === "deleteContentForward") {
332
342
  const blockIndex = this.blocks.findIndex((item) => item.id === this.selection.anchor.blockId);
@@ -334,12 +344,13 @@ class TextEditorStore {
334
344
  const nextBlock = this.blocks[blockIndex + 1];
335
345
  if (nextBlock) {
336
346
  this.blocks.splice(blockIndex + 1, 1);
337
- this.concatBlocks(this.blocks[blockIndex - 1], this.blocks[blockIndex]);
347
+ this.concatBlocks(this.blocks[blockIndex], nextBlock);
338
348
  }
339
349
  } else {
340
350
  block.text = block.text.slice(0, this.selection.focus.offset) + block.text.slice(this.selection.focus.offset + 1);
341
351
  this.moveStyles(block, this.selection.focus.offset + 1, 1);
342
352
  }
353
+ this.history.push("deleteContentForward");
343
354
  }
344
355
  }
345
356
  if (ev.inputType === "insertText") {
@@ -352,6 +363,7 @@ class TextEditorStore {
352
363
  const index2 = this.blocks.findIndex((item) => item.id === this.selection.anchor.blockId);
353
364
  const block2 = { id: uid(), text: "", styles: [] };
354
365
  this.blocks.splice(index2, 0, block2);
366
+ this.history.push("addNewLine");
355
367
  return;
356
368
  }
357
369
  this.deleteSelected();
@@ -364,7 +376,7 @@ class TextEditorStore {
364
376
  this.blocks.splice(index + 1, 0, block);
365
377
  this.selection.anchor = { blockId: block.id, offset: 0 };
366
378
  this.selection.focus = { blockId: block.id, offset: 0 };
367
- this.history.push("setText");
379
+ this.history.push("addNewLine");
368
380
  }
369
381
  addNewLineAfter() {
370
382
  const index = this.blocks.findIndex((item) => item.id === this.selection.anchor.blockId);
@@ -448,6 +460,8 @@ class TextEditorStore {
448
460
  return this._currentStyles.value;
449
461
  }
450
462
  applyStyle(_style, meta) {
463
+ if (this.isCollapsed)
464
+ return;
451
465
  const [start, end, startIndex, endIndex] = this.startAndEnd;
452
466
  for (let i = startIndex; i <= endIndex; i++) {
453
467
  const block = this.blocks[i];
@@ -481,6 +495,7 @@ class TextEditorStore {
481
495
  block.styles.sort((a, b) => a.start - b.start);
482
496
  }
483
497
  }
498
+ this.history.push("applyStyle");
484
499
  }
485
500
  removeStyleAt(block, _start, _end, _style) {
486
501
  const removeAll = typeof _style !== "string";
@@ -511,6 +526,7 @@ class TextEditorStore {
511
526
  const _end = i === endIndex ? end.offset : block.text.length;
512
527
  this.removeStyleAt(block, _start, _end, _style);
513
528
  }
529
+ this.history.push("applyStyle");
514
530
  }
515
531
  removeAllStyles() {
516
532
  const [start, end, startIndex, endIndex] = this.startAndEnd;
@@ -541,7 +557,7 @@ class TextEditorStore {
541
557
  let uidCounter = 0;
542
558
  const uid = () => (uidCounter++).toString();
543
559
  const _sfc_main$2 = defineComponent({
544
- props: ["block", "slots", "static", "decorator", "parser"],
560
+ props: ["block", "slots", "static", "decorator", "renderer", "parser"],
545
561
  emits: ["postrender"],
546
562
  setup(props, { emit }) {
547
563
  const slot = computed(() => {
@@ -549,9 +565,6 @@ const _sfc_main$2 = defineComponent({
549
565
  return props.slots["default"] ?? null;
550
566
  return props.slots[props.block.type] ?? null;
551
567
  });
552
- const blockProps = {
553
- "data-vw-block-id": props.block.id
554
- };
555
568
  const instance = getCurrentInstance();
556
569
  const getRef = () => {
557
570
  if (!instance)
@@ -675,18 +688,30 @@ const _sfc_main$2 = defineComponent({
675
688
  elementTag = tag;
676
689
  }
677
690
  }
678
- if (Object.keys(attrs).length === 0)
691
+ if (Object.keys(attrs).length === 0 && elementTag === "span")
679
692
  return text;
680
693
  return h(elementTag, attrs, text);
681
694
  };
682
695
  return () => {
696
+ let elementTag = "div";
697
+ const blockProps = {
698
+ "data-vw-block-id": props.block.id
699
+ };
700
+ const additionalProps = props.renderer && props.renderer(props.block);
701
+ if (additionalProps) {
702
+ if (additionalProps.tag) {
703
+ elementTag = additionalProps.tag;
704
+ delete additionalProps.tag;
705
+ }
706
+ Object.assign(blockProps, additionalProps);
707
+ }
683
708
  if (slot.value) {
684
709
  const component = slot.value({ content, props: blockProps, block: props.block });
685
710
  if (Array.isArray(component) && component.length === 1)
686
711
  return component[0];
687
712
  return component;
688
713
  }
689
- return h("div", blockProps, content());
714
+ return h(elementTag, blockProps, content());
690
715
  };
691
716
  }
692
717
  });
@@ -800,6 +825,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
800
825
  __name: "TextEditor",
801
826
  props: {
802
827
  decorator: { type: Function },
828
+ renderer: { type: Function },
803
829
  single: { type: Boolean },
804
830
  modelValue: {},
805
831
  parser: { type: Function },
@@ -1011,10 +1037,11 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
1011
1037
  key: block.id,
1012
1038
  block,
1013
1039
  slots: unref(slots),
1040
+ renderer: props.renderer,
1014
1041
  decorator: props.decorator,
1015
1042
  parser: props.parser,
1016
1043
  onPostrender: onPostRender
1017
- }, null, 8, ["block", "slots", "decorator", "parser"]);
1044
+ }, null, 8, ["block", "slots", "renderer", "decorator", "parser"]);
1018
1045
  }), 128)),
1019
1046
  unref(store).blocks.length === 1 && unref(store).blocks[0].text === "" && !unref(store).blocks[0].type ? renderSlot(_ctx.$slots, "placeholder", { key: 0 }) : createCommentVNode("", true)
1020
1047
  ], 544);
@@ -1022,7 +1049,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
1022
1049
  }
1023
1050
  });
1024
1051
  const _sfc_main = defineComponent({
1025
- props: ["modelValue", "decorator", "parser", "styles"],
1052
+ props: ["modelValue", "decorator", "parser", "styles", "renderer"],
1026
1053
  setup(props, { slots }) {
1027
1054
  const blocks = computed(() => {
1028
1055
  if (Array.isArray(props.modelValue)) {
@@ -1035,6 +1062,7 @@ const _sfc_main = defineComponent({
1035
1062
  block,
1036
1063
  slots,
1037
1064
  decorator: props.decorator,
1065
+ renderer: props.renderer,
1038
1066
  parser: props.parser,
1039
1067
  static: true
1040
1068
  })));
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "vuewrite",
3
- "description": "Rich Text Editor based on Vue3 reactivity",
4
- "private": false,
5
- "version": "0.0.20",
6
- "type": "module",
7
- "license": "MIT",
8
- "author": "den59k",
9
- "repository": {
10
- "type": "git",
11
- "url": "https://github.com/den59k/vuewrite.git"
12
- },
13
- "main": "dist/vuewrite.js",
14
- "types": "dist/vuewrite.d.ts",
15
- "scripts": {
16
- "dev": "vite",
17
- "build": "vue-tsc && vite build",
18
- "build:app": "vue-tsc && vite build -c vite-app.config.ts",
19
- "preview": "vite preview"
20
- },
21
- "dependencies": {
22
- "vue": "^3",
23
- "vuewrite": "^0.0.19"
24
- },
25
- "devDependencies": {
26
- "@vitejs/plugin-vue": "^5.0.4",
27
- "@vueuse/core": "^10.10.0",
28
- "color2k": "^2.0.3",
29
- "sass": "^1.77.2",
30
- "typescript": "^5.2.2",
31
- "vite": "^5.2.0",
32
- "vite-plugin-dts": "^3.9.1",
33
- "vue-tsc": "^2.0.6",
34
- "vuesix": "^1.0.12"
35
- },
36
- "files": [
37
- "dist"
38
- ]
39
- }
1
+ {
2
+ "name": "vuewrite",
3
+ "description": "Rich Text Editor based on Vue3 reactivity",
4
+ "private": false,
5
+ "version": "0.0.21-a",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "den59k",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/den59k/vuewrite.git"
12
+ },
13
+ "main": "dist/vuewrite.js",
14
+ "types": "dist/vuewrite.d.ts",
15
+ "scripts": {
16
+ "dev": "vite",
17
+ "build": "vue-tsc && vite build",
18
+ "build:app": "vue-tsc && vite build -c vite-app.config.ts",
19
+ "preview": "vite preview"
20
+ },
21
+ "dependencies": {
22
+ "vue": "^3",
23
+ "vuewrite": "^0.0.19"
24
+ },
25
+ "devDependencies": {
26
+ "@vitejs/plugin-vue": "^5.0.4",
27
+ "@vueuse/core": "^10.10.0",
28
+ "color2k": "^2.0.3",
29
+ "sass": "^1.77.2",
30
+ "typescript": "^5.2.2",
31
+ "vite": "^5.2.0",
32
+ "vite-plugin-dts": "^3.9.1",
33
+ "vue-tsc": "^2.0.6",
34
+ "vuesix": "^1.0.12"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ]
39
+ }