@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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/buildTokens.js +59 -0
  3. package/components/DpButton/DpButton.stories.mdx +136 -0
  4. package/components/DpButton/DpButton.vue +118 -0
  5. package/components/DpDetails/DpDetails.stories.mdx +55 -0
  6. package/components/DpDetails/DpDetails.vue +58 -0
  7. package/components/DpIcon/DpIcon.stories.mdx +396 -0
  8. package/components/DpIcon/DpIcon.vue +51 -0
  9. package/components/DpIcon/util/iconVariables.js +148 -0
  10. package/components/DpInput/DpInput.stories.mdx +127 -0
  11. package/components/DpInput/DpInput.vue +284 -0
  12. package/components/DpLabel/DpLabel.stories.mdx +103 -0
  13. package/components/DpLabel/DpLabel.vue +112 -0
  14. package/components/DpLoading/DpLoading.stories.mdx +63 -0
  15. package/components/DpLoading/DpLoading.vue +63 -0
  16. package/components/core/DpAccordion.vue +108 -0
  17. package/components/core/DpAnonymizeText.vue +136 -0
  18. package/components/core/DpAutocomplete.vue +133 -0
  19. package/components/core/DpBulkEditHeader.vue +53 -0
  20. package/components/core/DpButtonIcon.vue +47 -0
  21. package/components/core/DpButtonRow.vue +155 -0
  22. package/components/core/DpCard.vue +54 -0
  23. package/components/core/DpChangeStateAtDate.vue +223 -0
  24. package/components/core/DpCheckboxGroup.vue +93 -0
  25. package/components/core/DpContextualHelp.vue +54 -0
  26. package/components/core/DpCopyPasteButton.vue +47 -0
  27. package/components/core/DpDashboardTaskCard.vue +123 -0
  28. package/components/core/DpDataTable/DataTableSearch.js +44 -0
  29. package/components/core/DpDataTable/DpColumnSelector.vue +133 -0
  30. package/components/core/DpDataTable/DpDataTable.vue +647 -0
  31. package/components/core/DpDataTable/DpDataTableExtended.vue +377 -0
  32. package/components/core/DpDataTable/DpResizeHandle.vue +37 -0
  33. package/components/core/DpDataTable/DpSelectPageItemCount.vue +70 -0
  34. package/components/core/DpDataTable/DpTableHeader.vue +197 -0
  35. package/components/core/DpDataTable/DpTableRow.vue +355 -0
  36. package/components/core/DpDataTable/DpWrapTrigger.vue +48 -0
  37. package/components/core/DpDataTable/lib/ResizableColumns.js +83 -0
  38. package/components/core/DpEditableList.vue +161 -0
  39. package/components/core/DpEditor/DpBoilerPlate.vue +140 -0
  40. package/components/core/DpEditor/DpBoilerPlateModal.vue +166 -0
  41. package/components/core/DpEditor/DpEditor.vue +1281 -0
  42. package/components/core/DpEditor/DpLinkModal.vue +117 -0
  43. package/components/core/DpEditor/DpRecommendationModal/DpInsertableRecommendation.vue +137 -0
  44. package/components/core/DpEditor/DpRecommendationModal.vue +283 -0
  45. package/components/core/DpEditor/DpResizableImage.vue +121 -0
  46. package/components/core/DpEditor/DpUploadModal.vue +121 -0
  47. package/components/core/DpEditor/libs/Decoration.js +66 -0
  48. package/components/core/DpEditor/libs/SegmentRangeChangePlugin.js +35 -0
  49. package/components/core/DpEditor/libs/editorAnonymize.js +66 -0
  50. package/components/core/DpEditor/libs/editorBuildSuggestion.js +269 -0
  51. package/components/core/DpEditor/libs/editorCustomDelete.js +32 -0
  52. package/components/core/DpEditor/libs/editorCustomImage.js +100 -0
  53. package/components/core/DpEditor/libs/editorCustomInsert.js +32 -0
  54. package/components/core/DpEditor/libs/editorCustomLink.js +46 -0
  55. package/components/core/DpEditor/libs/editorCustomMark.js +32 -0
  56. package/components/core/DpEditor/libs/editorInsertAtCursorPos.js +41 -0
  57. package/components/core/DpEditor/libs/editorObscure.js +60 -0
  58. package/components/core/DpEditor/libs/editorUnAnonymize.js +56 -0
  59. package/components/core/DpEditor/libs/handleWordPaste.js +360 -0
  60. package/components/core/DpEditor/libs/preventDrop.js +31 -0
  61. package/components/core/DpEditor/libs/preventKeyboardInput.js +27 -0
  62. package/components/core/DpEditor/libs/preventPaste.js +28 -0
  63. package/components/core/DpFlyout.vue +119 -0
  64. package/components/core/DpInlineNotification.vue +116 -0
  65. package/components/core/DpModal.vue +208 -0
  66. package/components/core/DpObscure.vue +29 -0
  67. package/components/core/DpPager.vue +139 -0
  68. package/components/core/DpProgressBar.vue +67 -0
  69. package/components/core/DpRegisterFlyout.vue +58 -0
  70. package/components/core/DpResettableInput.vue +140 -0
  71. package/components/core/DpSkeletonBox.vue +32 -0
  72. package/components/core/DpSlidebar.vue +86 -0
  73. package/components/core/DpSlidingPagination.vue +45 -0
  74. package/components/core/DpSplitButton.vue +77 -0
  75. package/components/core/DpSwitcher.vue +62 -0
  76. package/components/core/DpTableCardList/DpTableCard.vue +61 -0
  77. package/components/core/DpTableCardList/DpTableCardListHeader.vue +83 -0
  78. package/components/core/DpTabs/DpTab.vue +52 -0
  79. package/components/core/DpTabs/DpTabs.vue +165 -0
  80. package/components/core/DpTextWrapper.vue +65 -0
  81. package/components/core/DpToggleForm.vue +72 -0
  82. package/components/core/DpTooltipIcon.vue +52 -0
  83. package/components/core/DpTransitionExpand.vue +87 -0
  84. package/components/core/DpTreeList/DpTreeList.vue +334 -0
  85. package/components/core/DpTreeList/DpTreeListCheckbox.vue +79 -0
  86. package/components/core/DpTreeList/DpTreeListNode.vue +348 -0
  87. package/components/core/DpTreeList/DpTreeListToggle.vue +71 -0
  88. package/components/core/DpTreeList/utils/constants.js +14 -0
  89. package/components/core/DpUpload/DpUpload.vue +223 -0
  90. package/components/core/DpUpload/DpUploadFiles.vue +269 -0
  91. package/components/core/DpUpload/DpUploadedFile.vue +80 -0
  92. package/components/core/DpUpload/DpUploadedFileList.vue +56 -0
  93. package/components/core/DpUpload/utils/GetFileIdsByHash.js +42 -0
  94. package/components/core/DpUpload/utils/UppyTranslations.js +31 -0
  95. package/components/core/DpVideoPlayer.vue +115 -0
  96. package/components/core/HeightLimit.vue +121 -0
  97. package/components/core/MultistepNav.vue +89 -0
  98. package/components/core/form/DpCheckbox.vue +108 -0
  99. package/components/core/form/DpDateRangePicker.vue +186 -0
  100. package/components/core/form/DpDatepicker.vue +160 -0
  101. package/components/core/form/DpDatetimePicker.vue +194 -0
  102. package/components/core/form/DpFormRow.vue +79 -0
  103. package/components/core/form/DpMultiselect.vue +164 -0
  104. package/components/core/form/DpRadio.vue +128 -0
  105. package/components/core/form/DpSearchField.vue +110 -0
  106. package/components/core/form/DpSelect.vue +149 -0
  107. package/components/core/form/DpTextArea.vue +152 -0
  108. package/components/core/form/DpTimePicker.vue +374 -0
  109. package/components/core/form/DpToggle.vue +78 -0
  110. package/components/core/index.js +132 -0
  111. package/components/core/notify/DpNotifyContainer.vue +122 -0
  112. package/components/core/notify/DpNotifyMessage.vue +95 -0
  113. package/components/core/shared/DpStickyElement.vue +95 -0
  114. package/components/index.js +24 -0
  115. package/components/shared/translations.js +15 -0
  116. package/directives/CleanHtml/CleanHtml.js +50 -0
  117. package/directives/CleanHtml/CleanHtml.stories.mdx +64 -0
  118. package/directives/Tooltip/Tooltip.js +40 -0
  119. package/directives/Tooltip/Tooltip.stories.mdx +42 -0
  120. package/directives/index.js +17 -0
  121. package/lib/index.js +14 -0
  122. package/lib/prefixClass.js +47 -0
  123. package/mixins/index.js +14 -0
  124. package/mixins/prefixClassMixin.js +22 -0
  125. package/package.json +52 -0
  126. package/shared/props.js +86 -0
  127. package/style/index.css +7 -0
  128. package/tailwind.config.js +24 -0
  129. package/tokens/color.json +358 -0
  130. package/tokens/color.stories.mdx +45 -0
  131. package/tokens/fontSize.json +100 -0
  132. package/tokens/space.json +33 -0
  133. 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
+ &#10072;
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
+ &#10072;
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
+ &#10072;
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
+ &#10072;
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
+ &#10072;
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>