frappe-ui 0.1.145 → 0.1.147

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 (22) hide show
  1. package/package.json +2 -1
  2. package/src/components/Button/Button.vue +1 -1
  3. package/src/components/Charts/Charts.story.vue +4 -0
  4. package/src/components/Charts/axisChartOptions.ts +54 -74
  5. package/src/components/Charts/eChartOptions.ts +16 -4
  6. package/src/components/Charts/helpers.ts +31 -0
  7. package/src/components/Charts/types.ts +21 -0
  8. package/src/components/FormControl.vue +8 -11
  9. package/src/components/Input.vue +1 -1
  10. package/src/components/ListView/ListView.vue +1 -0
  11. package/src/components/TextEditor/TextEditor.vue +45 -4
  12. package/src/components/TextEditor/{EmojiList.vue → extensions/emoji/EmojiList.vue} +15 -10
  13. package/src/components/TextEditor/extensions/emoji/emoji-extension.ts +55 -0
  14. package/src/components/TextEditor/extensions/heading/heading.ts +20 -0
  15. package/src/components/TextEditor/{SlashCommandsList.vue → extensions/slash-commands/SlashCommandsList.vue} +22 -10
  16. package/src/components/TextEditor/extensions/slash-commands/slash-commands-extension.ts +155 -0
  17. package/src/components/TextEditor/{SuggestionList.vue → extensions/suggestion/SuggestionList.vue} +4 -7
  18. package/src/components/TextEditor/extensions/suggestion/createSuggestionExtension.ts +151 -0
  19. package/src/components/TextEditor/extensions/tag/tag-extension.ts +155 -0
  20. package/src/components/TextEditor/emoji-extension.js +0 -100
  21. package/src/components/TextEditor/slash-commands-extension.ts +0 -244
  22. /package/src/components/TextEditor/{emojis.json → extensions/emoji/emojis.json} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
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
  "@tiptap/extension-code-block": "^2.11.9",
38
38
  "@tiptap/extension-code-block-lowlight": "^2.11.5",
39
39
  "@tiptap/extension-color": "^2.0.3",
40
+ "@tiptap/extension-heading": "^2.12.0",
40
41
  "@tiptap/extension-highlight": "^2.0.3",
41
42
  "@tiptap/extension-image": "^2.0.3",
42
43
  "@tiptap/extension-link": "^2.0.3",
@@ -36,7 +36,7 @@
36
36
  <component v-else-if="icon" :is="icon" :class="slotClasses" />
37
37
  <slot name="icon" v-else-if="$slots.icon" />
38
38
  </template>
39
- <span v-else :class="{ 'sr-only': isIconButton }">
39
+ <span v-else :class="{ 'sr-only': isIconButton }" class="truncate">
40
40
  <slot>{{ label }}</slot>
41
41
  </span>
42
42
 
@@ -47,6 +47,10 @@ const simpleConfig: AxisChartConfig = {
47
47
  },
48
48
  yAxis: {
49
49
  title: 'Amount ($)',
50
+ echartOptions: {
51
+ min: 0,
52
+ max: 800,
53
+ }
50
54
  },
51
55
  series: [{ name: 'sales', type: 'bar' }],
52
56
  }
@@ -1,5 +1,5 @@
1
1
  import useEchartsOptions from './eChartOptions'
2
- import { formatValue } from './helpers'
2
+ import { formatValue, mergeDeep } from './helpers'
3
3
  import {
4
4
  AreaSeriesConfig,
5
5
  AxisChartConfig,
@@ -33,61 +33,60 @@ export default function useAxisChartOptions(config: AxisChartConfig) {
33
33
  baseOptions.yAxis[1].show = true
34
34
  }
35
35
 
36
- return {
37
- ...baseOptions,
38
- series: config.series.map((s, idx) => {
39
- let labelPosition = 'top'
40
- if (s.type == 'bar' && config.stacked) {
41
- labelPosition = idx == lastBarSeriesIdx ? 'top' : 'inside'
42
- }
43
- if (s.type == 'bar' && swapXY) {
44
- labelPosition = 'right'
45
- }
46
-
47
- const standardOptions = {
48
- type: s.type,
49
- name: s.name,
50
- data: data.map((row: any) => {
51
- let x, y
52
- if (swapXY) {
53
- x = row[s.name]
54
- y = row[config.xAxis.key]
55
- } else {
56
- x = row[config.xAxis.key]
57
- y = row[s.name]
58
- }
59
- return [x, y]
60
- }),
61
- yAxisIndex: s.axis === 'y2' ? 1 : 0,
62
- label: {
63
- show: s.showDataLabels,
64
- position: labelPosition,
65
- formatter: (params: any) => {
66
- const _val = swapXY ? params.value?.[0] : params.value?.[1]
67
- return formatValue(_val, 1, true)
68
- },
69
- fontSize: 11,
70
- },
71
- labelLayout: { hideOverlap: true },
72
- itemStyle: {
73
- color: s.color,
36
+ baseOptions.series = config.series.map((s, idx) => {
37
+ let labelPosition = 'top'
38
+ if (s.type == 'bar' && config.stacked) {
39
+ labelPosition = idx == lastBarSeriesIdx ? 'top' : 'inside'
40
+ }
41
+ if (s.type == 'bar' && swapXY) {
42
+ labelPosition = 'right'
43
+ }
44
+
45
+ const standardSeriesOptions = {
46
+ type: s.type,
47
+ name: s.name,
48
+ data: data.map((row: any) => {
49
+ let x, y
50
+ if (swapXY) {
51
+ x = row[s.name]
52
+ y = row[config.xAxis.key]
53
+ } else {
54
+ x = row[config.xAxis.key]
55
+ y = row[s.name]
56
+ }
57
+ return [x, y]
58
+ }),
59
+ yAxisIndex: s.axis === 'y2' ? 1 : 0,
60
+ label: {
61
+ show: s.showDataLabels,
62
+ position: labelPosition,
63
+ formatter: (params: any) => {
64
+ const _val = swapXY ? params.value?.[0] : params.value?.[1]
65
+ return formatValue(_val, 1, true)
74
66
  },
75
- }
76
-
77
- let seriesTypeOptions = {}
78
- if (s.type === 'bar') {
79
- seriesTypeOptions = getBarSeriesOptions(config, s)
80
- }
81
- if (s.type === 'line') {
82
- seriesTypeOptions = getLineSeriesOptions(config, s)
83
- }
84
- if (s.type === 'area') {
85
- seriesTypeOptions = getAreaSeriesOptions(config, s)
86
- }
87
-
88
- return mergeDeep(standardOptions, seriesTypeOptions)
89
- }),
90
- }
67
+ fontSize: 11,
68
+ },
69
+ labelLayout: { hideOverlap: true },
70
+ itemStyle: {
71
+ color: s.color,
72
+ },
73
+ }
74
+
75
+ let seriesTypeOptions = {}
76
+ if (s.type === 'bar') {
77
+ seriesTypeOptions = getBarSeriesOptions(config, s)
78
+ }
79
+ if (s.type === 'line') {
80
+ seriesTypeOptions = getLineSeriesOptions(config, s)
81
+ }
82
+ if (s.type === 'area') {
83
+ seriesTypeOptions = getAreaSeriesOptions(config, s)
84
+ }
85
+
86
+ return mergeDeep(standardSeriesOptions, seriesTypeOptions, s.echartOptions)
87
+ })
88
+
89
+ return mergeDeep(baseOptions, config.echartOptions)
91
90
  }
92
91
 
93
92
  function getBarSeriesOptions(config: AxisChartConfig, series: BarSeriesConfig) {
@@ -143,22 +142,3 @@ function getAreaSeriesOptions(
143
142
  },
144
143
  }
145
144
  }
146
-
147
- function isObject(item: any) {
148
- return item && typeof item === 'object' && !Array.isArray(item)
149
- }
150
-
151
- function mergeDeep(target: any, source: any) {
152
- let output = Object.assign({}, target)
153
- if (isObject(target) && isObject(source)) {
154
- Object.keys(source).forEach((key) => {
155
- if (isObject(source[key])) {
156
- if (!(key in target)) Object.assign(output, { [key]: source[key] })
157
- else output[key] = mergeDeep(target[key], source[key])
158
- } else {
159
- Object.assign(output, { [key]: source[key] })
160
- }
161
- })
162
- }
163
- return output
164
- }
@@ -1,4 +1,4 @@
1
- import { formatDate, formatLabel, formatValue } from './helpers'
1
+ import { formatDate, formatLabel, formatValue, mergeDeep } from './helpers'
2
2
  import { AxisChartConfig } from './types'
3
3
 
4
4
  export const PADDING_TOP = 0
@@ -154,7 +154,7 @@ export function getTitleOptions(title: string, subtitle?: string) {
154
154
  }
155
155
 
156
156
  function getXAxisOptions(config: AxisChartConfig) {
157
- return config.swapXY
157
+ const options = config.swapXY
158
158
  ? {
159
159
  show: true,
160
160
  type: 'value',
@@ -220,10 +220,12 @@ function getXAxisOptions(config: AxisChartConfig) {
220
220
  margin: 8,
221
221
  },
222
222
  }
223
+
224
+ return mergeDeep(options, config.swapXY ? config.yAxis.echartOptions : config.xAxis.echartOptions)
223
225
  }
224
226
 
225
227
  function getYAxisOptions(config: AxisChartConfig) {
226
- const primaryYAxisOptions = config.swapXY
228
+ let primaryYAxisOptions = config.swapXY
227
229
  ? {
228
230
  show: true,
229
231
  type: config.xAxis.type,
@@ -288,7 +290,12 @@ function getYAxisOptions(config: AxisChartConfig) {
288
290
  max: config.yAxis.yMax,
289
291
  }
290
292
 
291
- const secondaryYAxisOptions = {
293
+ primaryYAxisOptions = mergeDeep(
294
+ primaryYAxisOptions,
295
+ config.swapXY ? config.xAxis.echartOptions : config.yAxis.echartOptions
296
+ )
297
+
298
+ let secondaryYAxisOptions = {
292
299
  show: false,
293
300
  type: 'value',
294
301
  z: 2,
@@ -330,6 +337,11 @@ function getYAxisOptions(config: AxisChartConfig) {
330
337
  max: config.y2Axis?.yMax,
331
338
  }
332
339
 
340
+ secondaryYAxisOptions = mergeDeep(
341
+ secondaryYAxisOptions,
342
+ config.swapXY ? config.y2Axis?.echartOptions : config.y2Axis?.echartOptions
343
+ )
344
+
333
345
  return config.swapXY
334
346
  ? [primaryYAxisOptions]
335
347
  : [primaryYAxisOptions, secondaryYAxisOptions]
@@ -73,3 +73,34 @@ export function formatDate(date: string, format?: string, grain: TimeGrain = 'da
73
73
 
74
74
  return dayjs(date).format(format)
75
75
  }
76
+
77
+
78
+ export function isObject(item: any) {
79
+ return item && typeof item === 'object' && !Array.isArray(item)
80
+ }
81
+
82
+ export function mergeDeep(target: any, ...sources: any[]) {
83
+ if (!sources.length) return target;
84
+ const source = sources.shift();
85
+
86
+ if (!source || !isObject(target) || !isObject(source)) {
87
+ // Skip the current source if it's not a proper object
88
+ return mergeDeep(target, ...sources);
89
+ }
90
+
91
+ let output = Object.assign({}, target);
92
+
93
+ Object.keys(source).forEach((key) => {
94
+ if (isObject(source[key])) {
95
+ if (!(key in output)) {
96
+ output[key] = source[key];
97
+ } else {
98
+ output[key] = mergeDeep(output[key], source[key]);
99
+ }
100
+ } else {
101
+ output[key] = source[key];
102
+ }
103
+ });
104
+
105
+ return mergeDeep(output, ...sources);
106
+ }
@@ -10,20 +10,32 @@ export type AxisChartConfig = {
10
10
  type: 'category' | 'time' | 'value'
11
11
  timeGrain?: TimeGrain
12
12
  title?: string
13
+ echartOptions?: {
14
+ [key: string]: any
15
+ }
13
16
  }
14
17
  yAxis: {
15
18
  title?: string
16
19
  yMin?: number
17
20
  yMax?: number
21
+ echartOptions?: {
22
+ [key: string]: any
23
+ }
18
24
  }
19
25
  y2Axis?: {
20
26
  title?: string
21
27
  yMin?: number
22
28
  yMax?: number
29
+ echartOptions?: {
30
+ [key: string]: any
31
+ }
23
32
  }
24
33
  swapXY?: boolean
25
34
  stacked?: boolean
26
35
  series: (BarSeriesConfig | LineSeriesConfig | AreaSeriesConfig)[]
36
+ echartOptions?: {
37
+ [key: string]: any
38
+ }
27
39
  }
28
40
 
29
41
  export type SeriesConfig = {
@@ -32,6 +44,9 @@ export type SeriesConfig = {
32
44
  color?: string
33
45
  axis?: 'y' | 'y2'
34
46
  showDataLabels?: boolean
47
+ echartOptions?: {
48
+ [key: string]: any
49
+ }
35
50
  }
36
51
 
37
52
  export type BarSeriesConfig = SeriesConfig & {
@@ -61,6 +76,9 @@ export type DonutChartConfig = {
61
76
  valueColumn: string
62
77
  maxSliceCount?: number
63
78
  showInlineLabels?: boolean
79
+ echartOptions?: {
80
+ [key: string]: any
81
+ }
64
82
  }
65
83
 
66
84
  export type FunnelChartConfig = {
@@ -71,6 +89,9 @@ export type FunnelChartConfig = {
71
89
  categoryColumn: string
72
90
  valueColumn: string
73
91
  showPercentages?: boolean
92
+ echartOptions?: {
93
+ [key: string]: any
94
+ }
74
95
  }
75
96
 
76
97
  export type NumberChartConfig = {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div v-if="type != 'checkbox'" :class="['space-y-1.5', attrs.class]">
2
+ <div v-if="type != 'checkbox'" :class="['space-y-1.5', attrs.class]" :style="attrs.style">
3
3
  <FormLabel
4
4
  v-if="label"
5
5
  :label="label"
@@ -11,6 +11,7 @@
11
11
  v-if="type === 'select'"
12
12
  :id="id"
13
13
  v-bind="{ ...controlAttrs, size }"
14
+ v-model="model"
14
15
  >
15
16
  <template #prefix v-if="$slots.prefix">
16
17
  <slot name="prefix" />
@@ -19,6 +20,7 @@
19
20
  <Autocomplete
20
21
  v-else-if="type === 'autocomplete'"
21
22
  v-bind="{ ...controlAttrs }"
23
+ v-model="model"
22
24
  >
23
25
  <template #prefix v-if="$slots.prefix">
24
26
  <slot name="prefix" />
@@ -31,11 +33,13 @@
31
33
  v-else-if="type === 'textarea'"
32
34
  :id="id"
33
35
  v-bind="{ ...controlAttrs, size }"
36
+ v-model="model"
34
37
  />
35
38
  <TextInput
36
39
  v-else
37
40
  :id="id"
38
41
  v-bind="{ ...controlAttrs, type, size, required }"
42
+ v-model="model"
39
43
  >
40
44
  <template #prefix v-if="$slots.prefix">
41
45
  <slot name="prefix" />
@@ -52,6 +56,7 @@
52
56
  v-else
53
57
  :id="id"
54
58
  v-bind="{ ...controlAttrs, label, size, class: attrs.class }"
59
+ v-model="model"
55
60
  />
56
61
  </template>
57
62
  <script setup lang="ts">
@@ -79,6 +84,8 @@ const props = withDefaults(defineProps<FormControlProps>(), {
79
84
  size: 'sm',
80
85
  })
81
86
 
87
+ const model = defineModel()
88
+
82
89
  const attrs = useAttrs()
83
90
  const controlAttrs = computed(() => {
84
91
  // pass everything except class and style
@@ -91,16 +98,6 @@ const controlAttrs = computed(() => {
91
98
  return _attrs
92
99
  })
93
100
 
94
- const labelClasses = computed(() => {
95
- return [
96
- {
97
- sm: 'text-xs',
98
- md: 'text-base',
99
- }[props.size],
100
- 'text-ink-gray-5',
101
- ]
102
- })
103
-
104
101
  const descriptionClasses = computed(() => {
105
102
  return [
106
103
  {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <label :class="[type == 'checkbox' ? 'flex' : 'block', $attrs.class]">
2
+ <label :class="[type == 'checkbox' ? 'flex' : 'block', $attrs.class]" :style="$attrs.style">
3
3
  <span
4
4
  v-if="label && type != 'checkbox'"
5
5
  class="mb-2 block text-sm leading-4 text-gray-700"
@@ -3,6 +3,7 @@
3
3
  <div
4
4
  class="flex w-max min-w-full flex-col overflow-y-hidden"
5
5
  :class="$attrs.class"
6
+ :style="$attrs.style"
6
7
  >
7
8
  <slot v-bind="{ showGroupedRows, selectable }">
8
9
  <ListHeader />
@@ -1,5 +1,10 @@
1
1
  <template>
2
- <div class="relative w-full" :class="$attrs.class" v-if="editor">
2
+ <div
3
+ class="relative w-full"
4
+ :class="$attrs.class"
5
+ :style="$attrs.style"
6
+ v-if="editor"
7
+ >
3
8
  <TextEditorBubbleMenu :buttons="bubbleMenu" :options="bubbleMenuOptions" />
4
9
  <TextEditorFixedMenu
5
10
  class="w-full overflow-x-auto rounded-t-lg border border-outline-gray-modals"
@@ -14,7 +19,7 @@
14
19
  </div>
15
20
  </template>
16
21
 
17
- <script>
22
+ <script lang="ts">
18
23
  import { normalizeClass, computed } from 'vue'
19
24
  import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3'
20
25
  import StarterKit from '@tiptap/starter-kit'
@@ -39,10 +44,12 @@ import configureMention from './mention'
39
44
  import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
40
45
  import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
41
46
  import TextEditorFloatingMenu from './TextEditorFloatingMenu.vue'
42
- import EmojiExtension from './emoji-extension'
43
- import SlashCommands from './slash-commands-extension'
47
+ import EmojiExtension from './extensions/emoji/emoji-extension'
48
+ import SlashCommands from './extensions/slash-commands/slash-commands-extension'
44
49
  import { detectMarkdown, markdownToHTML } from '../../utils/markdown'
45
50
  import { DOMParser } from 'prosemirror-model'
51
+ import { TagNode, TagExtension } from './extensions/tag/tag-extension'
52
+ import { Heading } from './extensions/heading/heading'
46
53
 
47
54
  const lowlight = createLowlight(common)
48
55
 
@@ -100,6 +107,10 @@ export default {
100
107
  type: Array,
101
108
  default: () => [],
102
109
  },
110
+ tags: {
111
+ type: Array,
112
+ default: () => [],
113
+ },
103
114
  uploadFunction: {
104
115
  type: Function,
105
116
  default: () => null,
@@ -147,6 +158,13 @@ export default {
147
158
  StarterKit.configure({
148
159
  ...this.starterkitOptions,
149
160
  codeBlock: false,
161
+ heading: false,
162
+ }),
163
+ Heading.configure({
164
+ ...(typeof this.starterkitOptions?.heading === 'object' &&
165
+ this.starterkitOptions.heading !== null
166
+ ? this.starterkitOptions.heading
167
+ : {}),
150
168
  }),
151
169
  Table.configure({
152
170
  resizable: true,
@@ -185,6 +203,10 @@ export default {
185
203
  configureMention(this.mentions),
186
204
  EmojiExtension,
187
205
  SlashCommands,
206
+ TagNode,
207
+ TagExtension.configure({
208
+ tags: () => this.tags,
209
+ }),
188
210
  ...(this.extensions || []),
189
211
  ],
190
212
  onUpdate: ({ editor }) => {
@@ -306,4 +328,23 @@ img.ProseMirror-selectednode {
306
328
  border-radius: 3px;
307
329
  padding: 0 2px;
308
330
  }
331
+ .tag-item,
332
+ .tag-suggestion-active {
333
+ background-color: var(--surface-gray-1, #f8f8f8);
334
+ color: inherit;
335
+ border: 1px solid transparent;
336
+ padding: 0px 2px;
337
+ border-radius: 4px;
338
+ font-size: 1em;
339
+ white-space: nowrap;
340
+ cursor: default;
341
+ }
342
+
343
+ .tag-item.ProseMirror-selectednode {
344
+ border-color: var(--outline-gray-3, #c7c7c7);
345
+ }
346
+
347
+ .tag-suggestion-active {
348
+ background-color: var(--surface-gray-2, #f3f3f3);
349
+ }
309
350
  </style>
@@ -2,7 +2,7 @@
2
2
  <SuggestionList
3
3
  ref="suggestionList"
4
4
  :items="items"
5
- :command="selectItem"
5
+ :command="(item) => onItemSelect(item as EmojiItem)"
6
6
  item-class="py-2"
7
7
  >
8
8
  <template #default="{ item }">
@@ -14,29 +14,34 @@
14
14
 
15
15
  <script setup lang="ts">
16
16
  import { ref, type PropType } from 'vue'
17
- import SuggestionList, { type SuggestionItem } from './SuggestionList.vue'
18
-
19
- interface EmojiItem extends SuggestionItem {
20
- name: string
21
- emoji: string
22
- }
17
+ import SuggestionList from '../suggestion/SuggestionList.vue'
18
+ import type { Editor, Range } from '@tiptap/core'
19
+ import type { EmojiItem } from './emoji-extension'
23
20
 
24
21
  const props = defineProps({
25
22
  items: {
26
23
  type: Array as PropType<EmojiItem[]>,
27
24
  required: true,
28
25
  },
26
+ editor: {
27
+ type: Object as PropType<Editor>,
28
+ required: true,
29
+ },
30
+ range: {
31
+ type: Object as PropType<Range>,
32
+ required: true,
33
+ },
29
34
  command: {
30
- type: Function as PropType<(params: { emoji: string }) => void>,
35
+ type: Function as PropType<(item: EmojiItem) => void>,
31
36
  required: true,
32
37
  },
33
38
  })
34
39
 
35
40
  const suggestionList = ref<InstanceType<typeof SuggestionList> | null>(null)
36
41
 
37
- const selectItem = (item: SuggestionItem) => {
42
+ const onItemSelect = (item: EmojiItem) => {
38
43
  if (item) {
39
- props.command({ emoji: item.emoji })
44
+ props.command(item)
40
45
  }
41
46
  }
42
47
 
@@ -0,0 +1,55 @@
1
+ import { PluginKey } from 'prosemirror-state'
2
+ import {
3
+ BaseSuggestionItem,
4
+ createSuggestionExtension,
5
+ } from '../suggestion/createSuggestionExtension'
6
+ import EmojiList from './EmojiList.vue'
7
+ import _EMOJIS from './emojis.json'
8
+
9
+ const EMOJIS = _EMOJIS as EmojiItem[]
10
+
11
+ export interface EmojiItem extends BaseSuggestionItem {
12
+ name: string
13
+ emoji: string
14
+ }
15
+
16
+ export default createSuggestionExtension<EmojiItem>({
17
+ name: 'emoji',
18
+ char: ':',
19
+ pluginKey: new PluginKey('emojiSuggestion'),
20
+ items: ({ query }: { query: string }) => {
21
+ return EMOJIS.filter((item) =>
22
+ item.name.toLowerCase().includes(query.toLowerCase()),
23
+ )
24
+ .sort((a, b) => {
25
+ const aName = a.name.toLowerCase()
26
+ const bName = b.name.toLowerCase()
27
+ const queryLower = query.toLowerCase()
28
+
29
+ // Exact matches first
30
+ if (aName === queryLower && bName !== queryLower) return -1
31
+ if (bName === queryLower && aName !== queryLower) return 1
32
+
33
+ // Then names starting with the query
34
+ if (aName.startsWith(queryLower) && !bName.startsWith(queryLower))
35
+ return -1
36
+ if (bName.startsWith(queryLower) && !aName.startsWith(queryLower))
37
+ return 1
38
+
39
+ // Then sort by name length (shorter first)
40
+ return aName.length - bName.length
41
+ })
42
+ .slice(0, 5)
43
+ },
44
+ command: ({ editor, range, props: item }) => {
45
+ if (item && item.emoji) {
46
+ editor.chain().focus().deleteRange(range).insertContent(item.emoji).run()
47
+ } else {
48
+ console.error(
49
+ 'Emoji command execution error: emoji property not found on selected item or item is invalid.',
50
+ item,
51
+ )
52
+ }
53
+ },
54
+ component: EmojiList,
55
+ })
@@ -0,0 +1,20 @@
1
+ import TiptapHeading from '@tiptap/extension-heading'
2
+ import { textblockTypeInputRule } from '@tiptap/core'
3
+
4
+ // This custom Heading extension modifies the default input rule behavior.
5
+ // The default Tiptap heading input rule converts text to a heading when '#' is followed by any whitespace (including Enter).
6
+ // This customization ensures that headings are only created when '#' is followed by a literal space.
7
+ // This change is necessary to prevent conflicts with other extensions, such as a tags extension,
8
+ // which uses '#' followed by Enter to add a tag.
9
+ export const Heading = TiptapHeading.extend({
10
+ addInputRules() {
11
+ return this.options.levels.map((level) => {
12
+ let regexp = new RegExp(`^(#{${level}}) $`)
13
+ return textblockTypeInputRule({
14
+ find: regexp,
15
+ type: this.type,
16
+ getAttributes: { level },
17
+ })
18
+ })
19
+ },
20
+ })