@lowdefy/blocks-tiptap 5.6.0 → 6.1.0

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.
@@ -50,27 +50,67 @@ function suggestion({ methods, char = '@', allowSpaces = true, limit = 5 }) {
50
50
  render: ()=>{
51
51
  let component;
52
52
  let popup;
53
- return {
54
- onStart: (props)=>{
55
- component = new ReactRenderer(MentionList, {
56
- props: {
57
- ...props,
58
- methods
59
- },
60
- editor: props.editor
61
- });
62
- if (!props.clientRect) return;
63
- popup = tippy('body', {
64
- getReferenceClientRect: props.clientRect,
65
- appendTo: ()=>document.body,
66
- content: component.element,
67
- showOnCreate: true,
68
- interactive: true,
69
- trigger: 'manual',
70
- placement: 'bottom-start'
53
+ let current;
54
+ const start = (props)=>{
55
+ current = {
56
+ from: props.range.from,
57
+ query: props.query
58
+ };
59
+ component = new ReactRenderer(MentionList, {
60
+ props: {
61
+ ...props,
62
+ methods
63
+ },
64
+ editor: props.editor
65
+ });
66
+ if (!props.clientRect) return;
67
+ popup = tippy('body', {
68
+ getReferenceClientRect: props.clientRect,
69
+ appendTo: ()=>document.body,
70
+ content: component.element,
71
+ showOnCreate: true,
72
+ interactive: true,
73
+ trigger: 'manual',
74
+ placement: 'bottom-start'
75
+ });
76
+ };
77
+ const exit = ()=>{
78
+ // Tear down in this order — React portal first, tippy popup after —
79
+ // to avoid "NotFoundError: Failed to execute 'removeChild'" when the
80
+ // user navigates away (e.g. clicks another item) while the mention
81
+ // popup is open. MentionList is rendered as a React portal into
82
+ // component.element, which tippy relocates into document.body.
83
+ // Destroying tippy first detaches that container out from under
84
+ // React's portal teardown; destroying the component first lets React
85
+ // remove the MentionList DOM cleanly while the container is still
86
+ // attached. The tippy teardown is then deferred to a microtask so it
87
+ // never runs synchronously inside React's own unmount commit.
88
+ component?.destroy();
89
+ component = undefined;
90
+ const instance = popup?.[0];
91
+ popup = undefined;
92
+ if (instance && !instance.state.isDestroyed) {
93
+ queueMicrotask(()=>{
94
+ if (!instance.state.isDestroyed) instance.destroy();
71
95
  });
72
- },
96
+ }
97
+ };
98
+ return {
99
+ onStart: start,
73
100
  onUpdate (props) {
101
+ // A suggestion that moves to a new trigger with a new query (e.g. a
102
+ // second "@" typed while the first was open) restarted in TipTap v2,
103
+ // which also showed a popup that an outside click had hidden. v3
104
+ // reports it as an update, so restart it here.
105
+ if (props.range.from !== current?.from && props.query !== current?.query) {
106
+ exit();
107
+ start(props);
108
+ return;
109
+ }
110
+ current = {
111
+ from: props.range.from,
112
+ query: props.query
113
+ };
74
114
  component?.updateProps(props);
75
115
  if (!props.clientRect) return;
76
116
  popup?.[0]?.setProps({
@@ -84,27 +124,7 @@ function suggestion({ methods, char = '@', allowSpaces = true, limit = 5 }) {
84
124
  }
85
125
  return component?.ref?.onKeyDown(props) ?? false;
86
126
  },
87
- onExit () {
88
- // Tear down in this order — React portal first, tippy popup after —
89
- // to avoid "NotFoundError: Failed to execute 'removeChild'" when the
90
- // user navigates away (e.g. clicks another item) while the mention
91
- // popup is open. MentionList is rendered as a React portal into
92
- // component.element, which tippy relocates into document.body.
93
- // Destroying tippy first detaches that container out from under
94
- // React's portal teardown; destroying the component first lets React
95
- // remove the MentionList DOM cleanly while the container is still
96
- // attached. The tippy teardown is then deferred to a microtask so it
97
- // never runs synchronously inside React's own unmount commit.
98
- component?.destroy();
99
- component = undefined;
100
- const instance = popup?.[0];
101
- popup = undefined;
102
- if (instance && !instance.state.isDestroyed) {
103
- queueMicrotask(()=>{
104
- if (!instance.state.isDestroyed) instance.destroy();
105
- });
106
- }
107
- }
127
+ onExit: exit
108
128
  };
109
129
  }
110
130
  };
@@ -0,0 +1,140 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ id: tiptap-mention-input
16
+ type: Box
17
+
18
+ events:
19
+ onMount:
20
+ - id: seed_content
21
+ type: SetState
22
+ params:
23
+ tiptap_mention_with_value:
24
+ html: '<p>Hi <span class="tiptap-mention">@Alice</span>.</p>'
25
+ tiptap_mention_disabled:
26
+ html: '<p>Disabled mention editor.</p>'
27
+ # Saved html round-trip: a mention node comes back with a STRING id
28
+ # (data-id) and its label in data-label.
29
+ tiptap_mention_saved:
30
+ html: '<p>Ping <span data-type="mention" class="tiptap-mention" data-id="user_9" data-label="Zed" data-mention-suggestion-char="@">@Zed</span> please.</p>'
31
+
32
+ blocks:
33
+ - id: tiptap_mention_empty
34
+ type: TiptapMentionInput
35
+
36
+ - id: tiptap_mention_placeholder
37
+ type: TiptapMentionInput
38
+ properties:
39
+ placeholder: 'Type @ to mention...'
40
+
41
+ - id: tiptap_mention_with_value
42
+ type: TiptapMentionInput
43
+
44
+ - id: tiptap_mention_disabled
45
+ type: TiptapMentionInput
46
+ properties:
47
+ disabled: true
48
+
49
+ - id: tiptap_mention_options
50
+ type: TiptapMentionInput
51
+ properties:
52
+ placeholder: 'Try @A'
53
+ mentions:
54
+ options:
55
+ - label: Alice
56
+ value:
57
+ _id: user_1
58
+ - label: Bob
59
+ value:
60
+ _id: user_2
61
+
62
+ - id: tiptap_mention_saved
63
+ type: TiptapMentionInput
64
+
65
+ # Options whose value has no scalar identity (the shape an app passes when
66
+ # the whole contact record is the value) — data-id must be omitted, never
67
+ # "[object Object]".
68
+ - id: tiptap_mention_object_values
69
+ type: TiptapMentionInput
70
+ properties:
71
+ placeholder: 'Try @C'
72
+ mentions:
73
+ options:
74
+ - label: Carol
75
+ value:
76
+ contact_id: c_1
77
+ name: Carol
78
+ email: carol@example.com
79
+
80
+ - id: tiptap_mention_groups
81
+ type: TiptapMentionInput
82
+ properties:
83
+ title: Group mentions playground
84
+ placeholder: 'Type @ — People + Roles sections, group chips, hover members'
85
+ mentions:
86
+ limit: 3
87
+ groupMembers:
88
+ finance:
89
+ - name: Jane Doe
90
+ email: jane@example.com
91
+ - name: John Smith
92
+ email: john@example.com
93
+ devs:
94
+ - name: Ada Lovelace
95
+ email: ada@example.com
96
+ - name: Alan Turing
97
+ email: alan@example.com
98
+ # people carry value.href → <a>; groups omit it → nullish → <span>
99
+ getHref:
100
+ _function:
101
+ __args: 0.value.href
102
+ options:
103
+ - label: Alice
104
+ value:
105
+ name: Alice
106
+ href: '/contacts?_id=user_1'
107
+ tag:
108
+ section: People
109
+ - label: Amir
110
+ value:
111
+ name: Amir
112
+ href: '/contacts?_id=user_2'
113
+ tag:
114
+ section: People
115
+ - label: Anna
116
+ value:
117
+ name: Anna
118
+ href: '/contacts?_id=user_3'
119
+ tag:
120
+ section: People
121
+ - label: Aaron
122
+ value:
123
+ name: Aaron
124
+ href: '/contacts?_id=user_4'
125
+ tag:
126
+ section: People
127
+ - label: Finance
128
+ value:
129
+ role: finance-admin
130
+ tag:
131
+ section: Roles
132
+ group: finance
133
+ color: '#722ed1'
134
+ - label: Developers
135
+ value:
136
+ role: developer
137
+ tag:
138
+ section: Roles
139
+ group: devs
140
+ color: '#13c2c2'
@@ -0,0 +1,90 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - id: default
16
+ type: TiptapMentionInput
17
+ - id: properties.placeholder
18
+ type: TiptapMentionInput
19
+ properties:
20
+ placeholder: 'Type @ to mention...'
21
+ - id: properties.disabled
22
+ type: TiptapMentionInput
23
+ properties:
24
+ disabled: true
25
+ - id: properties.mentions.options
26
+ type: TiptapMentionInput
27
+ properties:
28
+ mentions:
29
+ options:
30
+ - label: Alice
31
+ value:
32
+ _id: user_1
33
+ - label: Bob
34
+ value:
35
+ _id: user_2
36
+ - id: properties.mentions.char = '#'
37
+ type: TiptapMentionInput
38
+ properties:
39
+ placeholder: 'Type # to tag'
40
+ mentions:
41
+ char: '#'
42
+ options:
43
+ - label: bug
44
+ value:
45
+ _id: tag_bug
46
+ - id: properties.mentions.limit with sections
47
+ type: TiptapMentionInput
48
+ properties:
49
+ mentions:
50
+ limit: 2
51
+ options:
52
+ - label: Alice
53
+ value:
54
+ _id: user_1
55
+ tag:
56
+ section: People
57
+ - label: Bob
58
+ value:
59
+ _id: user_2
60
+ tag:
61
+ section: People
62
+ - label: Finance
63
+ value:
64
+ _id: role_finance
65
+ tag:
66
+ section: Roles
67
+ - id: properties.mentions group chips with tag.color
68
+ type: TiptapMentionInput
69
+ properties:
70
+ mentions:
71
+ options:
72
+ - label: Alice
73
+ value:
74
+ _id: user_1
75
+ tag:
76
+ section: People
77
+ - label: Finance
78
+ value:
79
+ role: finance-admin
80
+ tag:
81
+ section: Roles
82
+ group: finance
83
+ color: '#722ed1'
84
+ - label: Developers
85
+ value:
86
+ role: developer
87
+ tag:
88
+ section: Roles
89
+ group: devs
90
+ color: '#13c2c2'
@@ -14,10 +14,10 @@
14
14
  limitations under the License.
15
15
  */ import { useRef } from 'react';
16
16
  import TurndownService from 'turndown';
17
- import s3FileUpload from '../utils/s3FileUpload.js';
17
+ import fileUpload from '../utils/fileUpload.js';
18
18
  // Ref-tracked controller for the mention editor's value. Mirrors TiptapInput's
19
19
  // useTiptapState but also extracts mentions from the document.
20
- function useTiptapMentionState({ value, methods }) {
20
+ function useTiptapMentionState({ value, methods, hasDownloadRequest }) {
21
21
  const valueRef = useRef(value);
22
22
  valueRef.current = value;
23
23
  const turndownService = new TurndownService();
@@ -60,9 +60,10 @@ function useTiptapMentionState({ value, methods }) {
60
60
  });
61
61
  };
62
62
  const insertImage = async (editor, file, pos)=>{
63
- const url = await s3FileUpload({
63
+ const url = await fileUpload({
64
64
  file,
65
- methods
65
+ methods,
66
+ hasDownloadRequest
66
67
  });
67
68
  // The upload is async: bail if the editor was torn down meanwhile (e.g. the
68
69
  // surrounding page navigated away before the upload resolved), otherwise the
@@ -35,36 +35,69 @@ const HIGHLIGHT_SWATCHES = [
35
35
  fill: 'rgba(170, 0, 255, 0.5)'
36
36
  }
37
37
  ];
38
+ // Placement of the TipTap v2 (tippy.js) bubble menu: 10px above the selection,
39
+ // flipping below when that would leave less than 5px to the viewport edge, and
40
+ // kept 5px from the sides. v3 positions the menu with Floating UI and applies
41
+ // the offset after flip, so flip's padding includes the offset.
42
+ const FLOATING_OPTIONS = {
43
+ placement: 'top',
44
+ offset: 10,
45
+ flip: {
46
+ padding: 15
47
+ },
48
+ shift: {
49
+ padding: 5
50
+ }
51
+ };
38
52
  function hasExt(editor, name) {
39
53
  return editor.extensionManager.extensions.some((ext)=>ext.name === name);
40
54
  }
55
+ function createMenuElements() {
56
+ if (typeof document === 'undefined') return null;
57
+ // `root` is the element the plugin positions and mounts, like tippy's root
58
+ // in v2 (same z-index). `content` is the styled menu the buttons render into.
59
+ // The root is absolute from the start so the plugin measures its final size
60
+ // the first time it positions it.
61
+ const root = document.createElement('div');
62
+ root.style.position = 'absolute';
63
+ root.style.visibility = 'hidden';
64
+ root.style.zIndex = '9999';
65
+ const content = document.createElement('div');
66
+ content.className = 'tiptap-popover';
67
+ root.appendChild(content);
68
+ return {
69
+ root,
70
+ content
71
+ };
72
+ }
41
73
  // Custom bubble menu built on tiptap's BubbleMenuPlugin instead of the
42
- // `<BubbleMenu>` React wrapper. The wrapper renders its menu element with
43
- // React and then the plugin calls `element.remove()` on it (bubble-menu-plugin
44
- // detaches the menu from the DOM on construction). When the editor block later
45
- // unmounts — e.g. the surrounding page navigates away, or `disabled` flips and
46
- // unmounts this menu — React tries to removeChild an element that is no longer
47
- // where it rendered it, throwing "NotFoundError: Failed to execute
48
- // 'removeChild' on 'Node'".
74
+ // `<BubbleMenu>` React wrapper. The plugin moves its menu element in and out of
75
+ // the DOM itself. When the editor block later unmounts — e.g. the surrounding
76
+ // page navigates away, or `disabled` flips and unmounts this menu — React
77
+ // would try to removeChild an element that is no longer where it rendered it,
78
+ // throwing "NotFoundError: Failed to execute 'removeChild' on 'Node'".
49
79
  //
50
- // Here we own the container element ourselves and render the buttons into it
51
- // with a portal. React only ever removes the buttons from our container (always
52
- // their parent), never the plugin-detached element, so the teardown race is
53
- // gone.
80
+ // Here we own the menu elements ourselves and render the buttons into them
81
+ // with a portal. React only ever removes the buttons from our content element
82
+ // (always their parent), never the plugin-managed root, so the teardown race
83
+ // is gone.
54
84
  const PopoverMenu = ({ editor })=>{
55
- const [container] = useState(()=>typeof document === 'undefined' ? null : document.createElement('div'));
85
+ const [menu] = useState(createMenuElements);
56
86
  const showBold = hasExt(editor, 'bold');
57
87
  const showItalic = hasExt(editor, 'italic');
58
88
  const showStrike = hasExt(editor, 'strike');
59
89
  const showHighlight = hasExt(editor, 'highlight');
60
90
  const hasTools = showBold || showItalic || showStrike || showHighlight;
61
91
  useEffect(()=>{
62
- if (!container || !editor || editor.isDestroyed || !hasTools) return undefined;
63
- container.className = 'tiptap-popover';
92
+ if (!menu || !editor || editor.isDestroyed || !hasTools) return undefined;
64
93
  const plugin = BubbleMenuPlugin({
65
94
  pluginKey: 'bubbleMenu',
66
95
  editor,
67
- element: container,
96
+ element: menu.root,
97
+ // v2 mounted the menu beside the editor wrapper; v3 defaults to inside it.
98
+ // `options.element` is the EditorContent wrapper once it has mounted.
99
+ appendTo: ()=>editor.options.element?.parentElement,
100
+ options: FLOATING_OPTIONS,
68
101
  shouldShow: ({ editor: menuEditor, view, state, from, to })=>{
69
102
  if (menuEditor.isActive('image')) return false;
70
103
  const { selection } = state;
@@ -79,19 +112,19 @@ const PopoverMenu = ({ editor })=>{
79
112
  });
80
113
  editor.registerPlugin(plugin);
81
114
  return ()=>{
82
- // Tear the plugin (and its tippy instance) down first. React then
83
- // unmounts the portal, removing the buttons from our still-intact
84
- // container.
115
+ // Tear the plugin down first (it removes the root from the DOM). React
116
+ // then unmounts the portal, removing the buttons from our still-intact
117
+ // content element.
85
118
  if (!editor.isDestroyed) {
86
119
  editor.unregisterPlugin('bubbleMenu');
87
120
  }
88
121
  };
89
122
  }, [
90
123
  editor,
91
- container,
124
+ menu,
92
125
  hasTools
93
126
  ]);
94
- if (!container || !hasTools) return null;
127
+ if (!menu || !hasTools) return null;
95
128
  return /*#__PURE__*/ createPortal(/*#__PURE__*/ React.createElement(React.Fragment, null, showBold && /*#__PURE__*/ React.createElement(AiOutlineBold, {
96
129
  className: "tiptap-icon",
97
130
  onClick: ()=>editor.chain().focus().toggleBold().run()
@@ -110,6 +143,6 @@ const PopoverMenu = ({ editor })=>{
110
143
  onClick: ()=>editor.chain().focus().toggleHighlight({
111
144
  color: fill
112
145
  }).run()
113
- }))), container);
146
+ }))), menu.content);
114
147
  };
115
148
  export default PopoverMenu;
@@ -15,14 +15,11 @@
15
15
  */ import FileHandler from '@tiptap/extension-file-handler';
16
16
  import Highlight from '@tiptap/extension-highlight';
17
17
  import Image from '@tiptap/extension-image';
18
- import Placeholder from '@tiptap/extension-placeholder';
19
- import StarterKit from '@tiptap/starter-kit';
18
+ import { Placeholder } from '@tiptap/extensions';
20
19
  import LinkExtension from '@tiptap/extension-link';
21
- import Table from '@tiptap/extension-table';
22
- import TableCell from '@tiptap/extension-table-cell';
23
- import TableHeader from '@tiptap/extension-table-header';
24
- import TableRow from '@tiptap/extension-table-row';
20
+ import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table';
25
21
  import { type } from '@lowdefy/helpers';
22
+ import buildStarterKit from './buildStarterKit.js';
26
23
  const DEFAULT_IMAGE_MIME_TYPES = [
27
24
  'image/jpeg',
28
25
  'image/png',
@@ -58,15 +55,14 @@ function merge(defaults, overrides) {
58
55
  ...overrides
59
56
  };
60
57
  }
61
- function buildExtensions({ properties, insertImage, mentionExtension, uploadEnabled }) {
58
+ function buildExtensions({ properties, getPlaceholder, insertImage, mentionExtension, uploadEnabled }) {
62
59
  const image = merge(DEFAULTS.image, properties.image);
63
60
  const table = merge(DEFAULTS.table, properties.table);
64
61
  const link = merge(DEFAULTS.link, properties.link);
65
62
  const highlight = merge(DEFAULTS.highlight, properties.highlight);
66
- const starterKitOptions = type.isObject(properties.starterKit) ? properties.starterKit : {};
67
63
  const allowedMimeTypes = type.isArray(properties.allowedMimeTypes) ? properties.allowedMimeTypes : DEFAULT_IMAGE_MIME_TYPES;
68
64
  const extensions = [
69
- StarterKit.configure(starterKitOptions)
65
+ buildStarterKit(properties.starterKit)
70
66
  ];
71
67
  if (!table.disabled) {
72
68
  extensions.push(Table.configure({
@@ -84,9 +80,8 @@ function buildExtensions({ properties, insertImage, mentionExtension, uploadEnab
84
80
  }));
85
81
  }
86
82
  extensions.push(Placeholder.configure({
87
- placeholder: ()=>properties.placeholder ?? '',
88
- showOnlyWhenEditable: false,
89
- considerAnyAsEmpty: true
83
+ placeholder: getPlaceholder,
84
+ showOnlyWhenEditable: false
90
85
  }));
91
86
  if (!highlight.disabled) {
92
87
  extensions.push(Highlight.configure({
@@ -101,7 +96,10 @@ function buildExtensions({ properties, insertImage, mentionExtension, uploadEnab
101
96
  autolink: link.autolink,
102
97
  linkOnPaste: link.linkOnPaste,
103
98
  openOnClick: link.openOnClick,
104
- defaultProtocol: link.defaultProtocol
99
+ defaultProtocol: link.defaultProtocol,
100
+ // TipTap v2's default. v3's default also skips bare IP addresses and hosts without a TLD
101
+ // (e.g. localhost:3000), and applies to pasted URLs too.
102
+ shouldAutoLink: (url)=>!!url
105
103
  }));
106
104
  }
107
105
  if (mentionExtension) {
@@ -0,0 +1,48 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import StarterKit from '@tiptap/starter-kit';
16
+ import { type } from '@lowdefy/helpers';
17
+ // TipTap v3's StarterKit bundles extensions the v2 kit did not. They default to off so the
18
+ // editor keeps its v2 extension set: Underline would keep <u> in the saved html, TrailingNode
19
+ // appends an empty paragraph after a final table or image, and ListKeymap changes Backspace and
20
+ // Delete in lists.
21
+ const V3_KIT_DEFAULTS = {
22
+ listKeymap: false,
23
+ trailingNode: false,
24
+ underline: false
25
+ };
26
+ // Builds the StarterKit from the block's `starterKit` property, which is written against the v2
27
+ // kit, so that it enables the same extensions as v2 did.
28
+ function buildStarterKit(starterKit) {
29
+ const { history, ...options } = type.isObject(starterKit) ? starterKit : {};
30
+ // v3 renamed the History extension to UndoRedo.
31
+ if (!type.isUndefined(history) && type.isUndefined(options.undoRedo)) {
32
+ options.undoRedo = history;
33
+ }
34
+ // v3 CodeBlock inserts a paragraph above a code block at the top of the document on ArrowUp.
35
+ if (options.codeBlock !== false) {
36
+ options.codeBlock = {
37
+ exitOnArrowUp: false,
38
+ ...type.isObject(options.codeBlock) ? options.codeBlock : {}
39
+ };
40
+ }
41
+ return StarterKit.configure({
42
+ ...V3_KIT_DEFAULTS,
43
+ ...options,
44
+ // The Link extension is added by buildExtensions from the block's `link` property.
45
+ link: false
46
+ });
47
+ }
48
+ export default buildStarterKit;
@@ -0,0 +1,63 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import getLegacyObjectUrl from '@lowdefy/blocks-files/utils/getLegacyObjectUrl.js';
17
+ import getUploadPolicy from '@lowdefy/blocks-files/utils/getUploadPolicy.js';
18
+ import uploadFile from '@lowdefy/blocks-files/utils/uploadFile.js';
19
+ // Uploads a file through the app's upload-policy request (any storage
20
+ // provider) and resolves a display URL for the inline image. Inline images
21
+ // persist in saved markdown, so they must be public assets: the upload
22
+ // request should set the provider's public-read acl, and the download request
23
+ // should set public: true to return a stable, non-expiring URL. Without a
24
+ // download request the legacy unsigned object URL is used (S3-shaped,
25
+ // deprecated).
26
+ async function fileUpload({ file, methods, hasDownloadRequest }) {
27
+ if (type.isNone(file)) {
28
+ return;
29
+ }
30
+ const descriptor = await getUploadPolicy({
31
+ methods,
32
+ file
33
+ });
34
+ file.bucket = descriptor.bucket;
35
+ file.key = descriptor.key;
36
+ await uploadFile({
37
+ descriptor,
38
+ file
39
+ });
40
+ if (hasDownloadRequest) {
41
+ const response = await methods.triggerEvent({
42
+ name: '__getDownloadPolicy',
43
+ event: {
44
+ file: {
45
+ bucket: descriptor.bucket,
46
+ key: descriptor.key,
47
+ name: file.name,
48
+ type: file.type
49
+ }
50
+ }
51
+ });
52
+ if (response.success !== true) {
53
+ throw new Error('Download policy request error.');
54
+ }
55
+ file.url = response.responses.__getDownloadPolicy.response[0];
56
+ return file.url;
57
+ }
58
+ file.url = getLegacyObjectUrl({
59
+ descriptor
60
+ });
61
+ return file.url;
62
+ }
63
+ export default fileUpload;
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */ // Map validation.status → wrapper class. Matches the visual behavior of
16
16
  // passing status="error" | "warning" to an antd <Input />, but rendered
17
- // with our own CSS (see utils/style.module.css) because antd v6 uses
17
+ // with our own CSS (see utils/style.css) because antd v6 uses
18
18
  // CSS-in-JS, so the .ant-input-status-* classes are not global.
19
19
  function statusClass(status) {
20
20
  if (status === 'error') return 'tiptap-wrapper-error';