From 8ba69a5f7fa2f8c82daefdad0ec84e39b00f1e8d Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 8 Oct 2024 22:50:58 +0300 Subject: [PATCH] gridfix+editor-wip --- prompt-20steps.txt | 18 + src/components/Editor/Editor.tsx | 426 +++++++++++------- src/components/Editor/Panel/Panel.tsx | 2 +- .../Editor/Toolbar/TextBubbleMenu.tsx | 5 +- .../Editor/Toolbar/ToolbarControl.tsx | 2 +- src/components/Views/EditView.tsx | 316 ++++++------- src/components/_shared/LoadMoreWrapper.tsx | 2 +- src/context/editor.tsx | 29 +- src/styles/_grid.scss | 34 ++ src/styles/app.scss | 9 +- 10 files changed, 494 insertions(+), 349 deletions(-) create mode 100644 prompt-20steps.txt diff --git a/prompt-20steps.txt b/prompt-20steps.txt new file mode 100644 index 00000000..849a2b9b --- /dev/null +++ b/prompt-20steps.txt @@ -0,0 +1,18 @@ +Begin by enclosing all thoughts within tags, exploring multiple angles and approaches. +Break down the solution into clear steps within tags. Start with a 20-step budget, requesting more for complex problems if needed. +Use 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 tags. Be critical and honest about your reasoning process. +Assign a quality score between 0.0 and 1.0 using 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 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 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. diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 1468f9ad..a202aa72 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -1,4 +1,5 @@ import { HocuspocusProvider } from '@hocuspocus/provider' +import { UploadFile } from '@solid-primitives/upload' import { Editor, EditorOptions, isTextSelection } from '@tiptap/core' import { BubbleMenu } from '@tiptap/extension-bubble-menu' 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 { FloatingMenu } from '@tiptap/extension-floating-menu' 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 { Doc, Transaction } from 'yjs' +import { Doc } from 'yjs' import { useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' @@ -24,6 +26,8 @@ import { IncutBubbleMenu } from './Toolbar/IncutBubbleMenu' import { TextBubbleMenu } from './Toolbar/TextBubbleMenu' import './Editor.module.scss' +import { isServer } from 'solid-js/web' +import { Panel } from './Panel/Panel' export type EditorComponentProps = { shoutId: number @@ -37,35 +41,61 @@ const providers: Record = {} export const EditorComponent = (props: EditorComponentProps) => { const { t } = useLocalize() - const { session } = useSession() + const { session, requireAuthentication } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) const [isCommonMarkup, setIsCommonMarkup] = createSignal(false) const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) const { showSnackbar } = useSnackbar() - const { createEditor, countWords, editor } = useEditorContext() + const { countWords, setEditing } = useEditorContext() const [editorOptions, setEditorOptions] = createSignal>({}) const [editorElRef, setEditorElRef] = createSignal() const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal() - const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal() - const [figureBubbleMenuRef, setFigureBubbleMenuRef] = createSignal() - const [blockquoteBubbleMenuRef, setBlockquoteBubbleMenuRef] = createSignal() + const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal() + const [figureBubbleMenuRef, setFigureBubbleMenuRef] = createSignal() + const [blockquoteBubbleMenuRef, setBlockquoteBubbleMenuRef] = createSignal() const [floatingMenuRef, setFloatingMenuRef] = createSignal() + const [editor, setEditor] = createSignal(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) => { + 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 () => { try { - const clipboardItems = await navigator.clipboard.read() + const clipboardItems: ClipboardItems = await navigator.clipboard.read() if (clipboardItems.length === 0) return const [clipboardItem] = clipboardItems const { types } = clipboardItem - const imageType = types.find((type) => allowedImageTypes.has(type)) + const imageType: string | undefined = types.find((type) => allowedImageTypes.has(type)) if (!imageType) return const blob = await clipboardItem.getType(imageType) const extension = imageType.split('/')[1] const file = new File([blob], `clipboardImage.${extension}`) - const uplFile = { + const uplFile: UploadFile = { source: blob.toString(), name: file.name, size: file.size, @@ -73,7 +103,10 @@ export const EditorComponent = (props: EditorComponentProps) => { } 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) } catch (error) { console.error('[Paste Image Error]:', error) @@ -81,180 +114,243 @@ export const EditorComponent = (props: EditorComponentProps) => { return false } - createEffect( - on([editorOptions, editorElRef, author], ([opts, element, a]) => { - if (!opts && a && element) { - const options = { - element: editorElRef()!, - editorProps: { - attributes: { class: 'articleEditor' }, - transformPastedHTML: (c: string) => c.replaceAll(//g, ''), - handlePaste: handleClipboardPaste - }, - extensions: [ - ...base, - ...custom, - ...extended, - - Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }), - CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 - - // menus - - BubbleMenu.configure({ - pluginKey: 'textBubbleMenu', - element: textBubbleMenuRef(), - shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => { - const isEmptyTextBlock = - doc.textBetween(from, to).length === 0 && isTextSelection(selection) - isEmptyTextBlock && - e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run() - - setIsCommonMarkup(e?.isActive('figcaption')) - const result = - (view.hasFocus() && - !selection.empty && - !isEmptyTextBlock && - !e.isActive('image') && - !e.isActive('figure')) || - e.isActive('footnote') || - (e.isActive('figcaption') && !selection.empty) - setShouldShowTextBubbleMenu(result) - return result - }, - tippyOptions: { - onHide: () => editor()?.commands.focus() as false - } - }), - BubbleMenu.configure({ - pluginKey: 'blockquoteBubbleMenu', - element: blockquoteBubbleMenuRef(), - shouldShow: ({ editor: e, view, state }) => - view.hasFocus() && !state.selection.empty && e.isActive('blockquote') - }), - BubbleMenu.configure({ - pluginKey: 'figureBubbleMenu', - element: figureBubbleMenuRef(), - shouldShow: ({ editor: e, view, state }) => - view.hasFocus() && !state.selection.empty && e.isActive('figure') - }), - BubbleMenu.configure({ - pluginKey: 'incutBubbleMenu', - element: incutBubbleMenuRef(), - shouldShow: ({ editor: e, view, state }) => - view.hasFocus() && !state.selection.empty && e.isActive('figcaption') - }), - FloatingMenu.configure({ - element: floatingMenuRef(), - pluginKey: 'floatingMenu', - shouldShow: ({ editor: e, state: { selection } }) => { - const isRootDepth = selection.$anchor.depth === 1 - if (!(isRootDepth && selection.empty)) return false - return !(e.isActive('codeBlock') || e.isActive('heading')) - } - }) - - // dynamic - // Collaboration.configure({ document: yDocs[docName] }), - // CollaborationCursor.configure({ provider: providers[docName], user: { name: a.name, color: uniqolor(a.slug).color } }), - ], - onTransaction({ transaction, editor }: { transaction: Transaction; editor: Editor }) { - if (transaction.changed) { - // 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 || '' + // stage 0: update editor options + const setupEditor = () => { + console.log('stage 0: update editor options') + const options: Partial = { + element: editorElRef()!, + editorProps: { + attributes: { class: 'articleEditor' }, + transformPastedHTML: (c: string) => c.replaceAll(//g, ''), + handlePaste: (_view, _event, _slice) => { + handleClipboardPaste().then((result) => result) + return false } - setEditorOptions(options as unknown as Partial) - createEditor(options as unknown as Partial) - } - }) - ) - - createEffect( - on( - [ - editor, - () => !props.disableCollaboration, - () => `shout-${props.shoutId}`, - () => session()?.access_token || '', - author + }, + extensions: [ + ...base, + ...custom, + ...extended, + Placeholder.configure({ + placeholder: t('Add a link or click plus to embed media') + }), + CharacterCount.configure() ], - ([e, collab, docName, token, profile]) => { - if (!e) return - - if (!yDocs[docName]) { - yDocs[docName] = new Doc() + 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) + } - if (!providers[docName]) { - providers[docName] = new HocuspocusProvider({ - url: 'wss://hocuspocus.discours.io', - name: docName, - document: yDocs[docName], - token - }) - } - - collab && - createEditor({ - ...editorOptions(), - extensions: [ - ...(editor()?.options.extensions || []), - Collaboration.configure({ document: yDocs[docName] }), - CollaborationCursor.configure({ - provider: providers[docName], - user: { name: profile.name, color: uniqolor(profile.slug).color } - }) - ] - }) - } - ) - ) - + // stage 1: create editor options when got author profile createEffect( - on(editorElRef, (ee: HTMLElement | undefined) => { - ee?.addEventListener('focus', (_event) => { - if (editor()?.isActive('figcaption')) { - editor()?.commands.focus() - } - }) + on([editorOptions, author], ([opts, a]: [Partial | 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) }) ) + // Перенос всех эффектов, зависящих от 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({ + pluginKey: 'textBubbleMenu', + element: textBubbleMenuRef(), + shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => { + const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection) + isEmptyTextBlock && + e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run() + + setIsCommonMarkup(e?.isActive('figcaption')) + const result = + (view.hasFocus() && + !selection.empty && + !isEmptyTextBlock && + !e.isActive('image') && + !e.isActive('figure')) || + e.isActive('footnote') || + (e.isActive('figcaption') && !selection.empty) + setShouldShowTextBubbleMenu(result) + return result + }, + tippyOptions: { + onHide: () => editor()?.commands.focus() as false + } + }), + BubbleMenu.configure({ + pluginKey: 'blockquoteBubbleMenu', + element: blockquoteBubbleMenuRef(), + shouldShow: ({ editor: e, view, state }) => + view.hasFocus() && !state.selection.empty && e?.isActive('blockquote') + }), + BubbleMenu.configure({ + pluginKey: 'figureBubbleMenu', + element: figureBubbleMenuRef(), + shouldShow: ({ editor: e, view, state }) => + view.hasFocus() && !state.selection.empty && e?.isActive('figure') + }), + BubbleMenu.configure({ + pluginKey: 'incutBubbleMenu', + element: incutBubbleMenuRef(), + shouldShow: ({ editor: e, view, state }) => + view.hasFocus() && !state.selection.empty && e?.isActive('figcaption') + }), + FloatingMenu.configure({ + element: floatingMenuRef(), + pluginKey: 'floatingMenu', + shouldShow: ({ editor: e, state: { selection } }) => { + const isRootDepth = selection.$anchor.depth === 1 + const show = + isRootDepth && selection.empty && !(e?.isActive('codeBlock') || e?.isActive('heading')) + console.log('FloatingMenu shouldShow:', show) + return show + } + }) + ] + const extensions = [...(editorOptions().extensions || []), ...menus] + setEditorOptions((prev) => ({ ...prev, extensions })) + console.log('Editor menus initialized:', extensions) + setMenusInitialized(true) + } else { + console.error('Some menu references are missing') + } + } + + const initializeCollaboration = () => { + if (!editor()) { + console.error('Editor is not initialized') + return + } + + try { + const docName = `shout-${props.shoutId}` + const token = session()?.access_token || '' + const profile = author() + + if (!(token && profile)) { + throw new Error('Missing authentication data') + } + + if (!yDocs[docName]) { + yDocs[docName] = new Doc() + } + + if (!providers[docName]) { + providers[docName] = new HocuspocusProvider({ + url: 'wss://hocuspocus.discours.io', + name: docName, + document: yDocs[docName], + token + }) + console.log(`HocuspocusProvider установлен для ${docName}`) + } + + setEditorOptions((prev: Partial) => { + const extensions = [...(prev.extensions || [])] + extensions.push( + Collaboration.configure({ document: yDocs[docName] }), + CollaborationCursor.configure({ + provider: providers[docName], + 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') }) + } + } + + const handleFocus = (event: FocusEvent) => { + console.log('handling focus event', event) + if (editor()?.isActive('figcaption')) { + editor()?.commands.focus() + console.log('active figcaption detected, focusing editor') + } + } + onCleanup(() => { + editorElRef()?.removeEventListener('focus', handleFocus) editor()?.destroy() }) return ( <> +
+ + {(ed: Editor) => ( + <> + + + + + + + )} + +
+
- - - - - - + + + ) diff --git a/src/components/Editor/Panel/Panel.tsx b/src/components/Editor/Panel/Panel.tsx index 3afbce20..e8eee0e7 100644 --- a/src/components/Editor/Panel/Panel.tsx +++ b/src/components/Editor/Panel/Panel.tsx @@ -30,7 +30,7 @@ export const Panel = (props: Props) => { saveShout, saveDraft, publishShout, - editor + editing: editor } = useEditorContext() let containerRef: HTMLElement | undefined diff --git a/src/components/Editor/Toolbar/TextBubbleMenu.tsx b/src/components/Editor/Toolbar/TextBubbleMenu.tsx index 0f5d560b..cc03865d 100644 --- a/src/components/Editor/Toolbar/TextBubbleMenu.tsx +++ b/src/components/Editor/Toolbar/TextBubbleMenu.tsx @@ -23,7 +23,10 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { const isActive = (name: string, attributes?: Record) => createEditorTransaction( - () => props.editor, + () => { + console.log('isActive', name, attributes) + return props.editor + }, (editor) => editor?.isActive(name, attributes) ) diff --git a/src/components/Editor/Toolbar/ToolbarControl.tsx b/src/components/Editor/Toolbar/ToolbarControl.tsx index b0893578..0a3a585d 100644 --- a/src/components/Editor/Toolbar/ToolbarControl.tsx +++ b/src/components/Editor/Toolbar/ToolbarControl.tsx @@ -5,7 +5,7 @@ import { Popover } from '~/components/_shared/Popover' import styles from '../MiniEditor.module.scss' -interface ControlProps { +export interface ControlProps { editor: Editor | undefined title: string key: string diff --git a/src/components/Views/EditView.tsx b/src/components/Views/EditView.tsx index c7b9369d..1f0bc29e 100644 --- a/src/components/Views/EditView.tsx +++ b/src/components/Views/EditView.tsx @@ -4,7 +4,6 @@ import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from ' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' import { EditorComponent } from '~/components/Editor/Editor' -import { Panel } from '~/components/Editor/Panel/Panel' import { DropArea } from '~/components/_shared/DropArea' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' @@ -265,6 +264,164 @@ export const EditView = (props: Props) => { setIsLeadVisible(true) } + const HeadingActions = () => { + return ( +
+ +
+ +
+ {t('Add subtitle')} +
+
+ +
+ {t('Add intro')} +
+
+
+ <> +
+
+ handleTitleInputChange(value)} + class={styles.titleInput} + placeholder={articleTitle()} + initialValue={form.title} + maxLength={MAX_HEADER_LIMIT} + /> + + +
{formErrors.title}
+
+ + +
+ handleBaseFieldsChange('artist', event.target.value)} + /> + handleBaseFieldsChange('date', event.target.value)} + /> + handleBaseFieldsChange('genre', event.target.value)} + /> +
+
+ + + handleInputChange('subtitle', value || '')} + class={styles.subtitleInput} + placeholder={t('Subheader')} + initialValue={form.subtitle || ''} + maxLength={MAX_HEADER_LIMIT} + /> + + + handleInputChange('lead', value)} + /> + + +
+ + + {t('min. 1400×1400 pix')} +
+ {t('jpg, .png, max. 10 mb.')} + + } + isMultiply={false} + fileType={'image'} + onUpload={(val) => handleInputChange('coverImageUrl', val[0].url)} + /> + } + > +
+ + {(triggerRef: (_el: HTMLElement | null) => void) => ( +
handleInputChange('coverImageUrl', '')} + > + +
+ )} +
+
+
+
+
+ + + handleMediaDelete(index)} + onImagesAdd={(value: MediaItem[]) => handleAddMedia(value)} + onImagesSorted={(value) => handleSortedMedia(value)} + /> + + + + handleAddMedia(data)} + onVideoDelete={(index) => handleMediaDelete(index)} + /> + + + + handleAddMedia(value)} + onAudioChange={handleMediaChange} + onAudioSorted={(value) => handleSortedMedia(value)} + /> + + +
+
+ ) + } + return ( <>
@@ -289,159 +446,7 @@ export const EditView = (props: Props) => {
-
- -
- -
- {t('Add subtitle')} -
-
- -
- {t('Add intro')} -
-
-
- <> -
-
- handleTitleInputChange(value)} - class={styles.titleInput} - placeholder={articleTitle()} - initialValue={form.title} - maxLength={MAX_HEADER_LIMIT} - /> - - -
{formErrors.title}
-
- - -
- handleBaseFieldsChange('artist', event.target.value)} - /> - handleBaseFieldsChange('date', event.target.value)} - /> - handleBaseFieldsChange('genre', event.target.value)} - /> -
-
- - - handleInputChange('subtitle', value || '')} - class={styles.subtitleInput} - placeholder={t('Subheader')} - initialValue={form.subtitle || ''} - maxLength={MAX_HEADER_LIMIT} - /> - - - handleInputChange('lead', value)} - /> - - -
- - - {t('min. 1400×1400 pix')} -
- {t('jpg, .png, max. 10 mb.')} - - } - isMultiply={false} - fileType={'image'} - onUpload={(val) => handleInputChange('coverImageUrl', val[0].url)} - /> - } - > -
- - {(triggerRef: (_el: HTMLElement | null) => void) => ( -
handleInputChange('coverImageUrl', '')} - > - -
- )} -
-
-
-
-
- - - handleMediaDelete(index)} - onImagesAdd={(value: MediaItem[]) => handleAddMedia(value)} - onImagesSorted={(value) => handleSortedMedia(value)} - /> - - - - handleAddMedia(data)} - onVideoDelete={(index) => handleMediaDelete(index)} - /> - - - - handleAddMedia(value)} - onAudioChange={handleMediaChange} - onAudioSorted={(value) => handleSortedMedia(value)} - /> - - -
-
+
}> {
- - - diff --git a/src/components/_shared/LoadMoreWrapper.tsx b/src/components/_shared/LoadMoreWrapper.tsx index 493e94ef..5625e077 100644 --- a/src/components/_shared/LoadMoreWrapper.tsx +++ b/src/components/_shared/LoadMoreWrapper.tsx @@ -9,7 +9,7 @@ import { byCreated } from '~/utils/sort' export type LoadMoreItems = Shout[] | Author[] | Reaction[] type LoadMoreProps = { - loadFunction: (offset: number) => Promise + loadFunction: (offset: number) => Promise pageSize: number hidden?: boolean children: JSX.Element diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 3e3f569b..08ea789d 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -1,9 +1,8 @@ import { useMatch, useNavigate } from '@solidjs/router' -import { Editor, EditorOptions } from '@tiptap/core' +import { Editor } from '@tiptap/core' import type { JSX } from 'solid-js' import { Accessor, createContext, createSignal, useContext } from 'solid-js' import { SetStoreFunction, createStore } from 'solid-js/store' -import { createTiptapEditor } from 'solid-tiptap' import { useSnackbar } from '~/context/ui' import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import updateShoutQuery from '~/graphql/mutation/core/article-update' @@ -13,7 +12,7 @@ import { useFeed } from '../context/feed' import { useLocalize } from './localize' import { useSession } from './session' -type WordCounter = { +export type WordCounter = { characters: number words: number } @@ -49,8 +48,8 @@ export type EditorContextType = { countWords: (value: WordCounter) => void setForm: SetStoreFunction setFormErrors: SetStoreFunction> - editor: Accessor - createEditor: (opts?: Partial) => void + editing: Accessor + setEditing: SetStoreFunction } export const EditorContext = createContext({} as EditorContextType) @@ -84,7 +83,6 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const matchEdit = useMatch(() => '/edit') const matchEditSettings = useMatch(() => '/editSettings') const { client } = useSession() - const [editor, setEditor] = createSignal() const { addFeed } = useFeed() const snackbar = useSnackbar() const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal(false) @@ -268,17 +266,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } } - const createEditor = (opts?: Partial) => { - if (!opts) return - const old = editor() as Editor - const fresh = createTiptapEditor(() => ({ - ...old.options, - ...opts, - element: opts.element as HTMLElement - })) - old?.destroy() - setEditor(fresh()) - } + // current publishing editor instance to connect settings, panel and editor + const [editing, setEditing] = createSignal(undefined) const actions = { saveShout, @@ -292,8 +281,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { countWords, setForm, setFormErrors, - editor, - createEditor + setEditing } const value: EditorContextType = { @@ -301,7 +289,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => { form, formErrors, isEditorPanelVisible, - wordCounter + wordCounter, + editing } return {props.children} diff --git a/src/styles/_grid.scss b/src/styles/_grid.scss index a2032d26..3ddf7955 100644 --- a/src/styles/_grid.scss +++ b/src/styles/_grid.scss @@ -31,6 +31,40 @@ @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; } } } diff --git a/src/styles/app.scss b/src/styles/app.scss index 6b8f7525..376dbb3d 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -2,7 +2,9 @@ @import 'theme'; @import 'grid'; -* { +*, +*::before, +*::after { box-sizing: border-box; } @@ -170,9 +172,10 @@ button { background: none; border: none; cursor: pointer; - font-family: inherit; padding: 0; - + margin: 0; + font: inherit; + &[disabled] { cursor: default; opacity: 0.5 !important;