pimelon-ui 0.1.205 → 0.1.208

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.1.205",
3
+ "version": "0.1.208",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -37,6 +37,7 @@
37
37
  "@tailwindcss/line-clamp": "^0.4.4",
38
38
  "@tailwindcss/typography": "^0.5.16",
39
39
  "@tiptap/core": "^2.26.1",
40
+ "@tiptap/extension-code": "^2.26.1",
40
41
  "@tiptap/extension-code-block": "^2.26.1",
41
42
  "@tiptap/extension-code-block-lowlight": "^2.26.1",
42
43
  "@tiptap/extension-color": "^2.26.1",
@@ -14,7 +14,7 @@
14
14
  class="flex flex-col transition-all duration-300 ease-in-out"
15
15
  v-bind="{
16
16
  to: list.options.getRowRoute ? list.options.getRowRoute(row) : undefined,
17
- onClick: (e) => onRowClick(row, e),
17
+ onClick: onRowClick,
18
18
  }"
19
19
  >
20
20
  <component
@@ -40,7 +40,7 @@
40
40
  <Checkbox
41
41
  :modelValue="isSelected"
42
42
  class="cursor-pointer duration-300"
43
- @click.stop="list.toggleRow(row[list.rowKey])"
43
+ @click.stop="handleCheckboxClick"
44
44
  />
45
45
  </div>
46
46
  <div
@@ -89,7 +89,7 @@
89
89
  import Checkbox from '../Checkbox/Checkbox.vue'
90
90
  import ListRowItem from './ListRowItem.vue'
91
91
  import { alignmentMap, getGridTemplateColumns } from './utils'
92
- import { computed, inject, ref } from 'vue'
92
+ import { computed, inject } from 'vue'
93
93
 
94
94
  const props = defineProps({
95
95
  row: {
@@ -144,12 +144,34 @@ const roundedClass = computed(() => {
144
144
  }
145
145
  })
146
146
 
147
- const onRowClick = (row, e) => {
148
- if (list.value.options.onRowClick) list.value.options.onRowClick(row, e)
149
- if (list.value.activeRow.value === row.name) {
147
+ const onRowClick = (event) => {
148
+ if (list.value.options.onRowClick)
149
+ list.value.options.onRowClick(props.row, event)
150
+ if (list.value.activeRow.value === props.row.name) {
150
151
  list.value.activeRow.value = null
151
152
  } else {
152
- list.value.activeRow.value = row.name
153
+ list.value.activeRow.value = props.row.name
154
+ }
155
+ }
156
+
157
+ const handleCheckboxClick = (event) => {
158
+ const value = props.row[list.value.rowKey]
159
+ if (event.shiftKey && !list.value.selections.has(value)) {
160
+ const lastSelected = Array.from(list.value.selections).pop()
161
+ const rows = list.value.rows.find((k) => k.group)
162
+ ? list.value.rows.reduce((acc, curr) => acc.concat(curr.rows), [])
163
+ : list.value.rows
164
+ const lastIndex = rows.findIndex(
165
+ (k) => lastSelected === k[list.value.rowKey],
166
+ )
167
+ const curIndex = rows.findIndex((k) => value === k[list.value.rowKey])
168
+ const start = Math.min(lastIndex, curIndex)
169
+ const end = Math.max(lastIndex, curIndex)
170
+ for (let i = start; i <= end; i++) {
171
+ list.value.selections.add(rows[i][list.value.rowKey])
172
+ }
173
+ } else {
174
+ list.value.toggleRow(value)
153
175
  }
154
176
  }
155
177
  </script>
@@ -4,6 +4,8 @@
4
4
  class="relative w-full"
5
5
  :class="attrsClass"
6
6
  :style="attrsStyle"
7
+ v-bind="attrsWithoutClassStyle"
8
+ ref="rootRef"
7
9
  >
8
10
  <TextEditorBubbleMenu :buttons="bubbleMenu" :options="bubbleMenuOptions" />
9
11
  <TextEditorFixedMenu
@@ -30,11 +32,12 @@ import {
30
32
  provide,
31
33
  ref,
32
34
  useAttrs,
35
+ useTemplateRef,
33
36
  } from 'vue'
34
37
 
35
38
  defineOptions({ inheritAttrs: false })
36
39
 
37
- import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3'
40
+ import { Editor, EditorContent } from '@tiptap/vue-3'
38
41
  import StarterKit from '@tiptap/starter-kit'
39
42
  import Placeholder from '@tiptap/extension-placeholder'
40
43
  import TextAlign from '@tiptap/extension-text-align'
@@ -53,9 +56,7 @@ import TaskItem from '@tiptap/extension-task-item'
53
56
  import TaskList from '@tiptap/extension-task-list'
54
57
  import NamedColorExtension from './extensions/color'
55
58
  import NamedHighlightExtension from './extensions/highlight'
56
- import { common, createLowlight } from 'lowlight'
57
- import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
58
- import CodeBlockComponent from './CodeBlockComponent.vue'
59
+
59
60
  import { MentionExtension } from './extensions/mention'
60
61
  import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
61
62
  import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
@@ -66,11 +67,10 @@ import { ContentPasteExtension } from './extensions/content-paste-extension'
66
67
  import { TagNode, TagExtension } from './extensions/tag/tag-extension'
67
68
  import { Heading } from './extensions/heading/heading'
68
69
  import { ImageGroup } from './extensions/image-group/image-group-extension'
70
+ import { ExtendedCode, ExtendedCodeBlock } from './extensions/code-block'
69
71
  import { useFileUpload } from '../../utils/useFileUpload'
70
72
  import { TextEditorEmits, TextEditorProps } from './types'
71
73
 
72
- const lowlight = createLowlight(common)
73
-
74
74
  function defaultUploadFunction(file: File) {
75
75
  // useFileUpload is melon specific
76
76
  let fileUpload = useFileUpload()
@@ -100,6 +100,11 @@ const editor = ref<Editor | null>(null)
100
100
  const attrs = useAttrs()
101
101
  const attrsClass = computed(() => normalizeClass(attrs.class))
102
102
  const attrsStyle = computed(() => normalizeStyle(attrs.style))
103
+ const attrsWithoutClassStyle = computed(() => {
104
+ return Object.fromEntries(
105
+ Object.entries(attrs).filter(([key]) => key !== 'class' && key !== 'style'),
106
+ )
107
+ })
103
108
 
104
109
  const editorProps = computed(() => {
105
110
  return {
@@ -154,8 +159,23 @@ onMounted(() => {
154
159
  extensions: [
155
160
  StarterKit.configure({
156
161
  ...props.starterkitOptions,
162
+ code: false,
157
163
  codeBlock: false,
158
164
  heading: false,
165
+ }).extend({
166
+ addKeyboardShortcuts() {
167
+ return {
168
+ Backspace: () => {
169
+ const { $from } = this.editor.view.state.selection
170
+ if (
171
+ !this.editor.can().liftListItem('listItem') ||
172
+ $from.parentOffset > 0
173
+ )
174
+ return false
175
+ return this.editor.commands.liftListItem('listItem')
176
+ },
177
+ }
178
+ },
159
179
  }),
160
180
  Heading.configure({
161
181
  ...(typeof props.starterkitOptions?.heading === 'object' &&
@@ -180,11 +200,8 @@ onMounted(() => {
180
200
  TextStyle,
181
201
  NamedColorExtension,
182
202
  NamedHighlightExtension,
183
- CodeBlockLowlight.extend({
184
- addNodeView() {
185
- return VueNodeViewRenderer(CodeBlockComponent)
186
- },
187
- }).configure({ lowlight }),
203
+ ExtendedCode,
204
+ ExtendedCodeBlock,
188
205
  ImageExtension.configure({
189
206
  uploadFunction: props.uploadFunction || defaultUploadFunction,
190
207
  }),
@@ -222,7 +239,6 @@ onMounted(() => {
222
239
  }),
223
240
  ContentPasteExtension.configure({
224
241
  enabled: true,
225
- showConfirmation: true,
226
242
  uploadFunction: props.uploadFunction || defaultUploadFunction,
227
243
  }),
228
244
  ...(props.extensions || []),
@@ -254,8 +270,10 @@ provide(
254
270
  computed(() => editor.value),
255
271
  )
256
272
 
273
+ const rootRef = useTemplateRef('rootRef')
257
274
  defineExpose({
258
275
  editor,
276
+ rootRef,
259
277
  })
260
278
  </script>
261
279
 
@@ -0,0 +1,118 @@
1
+ import { common, createLowlight } from 'lowlight'
2
+ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
3
+ import Code from '@tiptap/extension-code'
4
+ import CodeBlockComponent from '../CodeBlockComponent.vue'
5
+ import { VueNodeViewRenderer } from '@tiptap/vue-3'
6
+ import { markInputRule } from '@tiptap/core'
7
+
8
+ const INDENT = ' '.repeat(4)
9
+ export const inputRegex = /(?<=^|[^`])`([^`]+)`(?!`)$/
10
+
11
+ function getCodeBlockCtx(state: any) {
12
+ const { $from, from, to } = state.selection
13
+ let d = $from.depth
14
+ while (d > 0 && $from.node(d).type.name !== 'codeBlock') d--
15
+ if (d === 0) return null
16
+
17
+ const node = $from.node(d)
18
+ const start = $from.start(d)
19
+ const text: string = node.textContent
20
+ const fromOffset = from - start
21
+ const toOffset = to - start
22
+ return { start, text, fromOffset, toOffset }
23
+ }
24
+
25
+ function lineStartsBetween(text: string, fromOffset: number, toOffset: number) {
26
+ let startOff = text.lastIndexOf('\n', Math.max(0, fromOffset - 1)) + 1
27
+ let endOff = toOffset
28
+ if (endOff > 0 && text[endOff - 1] === '\n') endOff--
29
+
30
+ const starts: number[] = [startOff]
31
+ let i = startOff
32
+ while (true) {
33
+ const nl = text.indexOf('\n', i)
34
+ if (nl === -1 || nl >= endOff) break
35
+ starts.push(nl + 1)
36
+ i = nl + 1
37
+ }
38
+ return starts
39
+ }
40
+
41
+ const lowlight = createLowlight(common)
42
+
43
+ export const ExtendedCodeBlock = CodeBlockLowlight.extend({
44
+ addNodeView() {
45
+ return VueNodeViewRenderer(CodeBlockComponent)
46
+ },
47
+ addKeyboardShortcuts() {
48
+ return {
49
+ Tab: () => {
50
+ const { state, dispatch } = this.editor.view
51
+ const ctx = getCodeBlockCtx(state)
52
+ if (!ctx) return false
53
+
54
+ const { start, text, fromOffset, toOffset } = ctx
55
+ const multiline = text.slice(fromOffset, toOffset).includes('\n')
56
+ const tr = state.tr
57
+ if (multiline) {
58
+ for (const off of lineStartsBetween(text, fromOffset, toOffset)) {
59
+ const pos = tr.mapping.map(start + off)
60
+ tr.insertText(INDENT, pos)
61
+ }
62
+ } else {
63
+ tr.insertText(INDENT, state.selection.from)
64
+ }
65
+ dispatch(tr)
66
+ return true
67
+ },
68
+ 'Shift-Tab': () => {
69
+ const { state, dispatch } = this.editor.view
70
+ const ctx = getCodeBlockCtx(state)
71
+ if (!ctx) return false
72
+
73
+ const { start, text, fromOffset, toOffset } = ctx
74
+ const tr = state.tr
75
+ for (const off of lineStartsBetween(text, fromOffset, toOffset)) {
76
+ let len = 0
77
+ if (text.substr(off, 4) === INDENT) len = 4
78
+ else if (text[off] === '\t') len = 1
79
+
80
+ if (len > 0) {
81
+ const s = tr.mapping.map(start + off)
82
+ const e = tr.mapping.map(start + off + len)
83
+ tr.delete(s, e)
84
+ }
85
+ }
86
+
87
+ dispatch(tr)
88
+ return true
89
+ },
90
+ '`': () => {
91
+ const { from, to } = this.editor.state.selection
92
+ if (from === to) return false
93
+ return this.editor.commands.toggleCode()
94
+ },
95
+ }
96
+ },
97
+ }).configure({ lowlight })
98
+
99
+ export const ExtendedCode = Code.extend({
100
+ addKeyboardShortcuts() {
101
+ return {
102
+ '`': () => {
103
+ const { from, to } = this.editor.state.selection
104
+ if (from === to) return false
105
+ return this.editor.commands.toggleCode()
106
+ },
107
+ }
108
+ },
109
+
110
+ addInputRules() {
111
+ return [
112
+ markInputRule({
113
+ find: inputRegex,
114
+ type: this.type,
115
+ }),
116
+ ]
117
+ },
118
+ })
@@ -7,7 +7,6 @@ import { processMultipleImages } from './image/image-extension'
7
7
 
8
8
  export interface ContentPasteOptions {
9
9
  enabled: boolean
10
- showConfirmation: boolean
11
10
  uploadFunction: Function | null
12
11
  }
13
12
 
@@ -17,7 +16,6 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
17
16
  addOptions() {
18
17
  return {
19
18
  enabled: true,
20
- showConfirmation: true,
21
19
  uploadFunction: null,
22
20
  }
23
21
  },
@@ -28,6 +26,30 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
28
26
  new Plugin({
29
27
  key: new PluginKey('contentPaste'),
30
28
  props: {
29
+ handleDOMEvents: {
30
+ copy: (view, event) => {
31
+ const selection = window.getSelection()
32
+ if (!selection) return false
33
+
34
+ const container = document.createElement('div')
35
+ for (let i = 0; i < selection.rangeCount; i++) container.appendChild(selection.getRangeAt(i).cloneContents())
36
+
37
+ // Update relative image srcs
38
+ const images = container.querySelectorAll('img')
39
+ images.forEach((img) => {
40
+ const src = img.getAttribute('src')
41
+ if (src && src.startsWith('/')) {
42
+ img.setAttribute('src', `${window.location.origin}${src}`)
43
+ }
44
+ })
45
+
46
+ // Override clipboard HTML
47
+ event.clipboardData?.setData('text/html', container.innerHTML)
48
+ event.clipboardData?.setData('text/plain', selection.toString())
49
+ event.preventDefault()
50
+ return true
51
+ },
52
+ },
31
53
  handlePaste: (
32
54
  view: EditorView,
33
55
  event: ClipboardEvent,
@@ -47,19 +69,19 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
47
69
  return true
48
70
  }
49
71
 
72
+ // handle html with media
73
+ const htmlData = event.clipboardData?.getData('text/html')
74
+ if (htmlData) {
75
+ processHTMLImages(htmlData, view, this.options)
76
+ return true
77
+ }
78
+
50
79
  // handle markdown pasting
51
80
  const text = event.clipboardData?.getData('text/plain')
52
81
  if (!text) return false
53
82
 
54
83
  if (!detectMarkdown(text)) return false
55
84
 
56
- if (this.options.showConfirmation) {
57
- const shouldConvert = confirm(
58
- 'Do you want to convert markdown content to HTML before pasting?',
59
- )
60
- if (!shouldConvert) return false
61
- }
62
-
63
85
  const htmlContent = markdownToHTML(text)
64
86
  const tempDiv = document.createElement('div')
65
87
  tempDiv.innerHTML = htmlContent
@@ -79,3 +101,51 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
79
101
  ]
80
102
  },
81
103
  })
104
+
105
+ async function processHTMLImages(
106
+ html: string,
107
+ view: EditorView,
108
+ extensionOptions: ContentPasteOptions,
109
+ ): Promise<undefined> {
110
+ const tempDiv = document.createElement('div')
111
+ tempDiv.innerHTML = html
112
+ const images = tempDiv.querySelectorAll('img')
113
+
114
+ const parser = DOMParser.fromSchema(view.state.schema)
115
+ const parsedSlice = parser.parseSlice(tempDiv, {
116
+ preserveWhitespace: true,
117
+ })
118
+ const tr = view.state.tr.replaceSelection(parsedSlice)
119
+ view.dispatch(tr)
120
+
121
+ const imageInfo: Array<{ src: string; pos: number }> = []
122
+ view.state.doc.descendants((node, pos) => {
123
+ for (let img of images) {
124
+ const src = img.getAttribute('src')
125
+ if (node.type.name === 'image' && node.attrs['src'] === src) {
126
+ imageInfo.push([src, pos])
127
+ }
128
+ }
129
+ })
130
+
131
+ // Process each image
132
+ const imagePromises = Array.from(imageInfo).map(async ([src, pos]) => {
133
+ if (
134
+ src.startsWith('data:') ||
135
+ src.startsWith('blob:')
136
+ ) {
137
+ try {
138
+ const response = await fetch(src)
139
+ const blob = await response.blob()
140
+ const file = new File([blob], 'pasted-data-image.png', {
141
+ type: blob.type,
142
+ })
143
+ processMultipleImages([file], view, pos, extensionOptions)
144
+ } catch (error) {
145
+ console.error('Failed to process image:', error)
146
+ }
147
+ }
148
+ })
149
+
150
+ await Promise.all(imagePromises)
151
+ }
@@ -9,7 +9,7 @@ import { textblockTypeInputRule } from '@tiptap/core'
9
9
  export const Heading = TiptapHeading.extend({
10
10
  addInputRules() {
11
11
  return this.options.levels.map((level) => {
12
- let regexp = new RegExp(`^(#{${level}}) $`)
12
+ let regexp = new RegExp(`^(#{${level}})( |\\u00A0)$`)
13
13
  return textblockTypeInputRule({
14
14
  find: regexp,
15
15
  type: this.type,
@@ -373,14 +373,18 @@ function uploadImageBase(
373
373
  })
374
374
 
375
375
  const tr = view.state.tr
376
-
377
- if (pos != null) {
378
- tr.insert(pos, node)
379
- } else if (insertMode === 'replace') {
380
- tr.replaceSelectionWith(node)
376
+ if (insertMode === 'replace') {
377
+ if (pos === null) tr.replaceSelectionWith(node)
378
+ else {
379
+ const nodeAtPos = view.state.doc.nodeAt(pos)
380
+ if (nodeAtPos) tr.replaceWith(pos, pos + nodeAtPos.nodeSize, node)
381
+ }
381
382
  } else {
382
- const insertPos = view.state.selection.from
383
- tr.insert(insertPos, node)
383
+ if (pos != null) tr.insert(pos, node)
384
+ else {
385
+ const insertPos = view.state.selection.from
386
+ tr.insert(insertPos, node)
387
+ }
384
388
  }
385
389
 
386
390
  view.dispatch(tr)
@@ -24,7 +24,6 @@ function createMentionNode(component?: Component) {
24
24
  inline: true,
25
25
  selectable: true,
26
26
  atom: true,
27
-
28
27
  addOptions() {
29
28
  return {
30
29
  component: undefined,
@@ -82,6 +81,9 @@ function createMentionNode(component?: Component) {
82
81
  `@${HTMLAttributes['data-label'] || HTMLAttributes.id || ''}`,
83
82
  ]
84
83
  },
84
+ renderText({ node }: any) {
85
+ return `@${node.attrs.label || node.attrs.id || ''}`
86
+ }
85
87
  }
86
88
 
87
89
  if (component) {
@@ -139,10 +141,6 @@ const MentionSuggestionExtension =
139
141
  type: 'mention',
140
142
  attrs: attributes,
141
143
  },
142
- {
143
- type: 'text',
144
- text: ' ',
145
- },
146
144
  ])
147
145
  .run()
148
146
  },
@@ -1,7 +1,11 @@
1
1
  .mention {
2
2
  font-weight: 600;
3
3
  box-decoration-break: clone;
4
- padding: 1px 3px;
4
+ padding: 0 1px;
5
5
  border-radius: 4px;
6
6
  display: inline-block;
7
7
  }
8
+
9
+ .mention.ProseMirror-selectednode, .mention:hover {
10
+ background-color: var(--surface-gray-1, #f8f8f8);
11
+ }
@@ -5,7 +5,7 @@ import {
5
5
  BaseSuggestionItem,
6
6
  } from '../suggestion/createSuggestionExtension'
7
7
  import SuggestionList from '../suggestion/SuggestionList.vue'
8
- import { toValue, unref } from 'vue'
8
+ import { toValue } from 'vue'
9
9
 
10
10
  export const TagNode = Node.create({
11
11
  name: 'tagItem',
@@ -59,7 +59,9 @@ export const TagNode = Node.create({
59
59
  `#${HTMLAttributes['data-tag-label']}`,
60
60
  ]
61
61
  },
62
-
62
+ renderText({ node }: any) {
63
+ return `#${node.attrs.tagLabel || ''}`
64
+ },
63
65
  addCommands() {
64
66
  return {
65
67
  setTag:
@@ -111,16 +111,6 @@ export const LinkExtension = Link.extend({
111
111
  return true
112
112
  })
113
113
 
114
- const posAfterLink = selectionTo
115
- const charAfter =
116
- posAfterLink < doc.content.size
117
- ? doc.textBetween(posAfterLink, posAfterLink + 1)
118
- : null
119
-
120
- if (charAfter === null || charAfter !== ' ') {
121
- chain = chain.insertContent(' ')
122
- }
123
-
124
114
  chain.run()
125
115
  })
126
116
  .catch(() => {})