Merge remote-tracking branch 'hub/main' into feature/sse-connect
This commit is contained in:
commit
c3a353fedb
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -120,7 +120,7 @@
|
||||||
"stylelint-config-standard-scss": "11.1.0",
|
"stylelint-config-standard-scss": "11.1.0",
|
||||||
"stylelint-order": "6.0.3",
|
"stylelint-order": "6.0.3",
|
||||||
"stylelint-scss": "5.3.1",
|
"stylelint-scss": "5.3.1",
|
||||||
"swiper": "9.4.1",
|
"swiper": "11.0.5",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"typograf": "7.1.0",
|
"typograf": "7.1.0",
|
||||||
|
@ -17552,12 +17552,6 @@
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ssr-window": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
|
@ -18050,9 +18044,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/swiper": {
|
"node_modules/swiper": {
|
||||||
"version": "9.4.1",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-9.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.5.tgz",
|
||||||
"integrity": "sha512-1nT2T8EzUpZ0FagEqaN/YAhRj33F2x/lN6cyB0/xoYJDMf8KwTFT3hMOeoB8Tg4o3+P/CKqskP+WX0Df046fqA==",
|
"integrity": "sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -18064,9 +18058,6 @@
|
||||||
"url": "http://opencollective.com/swiper"
|
"url": "http://opencollective.com/swiper"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
|
||||||
"ssr-window": "^4.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4.7.0"
|
"node": ">= 4.7.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@
|
||||||
"stylelint-config-standard-scss": "11.1.0",
|
"stylelint-config-standard-scss": "11.1.0",
|
||||||
"stylelint-order": "6.0.3",
|
"stylelint-order": "6.0.3",
|
||||||
"stylelint-scss": "5.3.1",
|
"stylelint-scss": "5.3.1",
|
||||||
"swiper": "9.4.1",
|
"swiper": "11.0.5",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"typograf": "7.1.0",
|
"typograf": "7.1.0",
|
||||||
|
|
|
@ -42,10 +42,10 @@ import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './Bubbl
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import Article from './extensions/Article'
|
import Article from './extensions/Article'
|
||||||
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||||
import { Embed } from './extensions/Embed'
|
|
||||||
import { Figcaption } from './extensions/Figcaption'
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
import { Footnote } from './extensions/Footnote'
|
import { Footnote } from './extensions/Footnote'
|
||||||
|
import { Iframe } from './extensions/Iframe'
|
||||||
import { TrailingNode } from './extensions/TrailingNode'
|
import { TrailingNode } from './extensions/TrailingNode'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
|
|
||||||
|
@ -130,11 +130,6 @@ export const Editor = (props: Props) => {
|
||||||
current: null,
|
current: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageFigure = Figure.extend({
|
|
||||||
name: 'capturedImage',
|
|
||||||
content: 'figcaption image',
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClipboardPaste = async () => {
|
const handleClipboardPaste = async () => {
|
||||||
try {
|
try {
|
||||||
const clipboardItems = await navigator.clipboard.read()
|
const clipboardItems = await navigator.clipboard.read()
|
||||||
|
@ -163,22 +158,16 @@ export const Editor = (props: Props) => {
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.insertContent({
|
.insertContent({
|
||||||
type: 'capturedImage',
|
type: 'figure',
|
||||||
|
attrs: { 'data-type': 'image' },
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'figcaption',
|
type: 'image',
|
||||||
content: [
|
attrs: { src: result.url },
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: result.originalFilename,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'figcaption',
|
||||||
attrs: {
|
content: [{ type: 'text', text: result.originalFilename }],
|
||||||
src: result.url,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -250,11 +239,11 @@ export const Editor = (props: Props) => {
|
||||||
class: 'highlight',
|
class: 'highlight',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
ImageFigure,
|
|
||||||
Image,
|
Image,
|
||||||
|
Iframe,
|
||||||
|
Figure,
|
||||||
Figcaption,
|
Figcaption,
|
||||||
Footnote,
|
Footnote,
|
||||||
Embed,
|
|
||||||
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
pluginKey: 'textBubbleMenu',
|
pluginKey: 'textBubbleMenu',
|
||||||
|
@ -265,8 +254,13 @@ export const Editor = (props: Props) => {
|
||||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
setIsCommonMarkup(e.isActive('figcaption'))
|
setIsCommonMarkup(e.isActive('figcaption'))
|
||||||
const result =
|
const result =
|
||||||
(view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')) ||
|
(view.hasFocus() &&
|
||||||
e.isActive('footnote')
|
!empty &&
|
||||||
|
!isEmptyTextBlock &&
|
||||||
|
!e.isActive('image') &&
|
||||||
|
!e.isActive('figure')) ||
|
||||||
|
e.isActive('footnote') ||
|
||||||
|
e.isActive('figcaption')
|
||||||
setShouldShowTextBubbleMenu(result)
|
setShouldShowTextBubbleMenu(result)
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,7 @@ const embedData = async (data) => {
|
||||||
const element = document.createRange().createContextualFragment(data)
|
const element = document.createRange().createContextualFragment(data)
|
||||||
const { attributes } = element.firstChild as HTMLIFrameElement
|
const { attributes } = element.firstChild as HTMLIFrameElement
|
||||||
|
|
||||||
const result: { src: string } = { src: '' }
|
const result: { src: string; width?: string; height?: string } = { src: '' }
|
||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
const attribute = attributes[i]
|
const attribute = attributes[i]
|
||||||
|
@ -45,7 +45,28 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
const handleEmbedFormSubmit = async (value: string) => {
|
const handleEmbedFormSubmit = async (value: string) => {
|
||||||
// TODO: add support instagram embed (blockquote)
|
// TODO: add support instagram embed (blockquote)
|
||||||
const emb = await embedData(value)
|
const emb = await embedData(value)
|
||||||
props.editor.chain().focus().setIframe(emb).run()
|
props.editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContent({
|
||||||
|
type: 'figure',
|
||||||
|
attrs: { 'data-type': 'iframe' },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'iframe',
|
||||||
|
attrs: {
|
||||||
|
src: emb.src,
|
||||||
|
width: emb.width,
|
||||||
|
height: emb.height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'figcaption',
|
||||||
|
content: [{ type: 'text', text: t('Description') }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateEmbed = async (value) => {
|
const validateEmbed = async (value) => {
|
||||||
|
|
|
@ -265,8 +265,26 @@ mark.highlight {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
figure[data-type='capturedImage'] {
|
.ProseMirror-hideselection figure[data-type='figure'] {
|
||||||
flex-direction: column-reverse;
|
& > figcaption {
|
||||||
|
--selection-color: rgb(0 0 0 / 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figure[data-type='figure'] {
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
|
.iframe-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stylelint-disable-next-line selector-type-no-unknown */
|
/* stylelint-disable-next-line selector-type-no-unknown */
|
||||||
|
|
|
@ -185,22 +185,16 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.insertContent({
|
.insertContent({
|
||||||
type: 'capturedImage',
|
type: 'figure',
|
||||||
|
attrs: { 'data-type': 'image' },
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'figcaption',
|
type: 'image',
|
||||||
content: [
|
attrs: { src: image.url },
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: image.originalFilename,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'figcaption',
|
||||||
attrs: {
|
content: [{ type: 'text', text: image.originalFilename }],
|
||||||
src: image.url,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -81,7 +81,6 @@
|
||||||
.formHolder {
|
.formHolder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,24 +16,39 @@ export const Figure = Node.create({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'block figcaption',
|
content: '(image | iframe) figcaption',
|
||||||
draggable: true,
|
draggable: true,
|
||||||
isolating: true,
|
isolating: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
'data-float': null,
|
'data-float': null,
|
||||||
|
'data-type': { default: null },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: `figure[data-type="${this.name}"]`,
|
tag: 'figure',
|
||||||
|
getAttrs: (node) => {
|
||||||
|
if (!(node instanceof HTMLElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const img = node.querySelector('img')
|
||||||
|
const iframe = node.querySelector('iframe')
|
||||||
|
let dataType = null
|
||||||
|
if (img) {
|
||||||
|
dataType = 'image'
|
||||||
|
} else if (iframe) {
|
||||||
|
dataType = 'iframe'
|
||||||
|
}
|
||||||
|
return { 'data-type': dataType }
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0]
|
return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { mergeAttributes, Node } from '@tiptap/core'
|
import { Node } from '@tiptap/core'
|
||||||
|
|
||||||
export interface IframeOptions {
|
export interface IframeOptions {
|
||||||
allowFullscreen: boolean
|
allowFullscreen: boolean
|
||||||
|
@ -15,19 +15,35 @@ declare module '@tiptap/core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Embed = Node.create<IframeOptions>({
|
export const Iframe = Node.create<IframeOptions>({
|
||||||
name: 'embed',
|
name: 'iframe',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
selectable: true,
|
|
||||||
atom: true,
|
atom: true,
|
||||||
draggable: true,
|
|
||||||
addAttributes() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
src: { default: null },
|
allowFullscreen: true,
|
||||||
width: { default: null },
|
HTMLAttributes: {
|
||||||
height: { default: null },
|
class: 'iframe-wrapper',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
frameborder: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
allowfullscreen: {
|
||||||
|
default: this.options.allowFullscreen,
|
||||||
|
parseHTML: () => this.options.allowFullscreen,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -35,28 +51,15 @@ export const Embed = Node.create<IframeOptions>({
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ['iframe', mergeAttributes(HTMLAttributes)]
|
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]]
|
||||||
},
|
|
||||||
addNodeView() {
|
|
||||||
return ({ node }) => {
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.className = 'embed-wrapper'
|
|
||||||
const iframe = document.createElement('iframe')
|
|
||||||
iframe.width = node.attrs.width
|
|
||||||
iframe.height = node.attrs.height
|
|
||||||
iframe.allowFullscreen = node.attrs.allowFullscreen
|
|
||||||
iframe.src = node.attrs.src
|
|
||||||
div.append(iframe)
|
|
||||||
return {
|
|
||||||
dom: div,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
setIframe:
|
setIframe:
|
||||||
(options) =>
|
(options: { src: string }) =>
|
||||||
({ tr, dispatch }) => {
|
({ tr, dispatch }) => {
|
||||||
const { selection } = tr
|
const { selection } = tr
|
||||||
const node = this.type.create(options)
|
const node = this.type.create(options)
|
|
@ -52,6 +52,7 @@ export type ArticleCardProps = {
|
||||||
desktopCoverSize?: 'XS' | 'S' | 'M' | 'L'
|
desktopCoverSize?: 'XS' | 'S' | 'M' | 'L'
|
||||||
article: Shout
|
article: Shout
|
||||||
onShare?: (article: Shout) => void
|
onShare?: (article: Shout) => void
|
||||||
|
onInvite?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const desktopCoverImageWidths: Record<ArticleCardProps['desktopCoverSize'], number> = {
|
const desktopCoverImageWidths: Record<ArticleCardProps['desktopCoverSize'], number> = {
|
||||||
|
@ -371,7 +372,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
isOwner={canEdit()}
|
isOwner={canEdit()}
|
||||||
containerCssClass={stylesHeader.control}
|
containerCssClass={stylesHeader.control}
|
||||||
onShareClick={() => props.onShare(props.article)}
|
onShareClick={() => props.onShare(props.article)}
|
||||||
onInviteClick={() => showModal('inviteCoAuthors')}
|
onInviteClick={props.onInvite}
|
||||||
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
|
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
|
||||||
trigger={
|
trigger={
|
||||||
<button>
|
<button>
|
||||||
|
@ -388,7 +389,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<InviteCoAuthorsModal title={t('Invite experts')} />
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { PopupProps } from '../../_shared/Popup'
|
import type { PopupProps } from '../../_shared/Popup'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show } from 'solid-js'
|
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Popup } from '../../_shared/Popup'
|
import { Popup } from '../../_shared/Popup'
|
||||||
|
@ -9,20 +9,35 @@ import { SoonChip } from '../../_shared/SoonChip'
|
||||||
|
|
||||||
import styles from './FeedArticlePopup.module.scss'
|
import styles from './FeedArticlePopup.module.scss'
|
||||||
|
|
||||||
type FeedArticlePopupProps = {
|
type Props = {
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
onInviteClick: () => void
|
onInviteClick: () => void
|
||||||
onShareClick: () => void
|
onShareClick: () => void
|
||||||
} & Omit<PopupProps, 'children'>
|
} & Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
|
export const FeedArticlePopup = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const [hidePopup, setHidePopup] = createSignal(false)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup {...props} horizontalAnchor={'right'} variant="tiny" popupCssClass={styles.feedArticlePopup}>
|
<Popup
|
||||||
|
{...props}
|
||||||
|
//TODO: fix hide logic
|
||||||
|
closePopup={hidePopup()}
|
||||||
|
horizontalAnchor={'right'}
|
||||||
|
variant="tiny"
|
||||||
|
popupCssClass={styles.feedArticlePopup}
|
||||||
|
>
|
||||||
<ul class={clsx('nodash', styles.actionList)}>
|
<ul class={clsx('nodash', styles.actionList)}>
|
||||||
<li>
|
<li>
|
||||||
<button class={styles.action} role="button" onClick={props.onShareClick}>
|
<button
|
||||||
|
class={styles.action}
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onShareClick()
|
||||||
|
setHidePopup(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('Share')}
|
{t('Share')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -33,6 +48,7 @@ export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
alert('Help to edit')
|
alert('Help to edit')
|
||||||
|
setHidePopup(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Help to edit')}
|
{t('Help to edit')}
|
||||||
|
@ -40,7 +56,14 @@ export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
</Show>
|
||||||
<li>
|
<li>
|
||||||
<button class={styles.action} role="button" onClick={props.onInviteClick}>
|
<button
|
||||||
|
class={styles.action}
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onInviteClick()
|
||||||
|
setHidePopup(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('Invite experts')}
|
{t('Invite experts')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -197,8 +197,8 @@
|
||||||
margin-bottom: 4rem;
|
margin-bottom: 4rem;
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
flex-direction: column-reverse;
|
margin: 1rem 0 0;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useTopicsStore } from '../../../stores/zine/topics'
|
||||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
import { DropDown } from '../../_shared/DropDown'
|
import { DropDown } from '../../_shared/DropDown'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
import { InviteCoAuthorsModal } from '../../_shared/InviteCoAuthorsModal'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
import { ShareModal } from '../../_shared/ShareModal'
|
import { ShareModal } from '../../_shared/ShareModal'
|
||||||
import { CommentDate } from '../../Article/CommentDate'
|
import { CommentDate } from '../../Article/CommentDate'
|
||||||
|
@ -303,6 +304,7 @@ export const FeedView = (props: Props) => {
|
||||||
{(article) => (
|
{(article) => (
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
onShare={(shared) => handleShare(shared)}
|
onShare={(shared) => handleShare(shared)}
|
||||||
|
onInvite={() => showModal('inviteCoAuthors')}
|
||||||
article={article}
|
article={article}
|
||||||
settings={{ isFeedMode: true }}
|
settings={{ isFeedMode: true }}
|
||||||
desktopCoverSize="M"
|
desktopCoverSize="M"
|
||||||
|
@ -429,6 +431,7 @@ export const FeedView = (props: Props) => {
|
||||||
shareUrl={getShareUrl({ pathname: `/${shareData().slug}` })}
|
shareUrl={getShareUrl({ pathname: `/${shareData().slug}` })}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
<InviteCoAuthorsModal title={t('Invite experts')} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export type PopupProps = {
|
||||||
onVisibilityChange?: (isVisible: boolean) => void
|
onVisibilityChange?: (isVisible: boolean) => void
|
||||||
horizontalAnchor?: HorizontalAnchor
|
horizontalAnchor?: HorizontalAnchor
|
||||||
variant?: 'bordered' | 'tiny'
|
variant?: 'bordered' | 'tiny'
|
||||||
|
closePopup?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = (props: PopupProps) => {
|
||||||
|
@ -28,14 +29,20 @@ export const Popup = (props: PopupProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const containerRef: { current: HTMLElement } = { current: null }
|
const containerRef: { current: HTMLElement } = { current: null }
|
||||||
|
const closePopup = () => setIsVisible(false)
|
||||||
|
|
||||||
useOutsideClickHandler({
|
useOutsideClickHandler({
|
||||||
containerRef,
|
containerRef,
|
||||||
predicate: () => isVisible(),
|
predicate: () => isVisible(),
|
||||||
handler: () => {
|
handler: () => closePopup(),
|
||||||
setIsVisible(false)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.closePopup) {
|
||||||
|
closePopup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
|
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
|
||||||
return (
|
return (
|
||||||
<span class={clsx(styles.container, props.containerCssClass)} ref={(el) => (containerRef.current = el)}>
|
<span class={clsx(styles.container, props.containerCssClass)} ref={(el) => (containerRef.current = el)}>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { For, onMount, Show } from 'solid-js'
|
import { For, onMount, Show } from 'solid-js'
|
||||||
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
import SwiperCore from 'swiper'
|
||||||
|
import { Manipulation, Navigation, Pagination } from 'swiper/modules'
|
||||||
|
|
||||||
import { Shout } from '../../../graphql/schema/core.gen'
|
import { Shout } from '../../../graphql/schema/core.gen'
|
||||||
import { ArticleCard } from '../../Feed/ArticleCard'
|
import { ArticleCard } from '../../Feed/ArticleCard'
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { createFileUploader } from '@solid-primitives/upload'
|
import { createFileUploader } from '@solid-primitives/upload'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createSignal, For, Show, on, onMount, lazy } from 'solid-js'
|
import { createEffect, createSignal, For, Show, on, onMount, lazy } from 'solid-js'
|
||||||
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
import SwiperCore from 'swiper'
|
||||||
|
import { Manipulation, Navigation, Pagination } from 'swiper/modules'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSnackbar } from '../../../context/snackbar'
|
import { useSnackbar } from '../../../context/snackbar'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createEffect, createSignal, For, Show, on, onMount, onCleanup } from 'solid-js'
|
import { createEffect, createSignal, For, Show, on, onMount, onCleanup } from 'solid-js'
|
||||||
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
import SwiperCore from 'swiper'
|
||||||
|
import { Manipulation, Navigation, Pagination } from 'swiper/modules'
|
||||||
import { throttle } from 'throttle-debounce'
|
import { throttle } from 'throttle-debounce'
|
||||||
|
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
|
@ -27,7 +28,6 @@ export const ImageSwiper = (props: Props) => {
|
||||||
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
const mainSwipeRef: { current: SwiperRef } = { current: null }
|
||||||
const thumbSwipeRef: { current: SwiperRef } = { current: null }
|
const thumbSwipeRef: { current: SwiperRef } = { current: null }
|
||||||
const swiperMainContainer: { current: HTMLDivElement } = { current: null }
|
const swiperMainContainer: { current: HTMLDivElement } = { current: null }
|
||||||
|
|
||||||
const [slideIndex, setSlideIndex] = createSignal(0)
|
const [slideIndex, setSlideIndex] = createSignal(0)
|
||||||
const [isMobileView, setIsMobileView] = createSignal(false)
|
const [isMobileView, setIsMobileView] = createSignal(false)
|
||||||
const [selectedImage, setSelectedImage] = createSignal('')
|
const [selectedImage, setSelectedImage] = createSignal('')
|
||||||
|
@ -52,18 +52,13 @@ export const ImageSwiper = (props: Props) => {
|
||||||
const { register } = await import('swiper/element/bundle')
|
const { register } = await import('swiper/element/bundle')
|
||||||
register()
|
register()
|
||||||
SwiperCore.use([Pagination, Navigation, Manipulation])
|
SwiperCore.use([Pagination, Navigation, Manipulation])
|
||||||
|
mainSwipeRef.current?.swiper?.on('slideChange', handleSlideChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const updateDirection = () => {
|
const updateDirection = () => {
|
||||||
const width = window.innerWidth
|
const width = window.innerWidth
|
||||||
const direction = width > MIN_WIDTH ? 'vertical' : 'horizontal'
|
setIsMobileView(width < MIN_WIDTH)
|
||||||
if (direction === 'horizontal') {
|
|
||||||
setIsMobileView(true)
|
|
||||||
} else {
|
|
||||||
setIsMobileView(false)
|
|
||||||
}
|
|
||||||
thumbSwipeRef.current?.swiper?.changeDirection(direction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDirection()
|
updateDirection()
|
||||||
|
@ -96,47 +91,6 @@ export const ImageSwiper = (props: Props) => {
|
||||||
<div class={clsx(styles.Swiper, styles.articleMode, { [styles.mobileView]: isMobileView() })}>
|
<div class={clsx(styles.Swiper, styles.articleMode, { [styles.mobileView]: isMobileView() })}>
|
||||||
<div class={styles.container} ref={(el) => (swiperMainContainer.current = el)}>
|
<div class={styles.container} ref={(el) => (swiperMainContainer.current = el)}>
|
||||||
<Show when={props.images.length > 0}>
|
<Show when={props.images.length > 0}>
|
||||||
<div class={styles.holder}>
|
|
||||||
<swiper-container
|
|
||||||
ref={(el) => (mainSwipeRef.current = el)}
|
|
||||||
slides-per-view={1}
|
|
||||||
thumbs-swiper={'.thumbSwiper'}
|
|
||||||
observer={true}
|
|
||||||
onSlideChange={handleSlideChange}
|
|
||||||
space-between={isMobileView() ? 20 : 10}
|
|
||||||
>
|
|
||||||
<For each={props.images}>
|
|
||||||
{(slide, index) => (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
<swiper-slide lazy="true" virtual-index={index()}>
|
|
||||||
<div class={styles.image} onClick={handleImageClick}>
|
|
||||||
<Image src={slide.url} alt={slide.title} width={800} />
|
|
||||||
</div>
|
|
||||||
</swiper-slide>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</swiper-container>
|
|
||||||
<div
|
|
||||||
class={clsx(styles.navigation, styles.prev, {
|
|
||||||
[styles.disabled]: slideIndex() === 0,
|
|
||||||
})}
|
|
||||||
onClick={() => mainSwipeRef.current.swiper.slidePrev()}
|
|
||||||
>
|
|
||||||
<Icon name="swiper-l-arr" class={styles.icon} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class={clsx(styles.navigation, styles.next, {
|
|
||||||
[styles.disabled]: slideIndex() + 1 === props.images.length,
|
|
||||||
})}
|
|
||||||
onClick={() => mainSwipeRef.current.swiper.slideNext()}
|
|
||||||
>
|
|
||||||
<Icon name="swiper-r-arr" class={styles.icon} />
|
|
||||||
</div>
|
|
||||||
<div class={styles.counter}>
|
|
||||||
{slideIndex() + 1} / {props.images.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class={clsx(styles.holder, styles.thumbsHolder)}>
|
<div class={clsx(styles.holder, styles.thumbsHolder)}>
|
||||||
<div class={styles.thumbs}>
|
<div class={styles.thumbs}>
|
||||||
<swiper-container
|
<swiper-container
|
||||||
|
@ -147,6 +101,8 @@ export const ImageSwiper = (props: Props) => {
|
||||||
auto-scroll-offset={1}
|
auto-scroll-offset={1}
|
||||||
watch-overflow={true}
|
watch-overflow={true}
|
||||||
watch-slides-visibility={true}
|
watch-slides-visibility={true}
|
||||||
|
direction={'horizontal'}
|
||||||
|
slides-per-group-auto={true}
|
||||||
>
|
>
|
||||||
<For each={props.images}>
|
<For each={props.images}>
|
||||||
{(slide, index) => (
|
{(slide, index) => (
|
||||||
|
@ -181,6 +137,47 @@ export const ImageSwiper = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class={styles.holder}>
|
||||||
|
<swiper-container
|
||||||
|
ref={(el) => (mainSwipeRef.current = el)}
|
||||||
|
slides-per-view={1}
|
||||||
|
thumbs-swiper={'.thumbSwiper'}
|
||||||
|
observer={true}
|
||||||
|
// slide-change={handleSlideChange}
|
||||||
|
space-between={isMobileView() ? 20 : 10}
|
||||||
|
>
|
||||||
|
<For each={props.images}>
|
||||||
|
{(slide, index) => (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
<swiper-slide lazy="true" virtual-index={index()}>
|
||||||
|
<div class={styles.image} onClick={handleImageClick}>
|
||||||
|
<Image src={slide.url} alt={slide.title} width={800} />
|
||||||
|
</div>
|
||||||
|
</swiper-slide>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</swiper-container>
|
||||||
|
<div
|
||||||
|
class={clsx(styles.navigation, styles.prev, {
|
||||||
|
[styles.disabled]: slideIndex() === 0,
|
||||||
|
})}
|
||||||
|
onClick={() => mainSwipeRef.current.swiper.slidePrev()}
|
||||||
|
>
|
||||||
|
<Icon name="swiper-l-arr" class={styles.icon} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={clsx(styles.navigation, styles.next, {
|
||||||
|
[styles.disabled]: slideIndex() + 1 === props.images.length,
|
||||||
|
})}
|
||||||
|
onClick={() => mainSwipeRef.current.swiper.slideNext()}
|
||||||
|
>
|
||||||
|
<Icon name="swiper-r-arr" class={styles.icon} />
|
||||||
|
</div>
|
||||||
|
<div class={styles.counter}>
|
||||||
|
{slideIndex() + 1} / {props.images.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.slideDescription}>
|
<div class={styles.slideDescription}>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
background: var(--background-color-invert);
|
background: var(--background-color-invert);
|
||||||
color: var(--default-color-invert);
|
color: var(--default-color-invert);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
@ -40,47 +41,44 @@
|
||||||
.container {
|
.container {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 24px 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.thumbsHolder {
|
.thumbsHolder {
|
||||||
min-width: 110px;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbs {
|
.thumbs {
|
||||||
padding: 52px 0;
|
//overflow: hidden;
|
||||||
width: 110px;
|
|
||||||
overflow: hidden;
|
|
||||||
height: calc(var(--slide-height) + 40px);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.thumbsNav {
|
& > swiper-container {
|
||||||
height: 52px;
|
|
||||||
padding: 14px 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: row;
|
||||||
justify-content: center;
|
gap: 10px;
|
||||||
width: 110px;
|
}
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
.icon {
|
.thumbsNav {
|
||||||
transform: rotate(90deg);
|
height: 100%;
|
||||||
}
|
overflow: hidden;
|
||||||
|
width: 24px;
|
||||||
|
|
||||||
&.prev {
|
&.prev {
|
||||||
top: 0;
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.next {
|
&.next {
|
||||||
top: auto;
|
top: 50%;
|
||||||
bottom: 0;
|
right: 0;
|
||||||
|
left: unset;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,43 +86,12 @@
|
||||||
|
|
||||||
&.mobileView {
|
&.mobileView {
|
||||||
.container {
|
.container {
|
||||||
flex-direction: column-reverse;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.thumbsHolder {
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs {
|
.thumbs {
|
||||||
width: 100%;
|
|
||||||
height: 80px;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
& swiper-slide {
|
& swiper-slide {
|
||||||
// bind to html element <swiper-slide/>
|
// bind to html element <swiper-slide/>
|
||||||
width: unset !important;
|
width: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbsNav {
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
width: 40px;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.prev {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,6 +227,10 @@
|
||||||
|
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: var(--default-color-invert) !important; //Force fix migration errors with inline styles
|
||||||
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
// margin-left: calc((100% + 130px) * 0.15);
|
// margin-left: calc((100% + 130px) * 0.15);
|
||||||
margin-left: calc(15% + 24px);
|
margin-left: calc(15% + 24px);
|
||||||
|
@ -398,4 +369,9 @@
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.swiper-button-prev,
|
||||||
|
.swiper-rtl .swiper-button-next {
|
||||||
|
top: 200px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,10 +37,16 @@ declare module 'solid-js' {
|
||||||
onSlideChange?: () => void
|
onSlideChange?: () => void
|
||||||
onBeforeSlideChangeStart?: () => void
|
onBeforeSlideChangeStart?: () => void
|
||||||
class?: string
|
class?: string
|
||||||
|
observer?: boolean
|
||||||
|
loop?: boolean
|
||||||
|
speed?: number
|
||||||
|
slidesPerGroupAuto?: boolean
|
||||||
|
navigation?: boolean
|
||||||
breakpoints?: {
|
breakpoints?: {
|
||||||
[width: number]: SwiperOptions
|
[width: number]: SwiperOptions
|
||||||
[ratio: string]: SwiperOptions
|
[ratio: string]: SwiperOptions
|
||||||
}
|
}
|
||||||
|
direction?: 'horizontal' | 'vertical'
|
||||||
autoplay?: AutoplayOptions | boolean
|
autoplay?: AutoplayOptions | boolean
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
|
|
@ -582,8 +582,8 @@ figure {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
gap: 16px;
|
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
|
gap: 0.6rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -596,11 +596,8 @@ figure {
|
||||||
figure {
|
figure {
|
||||||
figcaption {
|
figcaption {
|
||||||
color: rgb(0 0 0 / 60%);
|
color: rgb(0 0 0 / 60%);
|
||||||
|
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,22 +8,16 @@ export const renderUploadedImage = (editor: Editor, image: UploadedFile) => {
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.insertContent({
|
.insertContent({
|
||||||
type: 'capturedImage',
|
type: 'figure',
|
||||||
|
attrs: { 'data-type': 'image' },
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'figcaption',
|
type: 'image',
|
||||||
content: [
|
attrs: { src: image.url },
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: image.originalFilename ?? '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'figcaption',
|
||||||
attrs: {
|
content: [{ type: 'text', text: image.originalFilename }],
|
||||||
src: image.url,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user