@zipify/wysiwyg 1.0.0-dev.42 → 1.0.0-dev.45
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/config/vite/example.config.js +25 -0
- package/dist/wysiwyg.css +1 -1
- package/dist/wysiwyg.mjs +3143 -3046
- package/example/ExampleApp.vue +5 -0
- package/example/{example.html → index.html} +1 -0
- package/lib/Wysiwyg.vue +4 -5
- package/lib/__tests__/utils/NodeFactory.js +13 -0
- package/lib/components/base/__tests__/Modal.test.js +2 -3
- package/lib/components/base/composables/__tests__/useValidator.test.js +44 -0
- package/lib/components/base/composables/useValidator.js +7 -3
- package/lib/components/toolbar/__tests__/Toolbar.test.js +2 -3
- package/lib/components/toolbar/controls/StylePresetControl.vue +14 -1
- package/lib/components/toolbar/controls/__tests__/StylePresetControl.test.js +16 -0
- package/lib/components/toolbar/controls/link/LinkControl.vue +28 -25
- package/lib/components/toolbar/controls/link/__tests__/LinkControl.test.js +79 -0
- package/lib/components/toolbar/controls/link/__tests__/LinkControlHeader.test.js +42 -0
- package/lib/components/toolbar/controls/link/composables/__tests__/__snapshots__/useLink.test.js.snap +8 -0
- package/lib/components/toolbar/controls/link/composables/__tests__/useLink.test.js +114 -0
- package/lib/components/toolbar/controls/link/destination/LinkControlDestination.vue +2 -2
- package/lib/components/toolbar/controls/link/destination/LinkControlUrl.vue +2 -2
- package/lib/components/toolbar/controls/link/destination/__tests__/LinkControlPageBlock.test.js +36 -0
- package/lib/components/toolbar/controls/link/destination/__tests__/LinkControlUrl.test.js +46 -0
- package/lib/components/toolbar/controls/link/destination/__tests__/__snapshots__/LinkControlPageBlock.test.js.snap +9 -0
- package/lib/components/toolbar/controls/link/destination/__tests__/__snapshots__/LinkControlUrl.test.js.snap +17 -0
- package/lib/composables/useToolbar.js +11 -0
- package/lib/directives/__tests__/outClick.test.js +6 -0
- package/lib/directives/outClick.js +12 -15
- package/lib/enums/Alignments.js +10 -1
- package/lib/extensions/Alignment.js +7 -5
- package/lib/extensions/FontSize.js +8 -2
- package/lib/extensions/FontWeight.js +3 -1
- package/lib/extensions/LineHeight.js +30 -36
- package/lib/extensions/Link.js +3 -15
- package/lib/extensions/TextDecoration.js +18 -0
- package/lib/extensions/__tests__/FontSize.test.js +2 -1
- package/lib/extensions/__tests__/FontWeight.test.js +8 -0
- package/lib/extensions/__tests__/LineHeight.test.js +12 -6
- package/lib/extensions/__tests__/Link.test.js +102 -0
- package/lib/extensions/__tests__/TextDecoration.test.js +24 -0
- package/lib/extensions/__tests__/__snapshots__/FontWeight.test.js.snap +17 -0
- package/lib/extensions/__tests__/__snapshots__/Link.test.js.snap +225 -0
- package/lib/extensions/__tests__/__snapshots__/TextDecoration.test.js.snap +90 -0
- package/lib/extensions/core/plugins/PastePlugin.js +23 -14
- package/lib/extensions/index.js +10 -5
- package/lib/services/ContentNormalizer.js +55 -15
- package/lib/services/__tests__/ContentNormalizer.test.js +39 -4
- package/lib/utils/__tests__/convertAlignment.test.js +16 -0
- package/lib/utils/__tests__/convertFontSize.test.js +21 -0
- package/lib/utils/__tests__/convertLineHeight.test.js +21 -0
- package/lib/utils/convertAlignment.js +12 -0
- package/lib/utils/convertFontSize.js +8 -0
- package/lib/utils/convertLineHeight.js +17 -0
- package/lib/utils/index.js +3 -0
- package/package.json +3 -13
- package/config/webpack/example.config.js +0 -88
- package/config/webpack/lib.config.js +0 -43
- package/config/webpack/loaders/index.js +0 -6
- package/config/webpack/loaders/js-loader.js +0 -5
- package/config/webpack/loaders/style-loader.js +0 -9
- package/config/webpack/loaders/svg-loader.js +0 -4
- package/config/webpack/loaders/vue-loader.js +0 -4
- package/config/webpack/settings.js +0 -9
package/example/ExampleApp.vue
CHANGED
|
@@ -152,4 +152,9 @@ body {
|
|
|
152
152
|
.zpa-text__list--square { list-style-type: square }
|
|
153
153
|
.zpa-text__list--latin { list-style-type: upper-roman }
|
|
154
154
|
.zpa-text__list--roman { list-style-type: upper-latin }
|
|
155
|
+
|
|
156
|
+
p, h1, h2, h3, h4 {
|
|
157
|
+
margin-top: 0.3em;
|
|
158
|
+
margin-bottom: 0.3em;
|
|
159
|
+
}
|
|
155
160
|
</style>
|
package/lib/Wysiwyg.vue
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<script>
|
|
14
14
|
import { EditorContent } from '@tiptap/vue-2';
|
|
15
|
-
import { provide, toRef, ref } from 'vue';
|
|
15
|
+
import { provide, toRef, ref, computed } from 'vue';
|
|
16
16
|
import { Toolbar } from './components';
|
|
17
17
|
import { useToolbar, useEditor } from './composables';
|
|
18
18
|
import { buildExtensions } from './extensions';
|
|
@@ -114,10 +114,9 @@ export default {
|
|
|
114
114
|
ContextWindow.use(props.window);
|
|
115
115
|
|
|
116
116
|
const fonts = props.fonts.map((font) => new Font(font));
|
|
117
|
-
const presets = toRef(props, 'presets');
|
|
118
|
-
|
|
119
117
|
const toolbarRef = ref(null);
|
|
120
118
|
const wysiwygRef = ref(null);
|
|
119
|
+
const wrapperRef = computed(() => wysiwygRef.value?.$el || document.body);
|
|
121
120
|
|
|
122
121
|
const toolbar = useToolbar({
|
|
123
122
|
wrapperRef: wysiwygRef,
|
|
@@ -145,9 +144,9 @@ export default {
|
|
|
145
144
|
makePresetVariable: props.makePresetVariable,
|
|
146
145
|
basePresetClass: props.basePresetClass,
|
|
147
146
|
baseListClass: props.baseListClass,
|
|
148
|
-
linkPreset: presets.value.find((preset) => preset.id === 'link'),
|
|
149
147
|
deviceRef: toRef(props, 'device'),
|
|
150
|
-
pageBlocks
|
|
148
|
+
pageBlocksRef: pageBlocks,
|
|
149
|
+
wrapperRef
|
|
151
150
|
})
|
|
152
151
|
});
|
|
153
152
|
|
|
@@ -64,4 +64,17 @@ export class NodeFactory {
|
|
|
64
64
|
static mark(type, attrs) {
|
|
65
65
|
return { type, attrs };
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
static link(attrs) {
|
|
69
|
+
return {
|
|
70
|
+
type: 'text',
|
|
71
|
+
marks: [
|
|
72
|
+
{
|
|
73
|
+
type: 'link',
|
|
74
|
+
attrs: { ...attrs }
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
text: attrs.text
|
|
78
|
+
};
|
|
79
|
+
}
|
|
67
80
|
}
|
|
@@ -46,9 +46,8 @@ describe('open/close', () => {
|
|
|
46
46
|
expect(wrapper).toVueEmpty();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const toggler = createToggler(false);
|
|
49
|
+
test('should open', async () => {
|
|
50
|
+
const toggler = createToggler({ value: false });
|
|
52
51
|
const wrapper = createComponent({ toggler });
|
|
53
52
|
|
|
54
53
|
toggler.isOpened.value = true;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useValidator } from '../useValidator';
|
|
2
|
+
|
|
3
|
+
describe('validation', () => {
|
|
4
|
+
test('should show error', () => {
|
|
5
|
+
const value = '';
|
|
6
|
+
const isEmpty = () => value ? false : 'Can\'t be empty';
|
|
7
|
+
|
|
8
|
+
const validator = useValidator({
|
|
9
|
+
validations: [isEmpty]
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
validator.validate();
|
|
13
|
+
|
|
14
|
+
expect(validator.error.value).toBe('Can\'t be empty');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should pass validation', () => {
|
|
18
|
+
const value = 'hello';
|
|
19
|
+
const isEmpty = () => value ? false : 'Can\'t be empty';
|
|
20
|
+
|
|
21
|
+
const validator = useValidator({
|
|
22
|
+
validations: [isEmpty]
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
validator.validate();
|
|
26
|
+
|
|
27
|
+
expect(validator.error.value).toBe(null);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should reset error', () => {
|
|
31
|
+
const value = '';
|
|
32
|
+
const isEmpty = () => value ? false : 'Can\'t be empty';
|
|
33
|
+
|
|
34
|
+
const validator = useValidator({
|
|
35
|
+
validations: [isEmpty]
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
validator.validate();
|
|
39
|
+
expect(validator.error.value).toBe('Can\'t be empty');
|
|
40
|
+
|
|
41
|
+
validator.reset();
|
|
42
|
+
expect(validator.error.value).toBe(null);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -3,9 +3,9 @@ import { ref } from 'vue';
|
|
|
3
3
|
export function useValidator({ validations }) {
|
|
4
4
|
const error = ref(null);
|
|
5
5
|
|
|
6
|
-
function validate(
|
|
6
|
+
function validate() {
|
|
7
7
|
for (const validate of validations) {
|
|
8
|
-
const validationError = validate(
|
|
8
|
+
const validationError = validate();
|
|
9
9
|
|
|
10
10
|
if (validationError) {
|
|
11
11
|
return error.value = validationError;
|
|
@@ -15,5 +15,9 @@ export function useValidator({ validations }) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
function reset() {
|
|
19
|
+
error.value = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { error, validate, reset };
|
|
19
23
|
}
|
|
@@ -10,7 +10,7 @@ function createComponent({ device }) {
|
|
|
10
10
|
propsData: {
|
|
11
11
|
toolbar: {
|
|
12
12
|
mount: jest.fn(),
|
|
13
|
-
isActiveRef: ref(true),
|
|
13
|
+
isActiveRef: ref({ value: true }),
|
|
14
14
|
offsets: [0, 8]
|
|
15
15
|
},
|
|
16
16
|
device: device ?? Devices.DESKTOP
|
|
@@ -18,8 +18,7 @@ function createComponent({ device }) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
describe.skip('rendering', () => {
|
|
21
|
+
describe('rendering', () => {
|
|
23
22
|
test('should render desktop toolbar', () => {
|
|
24
23
|
const wrapper = createComponent({ device: Devices.DESKTOP });
|
|
25
24
|
|
|
@@ -25,6 +25,9 @@ import { computed, inject } from 'vue';
|
|
|
25
25
|
import { InjectionTokens } from '../../../injectionTokens';
|
|
26
26
|
import { Dropdown, Button, Icon } from '../../base';
|
|
27
27
|
import { tooltip } from '../../../directives';
|
|
28
|
+
import { TextSettings } from '../../../enums';
|
|
29
|
+
|
|
30
|
+
const CLEAR_MARKS = [TextSettings.FONT_SIZE, TextSettings.FONT_WEIGHT];
|
|
28
31
|
|
|
29
32
|
export default {
|
|
30
33
|
name: 'StylePresetControl',
|
|
@@ -59,7 +62,17 @@ export default {
|
|
|
59
62
|
}));
|
|
60
63
|
});
|
|
61
64
|
|
|
62
|
-
const apply = (value) =>
|
|
65
|
+
const apply = (value) => {
|
|
66
|
+
editor.chain()
|
|
67
|
+
.focus()
|
|
68
|
+
.applyPreset(value)
|
|
69
|
+
.storeSelection()
|
|
70
|
+
.expandSelectionToBlock()
|
|
71
|
+
.unsetMarks(CLEAR_MARKS)
|
|
72
|
+
.restoreSelection()
|
|
73
|
+
.run();
|
|
74
|
+
};
|
|
75
|
+
|
|
63
76
|
const removeCustomization = () => editor.chain().focus().removePresetCustomization().run();
|
|
64
77
|
|
|
65
78
|
return {
|
|
@@ -3,6 +3,7 @@ import { ref } from 'vue';
|
|
|
3
3
|
import { InjectionTokens } from '../../../../injectionTokens';
|
|
4
4
|
import { Button, Dropdown } from '../../../base';
|
|
5
5
|
import StylePresetControl from '../StylePresetControl';
|
|
6
|
+
import { TextSettings } from '../../../../enums';
|
|
6
7
|
|
|
7
8
|
const createEditor = ({ presets, preset, customization } = {}) => ({
|
|
8
9
|
commands: {
|
|
@@ -12,6 +13,10 @@ const createEditor = ({ presets, preset, customization } = {}) => ({
|
|
|
12
13
|
getPresetCustomization: () => ref(customization || { attributes: [], marks: [] }),
|
|
13
14
|
applyPreset: jest.fn().mockReturnThis(),
|
|
14
15
|
removePresetCustomization: jest.fn().mockReturnThis(),
|
|
16
|
+
storeSelection: jest.fn().mockReturnThis(),
|
|
17
|
+
restoreSelection: jest.fn().mockReturnThis(),
|
|
18
|
+
expandSelectionToBlock: jest.fn().mockReturnThis(),
|
|
19
|
+
unsetMarks: jest.fn().mockReturnThis(),
|
|
15
20
|
run: jest.fn()
|
|
16
21
|
},
|
|
17
22
|
|
|
@@ -114,6 +119,17 @@ describe('apply preset', () => {
|
|
|
114
119
|
|
|
115
120
|
expect(editor.commands.applyPreset).toHaveBeenCalledWith('regular-1');
|
|
116
121
|
});
|
|
122
|
+
|
|
123
|
+
test('should remove settings on preset change', () => {
|
|
124
|
+
const editor = createEditor();
|
|
125
|
+
const wrapper = createComponent({ editor });
|
|
126
|
+
const dropdownWrapper = wrapper.findComponent(Dropdown);
|
|
127
|
+
|
|
128
|
+
dropdownWrapper.vm.$emit('change', 'regular-1');
|
|
129
|
+
|
|
130
|
+
expect(editor.commands.expandSelectionToBlock).toHaveBeenCalled();
|
|
131
|
+
expect(editor.commands.unsetMarks).toHaveBeenCalledWith([TextSettings.FONT_SIZE, TextSettings.FONT_WEIGHT]);
|
|
132
|
+
});
|
|
117
133
|
});
|
|
118
134
|
|
|
119
135
|
describe('remove customization', () => {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
label="Text to display"
|
|
15
15
|
placeholder="Type Text"
|
|
16
16
|
:error="nameValidator.error.value"
|
|
17
|
-
@input="
|
|
17
|
+
@input="updateLinkText"
|
|
18
18
|
/>
|
|
19
19
|
|
|
20
20
|
<LinkControlDestination
|
|
@@ -61,16 +61,32 @@ export default {
|
|
|
61
61
|
setup() {
|
|
62
62
|
const wrapperRef = ref(null);
|
|
63
63
|
const modalRef = ref(null);
|
|
64
|
-
const nameError = ref(false);
|
|
65
|
-
const urlError = ref(false);
|
|
66
64
|
|
|
67
65
|
const editor = inject(InjectionTokens.EDITOR);
|
|
68
66
|
|
|
69
67
|
const link = useLink();
|
|
68
|
+
const urlRegExp = /(^(https?:\/\/|\/)(?:www\.|(?!www))?[^\s])/;
|
|
69
|
+
|
|
70
|
+
const isEmpty = () => {
|
|
71
|
+
return link.linkData.value.text ? false : 'Can\'t be empty';
|
|
72
|
+
};
|
|
73
|
+
const isUrl = () => {
|
|
74
|
+
if (link.currentDestination.value.id !== 'url') return false;
|
|
75
|
+
|
|
76
|
+
return urlRegExp.test(link.destinationHrefs.value.url) ? false : 'Please enter a valid URL';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const nameValidator = useValidator({
|
|
80
|
+
validations: [isEmpty]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const urlValidator = useValidator({
|
|
84
|
+
validations: [isUrl]
|
|
85
|
+
});
|
|
70
86
|
|
|
71
87
|
const resetErrors = () => {
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
nameValidator.reset();
|
|
89
|
+
urlValidator.reset();
|
|
74
90
|
};
|
|
75
91
|
|
|
76
92
|
const onBeforeOpened = () => {
|
|
@@ -86,30 +102,17 @@ export default {
|
|
|
86
102
|
});
|
|
87
103
|
|
|
88
104
|
const isActive = computed(() => toggler.isOpened.value || editor.isActive('link'));
|
|
89
|
-
const isEmpty = () => {
|
|
90
|
-
return link.linkData.value.text ? false : 'Can\'t be empty';
|
|
91
|
-
};
|
|
92
|
-
const isUrl = () => {
|
|
93
|
-
if (link.currentDestination.value.id !== 'url') return false;
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
const updateLinkText = (value) => {
|
|
107
|
+
resetErrors();
|
|
108
|
+
link.updateText(value);
|
|
96
109
|
};
|
|
97
110
|
|
|
98
|
-
const nameValidator = useValidator({
|
|
99
|
-
validations: [isEmpty],
|
|
100
|
-
value: link.linkData.value.text
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const urlValidator = useValidator({
|
|
104
|
-
validations: [isUrl],
|
|
105
|
-
value: link.linkData.value.text
|
|
106
|
-
});
|
|
107
|
-
|
|
108
111
|
const applyLink = () => {
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
urlValidator.validate();
|
|
113
|
+
nameValidator.validate();
|
|
111
114
|
|
|
112
|
-
if (
|
|
115
|
+
if (urlValidator.error.value || nameValidator.error.value) return;
|
|
113
116
|
|
|
114
117
|
link.apply();
|
|
115
118
|
toggler.close();
|
|
@@ -128,7 +131,7 @@ export default {
|
|
|
128
131
|
isActive,
|
|
129
132
|
nameValidator,
|
|
130
133
|
urlValidator,
|
|
131
|
-
|
|
134
|
+
updateLinkText,
|
|
132
135
|
resetErrors,
|
|
133
136
|
applyLink,
|
|
134
137
|
removeLink
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import { ref, nextTick } from 'vue';
|
|
3
|
+
import LinkControl from '../LinkControl';
|
|
4
|
+
import { InjectionTokens } from '../../../../../injectionTokens';
|
|
5
|
+
import { Button, TextField } from '../../../../base';
|
|
6
|
+
import LinkControlApply from '../LinkControlApply';
|
|
7
|
+
import { LinkControlDestination } from '../destination';
|
|
8
|
+
|
|
9
|
+
const createEditor = (isActive) => ({
|
|
10
|
+
isActive: () => isActive ?? false
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function createComponent({ editor }) {
|
|
14
|
+
return shallowMount(LinkControl, {
|
|
15
|
+
provide: {
|
|
16
|
+
[InjectionTokens.EDITOR]: editor,
|
|
17
|
+
[InjectionTokens.PAGE_BLOCKS]: ref([{ id: 1 }])
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('selection value', () => {
|
|
23
|
+
test('should render link status', () => {
|
|
24
|
+
const editor = createEditor(true );
|
|
25
|
+
const wrapper = createComponent({ editor });
|
|
26
|
+
const buttonWrapper = wrapper.findComponent(Button);
|
|
27
|
+
|
|
28
|
+
expect(buttonWrapper.props('active')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('validation', () => {
|
|
33
|
+
test('should show error on apply', async () => {
|
|
34
|
+
const editor = createEditor(true );
|
|
35
|
+
const wrapper = createComponent({ editor });
|
|
36
|
+
const textFieldWrapper = wrapper.findComponent(TextField);
|
|
37
|
+
const linkControlDestinationWrapper = wrapper.findComponent(LinkControlDestination);
|
|
38
|
+
const applyWrapper = wrapper.findComponent(LinkControlApply);
|
|
39
|
+
|
|
40
|
+
applyWrapper.vm.$emit('apply');
|
|
41
|
+
await nextTick();
|
|
42
|
+
|
|
43
|
+
expect(textFieldWrapper.props('error')).toBe('Can\'t be empty');
|
|
44
|
+
expect(linkControlDestinationWrapper.props('validator').error.value).toBe('Please enter a valid URL');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should reset errors on input', async () => {
|
|
48
|
+
const editor = createEditor(true );
|
|
49
|
+
const wrapper = createComponent({ editor });
|
|
50
|
+
const textFieldWrapper = wrapper.findComponent(TextField);
|
|
51
|
+
const linkControlDestinationWrapper = wrapper.findComponent(LinkControlDestination);
|
|
52
|
+
const applyWrapper = wrapper.findComponent(LinkControlApply);
|
|
53
|
+
|
|
54
|
+
applyWrapper.vm.$emit('apply');
|
|
55
|
+
textFieldWrapper.vm.$emit('input', 'hello');
|
|
56
|
+
|
|
57
|
+
await nextTick();
|
|
58
|
+
|
|
59
|
+
expect(textFieldWrapper.props('error')).toBeNull();
|
|
60
|
+
expect(linkControlDestinationWrapper.props('validator').error.value).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should reset errors on change destination', async () => {
|
|
64
|
+
const editor = createEditor(true );
|
|
65
|
+
const wrapper = createComponent({ editor });
|
|
66
|
+
const textFieldWrapper = wrapper.findComponent(TextField);
|
|
67
|
+
const linkControlDestinationWrapper = wrapper.findComponent(LinkControlDestination);
|
|
68
|
+
const applyWrapper = wrapper.findComponent(LinkControlApply);
|
|
69
|
+
|
|
70
|
+
applyWrapper.vm.$emit('apply');
|
|
71
|
+
linkControlDestinationWrapper.vm.$emit('reset-errors');
|
|
72
|
+
|
|
73
|
+
await nextTick();
|
|
74
|
+
|
|
75
|
+
expect(textFieldWrapper.props('error')).toBeNull();
|
|
76
|
+
expect(linkControlDestinationWrapper.props('validator').error.value).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import { InjectionTokens } from '../../../../../injectionTokens';
|
|
3
|
+
import { Button } from '../../../../base';
|
|
4
|
+
import LinkControlHeader from '../LinkControlHeader';
|
|
5
|
+
|
|
6
|
+
const createEditor = (isActive) => ({
|
|
7
|
+
isActive: () => isActive ?? false
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function createComponent({ editor }) {
|
|
11
|
+
return shallowMount(LinkControlHeader, {
|
|
12
|
+
provide: { [InjectionTokens.EDITOR]: editor }
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('rendering unlink button', () => {
|
|
17
|
+
test('should button be disabled when no link', () => {
|
|
18
|
+
const editor = createEditor(true );
|
|
19
|
+
const wrapper = createComponent({ editor });
|
|
20
|
+
const buttonWrapper = wrapper.findComponent(Button);
|
|
21
|
+
|
|
22
|
+
expect(buttonWrapper.props('disabled')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should button be enabled when link selected', () => {
|
|
26
|
+
const editor = createEditor(false );
|
|
27
|
+
const wrapper = createComponent({ editor });
|
|
28
|
+
const buttonWrapper = wrapper.findComponent(Button);
|
|
29
|
+
|
|
30
|
+
expect(buttonWrapper.props('disabled')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should send unlink event', () => {
|
|
34
|
+
const editor = createEditor(true );
|
|
35
|
+
const wrapper = createComponent({ editor });
|
|
36
|
+
const buttonWrapper = wrapper.findComponent(Button);
|
|
37
|
+
|
|
38
|
+
buttonWrapper.vm.$emit('click');
|
|
39
|
+
|
|
40
|
+
expect(wrapper.emitted('remove-link')).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { withComponentContext } from '../../../../../../__tests__/utils';
|
|
3
|
+
import { InjectionTokens } from '../../../../../../injectionTokens';
|
|
4
|
+
import { LinkDestinations, LinkTargets } from '../../../../../../enums';
|
|
5
|
+
import { useLink } from '../useLink';
|
|
6
|
+
|
|
7
|
+
const createEditor = (text, destination) => ({
|
|
8
|
+
commands: {
|
|
9
|
+
storeSelection: jest.fn(),
|
|
10
|
+
restoreSelection: jest.fn(),
|
|
11
|
+
getSelectedText: jest.fn(() => text ?? ''),
|
|
12
|
+
focus: jest.fn().mockReturnThis(),
|
|
13
|
+
unsetLink: jest.fn().mockReturnThis(),
|
|
14
|
+
applyLink: jest.fn().mockReturnThis(),
|
|
15
|
+
run: jest.fn()
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
chain() {
|
|
19
|
+
return this.commands;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
getAttributes: jest.fn(() => ({
|
|
23
|
+
destination: destination ?? null
|
|
24
|
+
}))
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function useComposable(text, destination) {
|
|
28
|
+
return withComponentContext(() => useLink(), {
|
|
29
|
+
provide: {
|
|
30
|
+
[InjectionTokens.EDITOR]: createEditor(text, destination),
|
|
31
|
+
[InjectionTokens.PAGE_BLOCKS]: ref([{ id: 1 }, { id: 2 }])
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('init link', () => {
|
|
38
|
+
test('should prepare initial fields', () => {
|
|
39
|
+
const link = useComposable();
|
|
40
|
+
|
|
41
|
+
link.prepareInitialFields();
|
|
42
|
+
|
|
43
|
+
expect(link.currentDestination.value.id).toBe(LinkDestinations.URL);
|
|
44
|
+
expect(link.linkData.value.target).toBe(LinkTargets.SELF);
|
|
45
|
+
expect(link.destinationHrefs.value).toMatchSnapshot();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should take text from selection', () => {
|
|
49
|
+
const link = useComposable('hello world');
|
|
50
|
+
|
|
51
|
+
link.prepareInitialFields();
|
|
52
|
+
|
|
53
|
+
expect(link.linkData.value.text).toBe('hello world');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('actions with link', () => {
|
|
58
|
+
test('should update url', () => {
|
|
59
|
+
const link = useComposable('hello world');
|
|
60
|
+
|
|
61
|
+
link.updateLink('https://hello.world');
|
|
62
|
+
|
|
63
|
+
expect(link.destinationHrefs.value.url).toBe('https://hello.world');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should update block destination', () => {
|
|
67
|
+
const link = useComposable('hello world', LinkDestinations.BLOCK);
|
|
68
|
+
|
|
69
|
+
link.prepareInitialFields();
|
|
70
|
+
link.updateLink('3456');
|
|
71
|
+
|
|
72
|
+
expect(link.destinationHrefs.value.block).toBe('3456');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should remove link', () => {
|
|
76
|
+
const link = useComposable();
|
|
77
|
+
|
|
78
|
+
link.removeLink();
|
|
79
|
+
|
|
80
|
+
expect(link.editor.commands.unsetLink).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should apply link', () => {
|
|
84
|
+
const link = useComposable();
|
|
85
|
+
|
|
86
|
+
link.updateText('hello');
|
|
87
|
+
link.updateLink('/world');
|
|
88
|
+
|
|
89
|
+
link.apply();
|
|
90
|
+
|
|
91
|
+
expect(link.editor.commands.applyLink).toHaveBeenCalledWith({
|
|
92
|
+
destination: 'url',
|
|
93
|
+
href: '/world',
|
|
94
|
+
target: LinkTargets.SELF,
|
|
95
|
+
text: 'hello'
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should apply link target', () => {
|
|
100
|
+
const link = useComposable();
|
|
101
|
+
|
|
102
|
+
link.updateTarget(false);
|
|
103
|
+
|
|
104
|
+
expect(link.linkData.value.target).toBe(LinkTargets.SELF);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should apply link target', () => {
|
|
108
|
+
const link = useComposable();
|
|
109
|
+
|
|
110
|
+
link.updateTarget(true);
|
|
111
|
+
|
|
112
|
+
expect(link.linkData.value.target).toBe(LinkTargets.BLANK);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
:href="link.destinationHrefs.value.url"
|
|
27
27
|
:isTargetBlank="isTargetBlank"
|
|
28
28
|
:validator="validator"
|
|
29
|
-
@
|
|
30
|
-
@
|
|
29
|
+
@update-link="updateLink"
|
|
30
|
+
@update-target="link.updateTarget"
|
|
31
31
|
/>
|
|
32
32
|
|
|
33
33
|
<LinkControlPageBlock
|
|
@@ -43,8 +43,8 @@ export default {
|
|
|
43
43
|
},
|
|
44
44
|
|
|
45
45
|
setup(props, { emit }) {
|
|
46
|
-
const updateLink = (value) => emit('
|
|
47
|
-
const updateTarget = (value) => emit('
|
|
46
|
+
const updateLink = (value) => emit('update-link', value);
|
|
47
|
+
const updateTarget = (value) => emit('update-target', value);
|
|
48
48
|
|
|
49
49
|
return { updateLink, updateTarget };
|
|
50
50
|
}
|
package/lib/components/toolbar/controls/link/destination/__tests__/LinkControlPageBlock.test.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ref, h } from 'vue';
|
|
2
|
+
import { shallowMount } from '@vue/test-utils';
|
|
3
|
+
import LinkControlPageBlock from '../LinkControlPageBlock';
|
|
4
|
+
import { InjectionTokens } from '../../../../../../injectionTokens';
|
|
5
|
+
import { Dropdown } from '../../../../../base';
|
|
6
|
+
|
|
7
|
+
function createComponent() {
|
|
8
|
+
return shallowMount(LinkControlPageBlock, {
|
|
9
|
+
propsData: {
|
|
10
|
+
value: 12
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
stubs: {
|
|
14
|
+
Dropdown: {
|
|
15
|
+
render: () => h('div'),
|
|
16
|
+
props: ['value']
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
provide: {
|
|
21
|
+
[InjectionTokens.PAGE_BLOCKS]: ref([{ id: 1 }, { id: 2 }])
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('applying block', () => {
|
|
27
|
+
test('should apply block', () => {
|
|
28
|
+
const wrapper = createComponent();
|
|
29
|
+
|
|
30
|
+
const dropdownWrapper = wrapper.findComponent(Dropdown);
|
|
31
|
+
|
|
32
|
+
dropdownWrapper.vm.$emit('change', 2);
|
|
33
|
+
|
|
34
|
+
expect(wrapper.emitted('update')).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
});
|