gridfix+editor-wip
This commit is contained in:
parent
6551263fe8
commit
8ba69a5f7f
18
prompt-20steps.txt
Normal file
18
prompt-20steps.txt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
Begin by enclosing all thoughts within <thinking> tags, exploring multiple angles and approaches.
|
||||||
|
Break down the solution into clear steps within <step> tags. Start with a 20-step budget, requesting more for complex problems if needed.
|
||||||
|
Use <count> tags after each step to show the remaining budget. Stop when reaching 0.
|
||||||
|
Continuously adjust your reasoning based on intermediate results and reflections, adapting your strategy as you progress.
|
||||||
|
Regularly evaluate progress using <reflection> tags. Be critical and honest about your reasoning process.
|
||||||
|
Assign a quality score between 0.0 and 1.0 using <reward> tags after each reflection. Use this to guide your approach:
|
||||||
|
|
||||||
|
0.8+: Continue current approach
|
||||||
|
0.5-0.7: Consider minor adjustments
|
||||||
|
Below 0.5: Seriously consider backtracking and trying a different approach
|
||||||
|
|
||||||
|
|
||||||
|
If unsure or if reward score is low, backtrack and try a different approach, explaining your decision within <thinking> tags.
|
||||||
|
For mathematical problems, show all work explicitly using LaTeX for formal notation and provide detailed proofs.
|
||||||
|
Explore multiple solutions individually if possible, comparing approaches in reflections.
|
||||||
|
Use thoughts as a scratchpad, writing out all calculations and reasoning explicitly.
|
||||||
|
Synthesize the final answer within <answer> tags, providing a clear, concise summary.
|
||||||
|
Conclude with a final reflection on the overall solution, discussing effectiveness, challenges, and solutions. Assign a final reward score.
|
|
@ -1,4 +1,5 @@
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
|
import { UploadFile } from '@solid-primitives/upload'
|
||||||
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
|
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||||
|
@ -6,9 +7,10 @@ import { Collaboration } from '@tiptap/extension-collaboration'
|
||||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
||||||
import { FloatingMenu } from '@tiptap/extension-floating-menu'
|
import { FloatingMenu } from '@tiptap/extension-floating-menu'
|
||||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||||
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js'
|
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
|
import { createTiptapEditor } from 'solid-tiptap'
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import { Doc, Transaction } from 'yjs'
|
import { Doc } from 'yjs'
|
||||||
import { useEditorContext } from '~/context/editor'
|
import { useEditorContext } from '~/context/editor'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useSession } from '~/context/session'
|
import { useSession } from '~/context/session'
|
||||||
|
@ -24,6 +26,8 @@ import { IncutBubbleMenu } from './Toolbar/IncutBubbleMenu'
|
||||||
import { TextBubbleMenu } from './Toolbar/TextBubbleMenu'
|
import { TextBubbleMenu } from './Toolbar/TextBubbleMenu'
|
||||||
|
|
||||||
import './Editor.module.scss'
|
import './Editor.module.scss'
|
||||||
|
import { isServer } from 'solid-js/web'
|
||||||
|
import { Panel } from './Panel/Panel'
|
||||||
|
|
||||||
export type EditorComponentProps = {
|
export type EditorComponentProps = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -37,35 +41,61 @@ const providers: Record<string, HocuspocusProvider> = {}
|
||||||
|
|
||||||
export const EditorComponent = (props: EditorComponentProps) => {
|
export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { session } = useSession()
|
const { session, requireAuthentication } = useSession()
|
||||||
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
|
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
|
||||||
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||||
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { createEditor, countWords, editor } = useEditorContext()
|
const { countWords, setEditing } = useEditorContext()
|
||||||
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
|
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
|
||||||
const [editorElRef, setEditorElRef] = createSignal<HTMLElement | undefined>()
|
const [editorElRef, setEditorElRef] = createSignal<HTMLElement | undefined>()
|
||||||
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal<HTMLElement | undefined>()
|
const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [figureBubbleMenuRef, setFigureBubbleMenuRef] = createSignal<HTMLElement | undefined>()
|
const [figureBubbleMenuRef, setFigureBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [blockquoteBubbleMenuRef, setBlockquoteBubbleMenuRef] = createSignal<HTMLElement | undefined>()
|
const [blockquoteBubbleMenuRef, setBlockquoteBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [floatingMenuRef, setFloatingMenuRef] = createSignal<HTMLDivElement | undefined>()
|
const [floatingMenuRef, setFloatingMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
|
const [editor, setEditor] = createSignal<Editor | null>(null)
|
||||||
|
const [menusInitialized, setMenusInitialized] = createSignal(false)
|
||||||
|
|
||||||
|
// store tiptap editor in context provider's signal to use it in Panel
|
||||||
|
createEffect(() => setEditing(editor() || undefined))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает экземпляр редактора с заданными опциями
|
||||||
|
* @param opts Опции редактора
|
||||||
|
*/
|
||||||
|
const createEditorInstance = (opts?: Partial<EditorOptions>) => {
|
||||||
|
if (!opts?.element) {
|
||||||
|
console.error('Editor options or element is missing')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('stage 2: create editor instance without menus', opts)
|
||||||
|
|
||||||
|
const old = editor() || { options: {} }
|
||||||
|
const fresh = createTiptapEditor(() => ({
|
||||||
|
...old?.options,
|
||||||
|
...opts,
|
||||||
|
element: opts.element as HTMLElement
|
||||||
|
}))
|
||||||
|
if (old instanceof Editor) old?.destroy()
|
||||||
|
setEditor(fresh() || null)
|
||||||
|
}
|
||||||
|
|
||||||
const handleClipboardPaste = async () => {
|
const handleClipboardPaste = async () => {
|
||||||
try {
|
try {
|
||||||
const clipboardItems = await navigator.clipboard.read()
|
const clipboardItems: ClipboardItems = await navigator.clipboard.read()
|
||||||
|
|
||||||
if (clipboardItems.length === 0) return
|
if (clipboardItems.length === 0) return
|
||||||
const [clipboardItem] = clipboardItems
|
const [clipboardItem] = clipboardItems
|
||||||
const { types } = clipboardItem
|
const { types } = clipboardItem
|
||||||
const imageType = types.find((type) => allowedImageTypes.has(type))
|
const imageType: string | undefined = types.find((type) => allowedImageTypes.has(type))
|
||||||
|
|
||||||
if (!imageType) return
|
if (!imageType) return
|
||||||
const blob = await clipboardItem.getType(imageType)
|
const blob = await clipboardItem.getType(imageType)
|
||||||
const extension = imageType.split('/')[1]
|
const extension = imageType.split('/')[1]
|
||||||
const file = new File([blob], `clipboardImage.${extension}`)
|
const file = new File([blob], `clipboardImage.${extension}`)
|
||||||
|
|
||||||
const uplFile = {
|
const uplFile: UploadFile = {
|
||||||
source: blob.toString(),
|
source: blob.toString(),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
@ -73,7 +103,10 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
showSnackbar({ body: t('Uploading image') })
|
showSnackbar({ body: t('Uploading image') })
|
||||||
const image = await handleImageUpload(uplFile, session()?.access_token || '')
|
const image: { url: string; originalFilename?: string } = await handleImageUpload(
|
||||||
|
uplFile,
|
||||||
|
session()?.access_token || ''
|
||||||
|
)
|
||||||
renderUploadedImage(editor() as Editor, image)
|
renderUploadedImage(editor() as Editor, image)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Paste Image Error]:', error)
|
console.error('[Paste Image Error]:', error)
|
||||||
|
@ -81,32 +114,95 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
// stage 0: update editor options
|
||||||
on([editorOptions, editorElRef, author], ([opts, element, a]) => {
|
const setupEditor = () => {
|
||||||
if (!opts && a && element) {
|
console.log('stage 0: update editor options')
|
||||||
const options = {
|
const options: Partial<EditorOptions> = {
|
||||||
element: editorElRef()!,
|
element: editorElRef()!,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: { class: 'articleEditor' },
|
attributes: { class: 'articleEditor' },
|
||||||
transformPastedHTML: (c: string) => c.replaceAll(/<img.*?>/g, ''),
|
transformPastedHTML: (c: string) => c.replaceAll(/<img.*?>/g, ''),
|
||||||
handlePaste: handleClipboardPaste
|
handlePaste: (_view, _event, _slice) => {
|
||||||
|
handleClipboardPaste().then((result) => result)
|
||||||
|
return false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
extensions: [
|
extensions: [
|
||||||
...base,
|
...base,
|
||||||
...custom,
|
...custom,
|
||||||
...extended,
|
...extended,
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: t('Add a link or click plus to embed media')
|
||||||
|
}),
|
||||||
|
CharacterCount.configure()
|
||||||
|
],
|
||||||
|
onTransaction({ transaction, editor }) {
|
||||||
|
if (transaction.docChanged) {
|
||||||
|
const html = editor.getHTML()
|
||||||
|
html && props.onChange(html)
|
||||||
|
const wordCount: number = editor.storage.characterCount.words()
|
||||||
|
const charsCount: number = editor.storage.characterCount.characters()
|
||||||
|
wordCount && countWords({ words: wordCount, characters: charsCount })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: props.initialContent ?? null
|
||||||
|
}
|
||||||
|
console.log('Editor options created:', options)
|
||||||
|
setEditorOptions(() => options)
|
||||||
|
}
|
||||||
|
|
||||||
Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }),
|
// stage 1: create editor options when got author profile
|
||||||
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
createEffect(
|
||||||
|
on([editorOptions, author], ([opts, a]: [Partial<EditorOptions> | undefined, Author | undefined]) => {
|
||||||
|
if (isServer) return
|
||||||
|
console.log('stage 1: create editor options when got author profile', { opts, a })
|
||||||
|
const noOptions = !opts || Object.keys(opts).length === 0
|
||||||
|
noOptions && a && setTimeout(setupEditor, 1)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// menus
|
// Перенос всех эффектов, зависящих от editor, внутрь onMount
|
||||||
|
onMount(() => {
|
||||||
|
console.log('Editor component mounted')
|
||||||
|
editorElRef()?.addEventListener('focus', handleFocus)
|
||||||
|
requireAuthentication(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setupEditor()
|
||||||
|
|
||||||
|
// Создаем экземпляр редактора после монтирования
|
||||||
|
createEditorInstance(editorOptions())
|
||||||
|
|
||||||
|
// Инициализируем меню после создания редактора
|
||||||
|
if (editor()) {
|
||||||
|
initializeMenus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем коллаборацию если необходимо
|
||||||
|
if (!props.disableCollaboration) {
|
||||||
|
initializeCollaboration()
|
||||||
|
}
|
||||||
|
}, 1200)
|
||||||
|
}, 'edit')
|
||||||
|
})
|
||||||
|
|
||||||
|
const initializeMenus = () => {
|
||||||
|
if (menusInitialized() || !editor()) return
|
||||||
|
|
||||||
|
console.log('stage 3: initialize menus when editor instance is ready')
|
||||||
|
|
||||||
|
if (
|
||||||
|
textBubbleMenuRef() &&
|
||||||
|
blockquoteBubbleMenuRef() &&
|
||||||
|
figureBubbleMenuRef() &&
|
||||||
|
incutBubbleMenuRef() &&
|
||||||
|
floatingMenuRef()
|
||||||
|
) {
|
||||||
|
const menus = [
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
pluginKey: 'textBubbleMenu',
|
pluginKey: 'textBubbleMenu',
|
||||||
element: textBubbleMenuRef(),
|
element: textBubbleMenuRef(),
|
||||||
shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => {
|
shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => {
|
||||||
const isEmptyTextBlock =
|
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
|
||||||
isEmptyTextBlock &&
|
isEmptyTextBlock &&
|
||||||
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
|
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
|
||||||
|
|
||||||
|
@ -130,68 +226,55 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
pluginKey: 'blockquoteBubbleMenu',
|
pluginKey: 'blockquoteBubbleMenu',
|
||||||
element: blockquoteBubbleMenuRef(),
|
element: blockquoteBubbleMenuRef(),
|
||||||
shouldShow: ({ editor: e, view, state }) =>
|
shouldShow: ({ editor: e, view, state }) =>
|
||||||
view.hasFocus() && !state.selection.empty && e.isActive('blockquote')
|
view.hasFocus() && !state.selection.empty && e?.isActive('blockquote')
|
||||||
}),
|
}),
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
pluginKey: 'figureBubbleMenu',
|
pluginKey: 'figureBubbleMenu',
|
||||||
element: figureBubbleMenuRef(),
|
element: figureBubbleMenuRef(),
|
||||||
shouldShow: ({ editor: e, view, state }) =>
|
shouldShow: ({ editor: e, view, state }) =>
|
||||||
view.hasFocus() && !state.selection.empty && e.isActive('figure')
|
view.hasFocus() && !state.selection.empty && e?.isActive('figure')
|
||||||
}),
|
}),
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
pluginKey: 'incutBubbleMenu',
|
pluginKey: 'incutBubbleMenu',
|
||||||
element: incutBubbleMenuRef(),
|
element: incutBubbleMenuRef(),
|
||||||
shouldShow: ({ editor: e, view, state }) =>
|
shouldShow: ({ editor: e, view, state }) =>
|
||||||
view.hasFocus() && !state.selection.empty && e.isActive('figcaption')
|
view.hasFocus() && !state.selection.empty && e?.isActive('figcaption')
|
||||||
}),
|
}),
|
||||||
FloatingMenu.configure({
|
FloatingMenu.configure({
|
||||||
element: floatingMenuRef(),
|
element: floatingMenuRef(),
|
||||||
pluginKey: 'floatingMenu',
|
pluginKey: 'floatingMenu',
|
||||||
shouldShow: ({ editor: e, state: { selection } }) => {
|
shouldShow: ({ editor: e, state: { selection } }) => {
|
||||||
const isRootDepth = selection.$anchor.depth === 1
|
const isRootDepth = selection.$anchor.depth === 1
|
||||||
if (!(isRootDepth && selection.empty)) return false
|
const show =
|
||||||
return !(e.isActive('codeBlock') || e.isActive('heading'))
|
isRootDepth && selection.empty && !(e?.isActive('codeBlock') || e?.isActive('heading'))
|
||||||
|
console.log('FloatingMenu shouldShow:', show)
|
||||||
|
return show
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
]
|
||||||
// dynamic
|
const extensions = [...(editorOptions().extensions || []), ...menus]
|
||||||
// Collaboration.configure({ document: yDocs[docName] }),
|
setEditorOptions((prev) => ({ ...prev, extensions }))
|
||||||
// CollaborationCursor.configure({ provider: providers[docName], user: { name: a.name, color: uniqolor(a.slug).color } }),
|
console.log('Editor menus initialized:', extensions)
|
||||||
],
|
setMenusInitialized(true)
|
||||||
onTransaction({ transaction, editor }: { transaction: Transaction; editor: Editor }) {
|
} else {
|
||||||
if (transaction.changed) {
|
console.error('Some menu references are missing')
|
||||||
// Get the current HTML content from the editor
|
|
||||||
const html = editor.getHTML()
|
|
||||||
|
|
||||||
// Trigger the onChange callback with the updated HTML
|
|
||||||
html && props.onChange(html)
|
|
||||||
|
|
||||||
// Get the word count from the editor's storage (using CharacterCount)
|
|
||||||
const wordCount = editor.storage.characterCount.words()
|
|
||||||
|
|
||||||
// Update the word count
|
|
||||||
wordCount && countWords(wordCount)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
content: props.initialContent || ''
|
|
||||||
}
|
}
|
||||||
setEditorOptions(options as unknown as Partial<EditorOptions>)
|
|
||||||
createEditor(options as unknown as Partial<EditorOptions>)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
const initializeCollaboration = () => {
|
||||||
on(
|
if (!editor()) {
|
||||||
[
|
console.error('Editor is not initialized')
|
||||||
editor,
|
return
|
||||||
() => !props.disableCollaboration,
|
}
|
||||||
() => `shout-${props.shoutId}`,
|
|
||||||
() => session()?.access_token || '',
|
try {
|
||||||
author
|
const docName = `shout-${props.shoutId}`
|
||||||
],
|
const token = session()?.access_token || ''
|
||||||
([e, collab, docName, token, profile]) => {
|
const profile = author()
|
||||||
if (!e) return
|
|
||||||
|
if (!(token && profile)) {
|
||||||
|
throw new Error('Missing authentication data')
|
||||||
|
}
|
||||||
|
|
||||||
if (!yDocs[docName]) {
|
if (!yDocs[docName]) {
|
||||||
yDocs[docName] = new Doc()
|
yDocs[docName] = new Doc()
|
||||||
|
@ -204,57 +287,70 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
document: yDocs[docName],
|
document: yDocs[docName],
|
||||||
token
|
token
|
||||||
})
|
})
|
||||||
|
console.log(`HocuspocusProvider установлен для ${docName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
collab &&
|
setEditorOptions((prev: Partial<EditorOptions>) => {
|
||||||
createEditor({
|
const extensions = [...(prev.extensions || [])]
|
||||||
...editorOptions(),
|
extensions.push(
|
||||||
extensions: [
|
|
||||||
...(editor()?.options.extensions || []),
|
|
||||||
Collaboration.configure({ document: yDocs[docName] }),
|
Collaboration.configure({ document: yDocs[docName] }),
|
||||||
CollaborationCursor.configure({
|
CollaborationCursor.configure({
|
||||||
provider: providers[docName],
|
provider: providers[docName],
|
||||||
user: { name: profile.name, color: uniqolor(profile.slug).color }
|
user: { name: profile.name, color: uniqolor(profile.slug).color }
|
||||||
})
|
})
|
||||||
]
|
)
|
||||||
|
console.log('collab extensions added:', extensions)
|
||||||
|
return { ...prev, extensions }
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing collaboration:', error)
|
||||||
|
showSnackbar({ body: t('Failed to initialize collaboration') })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
const handleFocus = (event: FocusEvent) => {
|
||||||
on(editorElRef, (ee: HTMLElement | undefined) => {
|
console.log('handling focus event', event)
|
||||||
ee?.addEventListener('focus', (_event) => {
|
|
||||||
if (editor()?.isActive('figcaption')) {
|
if (editor()?.isActive('figcaption')) {
|
||||||
editor()?.commands.focus()
|
editor()?.commands.focus()
|
||||||
|
console.log('active figcaption detected, focusing editor')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
editorElRef()?.removeEventListener('focus', handleFocus)
|
||||||
editor()?.destroy()
|
editor()?.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div>
|
||||||
|
<Show when={editor()} keyed>
|
||||||
|
{(ed: Editor) => (
|
||||||
|
<>
|
||||||
|
<TextBubbleMenu
|
||||||
|
shouldShow={shouldShowTextBubbleMenu()}
|
||||||
|
isCommonMarkup={isCommonMarkup()}
|
||||||
|
editor={ed}
|
||||||
|
ref={setTextBubbleMenuRef}
|
||||||
|
/>
|
||||||
|
<BlockquoteBubbleMenu editor={ed} ref={setBlockquoteBubbleMenuRef} />
|
||||||
|
<FigureBubbleMenu editor={ed} ref={setFigureBubbleMenuRef} />
|
||||||
|
<IncutBubbleMenu editor={ed} ref={setIncutBubbleMenuRef} />
|
||||||
|
<EditorFloatingMenu editor={ed} ref={setFloatingMenuRef} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5" />
|
<div class="col-md-5" />
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div ref={setEditorElRef} id="editorBody" />
|
<div ref={setEditorElRef} id="editorBody" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={editor()}>
|
|
||||||
<TextBubbleMenu
|
<Show when={props.shoutId}>
|
||||||
shouldShow={shouldShowTextBubbleMenu()}
|
<Panel shoutId={props.shoutId} />
|
||||||
isCommonMarkup={isCommonMarkup()}
|
|
||||||
editor={editor() as Editor}
|
|
||||||
ref={setTextBubbleMenuRef}
|
|
||||||
/>
|
|
||||||
<BlockquoteBubbleMenu ref={setBlockquoteBubbleMenuRef} editor={editor() as Editor} />
|
|
||||||
<FigureBubbleMenu editor={editor() as Editor} ref={setFigureBubbleMenuRef} />
|
|
||||||
<IncutBubbleMenu editor={editor() as Editor} ref={setIncutBubbleMenuRef} />
|
|
||||||
<EditorFloatingMenu editor={editor() as Editor} ref={setFloatingMenuRef} />
|
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const Panel = (props: Props) => {
|
||||||
saveShout,
|
saveShout,
|
||||||
saveDraft,
|
saveDraft,
|
||||||
publishShout,
|
publishShout,
|
||||||
editor
|
editing: editor
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
|
|
||||||
let containerRef: HTMLElement | undefined
|
let containerRef: HTMLElement | undefined
|
||||||
|
|
|
@ -23,7 +23,10 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
|
|
||||||
const isActive = (name: string, attributes?: Record<string, string | number>) =>
|
const isActive = (name: string, attributes?: Record<string, string | number>) =>
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => props.editor,
|
() => {
|
||||||
|
console.log('isActive', name, attributes)
|
||||||
|
return props.editor
|
||||||
|
},
|
||||||
(editor) => editor?.isActive(name, attributes)
|
(editor) => editor?.isActive(name, attributes)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Popover } from '~/components/_shared/Popover'
|
||||||
|
|
||||||
import styles from '../MiniEditor.module.scss'
|
import styles from '../MiniEditor.module.scss'
|
||||||
|
|
||||||
interface ControlProps {
|
export interface ControlProps {
|
||||||
editor: Editor | undefined
|
editor: Editor | undefined
|
||||||
title: string
|
title: string
|
||||||
key: string
|
key: string
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from '
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { debounce } from 'throttle-debounce'
|
import { debounce } from 'throttle-debounce'
|
||||||
import { EditorComponent } from '~/components/Editor/Editor'
|
import { EditorComponent } from '~/components/Editor/Editor'
|
||||||
import { Panel } from '~/components/Editor/Panel/Panel'
|
|
||||||
import { DropArea } from '~/components/_shared/DropArea'
|
import { DropArea } from '~/components/_shared/DropArea'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { Icon } from '~/components/_shared/Icon'
|
||||||
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
||||||
|
@ -265,30 +264,8 @@ export const EditView = (props: Props) => {
|
||||||
setIsLeadVisible(true)
|
setIsLeadVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HeadingActions = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div class={styles.container}>
|
|
||||||
<form>
|
|
||||||
<div class="wide-container">
|
|
||||||
<button
|
|
||||||
class={clsx(styles.scrollTopButton, {
|
|
||||||
[styles.visible]: isScrolled()
|
|
||||||
})}
|
|
||||||
onClick={handleScrollTopButtonClick}
|
|
||||||
>
|
|
||||||
<Icon name="up-button" class={styles.icon} />
|
|
||||||
<span class={styles.scrollTopButtonLabel}>{t('Scroll up')}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AutoSaveNotice active={saving()} />
|
|
||||||
|
|
||||||
<div class={styles.wrapperTableOfContents}>
|
|
||||||
<Show when={isDesktop() && form.body}>
|
|
||||||
<TableOfContents variant="editor" parentSelector="#editorBody" body={form.body} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
||||||
<Show when={props.shout}>
|
<Show when={props.shout}>
|
||||||
<div class={styles.headingActions}>
|
<div class={styles.headingActions}>
|
||||||
|
@ -442,6 +419,34 @@ export const EditView = (props: Props) => {
|
||||||
</>
|
</>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class={styles.container}>
|
||||||
|
<form>
|
||||||
|
<div class="wide-container">
|
||||||
|
<button
|
||||||
|
class={clsx(styles.scrollTopButton, {
|
||||||
|
[styles.visible]: isScrolled()
|
||||||
|
})}
|
||||||
|
onClick={handleScrollTopButtonClick}
|
||||||
|
>
|
||||||
|
<Icon name="up-button" class={styles.icon} />
|
||||||
|
<span class={styles.scrollTopButtonLabel}>{t('Scroll up')}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AutoSaveNotice active={saving()} />
|
||||||
|
|
||||||
|
<div class={styles.wrapperTableOfContents}>
|
||||||
|
<Show when={isDesktop() && form.body}>
|
||||||
|
<TableOfContents variant="editor" parentSelector="#editorBody" body={form.body} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<HeadingActions />
|
||||||
</div>
|
</div>
|
||||||
<Show when={draft()?.id} fallback={<Loading />}>
|
<Show when={draft()?.id} fallback={<Loading />}>
|
||||||
<EditorComponent
|
<EditorComponent
|
||||||
|
@ -453,9 +458,6 @@ export const EditView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.shout}>
|
|
||||||
<Panel shoutId={props.shout.id} />
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Modal variant="medium" name="inviteCoauthors">
|
<Modal variant="medium" name="inviteCoauthors">
|
||||||
<InviteMembers variant={'coauthors'} title={t('Invite experts')} />
|
<InviteMembers variant={'coauthors'} title={t('Invite experts')} />
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { byCreated } from '~/utils/sort'
|
||||||
export type LoadMoreItems = Shout[] | Author[] | Reaction[]
|
export type LoadMoreItems = Shout[] | Author[] | Reaction[]
|
||||||
|
|
||||||
type LoadMoreProps = {
|
type LoadMoreProps = {
|
||||||
loadFunction: (offset: number) => Promise<LoadMoreItems>
|
loadFunction: (offset: number) => Promise<LoadMoreItems | undefined>
|
||||||
pageSize: number
|
pageSize: number
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useMatch, useNavigate } from '@solidjs/router'
|
import { useMatch, useNavigate } from '@solidjs/router'
|
||||||
import { Editor, EditorOptions } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { Accessor, createContext, createSignal, useContext } from 'solid-js'
|
import { Accessor, createContext, createSignal, useContext } from 'solid-js'
|
||||||
import { SetStoreFunction, createStore } from 'solid-js/store'
|
import { SetStoreFunction, createStore } from 'solid-js/store'
|
||||||
import { createTiptapEditor } from 'solid-tiptap'
|
|
||||||
import { useSnackbar } from '~/context/ui'
|
import { useSnackbar } from '~/context/ui'
|
||||||
import deleteShoutQuery from '~/graphql/mutation/core/article-delete'
|
import deleteShoutQuery from '~/graphql/mutation/core/article-delete'
|
||||||
import updateShoutQuery from '~/graphql/mutation/core/article-update'
|
import updateShoutQuery from '~/graphql/mutation/core/article-update'
|
||||||
|
@ -13,7 +12,7 @@ import { useFeed } from '../context/feed'
|
||||||
import { useLocalize } from './localize'
|
import { useLocalize } from './localize'
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
|
||||||
type WordCounter = {
|
export type WordCounter = {
|
||||||
characters: number
|
characters: number
|
||||||
words: number
|
words: number
|
||||||
}
|
}
|
||||||
|
@ -49,8 +48,8 @@ export type EditorContextType = {
|
||||||
countWords: (value: WordCounter) => void
|
countWords: (value: WordCounter) => void
|
||||||
setForm: SetStoreFunction<ShoutForm>
|
setForm: SetStoreFunction<ShoutForm>
|
||||||
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
|
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
|
||||||
editor: Accessor<Editor | undefined>
|
editing: Accessor<Editor | undefined>
|
||||||
createEditor: (opts?: Partial<EditorOptions>) => void
|
setEditing: SetStoreFunction<Editor | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContext = createContext<EditorContextType>({} as EditorContextType)
|
export const EditorContext = createContext<EditorContextType>({} as EditorContextType)
|
||||||
|
@ -84,7 +83,6 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
const matchEdit = useMatch(() => '/edit')
|
const matchEdit = useMatch(() => '/edit')
|
||||||
const matchEditSettings = useMatch(() => '/editSettings')
|
const matchEditSettings = useMatch(() => '/editSettings')
|
||||||
const { client } = useSession()
|
const { client } = useSession()
|
||||||
const [editor, setEditor] = createSignal<Editor | undefined>()
|
|
||||||
const { addFeed } = useFeed()
|
const { addFeed } = useFeed()
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
||||||
|
@ -268,17 +266,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEditor = (opts?: Partial<EditorOptions>) => {
|
// current publishing editor instance to connect settings, panel and editor
|
||||||
if (!opts) return
|
const [editing, setEditing] = createSignal<Editor | undefined>(undefined)
|
||||||
const old = editor() as Editor
|
|
||||||
const fresh = createTiptapEditor(() => ({
|
|
||||||
...old.options,
|
|
||||||
...opts,
|
|
||||||
element: opts.element as HTMLElement
|
|
||||||
}))
|
|
||||||
old?.destroy()
|
|
||||||
setEditor(fresh())
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
saveShout,
|
saveShout,
|
||||||
|
@ -292,8 +281,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
countWords,
|
countWords,
|
||||||
setForm,
|
setForm,
|
||||||
setFormErrors,
|
setFormErrors,
|
||||||
editor,
|
setEditing
|
||||||
createEditor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const value: EditorContextType = {
|
const value: EditorContextType = {
|
||||||
|
@ -301,7 +289,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
form,
|
form,
|
||||||
formErrors,
|
formErrors,
|
||||||
isEditorPanelVisible,
|
isEditorPanelVisible,
|
||||||
wordCounter
|
wordCounter,
|
||||||
|
editing
|
||||||
}
|
}
|
||||||
|
|
||||||
return <EditorContext.Provider value={value}>{props.children}</EditorContext.Provider>
|
return <EditorContext.Provider value={value}>{props.children}</EditorContext.Provider>
|
||||||
|
|
|
@ -31,6 +31,40 @@
|
||||||
@include make-col($i, $grid-columns);
|
@include make-col($i, $grid-columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем классы для управления порядком колонок
|
||||||
|
.order#{$infix}-first { order: -1; }
|
||||||
|
.order#{$infix}-last { order: $grid-columns + 1; }
|
||||||
|
|
||||||
|
@for $i from 0 through $grid-columns {
|
||||||
|
.order#{$infix}-#{$i} { order: $i; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем классы для смещения колонок
|
||||||
|
@for $i from 0 through $grid-columns - 1 {
|
||||||
|
.offset#{$infix}-#{$i} {
|
||||||
|
@include make-col-offset($i, $grid-columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем только используемые классы display для разных размеров экрана
|
||||||
|
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||||
|
$infix: if($breakpoint == 'xs', '', "-#{$breakpoint}");
|
||||||
|
|
||||||
|
@include media-breakpoint-up($breakpoint) {
|
||||||
|
.d#{$infix}-flex { display: flex !important; }
|
||||||
|
.d#{$infix}-none { display: none !important; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем только используемый класс justify-content для разных размеров экрана
|
||||||
|
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||||
|
$infix: if($breakpoint == 'xs', '', "-#{$breakpoint}");
|
||||||
|
|
||||||
|
@include media-breakpoint-up($breakpoint) {
|
||||||
|
.justify-content#{$infix}-between { justify-content: space-between !important; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
@import 'theme';
|
@import 'theme';
|
||||||
@import 'grid';
|
@import 'grid';
|
||||||
|
|
||||||
* {
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,8 +172,9 @@ button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user