@zipify/wysiwyg 4.9.1 → 4.10.0-1

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.
Files changed (59) hide show
  1. package/.github/actions/lint-js/action.yaml +8 -3
  2. package/.lintstagedrc.mjs +32 -0
  3. package/.oxlintrc.json +185 -0
  4. package/dist/assets/wysiwyg-B8cHOAcD.css +908 -0
  5. package/dist/cli.js +35 -35
  6. package/dist/node.js +22 -22
  7. package/dist/types/Wysiwyg.vue.d.ts +38 -176
  8. package/dist/types/components/base/Button.vue.d.ts +2 -2
  9. package/dist/types/components/base/Modal.vue.d.ts +2 -2
  10. package/dist/types/components/base/ModalFloating.vue.d.ts +2 -2
  11. package/dist/types/components/base/NumberField.vue.d.ts +2 -2
  12. package/dist/types/entryLib.d.ts +2 -0
  13. package/dist/types/enums/Devices.d.ts +15 -6
  14. package/dist/types/enums/index.d.ts +9 -9
  15. package/dist/types/models/PageBlock.d.ts +4 -0
  16. package/dist/types/models/StylePreset.d.ts +27 -0
  17. package/dist/types/models/index.d.ts +3 -2
  18. package/dist/types/types/StylePresetVariableFactory.d.ts +8 -0
  19. package/dist/types/types/index.d.ts +1 -0
  20. package/dist/wysiwyg.css +44 -76
  21. package/dist/wysiwyg.mjs +2077 -1331
  22. package/eslint.config.mjs +6 -0
  23. package/example/ExampleApp.vue +48 -78
  24. package/example/presets.ts +250 -0
  25. package/lib/Wysiwyg.vue +63 -118
  26. package/lib/cli/commands/Command.js +1 -2
  27. package/lib/components/base/Checkbox.vue +3 -0
  28. package/lib/components/base/Modal.vue +1 -1
  29. package/lib/components/base/colorPicker/ColorPicker.vue +11 -9
  30. package/lib/components/toolbar/Toolbar.vue +4 -4
  31. package/lib/components/toolbar/controls/__tests__/FontColorControl.test.js +2 -2
  32. package/lib/components/toolbar/controls/link/LinkControlHeader.vue +7 -2
  33. package/lib/components/toolbar/controls/link/__tests__/LinkControl.test.js +5 -5
  34. package/lib/components/toolbar/controls/link/__tests__/LinkControlHeader.test.js +4 -4
  35. package/lib/entryLib.ts +2 -0
  36. package/lib/enums/Devices.ts +17 -0
  37. package/lib/enums/index.ts +9 -0
  38. package/lib/extensions/FontStyle.js +3 -3
  39. package/lib/extensions/__tests__/StylePreset.test.js +5 -5
  40. package/lib/extensions/list/List.js +3 -3
  41. package/lib/extensions/list/__tests__/List.test.js +4 -4
  42. package/lib/models/PageBlock.ts +4 -0
  43. package/lib/models/StylePreset.ts +30 -0
  44. package/lib/models/index.ts +3 -2
  45. package/lib/services/JsonSerializer.js +0 -1
  46. package/lib/services/Storage.js +0 -1
  47. package/lib/services/__tests__/Storage.test.js +9 -4
  48. package/lib/services/normalizer/__tests__/HtmlNormalizer.test.js +25 -25
  49. package/lib/styles/variables.css +2 -33
  50. package/lib/types/StylePresetVariableFactory.ts +10 -0
  51. package/lib/types/index.ts +1 -0
  52. package/package.json +18 -28
  53. package/tsconfig.lint.json +7 -0
  54. package/.eslintignore +0 -2
  55. package/.eslintrc.js +0 -95
  56. package/.lintstagedrc +0 -12
  57. package/example/presets.js +0 -247
  58. package/lib/enums/Devices.js +0 -15
  59. package/lib/enums/index.js +0 -9
@@ -0,0 +1,6 @@
1
+ import { configureWithVue } from '@zipify/eslint-config/preset-vue.mjs';
2
+
3
+ export default configureWithVue({
4
+ ignores: ['dist/**'],
5
+ tsconfig: new URL('./tsconfig.lint.json', import.meta.url)
6
+ });
@@ -36,20 +36,20 @@
36
36
  ref="wswgRef"
37
37
  :fonts="fonts"
38
38
  :presets="presets"
39
- :make-preset-variable="$options.makePresetVariable"
39
+ :make-preset-variable="renderPresetVariable"
40
40
  :favorite-colors="favoriteColors"
41
41
  :device="device"
42
42
  :page-blocks="pageBlocks"
43
43
  :active="isActive"
44
44
  :readonly="isReadonly"
45
45
  :ai-component="aiComponent"
46
- @update-favorite-colors="updateFavoriteColors"
46
+ @update:favorite-colors="updateFavoriteColors"
47
47
  />
48
48
  <pre class="zw-content-structure" v-html="structurePreview" />
49
49
  </div>
50
50
  </template>
51
51
 
52
- <script>
52
+ <script setup>
53
53
  import { computed, onMounted, ref, unref } from 'vue';
54
54
  import { Wysiwyg } from '@/entryLib';
55
55
  import { FONTS } from './fonts';
@@ -67,82 +67,52 @@ function getInitialContent() {
67
67
  }
68
68
  }
69
69
 
70
- export default {
71
- name: 'ExampleApp',
72
-
73
- makePresetVariable: renderPresetVariable,
74
-
75
- components: {
76
- Wysiwyg
77
- },
78
-
79
- setup() {
80
- const aiComponent = AiComponent;
81
- const wswgRef = ref(null);
82
- const content = ref(getInitialContent());
83
- const presets = ref(PRESETS);
84
- const pageBlocks = ref(PAGE_BLOCKS);
85
- const device = ref('desktop');
86
- const updatedAt = new Date(ZW_UPDATED_AT).toLocaleString('ua-UA');
87
- const isActive = ref(false);
88
- const isReadonly = ref(false);
89
-
90
- const structurePreview = computed(() => {
91
- const json = JSON.stringify(content.value, null, ' ');
92
- const { value } = window.hljs.highlight(json, { language: 'json' });
93
-
94
- return value;
95
- });
96
-
97
- const fonts = ref(FONTS);
98
- const favoriteColors = ref(JSON.parse(localStorage.getItem('favoriteColors') || '[]'));
99
-
100
- function updateFavoriteColors(colors) {
101
- localStorage.setItem('favoriteColors', JSON.stringify(colors));
102
- favoriteColors.value = colors;
103
- }
104
-
105
- onMounted(() => window.tiptap = wswgRef.value.editor);
106
-
107
- function loadContent() {
108
- const defaultValue = sessionStorage.getItem('wswg-data');
109
- const content = prompt('Insert editor content', defaultValue);
110
-
111
- if (content) {
112
- sessionStorage.setItem('wswg-data', content);
113
- } else {
114
- sessionStorage.removeItem('wswg-data');
115
- }
116
- window.location.reload();
117
- }
118
-
119
- function saveContent() {
120
- sessionStorage.setItem('wswg-data', JSON.stringify(unref(content)));
121
- }
122
-
123
- document.addEventListener('click', (event) => {
124
- isActive.value = wswgRef.value.$el.contains(event.target);
125
- });
126
-
127
- return {
128
- wswgRef,
129
- content,
130
- fonts,
131
- structurePreview,
132
- favoriteColors,
133
- updateFavoriteColors,
134
- loadContent,
135
- saveContent,
136
- device,
137
- updatedAt,
138
- presets,
139
- isActive,
140
- pageBlocks,
141
- isReadonly,
142
- aiComponent
143
- };
70
+ const aiComponent = AiComponent;
71
+ const wswgRef = ref(null);
72
+ const content = ref(getInitialContent());
73
+ const presets = ref(PRESETS);
74
+ const pageBlocks = ref(PAGE_BLOCKS);
75
+ const device = ref('desktop');
76
+ const updatedAt = new Date(ZW_UPDATED_AT).toLocaleString('ua-UA');
77
+ const isActive = ref(false);
78
+ const isReadonly = ref(false);
79
+
80
+ const structurePreview = computed(() => {
81
+ const json = JSON.stringify(content.value, null, ' ');
82
+ const { value } = window.hljs.highlight(json, { language: 'json' });
83
+
84
+ return value;
85
+ });
86
+
87
+ const fonts = ref(FONTS);
88
+ const favoriteColors = ref(JSON.parse(localStorage.getItem('favoriteColors') || '[]'));
89
+
90
+ function updateFavoriteColors(colors) {
91
+ localStorage.setItem('favoriteColors', JSON.stringify(colors));
92
+ favoriteColors.value = colors;
93
+ }
94
+
95
+ onMounted(() => window.tiptap = wswgRef.value.editor);
96
+
97
+ function loadContent() {
98
+ const defaultValue = sessionStorage.getItem('wswg-data');
99
+ const content = prompt('Insert editor content', defaultValue);
100
+
101
+ if (content) {
102
+ sessionStorage.setItem('wswg-data', content);
103
+ } else {
104
+ sessionStorage.removeItem('wswg-data');
144
105
  }
145
- };
106
+ window.location.reload();
107
+ }
108
+
109
+ function saveContent() {
110
+ sessionStorage.setItem('wswg-data', JSON.stringify(unref(content)));
111
+ }
112
+
113
+ document.addEventListener('click', (event) => {
114
+ isActive.value = !!event.target.closest('.zw-wysiwyg, .zw-toolbar');
115
+ });
146
116
  </script>
147
117
 
148
118
  <style>
@@ -0,0 +1,250 @@
1
+ import type { IStylePreset, StylePresetVariableFactory } from '../lib/entryLib';
2
+ import { Device } from '@/enums';
3
+
4
+ export const PRESETS: IStylePreset[] = [
5
+ {
6
+ id: 'h1',
7
+ name: 'Heading 1',
8
+ node: {
9
+ type: 'heading',
10
+ level: 1
11
+ },
12
+ desktop: {
13
+ alignment: null,
14
+ line_height: '1.2',
15
+ font_size: '40px'
16
+ },
17
+ common: {
18
+ font_family: 'Lato',
19
+ font_weight: '700',
20
+ color: '',
21
+ font_style: 'normal',
22
+ text_decoration: 'none'
23
+ },
24
+ tablet: {
25
+ alignment: null,
26
+ line_height: '1.2',
27
+ font_size: '40px'
28
+ },
29
+ mobile: {
30
+ alignment: null,
31
+ line_height: '1.2',
32
+ font_size: '28px'
33
+ }
34
+ },
35
+ {
36
+ id: 'h2',
37
+ name: 'Heading 2',
38
+ node: {
39
+ type: 'heading',
40
+ level: 2
41
+ },
42
+ desktop: {
43
+ alignment: null,
44
+ line_height: '1.2',
45
+ font_size: '32px'
46
+ },
47
+ common: {
48
+ font_family: 'Lato',
49
+ font_weight: '700',
50
+ color: '#262626',
51
+ font_style: 'normal',
52
+ text_decoration: 'none'
53
+ },
54
+ tablet: {
55
+ alignment: null,
56
+ line_height: '1.2',
57
+ font_size: '32px'
58
+ },
59
+ mobile: {
60
+ alignment: null,
61
+ line_height: '1.2',
62
+ font_size: '24px'
63
+ }
64
+ },
65
+ {
66
+ id: 'h3',
67
+ name: 'Sub-heading 1',
68
+ node: {
69
+ type: 'heading',
70
+ level: 3
71
+ },
72
+ desktop: {
73
+ alignment: null,
74
+ line_height: '1.2',
75
+ font_size: '28px'
76
+ },
77
+ common: {
78
+ font_family: 'Lato',
79
+ font_weight: '700',
80
+ color: '#262626',
81
+ font_style: 'normal',
82
+ text_decoration: 'none'
83
+ },
84
+ tablet: {
85
+ alignment: null,
86
+ line_height: '1.2',
87
+ font_size: '28px'
88
+ },
89
+ mobile: {
90
+ alignment: null,
91
+ line_height: '1.2',
92
+ font_size: '20px'
93
+ }
94
+ },
95
+ {
96
+ id: 'h4',
97
+ name: 'Sub-heading 2',
98
+ node: {
99
+ type: 'heading',
100
+ level: 4
101
+ },
102
+ desktop: {
103
+ alignment: null,
104
+ line_height: '1.2',
105
+ font_size: '24px'
106
+ },
107
+ common: {
108
+ font_family: 'Lato',
109
+ font_weight: '700',
110
+ color: '#262626',
111
+ font_style: 'normal',
112
+ text_decoration: 'none'
113
+ },
114
+ tablet: {
115
+ alignment: null,
116
+ line_height: '1.2',
117
+ font_size: '24px'
118
+ },
119
+ mobile: {
120
+ alignment: null,
121
+ line_height: '1.2',
122
+ font_size: '18px'
123
+ }
124
+ },
125
+ {
126
+ id: 'link',
127
+ name: 'Link',
128
+ hidden: true,
129
+ desktop: {
130
+ alignment: null,
131
+ line_height: 'inherit',
132
+ font_size: 'inherit'
133
+ },
134
+ common: {
135
+ font_family: 'inherit',
136
+ font_weight: 'inherit',
137
+ color: 'rgb(0, 0, 238)',
138
+ font_style: 'normal',
139
+ text_decoration: 'underline'
140
+ },
141
+ tablet: {
142
+ alignment: null,
143
+ line_height: 'inherit',
144
+ font_size: 'inherit'
145
+ },
146
+ mobile: {
147
+ alignment: null,
148
+ line_height: 'inherit',
149
+ font_size: 'inherit'
150
+ }
151
+ },
152
+ {
153
+ id: 'regular-1',
154
+ name: 'Body Copy 1',
155
+ desktop: {
156
+ alignment: null,
157
+ line_height: '1.43',
158
+ font_size: '18px'
159
+ },
160
+ common: {
161
+ font_family: 'Lato',
162
+ font_weight: '400',
163
+ color: '#262626',
164
+ font_style: 'normal',
165
+ text_decoration: 'none'
166
+ },
167
+ tablet: {
168
+ alignment: null,
169
+ line_height: '1.43',
170
+ font_size: '18px'
171
+ },
172
+ mobile: {
173
+ alignment: null,
174
+ line_height: '1.43',
175
+ font_size: '18px'
176
+ }
177
+ },
178
+ {
179
+ id: 'regular-2',
180
+ name: 'Body Copy 2',
181
+ fallbackClass: 'zpa-regular2',
182
+ desktop: {
183
+ alignment: null,
184
+ line_height: '1.43',
185
+ font_size: '16px'
186
+ },
187
+ common: {
188
+ font_family: 'Lato',
189
+ font_weight: '400',
190
+ color: '#262626',
191
+ font_style: 'normal',
192
+ text_decoration: 'none'
193
+ },
194
+ tablet: {
195
+ alignment: null,
196
+ line_height: '1.43',
197
+ font_size: '16px'
198
+ },
199
+ mobile: {
200
+ alignment: null,
201
+ line_height: '1.43',
202
+ font_size: '16px'
203
+ }
204
+ },
205
+ {
206
+ id: 'regular-3',
207
+ name: 'Body Copy 3',
208
+ fallbackClass: 'zpa-regular3',
209
+ desktop: {
210
+ alignment: null,
211
+ line_height: '1.43',
212
+ font_size: '12px'
213
+ },
214
+ common: {
215
+ font_family: 'Lato',
216
+ font_weight: '400',
217
+ color: '#262626',
218
+ font_style: 'normal',
219
+ text_decoration: 'none'
220
+ },
221
+ tablet: {
222
+ alignment: null,
223
+ line_height: '1.43',
224
+ font_size: '12px'
225
+ },
226
+ mobile: {
227
+ alignment: null,
228
+ line_height: '1.43',
229
+ font_size: '12px'
230
+ }
231
+ }
232
+ ];
233
+
234
+ export const renderPresetVariable: StylePresetVariableFactory = ({ device, preset, property }) => {
235
+ const formattedProperty = property.replace(/_/i, '-');
236
+
237
+ return `--${device}-${preset.id}-${formattedProperty}`;
238
+ };
239
+
240
+ export function renderPresets() {
241
+ for (const preset of PRESETS) {
242
+ for (const device of Object.values(Device)) {
243
+ for (const [property, value] of Object.entries(preset[device])) {
244
+ const variable = renderPresetVariable({ device, preset, property });
245
+
246
+ document.body.style.setProperty(variable, value);
247
+ }
248
+ }
249
+ }
250
+ }
package/lib/Wysiwyg.vue CHANGED
@@ -1,138 +1,67 @@
1
1
  <template>
2
2
  <div class="zw-wysiwyg" ref="wysiwygRef">
3
- <Toolbar
4
- :device="device"
5
- :visible="isToolbarVisible"
6
- :popup-mode="popupMode"
7
- :reference-ref="wysiwygRef"
8
- :offsets="toolbarOffsets"
9
- :ai-component="aiComponent"
10
- />
11
-
12
- <EditorContent :editor="editor" />
3
+ <Teleport :to="toolbarTeleportTo" v-if="toolbarTeleportTo">
4
+ <RenderToolbar />
5
+ </Teleport>
6
+
7
+ <RenderToolbar v-else />
8
+
9
+ <EditorContent :editor />
13
10
  </div>
14
11
  </template>
15
12
 
16
13
  <script lang="ts" setup>
17
14
  import { EditorContent } from '@tiptap/vue-3';
18
15
  import type { Content, JSONContent } from '@tiptap/core';
19
- import { provide, toRef, ref, computed } from 'vue';
20
- import type { PropType } from 'vue';
16
+ import { provide, toRef, ref, computed, type FunctionalComponent, h, type Component } from 'vue';
21
17
  import { Toolbar } from './components';
22
18
  import { useEditor } from './composables';
23
19
  import { buildExtensions } from './extensions';
24
20
  import { InjectionTokens } from './injectionTokens';
25
21
  import { ContextWindow, FavoriteColors, Storage } from './services';
26
- import { Devices } from './enums';
27
- import { Font, type IFontJson } from './models';
28
-
29
- const props = defineProps({
30
- presets: {
31
- type: Array,
32
- required: true
33
- },
34
-
35
- defaultPresetId: {
36
- type: [Number, String],
37
- required: true
38
- },
39
-
40
- linkPresetId: {
41
- type: [Number, String],
42
- required: true
43
- },
44
-
45
- basePresetClass: {
46
- type: String,
47
- required: true
48
- },
49
-
50
- makePresetVariable: {
51
- type: Function,
52
- required: true
53
- },
54
-
55
- fonts: {
56
- type: Array as PropType<IFontJson[]>,
57
- required: true
58
- },
59
-
60
- active: {
61
- type: Boolean,
62
- required: true
63
- },
64
-
65
- device: {
66
- type: String,
67
- required: false,
68
- default: Devices.DESKTOP
69
- },
70
-
71
- favoriteColors: {
72
- type: Array,
73
- required: false,
74
- default: () => []
75
- },
76
-
77
- toolbarOffsets: {
78
- type: Array,
79
- required: false,
80
- default: () => [0, 8]
81
- },
82
-
83
- baseListClass: {
84
- type: String,
85
- required: false,
86
- default: 'zw-list--'
87
- },
88
-
89
- pageBlocks: {
90
- type: Array,
91
- required: true
92
- },
93
-
94
- readonly: {
95
- type: Boolean,
96
- required: false,
97
- default: false
98
- },
99
-
100
- // Temporary until migrations static accordion to sidebar
101
- popupMode: {
102
- type: Boolean,
103
- required: false,
104
- default: false
105
- },
106
-
107
- // Requires Window type but it different in iframe and outside
108
- // eslint-disable-next-line vue/require-prop-types
109
- window: {
110
- required: false,
111
- default: () => window
112
- },
113
-
114
- aiComponent: {
115
- type: Object,
116
- required: false,
117
- default: null
118
- },
119
-
120
- defaultFont: {
121
- type: Object as PropType<IFontJson>,
122
- required: false,
123
- default: null
124
- }
22
+ import { Device } from './enums';
23
+ import { Font, type IFontJson, type IPageBlock, type IStylePreset } from './models';
24
+ import type { StylePresetVariableFactory } from '@/types';
25
+
26
+ interface IProps {
27
+ presets: IStylePreset[];
28
+ defaultPresetId: number | string;
29
+ linkPresetId: number | string;
30
+ basePresetClass: string;
31
+ makePresetVariable: StylePresetVariableFactory;
32
+ fonts: IFontJson[];
33
+ active: boolean;
34
+ pageBlocks: IPageBlock[];
35
+ device?: Device;
36
+ toolbarOffsets?: [crossAxis: number, mainAxis: number];
37
+ baseListClass?: string;
38
+ readonly?: boolean;
39
+ popupMode?: boolean;
40
+ window?: Window;
41
+ aiComponent?: Component | null;
42
+ defaultFont?: IFontJson | null;
43
+ }
44
+
45
+ const props = withDefaults(defineProps<IProps>(), {
46
+ device: Device.DESKTOP,
47
+ toolbarOffsets: () => [0, 8],
48
+ baseListClass: 'zw-list--',
49
+ readonly: false,
50
+ popupMode: false,
51
+ window: () => window,
52
+ aiComponent: null,
53
+ defaultFont: null
125
54
  });
126
55
 
127
- const emit = defineEmits( [
128
- 'update-favorite-colors'
129
- ]);
130
-
131
56
  const modelValue = defineModel<Content>({
132
57
  type: [String, Object],
133
58
  required: true
134
59
  });
135
60
 
61
+ const favoriteColorsModel = defineModel<string[]>('favoriteColors', {
62
+ default: () => []
63
+ });
64
+
136
65
  const MIN_FONT_SIZE = 5;
137
66
  const MAX_FONT_SIZE = 112;
138
67
 
@@ -174,12 +103,11 @@ const fontSizes = new Array(MAX_FONT_SIZE - MIN_FONT_SIZE + 1)
174
103
  .map((_, index) => String(index + MIN_FONT_SIZE));
175
104
 
176
105
  const favoriteColors = new FavoriteColors({
177
- listRef: toRef(props, 'favoriteColors'),
178
- triggerUpdate: (colors: string[]) => emit('update-favorite-colors', colors)
106
+ listRef: favoriteColorsModel,
107
+ triggerUpdate: (colors: string[]) => favoriteColorsModel.value = colors
179
108
  });
180
109
 
181
110
  // Requires a lot of effort to add types right now
182
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
183
111
  // @ts-expect-error
184
112
  const getContentCustomization = () => editor.commands.getContentCustomization();
185
113
 
@@ -190,6 +118,23 @@ provide(InjectionTokens.LOCAL_STORAGE, new Storage(localStorage));
190
118
  provide(InjectionTokens.FAVORITE_COLORS, favoriteColors);
191
119
  provide(InjectionTokens.PAGE_BLOCKS, pageBlocks);
192
120
 
121
+ const toolbarTeleportTo = computed(() => {
122
+ if (props.popupMode) {
123
+ return null;
124
+ }
125
+
126
+ return props.window.document.body;
127
+ });
128
+
129
+ const RenderToolbar: FunctionalComponent = () => h(Toolbar, {
130
+ device: props.device,
131
+ visible: isToolbarVisible.value,
132
+ popupMode: props.popupMode,
133
+ referenceRef: wysiwygRef,
134
+ offsets: props.toolbarOffsets,
135
+ aiComponent: props.aiComponent
136
+ });
137
+
193
138
  defineExpose({ getContentCustomization, getContent, editor });
194
139
  </script>
195
140
 
@@ -33,7 +33,6 @@ export class Command {
33
33
  }
34
34
 
35
35
  output(data) {
36
- // eslint-disable-next-line no-console
37
- console.log(data);
36
+
38
37
  }
39
38
  }
@@ -7,6 +7,9 @@
7
7
  </template>
8
8
 
9
9
  <script setup>
10
+ // Disable max-len for css 🫠
11
+ /* eslint-disable max-len */
12
+
10
13
  const props = defineProps({
11
14
  value: {
12
15
  type: Boolean,
@@ -89,7 +89,7 @@ const hostRef = ref(null);
89
89
  const hostEl = useElementRef(hostRef);
90
90
 
91
91
  useDeselectionLock({
92
- isActiveRef: toRef(toggler, 'isOpened'),
92
+ isActiveRef: toRef(toggler, 'isOpened'),
93
93
  hostRef
94
94
  });
95
95