@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,121 @@
|
|
|
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
|
+
<template>
|
|
11
|
+
<dp-modal
|
|
12
|
+
ref="uploadModal"
|
|
13
|
+
content-classes="u-2-of-3-lap-up u-1-of-2-desk-up">
|
|
14
|
+
<template>
|
|
15
|
+
<h3
|
|
16
|
+
v-if="editAltTextOnly"
|
|
17
|
+
class="u-mb">
|
|
18
|
+
{{ Translator.trans('image.edit') }}
|
|
19
|
+
</h3>
|
|
20
|
+
<h3
|
|
21
|
+
v-else
|
|
22
|
+
class="u-mb">
|
|
23
|
+
{{ Translator.trans('image.insert') }}
|
|
24
|
+
</h3>
|
|
25
|
+
<div v-show="editAltTextOnly === false">
|
|
26
|
+
<dp-upload-files
|
|
27
|
+
allowed-file-types="img"
|
|
28
|
+
id="imageFile"
|
|
29
|
+
:max-file-size="20 * 1024 * 1024/* 20 MiB */"
|
|
30
|
+
:max-number-of-files="1"
|
|
31
|
+
ref="uploader"
|
|
32
|
+
:translations="{ dropHereOr: Translator.trans('form.button.upload.image', { browse: '{browse}', maxUploadSize: '20MB' }) }"
|
|
33
|
+
@upload-success="setFile" />
|
|
34
|
+
</div>
|
|
35
|
+
<dp-input
|
|
36
|
+
id="altText"
|
|
37
|
+
v-model="altText"
|
|
38
|
+
class="u-mb"
|
|
39
|
+
:label="{
|
|
40
|
+
hint: Translator.trans('image.alt.explanation'),
|
|
41
|
+
text: Translator.trans('alternative.text')
|
|
42
|
+
}" />
|
|
43
|
+
<div class="u-mt text--right width-100p space-inline-s">
|
|
44
|
+
<button
|
|
45
|
+
class="btn btn--primary"
|
|
46
|
+
type="button"
|
|
47
|
+
@click="emitAndClose()">
|
|
48
|
+
{{ Translator.trans('insert') }}
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
class="btn btn--secondary"
|
|
52
|
+
type="button"
|
|
53
|
+
@click="resetAndClose()">
|
|
54
|
+
{{ Translator.trans('abort') }}
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
</dp-modal>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script>
|
|
62
|
+
import { DpInput } from 'demosplan-ui/components'
|
|
63
|
+
import DpModal from '../DpModal'
|
|
64
|
+
import DpUploadFiles from '../DpUpload/DpUploadFiles'
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
name: 'DpUploadModal',
|
|
68
|
+
|
|
69
|
+
components: {
|
|
70
|
+
DpInput,
|
|
71
|
+
DpModal,
|
|
72
|
+
DpUploadFiles
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
data () {
|
|
76
|
+
return {
|
|
77
|
+
fileUrl: '',
|
|
78
|
+
altText: '',
|
|
79
|
+
editAltTextOnly: false
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
methods: {
|
|
84
|
+
emitAndClose () {
|
|
85
|
+
if (this.editAltTextOnly) {
|
|
86
|
+
this.$emit('add-alt', this.altText)
|
|
87
|
+
} else if (this.fileUrl) {
|
|
88
|
+
this.$emit('insert-image', this.fileUrl, this.altText)
|
|
89
|
+
}
|
|
90
|
+
this.resetAndClose()
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
resetAndClose () {
|
|
94
|
+
this.altText = ''
|
|
95
|
+
this.fileUrl = ''
|
|
96
|
+
this.editAltTextOnly = false
|
|
97
|
+
this.$emit('close')
|
|
98
|
+
this.toggleModal()
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
setFile ({ hash }) {
|
|
102
|
+
this.fileUrl = Routing.generate('core_file', { hash: hash })
|
|
103
|
+
// Force-update the component so that DpModal updates and therefore check for new focusable elements
|
|
104
|
+
this.$forceUpdate()
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
toggleModal (data) {
|
|
108
|
+
const willCloseModal = this.$refs.uploadModal.isOpenModal === true
|
|
109
|
+
|
|
110
|
+
if (willCloseModal) {
|
|
111
|
+
this.$refs.uploader.clearFilesList()
|
|
112
|
+
} else if (data) {
|
|
113
|
+
this.editAltTextOnly = data.editAltOnly
|
|
114
|
+
this.altText = data.currentAlt
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.$refs.uploadModal.toggle()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
11
|
+
import { Plugin, TextSelection } from 'prosemirror-state'
|
|
12
|
+
|
|
13
|
+
const addHandle = (tr, set, pos, id) => {
|
|
14
|
+
const widget = document.createElement('span')
|
|
15
|
+
widget.classList.add('handle')
|
|
16
|
+
widget.setAttribute('data-position', pos)
|
|
17
|
+
widget.innerHTML = `<span id="container" data-position="${pos}"><div id="bubble" data-position="${pos}"></div>|</span>`
|
|
18
|
+
const deco = Decoration.widget(pos, widget, { id: id })
|
|
19
|
+
set = set.add(tr.doc, [deco])
|
|
20
|
+
return set
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const removeHandle = (set, id) => {
|
|
24
|
+
set = set.remove(set.find(null, null,
|
|
25
|
+
spec => spec.id === id))
|
|
26
|
+
return set
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const placeholderPlugin = new Plugin({
|
|
30
|
+
state: {
|
|
31
|
+
init () { return DecorationSet.empty },
|
|
32
|
+
apply (tr, set) {
|
|
33
|
+
// Adjust decoration positions to changes made by the transaction
|
|
34
|
+
set = set.map(tr.mapping, tr.doc)
|
|
35
|
+
// See if the transaction adds or removes any placeholders
|
|
36
|
+
const action = tr.getMeta(this)
|
|
37
|
+
if (action && action.add) {
|
|
38
|
+
set = addHandle(tr, set, action.add.pos, action.add.id)
|
|
39
|
+
} else if (action && action.remove) {
|
|
40
|
+
set = removeHandle(set, action.remove.id)
|
|
41
|
+
} else if (action && action.move) {
|
|
42
|
+
set = removeHandle(set, action.move.id)
|
|
43
|
+
set = addHandle(tr, set, action.move.pos, action.move.id)
|
|
44
|
+
}
|
|
45
|
+
return set
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
props: {
|
|
49
|
+
decorations (state) { return this.getState(state) }
|
|
50
|
+
},
|
|
51
|
+
filterTransaction (tr, state) {
|
|
52
|
+
const action = tr.getMeta(this)
|
|
53
|
+
/**
|
|
54
|
+
* The behaviour of text selections in prosemirror differs in Chrome and Firefox.
|
|
55
|
+
* In our case, Chrome treats the current caret position as selection range while Firefox applies the correct selection
|
|
56
|
+
* from initial caret position to current caret position.
|
|
57
|
+
* To fix this behaviour we modify the transaction and set the correct selection in this method.
|
|
58
|
+
*/
|
|
59
|
+
if (action && action.move) {
|
|
60
|
+
tr.setSelection(new TextSelection(state.doc.resolve(action.move.initialPos), state.doc.resolve(action.move.pos)))
|
|
61
|
+
}
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export { placeholderPlugin }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Plugin } from 'prosemirror-state'
|
|
11
|
+
|
|
12
|
+
export default new Plugin({
|
|
13
|
+
/*
|
|
14
|
+
* State: {
|
|
15
|
+
* init () {
|
|
16
|
+
* return {}
|
|
17
|
+
* },
|
|
18
|
+
* apply (tr, set) {
|
|
19
|
+
* console.log(set)
|
|
20
|
+
* }
|
|
21
|
+
* },
|
|
22
|
+
*/
|
|
23
|
+
appendTransaction (transactions) {
|
|
24
|
+
// Console.log(transactions)
|
|
25
|
+
}
|
|
26
|
+
/*
|
|
27
|
+
* View () {
|
|
28
|
+
* return {
|
|
29
|
+
* update (editorView, prevState) {
|
|
30
|
+
* debugger
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* This is the anonymize-extension for tiptap, built on the basis of tiptap bold-extension.
|
|
12
|
+
* On mark-anonymize in tiptap, we wrap up the marked content in <span class='anonymize'></span> tags, and then before
|
|
13
|
+
* saving the changes we convert them to <dp-obscure>, so that they are correctly saved in BE. But to display the
|
|
14
|
+
* <span class='u-obscure'> tags in the editor we need to use the toDOM function provided by tiptap/prosemirror.
|
|
15
|
+
*
|
|
16
|
+
* InputRules and pasteRules help to handle diverse behaviour when we want to obscure only part of words or we want to
|
|
17
|
+
* use more than one tool (e.g. obscure and bold) simultaneously, etc.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { markInputRule, markPasteRule, toggleMark } from 'tiptap-commands'
|
|
21
|
+
import { Mark } from 'tiptap'
|
|
22
|
+
|
|
23
|
+
export default class EditorAnonymize extends Mark {
|
|
24
|
+
get name () {
|
|
25
|
+
return 'anonymize'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get schema () {
|
|
29
|
+
return {
|
|
30
|
+
attrs: {
|
|
31
|
+
title: {
|
|
32
|
+
default: null
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
spanning: false,
|
|
36
|
+
parseDOM: [{
|
|
37
|
+
tag: '.anonymize-me',
|
|
38
|
+
getAttrs: dom => ({
|
|
39
|
+
title: dom.getAttribute('title')
|
|
40
|
+
})
|
|
41
|
+
}],
|
|
42
|
+
toDOM: node => {
|
|
43
|
+
return ['span', {
|
|
44
|
+
...node.attrs,
|
|
45
|
+
class: 'anonymize-me'
|
|
46
|
+
}, 0]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
commands ({ type }) {
|
|
52
|
+
return () => toggleMark(type)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
inputRules ({ type }) {
|
|
56
|
+
return [
|
|
57
|
+
markInputRule(/(?:<o>)([^<o>]+)(?:<o>)$/, type)
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pasteRules ({ type }) {
|
|
62
|
+
return [
|
|
63
|
+
markPasteRule(/(?:<o>)([^<o>]+)(?:<o>)/g, type)
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import tippy, { sticky } from 'tippy.js'
|
|
11
|
+
import { Mention } from 'tiptap-extensions'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A utility function to attach event listeners to the suggestion popup.
|
|
15
|
+
*
|
|
16
|
+
* @param args {Object} One or more objects containing eventType and callback { eventType: 'click', handler: () => ({}) }
|
|
17
|
+
*/
|
|
18
|
+
function attachEventListeners (...args) {
|
|
19
|
+
const listWrapper = document.querySelector('[data-suggestion-id="suggestion-popup"]')
|
|
20
|
+
args.forEach(({ eventType, handler }) => attachSingleEventListener(listWrapper, eventType, handler))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A utility function to attach event listeners to an element.
|
|
25
|
+
*
|
|
26
|
+
* @param el {Element} A DOM Element that the event listener should be attached to.
|
|
27
|
+
* @param eventType {String} An event type that should be listened to (e.g. 'click')
|
|
28
|
+
* @param handler {Function} A callback that should be triggered when the event occurs.
|
|
29
|
+
*/
|
|
30
|
+
function attachSingleEventListener (el, eventType, handler) {
|
|
31
|
+
if (el) {
|
|
32
|
+
el.removeEventListener(eventType, handler)
|
|
33
|
+
el.addEventListener(eventType, handler)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Used to highlight the currently selected suggestion item in the suggestion popup.
|
|
39
|
+
*
|
|
40
|
+
* @param listIndex {Number} The index of the suggestion item that should be highlighted.
|
|
41
|
+
*/
|
|
42
|
+
function highlightActiveElement (listIndex) {
|
|
43
|
+
const wrapper = document.querySelector('[data-suggestion-id="suggestion-popup"]')
|
|
44
|
+
if (wrapper) {
|
|
45
|
+
const nodeList = wrapper.childNodes
|
|
46
|
+
nodeList.forEach((node, idx) => idx === listIndex
|
|
47
|
+
? node.classList.add('suggestion__list-item--is-active')
|
|
48
|
+
: node.classList.remove('suggestion__list-item--is-active'))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Used to generate content for the suggestion popup.
|
|
54
|
+
*
|
|
55
|
+
* @param items {Array} The suggestions that will be rendered insided the suggestion popup ([{ id: <id>, name: <name> }]).
|
|
56
|
+
*
|
|
57
|
+
* @return {HTMLUListElement}
|
|
58
|
+
*/
|
|
59
|
+
function createFragment (items) {
|
|
60
|
+
const fragment = document.createElement('ul')
|
|
61
|
+
fragment.setAttribute('data-suggestion-id', 'suggestion-popup')
|
|
62
|
+
fragment.setAttribute('class', 'o-list suggestion__popup')
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < items.length; i++) {
|
|
65
|
+
const li = document.createElement('li')
|
|
66
|
+
li.setAttribute('class', 'o-list__item suggestion__list-item u-ph-0_5 u-pv-0_25')
|
|
67
|
+
li.setAttribute('data-suggestion-item', `${i}`)
|
|
68
|
+
|
|
69
|
+
const button = document.createElement('button')
|
|
70
|
+
button.setAttribute('class', 'btn--blank')
|
|
71
|
+
button.appendChild(document.createTextNode(items[i].name))
|
|
72
|
+
|
|
73
|
+
li.appendChild(button)
|
|
74
|
+
fragment.appendChild(li)
|
|
75
|
+
}
|
|
76
|
+
if (items.length === 0) {
|
|
77
|
+
const li = document.createElement('li')
|
|
78
|
+
li.appendChild(document.createTextNode('Keine passenden Platzhalter gefunden'))
|
|
79
|
+
fragment.appendChild(li)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return fragment
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Used to create a suggestion plugin which triggers on certain characters (e.g. @) and inserts a Mention node into tiptap
|
|
87
|
+
* when a suggestion was selected.
|
|
88
|
+
*
|
|
89
|
+
* @param matcher {Object} The char that should trigger a suggestion and some config to determine possible suggestion trigger positions ({ char: <char>, allowSpaces: <boolean>, startOfLine: <boolean> }).
|
|
90
|
+
* @param suggestions {Array} All available suggestions ([{ id: <id>, name: <name> }]).
|
|
91
|
+
* @param vueEditorInstance {VueInstance} Used to focus the tiptap editor on insertion of a suggestion.
|
|
92
|
+
*
|
|
93
|
+
* @return {Mention} A mention extension that can be used by tiptap.
|
|
94
|
+
*/
|
|
95
|
+
function createSuggestion ({ matcher, suggestions }, vueEditorInstance) {
|
|
96
|
+
/*
|
|
97
|
+
*Adding the whitespace to the name is a hack to make the suggestions work
|
|
98
|
+
*@see https://github.com/ueberdosis/tiptap/issues/932
|
|
99
|
+
*after the upgrade to tiptap 2 this workaround should be checked again
|
|
100
|
+
*and may hopefully be removed again
|
|
101
|
+
*/
|
|
102
|
+
const fixedSuggestions = suggestions.map(el => ({ id: el.id, name: el.name + ' ' }))
|
|
103
|
+
let popup = null
|
|
104
|
+
let selectedElement = 0
|
|
105
|
+
let filteredSuggestions = fixedSuggestions
|
|
106
|
+
let insertSuggestion = (...args) => ({ args })
|
|
107
|
+
let suggestionRange = null
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Triggers insertion of a suggestion.
|
|
111
|
+
*/
|
|
112
|
+
function clickHandler (e) {
|
|
113
|
+
let el = e.target
|
|
114
|
+
if (el.tagName === 'BUTTON') {
|
|
115
|
+
el = el.parentElement
|
|
116
|
+
}
|
|
117
|
+
const itemIdx = parseInt(el.getAttribute('data-suggestion-item'))
|
|
118
|
+
selectSuggestion(fixedSuggestions[itemIdx])
|
|
119
|
+
vueEditorInstance.editor.focus()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Removes the suggestion popup from the DOM.
|
|
124
|
+
*/
|
|
125
|
+
function destroyPopup () {
|
|
126
|
+
if (popup) {
|
|
127
|
+
popup[0].destroy()
|
|
128
|
+
popup = null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Increments the currently selected suggestion.
|
|
134
|
+
*/
|
|
135
|
+
function downHandler () {
|
|
136
|
+
if (selectedElement < fixedSuggestions.length - 1) {
|
|
137
|
+
selectedElement += 1
|
|
138
|
+
highlightActiveElement(selectedElement)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Triggers insertion of a suggestion.
|
|
144
|
+
*/
|
|
145
|
+
function enterHandler () {
|
|
146
|
+
selectSuggestion(fixedSuggestions[selectedElement])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Used to render a popup containing applicable suggestions.
|
|
151
|
+
*
|
|
152
|
+
* @param items {Array} The suggestions that will be available to the suggestion popup ([{ id: <id>, name: <name> }]).
|
|
153
|
+
* @param listIndex {Number} The index of the currently selected suggestion.
|
|
154
|
+
* @param node {TipTapVirtualNode} A virtual node used for placement of the suggestion popup.
|
|
155
|
+
*/
|
|
156
|
+
function renderPopup (items, listIndex, node) {
|
|
157
|
+
const boundingClientRect = node.getBoundingClientRect()
|
|
158
|
+
const { x, y } = boundingClientRect
|
|
159
|
+
if (x === 0 && y === 0) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if (popup) {
|
|
163
|
+
popup[0].setContent(createFragment(items))
|
|
164
|
+
} else {
|
|
165
|
+
popup = tippy('#mainContent', {
|
|
166
|
+
getReferenceClientRect: () => boundingClientRect,
|
|
167
|
+
appendTo: () => document.body,
|
|
168
|
+
interactive: true,
|
|
169
|
+
sticky: true, // Make sure position of tippy is updated when content changes
|
|
170
|
+
plugins: [sticky],
|
|
171
|
+
content: createFragment(items),
|
|
172
|
+
trigger: 'manual', // Manual
|
|
173
|
+
showOnCreate: true,
|
|
174
|
+
theme: 'dark',
|
|
175
|
+
placement: 'top-start',
|
|
176
|
+
inertia: true,
|
|
177
|
+
duration: [400, 200]
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
highlightActiveElement(listIndex)
|
|
181
|
+
attachEventListeners({ eventType: 'click', handler: clickHandler })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Used to insert a suggestion into tiptap.
|
|
186
|
+
*
|
|
187
|
+
* @param suggestion {Object} The suggestion that should be inserted ({ id: <id>, name: <name> })
|
|
188
|
+
*/
|
|
189
|
+
function selectSuggestion (suggestion) {
|
|
190
|
+
insertSuggestion({
|
|
191
|
+
range: suggestionRange,
|
|
192
|
+
attrs: {
|
|
193
|
+
id: suggestion.id,
|
|
194
|
+
label: suggestion.name
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Triggers insertion of a suggestion.
|
|
201
|
+
*/
|
|
202
|
+
function spaceHandler () {
|
|
203
|
+
if (fixedSuggestions.length === 1) {
|
|
204
|
+
selectSuggestion(fixedSuggestions[0])
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Decrements the currently selected suggestion.
|
|
210
|
+
*/
|
|
211
|
+
function upHandler () {
|
|
212
|
+
if (selectedElement > 0) {
|
|
213
|
+
selectedElement -= 1
|
|
214
|
+
highlightActiveElement(selectedElement)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return new Mention({
|
|
219
|
+
items: fixedSuggestions,
|
|
220
|
+
matcher: matcher,
|
|
221
|
+
mentionClass: 'suggestion__node',
|
|
222
|
+
// Is called when a suggestion starts
|
|
223
|
+
onEnter: ({ items, range, command, virtualNode }) => {
|
|
224
|
+
filteredSuggestions = items
|
|
225
|
+
suggestionRange = range
|
|
226
|
+
selectedElement = 0
|
|
227
|
+
renderPopup(filteredSuggestions, selectedElement, virtualNode)
|
|
228
|
+
insertSuggestion = command
|
|
229
|
+
},
|
|
230
|
+
// Is called when a suggestion has changed
|
|
231
|
+
onChange: ({ items, range, virtualNode }) => {
|
|
232
|
+
filteredSuggestions = items
|
|
233
|
+
suggestionRange = range
|
|
234
|
+
selectedElement = 0
|
|
235
|
+
renderPopup(filteredSuggestions, selectedElement, virtualNode)
|
|
236
|
+
},
|
|
237
|
+
// Is called when a suggestion is cancelled
|
|
238
|
+
onExit: () => {
|
|
239
|
+
// Reset all saved values
|
|
240
|
+
filteredSuggestions = []
|
|
241
|
+
suggestionRange = null
|
|
242
|
+
selectedElement = 0
|
|
243
|
+
destroyPopup()
|
|
244
|
+
},
|
|
245
|
+
// Is called on every keyDown event while a suggestion is active
|
|
246
|
+
onKeyDown: ({ event }) => {
|
|
247
|
+
if (event.key === 'ArrowUp') {
|
|
248
|
+
upHandler()
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
if (event.key === 'ArrowDown') {
|
|
252
|
+
downHandler()
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
if (event.key === 'Enter') {
|
|
256
|
+
enterHandler()
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (event.key === ' ') {
|
|
261
|
+
spaceHandler()
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { createSuggestion }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Mark } from 'tiptap'
|
|
11
|
+
import { toggleMark } from 'tiptap-commands'
|
|
12
|
+
|
|
13
|
+
export default class EditorCustomDelete extends Mark {
|
|
14
|
+
get name () {
|
|
15
|
+
return 'delete'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get schema () {
|
|
19
|
+
return {
|
|
20
|
+
parseDOM: [
|
|
21
|
+
{
|
|
22
|
+
tag: 'del'
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
toDOM: () => ['del', { title: Translator.trans('text.deleted') }, 0]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
commands ({ type }) {
|
|
30
|
+
return () => toggleMark(type)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import DpResizableImage from '../DpResizableImage'
|
|
11
|
+
import { Node } from 'tiptap'
|
|
12
|
+
import { nodeInputRule } from 'tiptap-commands'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Matches following attributes in Markdown-typed image: [, alt, src, title]
|
|
16
|
+
*
|
|
17
|
+
* Example:
|
|
18
|
+
*  -> [, "Lorem", "image.jpg"]
|
|
19
|
+
*  -> [, "", "image.jpg", "Ipsum"]
|
|
20
|
+
*  -> [, "Lorem", "image.jpg", "Ipsum"]
|
|
21
|
+
*/
|
|
22
|
+
const IMAGE_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
|
|
23
|
+
|
|
24
|
+
export default class Image extends Node {
|
|
25
|
+
get name () {
|
|
26
|
+
return 'image'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get schema () {
|
|
30
|
+
return {
|
|
31
|
+
inline: true,
|
|
32
|
+
attrs: {
|
|
33
|
+
src: {},
|
|
34
|
+
alt: {
|
|
35
|
+
default: null
|
|
36
|
+
},
|
|
37
|
+
title: {
|
|
38
|
+
default: null
|
|
39
|
+
},
|
|
40
|
+
width: {
|
|
41
|
+
default: null
|
|
42
|
+
},
|
|
43
|
+
height: {
|
|
44
|
+
default: null
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
group: 'inline',
|
|
48
|
+
draggable: true,
|
|
49
|
+
parseDOM: [
|
|
50
|
+
{
|
|
51
|
+
tag: 'img[src]',
|
|
52
|
+
getAttrs: dom => {
|
|
53
|
+
return ({
|
|
54
|
+
src: dom.getAttribute('src'),
|
|
55
|
+
title: dom.getAttribute('title'),
|
|
56
|
+
alt: dom.getAttribute('alt'),
|
|
57
|
+
width: dom.getAttribute('width'),
|
|
58
|
+
height: dom.getAttribute('height')
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
toDOM: node => {
|
|
64
|
+
return ['img', { ...node.attrs, unselectable: 'on' }]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
commands ({ type }) {
|
|
70
|
+
return {
|
|
71
|
+
insertImage: attrs => (state, dispatch) => {
|
|
72
|
+
const { selection } = state
|
|
73
|
+
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
|
|
74
|
+
const node = type.create(attrs)
|
|
75
|
+
const transaction = state.tr.insert(position, node)
|
|
76
|
+
dispatch(transaction)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
inputRules ({ type }) {
|
|
82
|
+
return [
|
|
83
|
+
nodeInputRule(IMAGE_INPUT_REGEX, type, match => {
|
|
84
|
+
const [, alt, src, title, width, height] = match
|
|
85
|
+
return {
|
|
86
|
+
src,
|
|
87
|
+
alt,
|
|
88
|
+
title,
|
|
89
|
+
width,
|
|
90
|
+
height
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Return a vue component
|
|
97
|
+
get view () {
|
|
98
|
+
return DpResizableImage
|
|
99
|
+
}
|
|
100
|
+
}
|