@demos-europe/demosplan-ui 0.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.
- package/LICENSE +21 -0
- package/buildTokens.js +59 -0
- package/components/DpButton/DpButton.stories.mdx +136 -0
- package/components/DpButton/DpButton.vue +118 -0
- package/components/DpDetails/DpDetails.stories.mdx +55 -0
- package/components/DpDetails/DpDetails.vue +58 -0
- package/components/DpIcon/DpIcon.stories.mdx +396 -0
- package/components/DpIcon/DpIcon.vue +51 -0
- package/components/DpIcon/util/iconVariables.js +148 -0
- package/components/DpInput/DpInput.stories.mdx +127 -0
- package/components/DpInput/DpInput.vue +284 -0
- package/components/DpLabel/DpLabel.stories.mdx +103 -0
- package/components/DpLabel/DpLabel.vue +112 -0
- package/components/DpLoading/DpLoading.stories.mdx +63 -0
- package/components/DpLoading/DpLoading.vue +63 -0
- package/components/core/DpAccordion.vue +108 -0
- package/components/core/DpAnonymizeText.vue +136 -0
- package/components/core/DpAutocomplete.vue +133 -0
- package/components/core/DpBulkEditHeader.vue +53 -0
- package/components/core/DpButtonIcon.vue +47 -0
- package/components/core/DpButtonRow.vue +155 -0
- package/components/core/DpCard.vue +54 -0
- package/components/core/DpChangeStateAtDate.vue +223 -0
- package/components/core/DpCheckboxGroup.vue +93 -0
- package/components/core/DpContextualHelp.vue +54 -0
- package/components/core/DpCopyPasteButton.vue +47 -0
- package/components/core/DpDashboardTaskCard.vue +123 -0
- package/components/core/DpDataTable/DataTableSearch.js +44 -0
- package/components/core/DpDataTable/DpColumnSelector.vue +133 -0
- package/components/core/DpDataTable/DpDataTable.vue +647 -0
- package/components/core/DpDataTable/DpDataTableExtended.vue +377 -0
- package/components/core/DpDataTable/DpResizeHandle.vue +37 -0
- package/components/core/DpDataTable/DpSelectPageItemCount.vue +70 -0
- package/components/core/DpDataTable/DpTableHeader.vue +197 -0
- package/components/core/DpDataTable/DpTableRow.vue +355 -0
- package/components/core/DpDataTable/DpWrapTrigger.vue +48 -0
- package/components/core/DpDataTable/lib/ResizableColumns.js +83 -0
- package/components/core/DpEditableList.vue +161 -0
- package/components/core/DpEditor/DpBoilerPlate.vue +140 -0
- package/components/core/DpEditor/DpBoilerPlateModal.vue +166 -0
- package/components/core/DpEditor/DpEditor.vue +1281 -0
- package/components/core/DpEditor/DpLinkModal.vue +117 -0
- package/components/core/DpEditor/DpRecommendationModal/DpInsertableRecommendation.vue +137 -0
- package/components/core/DpEditor/DpRecommendationModal.vue +283 -0
- package/components/core/DpEditor/DpResizableImage.vue +121 -0
- package/components/core/DpEditor/DpUploadModal.vue +121 -0
- package/components/core/DpEditor/libs/Decoration.js +66 -0
- package/components/core/DpEditor/libs/SegmentRangeChangePlugin.js +35 -0
- package/components/core/DpEditor/libs/editorAnonymize.js +66 -0
- package/components/core/DpEditor/libs/editorBuildSuggestion.js +269 -0
- package/components/core/DpEditor/libs/editorCustomDelete.js +32 -0
- package/components/core/DpEditor/libs/editorCustomImage.js +100 -0
- package/components/core/DpEditor/libs/editorCustomInsert.js +32 -0
- package/components/core/DpEditor/libs/editorCustomLink.js +46 -0
- package/components/core/DpEditor/libs/editorCustomMark.js +32 -0
- package/components/core/DpEditor/libs/editorInsertAtCursorPos.js +41 -0
- package/components/core/DpEditor/libs/editorObscure.js +60 -0
- package/components/core/DpEditor/libs/editorUnAnonymize.js +56 -0
- package/components/core/DpEditor/libs/handleWordPaste.js +360 -0
- package/components/core/DpEditor/libs/preventDrop.js +31 -0
- package/components/core/DpEditor/libs/preventKeyboardInput.js +27 -0
- package/components/core/DpEditor/libs/preventPaste.js +28 -0
- package/components/core/DpFlyout.vue +119 -0
- package/components/core/DpInlineNotification.vue +116 -0
- package/components/core/DpModal.vue +208 -0
- package/components/core/DpObscure.vue +29 -0
- package/components/core/DpPager.vue +139 -0
- package/components/core/DpProgressBar.vue +67 -0
- package/components/core/DpRegisterFlyout.vue +58 -0
- package/components/core/DpResettableInput.vue +140 -0
- package/components/core/DpSkeletonBox.vue +32 -0
- package/components/core/DpSlidebar.vue +86 -0
- package/components/core/DpSlidingPagination.vue +45 -0
- package/components/core/DpSplitButton.vue +77 -0
- package/components/core/DpSwitcher.vue +62 -0
- package/components/core/DpTableCardList/DpTableCard.vue +61 -0
- package/components/core/DpTableCardList/DpTableCardListHeader.vue +83 -0
- package/components/core/DpTabs/DpTab.vue +52 -0
- package/components/core/DpTabs/DpTabs.vue +165 -0
- package/components/core/DpTextWrapper.vue +65 -0
- package/components/core/DpToggleForm.vue +72 -0
- package/components/core/DpTooltipIcon.vue +52 -0
- package/components/core/DpTransitionExpand.vue +87 -0
- package/components/core/DpTreeList/DpTreeList.vue +334 -0
- package/components/core/DpTreeList/DpTreeListCheckbox.vue +79 -0
- package/components/core/DpTreeList/DpTreeListNode.vue +348 -0
- package/components/core/DpTreeList/DpTreeListToggle.vue +71 -0
- package/components/core/DpTreeList/utils/constants.js +14 -0
- package/components/core/DpUpload/DpUpload.vue +223 -0
- package/components/core/DpUpload/DpUploadFiles.vue +269 -0
- package/components/core/DpUpload/DpUploadedFile.vue +80 -0
- package/components/core/DpUpload/DpUploadedFileList.vue +56 -0
- package/components/core/DpUpload/utils/GetFileIdsByHash.js +42 -0
- package/components/core/DpUpload/utils/UppyTranslations.js +31 -0
- package/components/core/DpVideoPlayer.vue +115 -0
- package/components/core/HeightLimit.vue +121 -0
- package/components/core/MultistepNav.vue +89 -0
- package/components/core/form/DpCheckbox.vue +108 -0
- package/components/core/form/DpDateRangePicker.vue +186 -0
- package/components/core/form/DpDatepicker.vue +160 -0
- package/components/core/form/DpDatetimePicker.vue +194 -0
- package/components/core/form/DpFormRow.vue +79 -0
- package/components/core/form/DpMultiselect.vue +164 -0
- package/components/core/form/DpRadio.vue +128 -0
- package/components/core/form/DpSearchField.vue +110 -0
- package/components/core/form/DpSelect.vue +149 -0
- package/components/core/form/DpTextArea.vue +152 -0
- package/components/core/form/DpTimePicker.vue +374 -0
- package/components/core/form/DpToggle.vue +78 -0
- package/components/core/index.js +132 -0
- package/components/core/notify/DpNotifyContainer.vue +122 -0
- package/components/core/notify/DpNotifyMessage.vue +95 -0
- package/components/core/shared/DpStickyElement.vue +95 -0
- package/components/index.js +24 -0
- package/components/shared/translations.js +15 -0
- package/directives/CleanHtml/CleanHtml.js +50 -0
- package/directives/CleanHtml/CleanHtml.stories.mdx +64 -0
- package/directives/Tooltip/Tooltip.js +40 -0
- package/directives/Tooltip/Tooltip.stories.mdx +42 -0
- package/directives/index.js +17 -0
- package/lib/index.js +14 -0
- package/lib/prefixClass.js +47 -0
- package/mixins/index.js +14 -0
- package/mixins/prefixClassMixin.js +22 -0
- package/package.json +52 -0
- package/shared/props.js +86 -0
- package/style/index.css +7 -0
- package/tailwind.config.js +24 -0
- package/tokens/color.json +358 -0
- package/tokens/color.stories.mdx +45 -0
- package/tokens/fontSize.json +100 -0
- package/tokens/space.json +33 -0
- package/utils/lengthHint.js +69 -0
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
<license>
|
|
2
|
+
(c) 2010-present DEMOS E-Partizipation GmbH.
|
|
3
|
+
|
|
4
|
+
This file is part of the package @demos-europe/demosplan-ui,
|
|
5
|
+
for more information see the license file.
|
|
6
|
+
|
|
7
|
+
All rights reserved
|
|
8
|
+
</license>
|
|
9
|
+
|
|
10
|
+
<documentation>
|
|
11
|
+
<!-- DpEditor component
|
|
12
|
+
- contains menubar with a number of buttons and an editor
|
|
13
|
+
- use this component without the inline-editing-wrapper TiptapEditText.vue if you want to add a text editor to a form element (as in new statement view)
|
|
14
|
+
- use this component with the inline-editing-wrapper TiptapEditText.vue if you want to save the text directly via inline-editing (as in assessment table)
|
|
15
|
+
- the editor-content component needs a prop with editor instance to work correctly
|
|
16
|
+
|
|
17
|
+
To properly set the content we have to update this.currentValue and use editor.setContent() - both actions are needed!
|
|
18
|
+
To use boilerplates we need to mount tiptap editor, where boilerplate modal is imported dynamically. The boilerplates are stored in boilerplates vuex store, which is also registered dynamically if boilerplate prop is specified.
|
|
19
|
+
|
|
20
|
+
Possible component props:
|
|
21
|
+
|
|
22
|
+
boilerPlate - add it if you want to have boilerplate button in editor menu. Prop value should be a string with boilerplate category (email/consideration/news.notes): boiler-plate="email"
|
|
23
|
+
editorId - to identify it in boilerplates modal
|
|
24
|
+
procedureId
|
|
25
|
+
hiddenInput - to send data with submit form action we sometimes need to have a hidden input with tiptap's content. If the hidden input should be added, the prop should be a string with input name (e.g. r_name),
|
|
26
|
+
obscure - set to true if you want to use the 'obscure text' button. if the permission is not activated the button will not be shown anyay
|
|
27
|
+
value - initial editor's value
|
|
28
|
+
required - determine if hidden input is required, used in with dp-validate-plugin
|
|
29
|
+
linkButton - define if a button to add links should be visible in menu
|
|
30
|
+
readonly - true/false
|
|
31
|
+
headings - determine which heading level (h1-h6) buttons should be visible in menu. It is an array with numbers , e.g. [1,2,3,4,5,6]
|
|
32
|
+
table - true/false - if tables should be supported and buttons for inserting tables should be added this prop has to be true
|
|
33
|
+
|
|
34
|
+
To use tiptap import the component dynamically: components = { DpEditor: () => import('@DpJs/components/core/DpEditor/DpEditor') } }
|
|
35
|
+
|
|
36
|
+
-->
|
|
37
|
+
</documentation>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="o-form__control-tiptap">
|
|
41
|
+
<div
|
|
42
|
+
v-if="maxlength !== 0"
|
|
43
|
+
:class="prefixClass('lbl__hint')"
|
|
44
|
+
v-cleanhtml="counterText" />
|
|
45
|
+
<dp-boiler-plate-modal
|
|
46
|
+
v-if="toolbar.boilerPlate && boilerPlateEnabled"
|
|
47
|
+
ref="boilerPlateModal"
|
|
48
|
+
:editor-id="editorId"
|
|
49
|
+
:procedure-id="procedureId"
|
|
50
|
+
:boiler-plate-type="toolbar.boilerPlate"
|
|
51
|
+
@insertBoilerPlate="text => handleInsertText(text)" />
|
|
52
|
+
<dp-link-modal
|
|
53
|
+
v-if="toolbar.linkButton"
|
|
54
|
+
ref="linkModal"
|
|
55
|
+
@insert="insertUrl" />
|
|
56
|
+
<dp-upload-modal
|
|
57
|
+
v-if="toolbar.imageButton"
|
|
58
|
+
ref="uploadModal"
|
|
59
|
+
@insert-image="insertImage"
|
|
60
|
+
@add-alt="addAltTextToImage"
|
|
61
|
+
@close="resetEditingImage" />
|
|
62
|
+
<dp-recommendation-modal
|
|
63
|
+
v-if="toolbar.recommendationButton"
|
|
64
|
+
ref="recommendationModal"
|
|
65
|
+
@insert-recommendation="text => appendText(text)"
|
|
66
|
+
:procedure-id="procedureId"
|
|
67
|
+
:segment-id="segmentId" />
|
|
68
|
+
<div :class="prefixClass('row tiptap')">
|
|
69
|
+
<div :class="prefixClass('col')">
|
|
70
|
+
<div
|
|
71
|
+
:class="[isFullscreen ? 'fullscreen': '', prefixClass('editor')]">
|
|
72
|
+
<editor-menu-bar :editor="editor">
|
|
73
|
+
<div
|
|
74
|
+
slot-scope="{ commands, isActive, getMarkAttrs }"
|
|
75
|
+
:class="[readonly ? prefixClass('readonly'): '', prefixClass('menubar')]">
|
|
76
|
+
<!-- Cut -->
|
|
77
|
+
<button
|
|
78
|
+
@click="cut"
|
|
79
|
+
:class="prefixClass('menubar__button')"
|
|
80
|
+
type="button"
|
|
81
|
+
:aria-label="Translator.trans('editor.cut')"
|
|
82
|
+
v-tooltip="Translator.trans('editor.cut')"
|
|
83
|
+
:disabled="readonly">
|
|
84
|
+
<i
|
|
85
|
+
:class="prefixClass('fa fa-scissors')"
|
|
86
|
+
aria-hidden="true" />
|
|
87
|
+
</button>
|
|
88
|
+
❘
|
|
89
|
+
<!-- Undo -->
|
|
90
|
+
<button
|
|
91
|
+
@click="commands.undo"
|
|
92
|
+
:class="prefixClass('menubar__button')"
|
|
93
|
+
type="button"
|
|
94
|
+
:aria-label="Translator.trans('editor.undo')"
|
|
95
|
+
v-tooltip="Translator.trans('editor.undo')"
|
|
96
|
+
:disabled="readonly">
|
|
97
|
+
<i
|
|
98
|
+
:class="prefixClass('fa fa-reply')"
|
|
99
|
+
aria-hidden="true" />
|
|
100
|
+
</button>
|
|
101
|
+
<!-- Redo -->
|
|
102
|
+
<button
|
|
103
|
+
@click="commands.redo"
|
|
104
|
+
:class="prefixClass('menubar__button')"
|
|
105
|
+
type="button"
|
|
106
|
+
:aria-label="Translator.trans('editor.redo')"
|
|
107
|
+
v-tooltip="Translator.trans('editor.redo')"
|
|
108
|
+
:disabled="readonly">
|
|
109
|
+
<i
|
|
110
|
+
:class="prefixClass('fa fa-share')"
|
|
111
|
+
aria-hidden="true" />
|
|
112
|
+
</button>
|
|
113
|
+
<template v-if="toolbar.textDecoration">
|
|
114
|
+
❘
|
|
115
|
+
<!-- Bold -->
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
@click="commands.bold"
|
|
119
|
+
:class="[isActive.bold() ? prefixClass('is-active'): '', prefixClass('menubar__button')]"
|
|
120
|
+
type="button"
|
|
121
|
+
:aria-label="Translator.trans('editor.bold')"
|
|
122
|
+
v-tooltip="Translator.trans('editor.bold')"
|
|
123
|
+
:disabled="readonly">
|
|
124
|
+
<i
|
|
125
|
+
:class="prefixClass('fa fa-bold')"
|
|
126
|
+
aria-hidden="true" />
|
|
127
|
+
</button>
|
|
128
|
+
|
|
129
|
+
<!-- Italic -->
|
|
130
|
+
<button
|
|
131
|
+
@click="commands.italic"
|
|
132
|
+
:class="[isActive.italic() ? prefixClass('is-active') : '', prefixClass('menubar__button') ]"
|
|
133
|
+
type="button"
|
|
134
|
+
:aria-label="Translator.trans('editor.italic')"
|
|
135
|
+
v-tooltip="Translator.trans('editor.italic')"
|
|
136
|
+
:disabled="readonly">
|
|
137
|
+
<i
|
|
138
|
+
:class="prefixClass('fa fa-italic')"
|
|
139
|
+
aria-hidden="true" />
|
|
140
|
+
</button>
|
|
141
|
+
<!-- Underline -->
|
|
142
|
+
<button
|
|
143
|
+
@click="commands.underline"
|
|
144
|
+
:class="[isActive.underline() ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
145
|
+
type="button"
|
|
146
|
+
:aria-label="Translator.trans('editor.underline')"
|
|
147
|
+
v-tooltip="Translator.trans('editor.underline')"
|
|
148
|
+
:disabled="readonly">
|
|
149
|
+
<i
|
|
150
|
+
:class="prefixClass('fa fa-underline')"
|
|
151
|
+
aria-hidden="true" />
|
|
152
|
+
</button>
|
|
153
|
+
</template>
|
|
154
|
+
<!-- Strike through -->
|
|
155
|
+
<button
|
|
156
|
+
v-if="toolbar.strikethrough"
|
|
157
|
+
@click="commands.strike"
|
|
158
|
+
:class="[isActive.strike() ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
159
|
+
type="button"
|
|
160
|
+
:aria-label="Translator.trans('editor.strikethrough')"
|
|
161
|
+
v-tooltip="Translator.trans('editor.strikethrough')"
|
|
162
|
+
:disabled="readonly">
|
|
163
|
+
<i
|
|
164
|
+
:class="prefixClass('fa fa-strikethrough')"
|
|
165
|
+
aria-hidden="true" />
|
|
166
|
+
</button>
|
|
167
|
+
<div
|
|
168
|
+
v-if="toolbar.insertAndDelete"
|
|
169
|
+
:class="prefixClass('display--inline-block position--relative')">
|
|
170
|
+
<button
|
|
171
|
+
:class="[isActive.insert() || isActive.delete() ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
172
|
+
type="button"
|
|
173
|
+
@click.stop="toggleSubMenu('diffMenu', !diffMenu.isOpen)"
|
|
174
|
+
@keydown.tab.shift.exact="toggleSubMenu('diffMenu', false)"
|
|
175
|
+
:disabled="readonly">
|
|
176
|
+
<dp-icon
|
|
177
|
+
class="u-valign--text-top"
|
|
178
|
+
icon="highlighter" />
|
|
179
|
+
<i :class="prefixClass('fa fa-caret-down')" />
|
|
180
|
+
</button>
|
|
181
|
+
<div
|
|
182
|
+
v-if="diffMenu.isOpen"
|
|
183
|
+
:class="prefixClass('button_submenu')">
|
|
184
|
+
<button
|
|
185
|
+
v-for="(button, idx) in diffMenu.buttons"
|
|
186
|
+
:key="`diffMenu_${idx}`"
|
|
187
|
+
:class="{ 'is-active': isActive[button.name]() }"
|
|
188
|
+
type="button"
|
|
189
|
+
:disabled="readonly"
|
|
190
|
+
@keydown.tab.exact="() => { idx === diffMenu.buttons.length -1 ? toggleSubMenu('diffMenu', false) : null }"
|
|
191
|
+
@keydown.tab.shift.exact="() => { idx === 0 ? toggleSubMenu('diffMenu', false) : null }"
|
|
192
|
+
@click.stop="executeSubMenuButtonAction(button, 'diffMenu', true)">
|
|
193
|
+
{{ Translator.trans(button.label) }}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
❘
|
|
197
|
+
</div>
|
|
198
|
+
<div
|
|
199
|
+
v-else-if="toolbar.mark /* display the Button without fold out, if ony 'mark' is enabled */"
|
|
200
|
+
:class="prefixClass('display--inline-block position--relative')">
|
|
201
|
+
<button
|
|
202
|
+
v-for="(button, idx) in diffMenu.buttons"
|
|
203
|
+
:key="`diffMenu_${idx}`"
|
|
204
|
+
:class="[isActive[button.name]() ? prefixClass('is-active') : '' , prefixClass('menubar__button')]"
|
|
205
|
+
type="button"
|
|
206
|
+
:disabled="readonly"
|
|
207
|
+
:aria-label="Translator.trans(button.label)"
|
|
208
|
+
v-tooltip="Translator.trans(button.label)"
|
|
209
|
+
@keydown.tab.exact="() => { idx === diffMenu.buttons.length -1 ? toggleSubMenu('diffMenu', false) : null }"
|
|
210
|
+
@keydown.tab.shift.exact="() => { idx === 0 ? toggleSubMenu('diffMenu', false) : null }"
|
|
211
|
+
@click.stop="executeSubMenuButtonAction(button, 'diffMenu', true)">
|
|
212
|
+
<dp-icon
|
|
213
|
+
class="u-valign--text-top"
|
|
214
|
+
icon="highlighter" />
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
<!-- lists -->
|
|
218
|
+
<template v-if="toolbar.listButtons">
|
|
219
|
+
<!-- Unordered List -->
|
|
220
|
+
<button
|
|
221
|
+
@click="commands.bullet_list"
|
|
222
|
+
:class="[isActive.bullet_list() ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
223
|
+
type="button"
|
|
224
|
+
:aria-label="Translator.trans('editor.unordered.list')"
|
|
225
|
+
v-tooltip="Translator.trans('editor.unordered.list')"
|
|
226
|
+
:disabled="readonly">
|
|
227
|
+
<i :class="prefixClass('fa fa-list-ul')" />
|
|
228
|
+
</button>
|
|
229
|
+
<!-- Ordered List -->
|
|
230
|
+
<button
|
|
231
|
+
@click="commands.ordered_list"
|
|
232
|
+
:class="[isActive.ordered_list() ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
233
|
+
type="button"
|
|
234
|
+
:aria-label="Translator.trans('editor.ordered.list')"
|
|
235
|
+
v-tooltip="Translator.trans('editor.ordered.list')"
|
|
236
|
+
:disabled="readonly">
|
|
237
|
+
<i :class="prefixClass('fa fa-list-ol')" />
|
|
238
|
+
</button>
|
|
239
|
+
❘
|
|
240
|
+
</template>
|
|
241
|
+
<!--Heading Buttons - for each heading level in props a button will be rendered. We want to keep it
|
|
242
|
+
flexible because the user should not always be able to define e.g. H1. It depends where the text should
|
|
243
|
+
appear.-->
|
|
244
|
+
<template v-if="toolbar.headings.length > 0">
|
|
245
|
+
<button
|
|
246
|
+
v-for="heading in toolbar.headings"
|
|
247
|
+
:key="'heading_' + heading"
|
|
248
|
+
type="button"
|
|
249
|
+
:class="[isActive.heading({ level: heading }) ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
250
|
+
@click="commands.heading({ level: heading })"
|
|
251
|
+
v-tooltip="Translator.trans('editor.heading.level', {level: heading})"
|
|
252
|
+
:disabled="readonly">
|
|
253
|
+
{{ `H${heading}` }}
|
|
254
|
+
</button>
|
|
255
|
+
❘
|
|
256
|
+
</template>
|
|
257
|
+
<!-- Obscure text -->
|
|
258
|
+
<button
|
|
259
|
+
v-if="obscureEnabled"
|
|
260
|
+
@click="commands.obscure"
|
|
261
|
+
:class="[isActive.obscure() ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
262
|
+
type="button"
|
|
263
|
+
v-tooltip="Translator.trans('obscure.title')"
|
|
264
|
+
:disabled="readonly">
|
|
265
|
+
<i
|
|
266
|
+
:class="prefixClass('fa fa-pencil-square')"
|
|
267
|
+
aria-hidden="true" />
|
|
268
|
+
</button>
|
|
269
|
+
<!--Add links-->
|
|
270
|
+
<button
|
|
271
|
+
v-if="toolbar.linkButton"
|
|
272
|
+
@click.stop="showLinkPrompt(commands.link, getMarkAttrs('link'))"
|
|
273
|
+
:class="prefixClass('menubar__button')"
|
|
274
|
+
type="button"
|
|
275
|
+
v-tooltip="Translator.trans('editor.link.edit.insert')">
|
|
276
|
+
<i
|
|
277
|
+
:class="prefixClass('fa fa-link')" />
|
|
278
|
+
</button>
|
|
279
|
+
<!-- Add Boilerplate -->
|
|
280
|
+
<button
|
|
281
|
+
v-if="boilerPlateEnabled"
|
|
282
|
+
@click.stop="openBoilerPlateModal"
|
|
283
|
+
:class="prefixClass('menubar__button')"
|
|
284
|
+
type="button"
|
|
285
|
+
v-tooltip="Translator.trans('boilerplate.insert')"
|
|
286
|
+
:disabled="readonly">
|
|
287
|
+
<i
|
|
288
|
+
:class="prefixClass('fa fa-puzzle-piece')" />
|
|
289
|
+
</button>
|
|
290
|
+
<!-- Insert related recommendations -->
|
|
291
|
+
<button
|
|
292
|
+
v-if="toolbar.recommendationButton"
|
|
293
|
+
@click.stop="openRecommendationModal"
|
|
294
|
+
:class="prefixClass('menubar__button')"
|
|
295
|
+
v-tooltip="Translator.trans('segment.recommendation.insert.similar')"
|
|
296
|
+
type="button">
|
|
297
|
+
<i :class="prefixClass('fa fa-lightbulb-o')" />
|
|
298
|
+
</button>
|
|
299
|
+
<!-- Insert images-->
|
|
300
|
+
<button
|
|
301
|
+
v-if="toolbar.imageButton"
|
|
302
|
+
@click.stop="openUploadModal(null)"
|
|
303
|
+
:class="prefixClass('menubar__button')"
|
|
304
|
+
type="button"
|
|
305
|
+
v-tooltip="Translator.trans('image.insert')"
|
|
306
|
+
:disabled="readonly">
|
|
307
|
+
<i
|
|
308
|
+
:class="prefixClass('fa fa-picture-o')" />
|
|
309
|
+
</button>
|
|
310
|
+
<!-- Insert and edit tables -->
|
|
311
|
+
<div
|
|
312
|
+
v-if="toolbar.table"
|
|
313
|
+
:class="prefixClass('display--inline-block position--relative')">
|
|
314
|
+
<button
|
|
315
|
+
:class="[tableMenu.isOpen ? prefixClass('is-active') : '', prefixClass('menubar__button')]"
|
|
316
|
+
type="button"
|
|
317
|
+
@click.stop="toggleSubMenu('tableMenu', !tableMenu.isOpen)"
|
|
318
|
+
@keydown.tab.shift.exact="toggleSubMenu('tableMenu', false)"
|
|
319
|
+
:disabled="readonly">
|
|
320
|
+
<i :class="prefixClass('fa fa-table')" />
|
|
321
|
+
<i :class="prefixClass('fa fa-caret-down')" />
|
|
322
|
+
</button>
|
|
323
|
+
<div
|
|
324
|
+
v-if="tableMenu.isOpen"
|
|
325
|
+
:class="prefixClass('button_submenu')">
|
|
326
|
+
<button
|
|
327
|
+
v-for="(button, idx) in tableMenu.buttons"
|
|
328
|
+
:key="`tableMenu_${idx}`"
|
|
329
|
+
type="button"
|
|
330
|
+
:disabled="readonly"
|
|
331
|
+
@keydown.tab.exact="() => { idx === tableMenu.buttons.length -1 ? toggleSubMenu('tableMenu', false) : null }"
|
|
332
|
+
@keydown.tab.shift.exact="() => { idx === 0 ? toggleSubMenu('tableMenu', false) : null }"
|
|
333
|
+
@click.stop="executeSubMenuButtonAction(button, 'tableMenu')">
|
|
334
|
+
{{ Translator.trans(button.label) }}
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<!-- Fullscreen -->
|
|
339
|
+
<button
|
|
340
|
+
v-if="toolbar.fullscreenButton"
|
|
341
|
+
@click="fullscreen"
|
|
342
|
+
:class="[isFullscreen ? prefixClass('is-active') : '', prefixClass('menubar__button float--right')]"
|
|
343
|
+
type="button"
|
|
344
|
+
:aria-label="Translator.trans('editor.fullscreen')"
|
|
345
|
+
v-tooltip="Translator.trans('editor.fullscreen')">
|
|
346
|
+
<i
|
|
347
|
+
:class="prefixClass('fa fa-arrows-alt')"
|
|
348
|
+
aria-hidden="true" />
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
</editor-menu-bar>
|
|
352
|
+
<editor-content
|
|
353
|
+
v-if="editor"
|
|
354
|
+
:data-cy="`editor${editorId}`"
|
|
355
|
+
:editor="editor"
|
|
356
|
+
:class="prefixClass('editor__content overflow-hidden')" />
|
|
357
|
+
<!-- this hidden input is needed if we use this component without the inline-editing-wrapper TiptapEditText.vue,
|
|
358
|
+
so we can save the text entered in the textarea via a form element -->
|
|
359
|
+
<input
|
|
360
|
+
v-if="hiddenInput !== ''"
|
|
361
|
+
:data-dp-validate-if="dataDpValidateIf || false"
|
|
362
|
+
type="hidden"
|
|
363
|
+
:id="hiddenInput"
|
|
364
|
+
:name="hiddenInput"
|
|
365
|
+
:class="[required ? prefixClass('is-required') : '', prefixClass('tiptap__input--hidden')]"
|
|
366
|
+
:data-dp-validate-maxlength="maxlength"
|
|
367
|
+
:value="hiddenInputValue">
|
|
368
|
+
<i
|
|
369
|
+
v-if="!isFullscreen"
|
|
370
|
+
aria-hidden="true"
|
|
371
|
+
:class="prefixClass('fa fa-angle-down resizeVertical')"
|
|
372
|
+
@mousedown="resizeVertically"
|
|
373
|
+
draggable="true" />
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</template>
|
|
379
|
+
|
|
380
|
+
<script>
|
|
381
|
+
import {
|
|
382
|
+
Bold,
|
|
383
|
+
BulletList,
|
|
384
|
+
HardBreak,
|
|
385
|
+
Heading,
|
|
386
|
+
History,
|
|
387
|
+
Italic,
|
|
388
|
+
Link,
|
|
389
|
+
ListItem,
|
|
390
|
+
OrderedList,
|
|
391
|
+
Strike,
|
|
392
|
+
Table,
|
|
393
|
+
TableCell,
|
|
394
|
+
TableHeader,
|
|
395
|
+
TableRow,
|
|
396
|
+
Underline
|
|
397
|
+
} from 'tiptap-extensions'
|
|
398
|
+
|
|
399
|
+
import {
|
|
400
|
+
Editor, // Wrapper for prosemirror state
|
|
401
|
+
EditorContent, // Renderless content element
|
|
402
|
+
EditorMenuBar // Renderless menubar
|
|
403
|
+
} from 'tiptap'
|
|
404
|
+
|
|
405
|
+
import { CleanHtml } from 'demosplan-ui/directives'
|
|
406
|
+
import { createSuggestion } from './libs/editorBuildSuggestion'
|
|
407
|
+
import { DpIcon } from 'demosplan-ui/components'
|
|
408
|
+
import EditorCustomDelete from './libs/editorCustomDelete'
|
|
409
|
+
import EditorCustomImage from './libs/editorCustomImage'
|
|
410
|
+
import EditorCustomInsert from './libs/editorCustomInsert'
|
|
411
|
+
import EditorCustomLink from './libs/editorCustomLink'
|
|
412
|
+
import EditorCustomMark from './libs/editorCustomMark'
|
|
413
|
+
import EditorInsertAtCursorPos from './libs/editorInsertAtCursorPos'
|
|
414
|
+
import EditorObscure from './libs/editorObscure'
|
|
415
|
+
import { handleWordPaste } from './libs/handleWordPaste'
|
|
416
|
+
import { maxlengthHint } from 'demosplan-ui/utils/lengthHint'
|
|
417
|
+
import { prefixClassMixin } from 'demosplan-ui/mixins'
|
|
418
|
+
|
|
419
|
+
export default {
|
|
420
|
+
name: 'DpEditor',
|
|
421
|
+
|
|
422
|
+
components: {
|
|
423
|
+
DpIcon,
|
|
424
|
+
EditorMenuBar,
|
|
425
|
+
EditorContent,
|
|
426
|
+
DpBoilerPlateModal: () => import('./DpBoilerPlateModal'),
|
|
427
|
+
DpLinkModal: () => import('./DpLinkModal'),
|
|
428
|
+
DpRecommendationModal: () => import('./DpRecommendationModal'),
|
|
429
|
+
DpUploadModal: () => import('./DpUploadModal')
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
directives: {
|
|
433
|
+
cleanhtml: CleanHtml
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
mixins: [prefixClassMixin],
|
|
437
|
+
|
|
438
|
+
props: {
|
|
439
|
+
/**
|
|
440
|
+
* Defines which boilerplate types we want to see in modal. Possible are: consideration, email, news.notes
|
|
441
|
+
* if this property is set, the boilerPlate button appears in the tiptap editor
|
|
442
|
+
* @deprecated use toolbarItems instead
|
|
443
|
+
*/
|
|
444
|
+
boilerPlate: {
|
|
445
|
+
type: [String, Array],
|
|
446
|
+
default: '',
|
|
447
|
+
required: false
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Needed to get the correct textarea for adding boilerplates via DpBoilerPlateModal.vue
|
|
452
|
+
*/
|
|
453
|
+
editorId: {
|
|
454
|
+
type: String,
|
|
455
|
+
required: false,
|
|
456
|
+
default: ''
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Array with numbers 1-6 defining which heading-buttons we want to show
|
|
461
|
+
*
|
|
462
|
+
* @deprecated use toolbarItems instead
|
|
463
|
+
*/
|
|
464
|
+
headings: {
|
|
465
|
+
required: false,
|
|
466
|
+
type: Array,
|
|
467
|
+
default: () => []
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* To send data with submit form action we sometimes need to have a hidden input with tiptap's content. If the
|
|
472
|
+
* hidden input should be added, the prop should be a string with input name (e.g. r_name)
|
|
473
|
+
*/
|
|
474
|
+
hiddenInput: {
|
|
475
|
+
type: String,
|
|
476
|
+
required: false,
|
|
477
|
+
default: ''
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* If true, the button to add images will be shown and the initial text will be scanned for img placeholders which will be then replaced by actual images.
|
|
482
|
+
* Inserted pictures will also be converted to placeholders on save.
|
|
483
|
+
*
|
|
484
|
+
* @deprecated use toolbarItems instead
|
|
485
|
+
*/
|
|
486
|
+
imageButton: {
|
|
487
|
+
type: Boolean,
|
|
488
|
+
required: false,
|
|
489
|
+
default: false
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Enables menu buttons to mark text as deleted and inserted.
|
|
494
|
+
* The buttons will wrap the current text selection with a `del` or `ins` element,
|
|
495
|
+
* enabling users to indicate content changes in relation to a prior content version.
|
|
496
|
+
* This feature is currently only used for planning document paragraphs.
|
|
497
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del
|
|
498
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins
|
|
499
|
+
*
|
|
500
|
+
* @deprecated use toolbarItems instead
|
|
501
|
+
*/
|
|
502
|
+
insertAndDelete: {
|
|
503
|
+
type: Boolean,
|
|
504
|
+
required: false,
|
|
505
|
+
default: false
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* @deprecated use toolbarItems instead
|
|
510
|
+
*/
|
|
511
|
+
fullscreenButton: {
|
|
512
|
+
type: Boolean,
|
|
513
|
+
required: false,
|
|
514
|
+
default: true
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
required: {
|
|
518
|
+
type: Boolean,
|
|
519
|
+
required: false,
|
|
520
|
+
default: false
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Define if a button to add links should be visible in menu
|
|
525
|
+
*
|
|
526
|
+
* @deprecated use toolbarItems instead
|
|
527
|
+
*/
|
|
528
|
+
linkButton: {
|
|
529
|
+
required: false,
|
|
530
|
+
type: Boolean,
|
|
531
|
+
default: false
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Define if a button to add ordered/unordered list should be visible in menu
|
|
536
|
+
*
|
|
537
|
+
* @deprecated use toolbarItems instead
|
|
538
|
+
*/
|
|
539
|
+
listButtons: {
|
|
540
|
+
required: false,
|
|
541
|
+
type: Boolean,
|
|
542
|
+
default: true
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Enables a menu button to highlight/mark text.
|
|
547
|
+
* This will wrap the current text selection with a `mark` element,
|
|
548
|
+
* enabling users to enrich content with a semantic element to highlight text.
|
|
549
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark
|
|
550
|
+
*
|
|
551
|
+
* @deprecated use toolbarItems instead
|
|
552
|
+
*/
|
|
553
|
+
mark: {
|
|
554
|
+
required: false,
|
|
555
|
+
type: Boolean,
|
|
556
|
+
default: false
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Defaults will be set in this.menu:
|
|
561
|
+
* {
|
|
562
|
+
* boilerPlate: '', # [] || 'string'
|
|
563
|
+
* headings: [], # Array of numbers 1-6
|
|
564
|
+
* imageButton: false,
|
|
565
|
+
* insertAndDelete: false,
|
|
566
|
+
* fullscreenButton: true,
|
|
567
|
+
* linkButton: false,
|
|
568
|
+
* listButtons: true,
|
|
569
|
+
* mark: false,
|
|
570
|
+
* recommendationButton: false,
|
|
571
|
+
* strikethrough: false,
|
|
572
|
+
* table: false,
|
|
573
|
+
* textDecoration: true
|
|
574
|
+
* }
|
|
575
|
+
*
|
|
576
|
+
* and can be overwritten
|
|
577
|
+
*/
|
|
578
|
+
toolbarItems: {
|
|
579
|
+
required: false,
|
|
580
|
+
type: Object,
|
|
581
|
+
default: () => ({})
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
maxlength: {
|
|
585
|
+
type: [Number, null],
|
|
586
|
+
default: null
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Set to true if you want to use the 'obscure text' button
|
|
591
|
+
*/
|
|
592
|
+
obscure: {
|
|
593
|
+
type: Boolean,
|
|
594
|
+
required: false,
|
|
595
|
+
default: false
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* ProcedureId is required if we want to enable boilerplates
|
|
600
|
+
*/
|
|
601
|
+
procedureId: {
|
|
602
|
+
type: String,
|
|
603
|
+
required: false,
|
|
604
|
+
default: ''
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
readonly: {
|
|
608
|
+
required: false,
|
|
609
|
+
default: false,
|
|
610
|
+
type: Boolean
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
recommendationButton: {
|
|
614
|
+
required: false,
|
|
615
|
+
type: Boolean,
|
|
616
|
+
default: false
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
segmentId: {
|
|
620
|
+
type: String,
|
|
621
|
+
required: false,
|
|
622
|
+
default: ''
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Enables a menu button to strike out text.
|
|
627
|
+
* This will wrap the current text selection with a `s` element, enabling users
|
|
628
|
+
* to enrich content with a semantic element to mark text as no longer relevant.
|
|
629
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s
|
|
630
|
+
* @deprecated use toolbarItems instead
|
|
631
|
+
*/
|
|
632
|
+
strikethrough: {
|
|
633
|
+
required: false,
|
|
634
|
+
type: Boolean,
|
|
635
|
+
default: false
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Pass in an Array of suggestions if you would like to use the suggestion plugin in tiptap.
|
|
640
|
+
*/
|
|
641
|
+
suggestions: {
|
|
642
|
+
type: Array,
|
|
643
|
+
validator: (value) => {
|
|
644
|
+
const suggestionGroupSchema = {
|
|
645
|
+
matcher: {
|
|
646
|
+
char: '@|$|#', // A single char that should trigger a suggestion
|
|
647
|
+
allowSpaces: true || false,
|
|
648
|
+
startOfLine: true || false
|
|
649
|
+
},
|
|
650
|
+
suggestions: [{ id: 'a unique id', name: 'a string that should be displayed when inserting the suggestion' }]
|
|
651
|
+
}
|
|
652
|
+
return Array.isArray(value) && value.filter(suggestionGroup => {
|
|
653
|
+
let isValid = suggestionGroup.matcher && suggestionGroup.suggestions
|
|
654
|
+
isValid = isValid && typeof suggestionGroup.matcher.char === typeof suggestionGroupSchema.matcher.char
|
|
655
|
+
isValid = isValid && typeof suggestionGroup.matcher.allowSpaces === typeof suggestionGroupSchema.matcher.allowSpaces
|
|
656
|
+
isValid = isValid && typeof suggestionGroup.matcher.startOfLine === typeof suggestionGroupSchema.matcher.startOfLine
|
|
657
|
+
isValid = isValid && suggestionGroup.suggestions.filter(suggestion => {
|
|
658
|
+
return typeof suggestion.id === typeof suggestionGroupSchema.suggestions[0].id && typeof suggestion.name === typeof suggestionGroupSchema.suggestions[0].name
|
|
659
|
+
}).length === suggestionGroup.suggestions.length
|
|
660
|
+
return isValid
|
|
661
|
+
}).length === value.length
|
|
662
|
+
},
|
|
663
|
+
required: false,
|
|
664
|
+
default: () => ([])
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Set to true if you want table-insert button
|
|
669
|
+
*
|
|
670
|
+
* @deprecated use toolbarItems instead
|
|
671
|
+
*/
|
|
672
|
+
table: {
|
|
673
|
+
required: false,
|
|
674
|
+
type: Boolean,
|
|
675
|
+
default: false
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* @deprecated use toolbarItems instead
|
|
680
|
+
*/
|
|
681
|
+
textDecoration: {
|
|
682
|
+
type: Boolean,
|
|
683
|
+
required: false,
|
|
684
|
+
default: true
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
value: {
|
|
688
|
+
type: String,
|
|
689
|
+
required: true
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
dataDpValidateIf: {
|
|
693
|
+
type: String,
|
|
694
|
+
default: '',
|
|
695
|
+
required: false
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
data () {
|
|
700
|
+
return {
|
|
701
|
+
currentValue: '',
|
|
702
|
+
diffMenu: {
|
|
703
|
+
isOpen: false,
|
|
704
|
+
buttons: []
|
|
705
|
+
},
|
|
706
|
+
editingImage: null,
|
|
707
|
+
editor: null,
|
|
708
|
+
editorHeight: '',
|
|
709
|
+
isDiffMenuOpen: false,
|
|
710
|
+
isFullscreen: false,
|
|
711
|
+
isTableMenuOpen: false,
|
|
712
|
+
linkUrl: '',
|
|
713
|
+
// We have to check if we have a hidden input and a form, then we have to update the field manually. For Api-requests its not neccessary
|
|
714
|
+
manuallyResetForm: true,
|
|
715
|
+
tableMenu: {
|
|
716
|
+
isOpen: false,
|
|
717
|
+
buttons: [
|
|
718
|
+
{
|
|
719
|
+
label: 'editor.table.create',
|
|
720
|
+
command: (commands) => commands.createTable({ rowsCount: 3, colsCount: 3, withHeaderRow: false }),
|
|
721
|
+
name: 'createTable'
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
label: 'editor.table.delete',
|
|
725
|
+
command: (commands) => commands.deleteTable(),
|
|
726
|
+
name: 'deleteTable'
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
label: 'editor.table.addColumnBefore',
|
|
730
|
+
command: (commands) => commands.addColumnBefore(),
|
|
731
|
+
name: 'addColumnBefore'
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
label: 'editor.table.addColumnAfter',
|
|
735
|
+
command: (commands) => commands.addColumnAfter(),
|
|
736
|
+
name: 'addColumnAfter'
|
|
737
|
+
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
label: 'editor.table.deleteColumn',
|
|
741
|
+
command: (commands) => commands.deleteColumn(),
|
|
742
|
+
name: 'deleteColumn'
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
label: 'editor.table.addRowBefore',
|
|
746
|
+
command: (commands) => commands.addRowBefore(),
|
|
747
|
+
name: 'addRowBefore'
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
label: 'editor.table.addRowAfter',
|
|
751
|
+
command: (commands) => commands.addRowAfter(),
|
|
752
|
+
name: 'addRowAfter'
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
label: 'editor.table.deleteRow',
|
|
756
|
+
command: (commands) => commands.deleteRow(),
|
|
757
|
+
name: 'deleteRow'
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
label: 'editor.table.toggleCellMerge',
|
|
761
|
+
command: (commands) => commands.toggleCellMerge(),
|
|
762
|
+
name: 'toggleCellMerge'
|
|
763
|
+
}
|
|
764
|
+
]
|
|
765
|
+
},
|
|
766
|
+
toolbar: Object.assign({
|
|
767
|
+
boilerPlate: this.boilerPlate,
|
|
768
|
+
headings: this.headings,
|
|
769
|
+
imageButton: this.imageButton,
|
|
770
|
+
insertAndDelete: this.insertAndDelete,
|
|
771
|
+
fullscreenButton: this.fullscreenButton,
|
|
772
|
+
linkButton: this.linkButton,
|
|
773
|
+
listButtons: this.listButtons,
|
|
774
|
+
mark: this.mark,
|
|
775
|
+
strikethrough: this.strikethrough,
|
|
776
|
+
table: this.table,
|
|
777
|
+
textDecoration: this.textDecoration
|
|
778
|
+
}, this.toolbarItems)
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
computed: {
|
|
783
|
+
boilerPlateEnabled () {
|
|
784
|
+
return hasPermission('area_admin_boilerplates') && Boolean(this.toolbar.boilerPlate)
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
counterText () {
|
|
788
|
+
return maxlengthHint(this.hiddenInputValue.length, this.maxlength)
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
hiddenInputValue () {
|
|
792
|
+
// The blank tiptap editor still contains an empty p element, which shall not be passed into hidden input.
|
|
793
|
+
return (this.currentValue.replace('<p></p>', '') === '') ? '' : this.currentValue
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
obscureEnabled () {
|
|
797
|
+
return hasPermission('feature_obscure_text') && this.toolbar.obscure
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
|
|
801
|
+
watch: {
|
|
802
|
+
value (newValue) {
|
|
803
|
+
if (!this.editor.focused) {
|
|
804
|
+
this.currentValue = newValue
|
|
805
|
+
this.editor.setContent(newValue, false)
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* The readonly watcher provides the dynamic enabling/disabling of the editor.
|
|
811
|
+
* Also mentioned in the GitHub issue: https://github.com/ueberdosis/tiptap/issues/111
|
|
812
|
+
*/
|
|
813
|
+
readonly () {
|
|
814
|
+
this.editor.setOptions({ editable: !this.readonly })
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
methods: {
|
|
819
|
+
addAltTextToImage (text) {
|
|
820
|
+
this.$root.$emit('update-image:' + this.editingImage, { alt: text })
|
|
821
|
+
this.resetEditingImage()
|
|
822
|
+
this.setValue()
|
|
823
|
+
},
|
|
824
|
+
|
|
825
|
+
appendText (text) {
|
|
826
|
+
let newText
|
|
827
|
+
|
|
828
|
+
// Check if any of the two texts is wrapped in a 'p' tag to avoid inserting too many newlines
|
|
829
|
+
const isAnyNodeBlock = this.startsWithTag(this.currentValue, 'p') || this.startsWithTag(text, 'p')
|
|
830
|
+
|
|
831
|
+
// If editor is empty, insert only text; if editor contains text, insert empty paragraph + text
|
|
832
|
+
if (this.currentValue === 'k.A.' || this.currentValue === '') {
|
|
833
|
+
newText = text
|
|
834
|
+
} else if (this.currentValue !== 'k.A' && this.currentValue !== '' && isAnyNodeBlock) {
|
|
835
|
+
newText = this.currentValue + text
|
|
836
|
+
} else if (this.currentValue !== 'k.A' && this.currentValue !== '') {
|
|
837
|
+
newText = this.currentValue + '<br>' + text
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
this.editor.setContent(newText)
|
|
841
|
+
this.currentValue = newText
|
|
842
|
+
this.$emit('input', this.currentValue)
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
cut () {
|
|
846
|
+
document.execCommand('cut')
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
fullscreen (e) {
|
|
850
|
+
const editor = e.target.parentElement.parentElement.parentElement.querySelector('.tiptap .editor__content')
|
|
851
|
+
if (this.isFullscreen === false && editor.hasAttribute('style')) {
|
|
852
|
+
this.editorHeight = editor.getAttribute('style')
|
|
853
|
+
editor.removeAttribute('style')
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
this.isFullscreen = !this.isFullscreen
|
|
857
|
+
|
|
858
|
+
if (this.isFullscreen === false && this.editorHeight !== '') {
|
|
859
|
+
editor.setAttribute('style', this.editorHeight)
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
handleInsertText (text) {
|
|
864
|
+
text = text.replace(/\n/g, '<br>')
|
|
865
|
+
|
|
866
|
+
// If user hasn't clicked into tiptap editor yet
|
|
867
|
+
if (this.editor.view.input.lastClick.x === 0 && this.editor.view.input.lastClick.y === 0) {
|
|
868
|
+
this.appendText(text)
|
|
869
|
+
} else { // If user has clicked into tiptap editor at some point, but editor may currently not have focus
|
|
870
|
+
this.insertTextAtCursorPos(text)
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
insertTextAtCursorPos (text) {
|
|
875
|
+
// Remove p tags so text is inserted without adding new paragraph
|
|
876
|
+
if (this.startsWithTag(text, 'p')) {
|
|
877
|
+
text = text.slice(3, -4)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
this.editor.commands.insertHTML(text)
|
|
881
|
+
this.currentValue = this.editor.getHTML()
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
startsWithTag (htmlString, tag) {
|
|
885
|
+
const el = document.createElement('div')
|
|
886
|
+
el.innerHTML = htmlString
|
|
887
|
+
const firstChild = el.firstChild && el.firstChild.nodeName
|
|
888
|
+
return firstChild === tag.toUpperCase()
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
insertImage (url, alt) {
|
|
892
|
+
this.editor.commands.insertImage({ src: url, alt })
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
insertUrl (linkUrl, newTab, linkText) {
|
|
896
|
+
if (linkUrl === null) {
|
|
897
|
+
this.editor.commands.link({ href: null, ...(newTab && { target: '_blank' }) })
|
|
898
|
+
return
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (linkUrl !== '' && linkText !== '') {
|
|
902
|
+
const newNode = this.editor.schema.text(linkText, [this.editor.schema.marks.link.create({ href: linkUrl, ...(newTab && { target: '_blank' }) })])
|
|
903
|
+
this.editor.view.dispatch(this.editor.state.tr.replaceSelectionWith(newNode, false))
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
getLinkMark (node) {
|
|
908
|
+
const linkMark = node.marks && node.marks.find(mark => mark.type.name === 'link')
|
|
909
|
+
|
|
910
|
+
return linkMark
|
|
911
|
+
},
|
|
912
|
+
|
|
913
|
+
openBoilerPlateModal () {
|
|
914
|
+
this.$refs.boilerPlateModal.toggleModal()
|
|
915
|
+
},
|
|
916
|
+
|
|
917
|
+
openRecommendationModal () {
|
|
918
|
+
this.$refs.recommendationModal.toggleModal('open')
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
openUploadModal (data) {
|
|
922
|
+
this.$refs.uploadModal.toggleModal(data)
|
|
923
|
+
},
|
|
924
|
+
|
|
925
|
+
prepareInitText () {
|
|
926
|
+
this.currentValue = this.replaceLinebreaks(this.currentValue)
|
|
927
|
+
this.currentValue = this.replacePlaceholdersWithImages(this.currentValue)
|
|
928
|
+
},
|
|
929
|
+
|
|
930
|
+
replaceLinebreaks (text) {
|
|
931
|
+
let returnText = text
|
|
932
|
+
returnText = returnText.replace(/<\/p>[\n|\r|\s|\\n|\\r]*?<p>/g, '</p><p>')
|
|
933
|
+
returnText = returnText.replace(/<ul>[\n|\r|\s|\\n|\\r]*?<li>/g, '<ul><li>')
|
|
934
|
+
return returnText.replace(/<\/li>[\n|\r|\s|\\n|\\r]*?<li>/g, '</li><li>')
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
replacePlaceholdersWithImages (text = this.currentValue) {
|
|
938
|
+
const placeholder = Translator.trans('image.placeholder')
|
|
939
|
+
const placeholderText = placeholder.startsWith('[') && placeholder.endsWith(']') ? placeholder.slice(1, -1) : placeholder
|
|
940
|
+
const regex = new RegExp(`(\\[${placeholderText}\\].*?-->)`, 'gm')
|
|
941
|
+
try {
|
|
942
|
+
return text.replace(regex, (match, p1) => {
|
|
943
|
+
const altText = p1.match(/{([^}]*?)}/)[1] === Translator.trans('image.alt.placeholder') ? '' : p1.match(/{([^}]*?)}/)[1]
|
|
944
|
+
const placeholder = p1.match(/<!-- (.*?) -->/)[1]
|
|
945
|
+
const imageHash = placeholder.substr(7, 36)
|
|
946
|
+
const imageWidth = placeholder.match(/width=(\d*?)&/)[1]
|
|
947
|
+
const imageHeight = placeholder.match(/height=(\d*?)$/)[1]
|
|
948
|
+
return `<img src="${Routing.generate('core_file', { hash: imageHash })}" width="${imageWidth}" height="${imageHeight}" alt="${altText}">`
|
|
949
|
+
})
|
|
950
|
+
} catch (e) {
|
|
951
|
+
return text
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
resetEditingImage () {
|
|
956
|
+
this.editingImage = null
|
|
957
|
+
},
|
|
958
|
+
|
|
959
|
+
resizeVertically (e) {
|
|
960
|
+
const editor = e.target.parentElement.querySelector('.tiptap .editor__content')
|
|
961
|
+
|
|
962
|
+
e.preventDefault()
|
|
963
|
+
const originalHeight = parseFloat(getComputedStyle(editor, null).getPropertyValue('height').replace('px', ''))
|
|
964
|
+
const originalMouseY = e.pageY
|
|
965
|
+
window.addEventListener('mousemove', resize)
|
|
966
|
+
window.addEventListener('mouseup', stopResize)
|
|
967
|
+
|
|
968
|
+
function resize (e) {
|
|
969
|
+
const height = originalHeight + (e.pageY - originalMouseY)
|
|
970
|
+
editor.style.height = height + 'px'
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function stopResize () {
|
|
974
|
+
window.removeEventListener('mousemove', resize)
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
resetEditor () {
|
|
979
|
+
this.editor.setContent('')
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
setValue () {
|
|
983
|
+
this.currentValue = this.editor.getHTML()
|
|
984
|
+
const regex = new RegExp('<span class="' + this.prefixClass('u-obscure') + '">(.*?)<\\/span>', 'g')
|
|
985
|
+
this.currentValue = this.currentValue.replace(regex, '<dp-obscure>$1</dp-obscure>')
|
|
986
|
+
const isEmpty = (this.currentValue.split('<p>').join('').split('</p>').join('').trim()) === ''
|
|
987
|
+
this.$emit('input', isEmpty ? '' : this.currentValue)
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
setSelectionByEditor (nodeBefore, nodeAfter, attrs) {
|
|
991
|
+
const tr = this.editor.view.state.tr
|
|
992
|
+
|
|
993
|
+
if (nodeBefore) {
|
|
994
|
+
const linkMark = this.getLinkMark(nodeBefore)
|
|
995
|
+
if (linkMark && linkMark.attrs.href === attrs.href) {
|
|
996
|
+
this.editor.setSelection((tr.selection.anchor - tr.selection.$anchor.nodeBefore.nodeSize), tr.selection.anchor)
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (nodeAfter) {
|
|
1001
|
+
const linkMark = this.getLinkMark(nodeAfter)
|
|
1002
|
+
if (linkMark && linkMark.attrs.href === attrs.href) {
|
|
1003
|
+
this.editor.setSelection(tr.selection.anchor, (tr.selection.anchor + tr.selection.$anchor.nodeAfter.nodeSize))
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
|
|
1008
|
+
showLinkPrompt (command, attrs) {
|
|
1009
|
+
this.linkUrl = attrs.href ? attrs.href : ''
|
|
1010
|
+
const selection = this.editor.view.state.tr.selection
|
|
1011
|
+
|
|
1012
|
+
if (attrs.href) {
|
|
1013
|
+
// If only a part of existing link text is selected, we want to add the rest of the link to the selection so that the user edits the whole link and not only part of it. To do that we take node before and after selection and check if the href attribute of these nodes is the same as href of the user's selection.
|
|
1014
|
+
const selectToLeft = selection.anchor > selection.head
|
|
1015
|
+
|
|
1016
|
+
const selectionBeginning = selectToLeft ? '$head' : '$anchor'
|
|
1017
|
+
const selectionEnd = selectToLeft ? '$anchor' : '$head'
|
|
1018
|
+
|
|
1019
|
+
const nodeBefore = selection[selectionBeginning].nodeBefore
|
|
1020
|
+
const nodeAfter = selection[selectionEnd].nodeAfter
|
|
1021
|
+
|
|
1022
|
+
this.setSelectionByEditor(nodeBefore, nodeAfter, attrs)
|
|
1023
|
+
}
|
|
1024
|
+
const selectionText = this.editor.state.doc.textBetween(this.editor.view.state.tr.selection.from, this.editor.view.state.tr.selection.to, ' ')
|
|
1025
|
+
this.$refs.linkModal.toggleModal(this.linkUrl, selectionText, attrs.target)
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
executeSubMenuButtonAction (button, menu, activateOne = false) {
|
|
1029
|
+
// If only one button in submenu can be enabled, deactivate the rest
|
|
1030
|
+
if (activateOne) {
|
|
1031
|
+
this[menu].buttons.forEach(subMenuButton => {
|
|
1032
|
+
if (this.editor.isActive[subMenuButton.name]() || subMenuButton === button) {
|
|
1033
|
+
subMenuButton.command(this.editor.commands)
|
|
1034
|
+
}
|
|
1035
|
+
})
|
|
1036
|
+
} else {
|
|
1037
|
+
// If we just want to activate the clicked button without deactivating the other buttons in the submenu
|
|
1038
|
+
button.command(this.editor.commands)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
this[menu].isOpen = false
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
toggleSubMenu (menu, isOpen) {
|
|
1045
|
+
this[menu].isOpen = isOpen
|
|
1046
|
+
|
|
1047
|
+
if (isOpen === true) {
|
|
1048
|
+
const menuToClose = menu === 'tableMenu' ? 'diffMenu' : 'tableMenu'
|
|
1049
|
+
this[menuToClose].isOpen = false
|
|
1050
|
+
const closeMenu = () => {
|
|
1051
|
+
this[menu].isOpen = false
|
|
1052
|
+
document.removeEventListener('click', closeMenu)
|
|
1053
|
+
}
|
|
1054
|
+
document.addEventListener('click', closeMenu)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
|
|
1059
|
+
created () {
|
|
1060
|
+
this.currentValue = this.value
|
|
1061
|
+
this.prepareInitText()
|
|
1062
|
+
},
|
|
1063
|
+
|
|
1064
|
+
mounted () {
|
|
1065
|
+
const extensions = [
|
|
1066
|
+
new History(),
|
|
1067
|
+
new HardBreak(),
|
|
1068
|
+
new Heading({ levels: this.toolbar.headings })
|
|
1069
|
+
]
|
|
1070
|
+
|
|
1071
|
+
if (this.toolbar.boilerPlate) {
|
|
1072
|
+
extensions.push(new EditorInsertAtCursorPos())
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (this.suggestions.length > 0) {
|
|
1076
|
+
this.suggestions.forEach(suggestionGroup => {
|
|
1077
|
+
extensions.push(createSuggestion(suggestionGroup, this))
|
|
1078
|
+
})
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (this.toolbar.headings.length > 0) {
|
|
1082
|
+
extensions.push(new Heading({ levels: this.toolbar.headings }))
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (this.toolbar.imageButton) {
|
|
1086
|
+
extensions.push(new EditorCustomImage())
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (this.toolbar.linkButton) {
|
|
1090
|
+
extensions.push(new Link())
|
|
1091
|
+
extensions.push(new EditorCustomLink())
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (this.toolbar.obscure) {
|
|
1095
|
+
extensions.push(new EditorObscure())
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (this.toolbar.listButtons) {
|
|
1099
|
+
extensions.push(new BulletList())
|
|
1100
|
+
extensions.push(new OrderedList())
|
|
1101
|
+
extensions.push(new ListItem())
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (this.toolbar.table) {
|
|
1105
|
+
extensions.push(new Table({
|
|
1106
|
+
resizable: true
|
|
1107
|
+
}))
|
|
1108
|
+
extensions.push(new TableHeader())
|
|
1109
|
+
extensions.push(new TableCell())
|
|
1110
|
+
extensions.push(new TableRow())
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (this.toolbar.insertAndDelete) {
|
|
1114
|
+
extensions.push(new EditorCustomDelete())
|
|
1115
|
+
extensions.push(new EditorCustomInsert())
|
|
1116
|
+
|
|
1117
|
+
this.diffMenu.buttons = [
|
|
1118
|
+
{
|
|
1119
|
+
label: 'editor.diff.insert',
|
|
1120
|
+
command: (commands) => commands.insert(),
|
|
1121
|
+
name: 'insert'
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
label: 'editor.diff.delete',
|
|
1125
|
+
command: (commands) => commands.delete(),
|
|
1126
|
+
name: 'delete'
|
|
1127
|
+
}
|
|
1128
|
+
]
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (this.toolbar.mark) {
|
|
1132
|
+
extensions.push(new EditorCustomMark())
|
|
1133
|
+
|
|
1134
|
+
this.diffMenu.buttons.unshift({
|
|
1135
|
+
label: 'editor.mark',
|
|
1136
|
+
command: (commands) => commands.mark(),
|
|
1137
|
+
name: 'mark'
|
|
1138
|
+
})
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (this.toolbar.textDecoration) {
|
|
1142
|
+
extensions.push(new Bold())
|
|
1143
|
+
extensions.push(new Italic())
|
|
1144
|
+
extensions.push(new Underline())
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (this.toolbar.strikethrough) {
|
|
1148
|
+
extensions.push(new Strike())
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
this.editor = new Editor({
|
|
1152
|
+
editable: !this.readonly,
|
|
1153
|
+
extensions: extensions,
|
|
1154
|
+
content: this.currentValue,
|
|
1155
|
+
disableInputRules: true,
|
|
1156
|
+
disablePasteRules: true,
|
|
1157
|
+
onUpdate: () => {
|
|
1158
|
+
this.setValue()
|
|
1159
|
+
},
|
|
1160
|
+
editorProps: {
|
|
1161
|
+
handleDrop: (view, event, slice, moved) => {
|
|
1162
|
+
if (!moved) {
|
|
1163
|
+
return true
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
handleClick: (view, pos, event) => {
|
|
1167
|
+
if (event.target.tagName.toLowerCase() === 'img' && event.ctrlKey) {
|
|
1168
|
+
const image = event.target
|
|
1169
|
+
this.openUploadModal({ editAltOnly: true, currentAlt: image.getAttribute('alt') })
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
transformPastedHTML: (slice) => {
|
|
1173
|
+
/*
|
|
1174
|
+
* Due to the strange Html format from Word clipbord, lists would not be displayed properly,
|
|
1175
|
+
* so we have to handle paste from word manually.
|
|
1176
|
+
*/
|
|
1177
|
+
slice = handleWordPaste(slice)
|
|
1178
|
+
// Handle obscure tags - to handle the paste of fully obscured strings we need to overwrite the default paste behaviour and before the content is pasted we replace the obscure-styles with 'u-obscure' class
|
|
1179
|
+
const obscureClass = this.prefixClass('u-obscure')
|
|
1180
|
+
const obscureColor = getColorFromCSS(obscureClass)
|
|
1181
|
+
let returnContent = slice
|
|
1182
|
+
if (slice.includes(`span style="color: ${obscureColor}`)) {
|
|
1183
|
+
returnContent = slice.replace(/(?:<meta [^>]*>\s*<span [^>]*>\s*)([^<]*?)(?:\s*<\/span>)/g, '$1')
|
|
1184
|
+
returnContent = '<span class="' + obscureClass + '">' + returnContent + '</span>'
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Strip anchor tags if link functionality is not active
|
|
1188
|
+
if (this.linkButton === false) {
|
|
1189
|
+
returnContent = returnContent.replace(/<a[^>]*>(.*?)<\/a>/g, '$1')
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Strip img tags from pasted and dropped content
|
|
1193
|
+
returnContent = returnContent.replace(/<img.*?>/g, '')
|
|
1194
|
+
|
|
1195
|
+
return returnContent
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
onInit: ({ state, view }) => {
|
|
1199
|
+
view._props.handleScrollToSelection = customHandleScrollToSelection
|
|
1200
|
+
}
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
this.$root.$on('open-image-alt-modal', (e, id) => {
|
|
1204
|
+
this.editingImage = id
|
|
1205
|
+
this.openUploadModal({ editAltOnly: true, currentAlt: e.target.getAttribute('alt') })
|
|
1206
|
+
})
|
|
1207
|
+
/*
|
|
1208
|
+
* On form-reset the editor has to be cleared manually.
|
|
1209
|
+
* the inputs doesn't fire events in this case.
|
|
1210
|
+
* in the data methods its to early to get the elements
|
|
1211
|
+
*/
|
|
1212
|
+
this.manuallyResetForm = (this.hiddenInput !== '' && this.$el.closest('form') !== null)
|
|
1213
|
+
if (this.manuallyResetForm) {
|
|
1214
|
+
this.$el.closest('form').addEventListener('reset', this.resetEditor)
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
|
|
1218
|
+
beforeDestroy () {
|
|
1219
|
+
if (this.editor) {
|
|
1220
|
+
this.editor.destroy()
|
|
1221
|
+
if (this.manuallyResetForm) {
|
|
1222
|
+
this.$el.closest('form').removeEventListener('reset', this.resetEditor)
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Custom handling of scrolling after paste
|
|
1229
|
+
function windowRect (win) {
|
|
1230
|
+
return {
|
|
1231
|
+
left: 0,
|
|
1232
|
+
right: win.innerWidth,
|
|
1233
|
+
top: 0,
|
|
1234
|
+
bottom: win.innerHeight
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function getSide (value, side) {
|
|
1238
|
+
return typeof value === 'number' ? value : value[side]
|
|
1239
|
+
}
|
|
1240
|
+
const parentNode = function (node) {
|
|
1241
|
+
const parent = node.parentNode
|
|
1242
|
+
return parent && parent.nodeType === 11 ? parent.host : parent
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function customHandleScrollToSelection (view, rect = view.coordsAtPos(view.state.selection.head), startDOM = view.docView.dom) {
|
|
1246
|
+
const scrollThreshold = view.someProp('scrollThreshold') || 0
|
|
1247
|
+
const scrollMargin = view.someProp('scrollMargin') || 5
|
|
1248
|
+
const doc = view.dom.ownerDocument
|
|
1249
|
+
const win = doc.defaultView
|
|
1250
|
+
for (let parent = startDOM || view.dom; ; parent = parentNode(parent)) {
|
|
1251
|
+
if (!parent) break
|
|
1252
|
+
if (parent.nodeType !== 1) continue
|
|
1253
|
+
const parentStyle = window.getComputedStyle(parent, null)
|
|
1254
|
+
const atTop = (parentStyle['overflow-y'] === 'auto' || parentStyle['overflow-y'] === 'scroll' || parent.nodeType !== 1)
|
|
1255
|
+
const bounding = atTop ? windowRect(win) : parent.getBoundingClientRect()
|
|
1256
|
+
let moveX = 0
|
|
1257
|
+
let moveY = 0
|
|
1258
|
+
if (rect.top < bounding.top + getSide(scrollThreshold, 'top')) { moveY = -(bounding.top - rect.top + getSide(scrollMargin, 'top')) } else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, 'bottom')) { moveY = rect.bottom - bounding.bottom + getSide(scrollMargin, 'bottom') }
|
|
1259
|
+
if (rect.left < bounding.left + getSide(scrollThreshold, 'left')) { moveX = -(bounding.left - rect.left + getSide(scrollMargin, 'left')) } else if (rect.right > bounding.right - getSide(scrollThreshold, 'right')) { moveX = rect.right - bounding.right + getSide(scrollMargin, 'right') }
|
|
1260
|
+
if (moveX || moveY) {
|
|
1261
|
+
if (moveY) parent.scrollTop += moveY
|
|
1262
|
+
if (moveX) parent.scrollLeft += moveX
|
|
1263
|
+
}
|
|
1264
|
+
if (atTop) break
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// The function below is used to get the font color of obscured elements to be able to change the HTML on copy/paste of fully-obscured strings (used above in transformPastedHTML)
|
|
1269
|
+
function getColorFromCSS (className) {
|
|
1270
|
+
const body = document.getElementsByTagName('body')[0]
|
|
1271
|
+
const div = document.createElement('div')
|
|
1272
|
+
div.className = className
|
|
1273
|
+
div.id = 'tmpIdToGetColor'
|
|
1274
|
+
body.appendChild(div)
|
|
1275
|
+
const tmpDiv = document.getElementById('tmpIdToGetColor')
|
|
1276
|
+
const color = window.getComputedStyle(tmpDiv).getPropertyValue('color')
|
|
1277
|
+
|
|
1278
|
+
body.removeChild(tmpDiv)
|
|
1279
|
+
return color
|
|
1280
|
+
}
|
|
1281
|
+
</script>
|