From 424af47b388b7a32c4cac1d9dedd32c1bc798068 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 11 Oct 2024 23:49:34 +0300 Subject: [PATCH] tippy-floating-fix --- src/components/Editor/Editor.tsx | 7 +- src/components/Views/EditView.tsx | 133 +++++----------- .../GrowingTextarea/GrowingTextarea.tsx | 2 +- src/context/editor.tsx | 92 ++++++----- src/styles/app.scss | 145 +++++++++++++++++- 5 files changed, 231 insertions(+), 148 deletions(-) diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 9065e1f1..1acf1abe 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -140,7 +140,8 @@ export const EditorComponent = (props: Props) => { }), FloatingMenu.configure({ tippyOptions: { - placement: 'left' + placement: 'left', + appendTo: document.body }, element: floatingMenuRef()! }), @@ -151,8 +152,8 @@ export const EditorComponent = (props: Props) => { content: props.initialContent || null, onTransaction: ({ editor: e, transaction }) => { if (transaction.docChanged) { - const html = e.getHTML() - html && props.onChange(html) + //const html = e.getHTML() + //html && props.onChange(html) const wordCount: number = e.storage.characterCount.words() const charsCount: number = e.storage.characterCount.characters() charsCount && countWords({ words: wordCount, characters: charsCount }) diff --git a/src/components/Views/EditView.tsx b/src/components/Views/EditView.tsx index a7782eed..852790ee 100644 --- a/src/components/Views/EditView.tsx +++ b/src/components/Views/EditView.tsx @@ -1,8 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' -import { createStore } from 'solid-js/store' -import { debounce } from 'throttle-debounce' import { EditorComponent } from '~/components/Editor/Editor' import { DropArea } from '~/components/_shared/DropArea' import { Icon } from '~/components/_shared/Icon' @@ -20,17 +18,16 @@ import { getImageUrl } from '~/lib/getThumbUrl' import { isDesktop } from '~/lib/mediaQuery' import { LayoutType } from '~/types/common' import { MediaItem } from '~/types/mediaitem' -import { clone } from '~/utils/clone' import { AutoSaveNotice } from '../Editor/AutoSaveNotice' +import { MicroEditor } from '../Editor/MicroEditor' import { Panel } from '../Editor/Panel/Panel' import { AudioUploader } from '../Upload/AudioUploader' import { VideoUploader } from '../Upload/VideoUploader' +import { GrowingTextarea } from '../_shared/GrowingTextarea/GrowingTextarea' import { Modal } from '../_shared/Modal' import { TableOfContents } from '../_shared/TableOfContents' import styles from '~/styles/views/EditView.module.scss' -import MicroEditor from '../Editor/MicroEditor' -import GrowingTextarea from '../_shared/GrowingTextarea/GrowingTextarea' type Props = { shout: Shout @@ -42,8 +39,6 @@ export const EMPTY_TOPIC: Topic = { slug: '' } -const AUTO_SAVE_DELAY = 3000 - const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { ev.preventDefault() window?.scrollTo({ @@ -55,19 +50,10 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditView = (props: Props) => { const { t } = useLocalize() const { client } = useSession() - const { - form, - formErrors, - setForm, - setFormErrors, - saveDraft, - saveDraftToLocalStorage, - getDraftFromLocalStorage - } = useEditorContext() + const { form, formErrors, setForm, setFormErrors, handleInputChange, getDraftFromLocalStorage, saving } = + useEditorContext() const [subtitleInput, setSubtitleInput] = createSignal() - const [prevForm, setPrevForm] = createStore(clone(form)) - const [saving, setSaving] = createSignal(false) const [isSubtitleVisible, setIsSubtitleVisible] = createSignal(Boolean(form.subtitle)) const [isLeadVisible, setIsLeadVisible] = createSignal(Boolean(form.lead)) const [isScrolled, setIsScrolled] = createSignal(false) @@ -80,68 +66,46 @@ export const EditView = (props: Props) => { createEffect( on( () => props.shout, - (shout) => { + async (shout) => { if (shout) { - // console.debug(`[EditView] shout is loaded: ${shout}`) setShoutTopics((shout.topics as Topic[]) || []) + const stored = getDraftFromLocalStorage(shout.id) if (stored) { - // console.info(`[EditView] got stored shout: ${stored}`) setDraft((old) => ({ ...old, ...stored }) as Shout) + setForm(stored as ShoutForm) } else { if (!shout.slug) { console.warn(`[EditView] shout has no slug! ${shout}`) } - const draftForm = { - slug: shout.slug || '', - shoutId: shout.id || 0, - title: shout.title || '', - lead: shout.lead || '', - description: shout.description || '', - subtitle: shout.subtitle || '', - selectedTopics: (shoutTopics() || []) as Topic[], - mainTopic: shoutTopics()[0] || '', - body: shout.body || '', - coverImageUrl: shout.cover || '', - media: shout.media || '', - layout: shout.layout + + const resp = await client()?.query(getMyShoutQuery, { shout_id: shout.id }) + const result = resp?.data?.get_my_shout + + if (result) { + const { shout: loadedShout, error } = result + if (error) { + console.log(error) + } else { + setDraft(loadedShout) + + const draftForm = { + slug: loadedShout.slug || '', + shoutId: loadedShout.id || 0, + title: loadedShout.title || '', + lead: loadedShout.lead || '', + description: loadedShout.description || '', + subtitle: loadedShout.subtitle || '', + selectedTopics: (shoutTopics() || []) as Topic[], + mainTopic: shoutTopics()[0] || '', + body: loadedShout.body || '', + coverImageUrl: loadedShout.cover || '', + media: loadedShout.media || '', + layout: loadedShout.layout + } + setForm(draftForm) + } } - setForm((_) => draftForm) - console.debug('draft from props data: ', draftForm) - } - } - }, - { defer: true } - ) - ) - - createEffect( - on( - draft, - (d) => { - if (d) { - const draftForm = Object.keys(d) ? d : { shoutId: props.shout.id } - setForm(draftForm as ShoutForm) - console.debug('draft from localstorage: ', draftForm) - } - }, - { defer: true } - ) - ) - - createEffect( - on( - () => props.shout?.id, - async (shoutId) => { - if (shoutId) { - const resp = await client()?.query(getMyShoutQuery, { shout_id: shoutId }) - const result = resp?.data?.get_my_shout - if (result) { - // console.debug('[EditView] getMyShout result: ', result) - const { shout: loadedShout, error } = result - setDraft(loadedShout) - // console.debug('[EditView] loadedShout:', loadedShout) - error && console.log(error) } } }, @@ -160,6 +124,7 @@ export const EditView = (props: Props) => { }) const handleBeforeUnload = (event: BeforeUnloadEvent) => { + const prevForm = getDraftFromLocalStorage(form.shoutId) || {} if (!deepEqual(prevForm, form)) { event.returnValue = t( 'There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?' @@ -226,34 +191,6 @@ export const EditView = (props: Props) => { } } } - const [hasChanges, setHasChanges] = createSignal(false) - const autoSave = async () => { - console.log('autoSave called') - if (hasChanges()) { - console.debug('saving draft', form) - setSaving(true) - saveDraftToLocalStorage(form) - await saveDraft(form) - setPrevForm(clone(form)) - setSaving(false) - setHasChanges(false) - } - } - - const debouncedAutoSave = debounce(AUTO_SAVE_DELAY, autoSave) - - const handleInputChange = (key: keyof ShoutForm, value: string) => { - console.log(`[handleInputChange] ${key}: ${value}`) - setForm(key, value) - setHasChanges(true) - debouncedAutoSave() - } - - onMount(() => { - onCleanup(() => { - debouncedAutoSave.cancel() - }) - }) const showSubtitleInput = () => { setIsSubtitleVisible(true) diff --git a/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx b/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx index c4dfea5f..6dc6de4e 100644 --- a/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx +++ b/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx @@ -17,7 +17,7 @@ type Props = { textAreaRef?: (el: HTMLTextAreaElement) => void } -const GrowingTextarea = (props: Props) => { +export const GrowingTextarea = (props: Props) => { const [value, setValue] = createSignal('') const [isFocused, setIsFocused] = createSignal(false) diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 2d1de6fb..65bcac43 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -1,8 +1,9 @@ import { useMatch, useNavigate } from '@solidjs/router' import { Editor } from '@tiptap/core' import type { JSX } from 'solid-js' -import { Accessor, createContext, createSignal, useContext } from 'solid-js' +import { Accessor, createContext, createSignal, onCleanup, useContext } from 'solid-js' import { SetStoreFunction, createStore } from 'solid-js/store' +import { debounce } from 'throttle-debounce' import { useSnackbar } from '~/context/ui' import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import updateShoutQuery from '~/graphql/mutation/core/article-update' @@ -12,6 +13,8 @@ import { useFeed } from '../context/feed' import { useLocalize } from './localize' import { useSession } from './session' +export const AUTO_SAVE_DELAY = 3000 + export type WordCounter = { characters: number words: number @@ -52,6 +55,9 @@ export type EditorContextType = { setEditing: SetStoreFunction isCollabMode: Accessor setIsCollabMode: SetStoreFunction + handleInputChange: (key: keyof ShoutForm, value: string) => void + saving: Accessor + hasChanges: Accessor } export const EditorContext = createContext({} as EditorContextType) @@ -79,6 +85,14 @@ const removeDraftFromLocalStorage = (shoutId: number) => { localStorage?.removeItem(`shout-${shoutId}`) } +const defaultForm: ShoutForm = { + body: '', + slug: '', + shoutId: 0, + title: '', + selectedTopics: [] +} + export const EditorProvider = (props: { children: JSX.Element }) => { const localize = useLocalize() const navigate = useNavigate() @@ -88,20 +102,17 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const { addFeed } = useFeed() const snackbar = useSnackbar() const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal(false) - const [form, setForm] = createStore({ - body: '', - slug: '', - shoutId: 0, - title: '', - selectedTopics: [] - }) + const [form, setForm] = createStore(defaultForm) const [formErrors, setFormErrors] = createStore({} as Record) - const [wordCounter, setWordCounter] = createSignal({ - characters: 0, - words: 0 - }) + const [wordCounter, setWordCounter] = createSignal({ characters: 0, words: 0 }) const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value) const [isCollabMode, setIsCollabMode] = createSignal(false) + + // current publishing editor instance to connect settings, panel and editor + const [editing, setEditing] = createSignal(undefined) + const [saving, setSaving] = createSignal(false) + const [hasChanges, setHasChanges] = createSignal(false) + const countWords = (value: WordCounter) => setWordCounter(value) const validate = () => { if (!form.title) { @@ -157,15 +168,9 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } const saveShout = async (formToSave: ShoutForm) => { - if (isEditorPanelVisible()) { - toggleEditorPanel() - } + isEditorPanelVisible() && toggleEditorPanel() - if (matchEdit() && !validate()) { - return - } - - if (matchEditSettings() && !validateSettings()) { + if ((matchEdit() && !validate()) || (matchEditSettings() && !validateSettings())) { return } @@ -176,12 +181,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { return } removeDraftFromLocalStorage(formToSave.shoutId) - - if (shout?.published_at) { - navigate(`/article/${shout.slug}`) - } else { - navigate('/edit') - } + navigate(shout?.published_at ? `/article/${shout.slug}` : '/edit') } catch (error) { console.error('[saveShout]', error) snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) @@ -197,25 +197,21 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } const publishShout = async (formToPublish: ShoutForm) => { - if (isEditorPanelVisible()) { - toggleEditorPanel() + isEditorPanelVisible() && toggleEditorPanel() + + if ((matchEdit() && !validate()) || (matchEditSettings() && !validateSettings())) { + return } if (matchEdit()) { - if (!validate()) return - const slug = slugify(form.title) setForm('slug', slug) navigate(`/edit/${form.shoutId}/settings`) const { error } = await updateShout(formToPublish, { publish: false }) if (error) { snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' }) + return } - return - } - - if (!validateSettings()) { - return } try { @@ -269,8 +265,25 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } } - // current publishing editor instance to connect settings, panel and editor - const [editing, setEditing] = createSignal(undefined) + const debouncedAutoSave = debounce(AUTO_SAVE_DELAY, async () => { + console.log('autoSave called') + if (hasChanges()) { + console.debug('saving draft', form) + setSaving(true) + saveDraftToLocalStorage(form) + await saveDraft(form) + setSaving(false) + setHasChanges(false) + } + }) + onCleanup(debouncedAutoSave.cancel) + + const handleInputChange = (key: keyof ShoutForm, value: string) => { + console.log(`[handleInputChange] ${key}: ${value}`) + setForm(key, value) + setHasChanges(true) + debouncedAutoSave() + } const actions = { saveShout, @@ -286,7 +299,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => { setFormErrors, setEditing, isCollabMode, - setIsCollabMode + setIsCollabMode, + handleInputChange, + saving, + hasChanges } const value: EditorContextType = { diff --git a/src/styles/app.scss b/src/styles/app.scss index d990f9f7..646174a4 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -198,6 +198,143 @@ button { } } +.button--subscribe { + background: var(--background-color); + color: var(--default-color); + border: 2px solid var(--black-100); + font-size: 1.5rem; + justify-content: center; + padding: 0.6rem 1.2rem; + transition: background-color 0.2s; + + img { + height: auto; + transition: filter 0.2s; + } + + &:hover { + background: var(--background-color-invert); + color: var(--default-color-invert); + + img { + filter: invert(1); + } + } +} + +.button--light { + font-size:1.5rem; + background-color: var(--black-100); + border-radius: 0.8rem; + color: var(--default-color); + font-weight: 500; + height: auto; + padding: 0.6rem 1.2rem 0.6rem 1rem; + + &:hover { + background: var(--black-300); + } +} + +.button--subscribe-topic { + background: var(--background-color); + color: var(--default-color); + border: 2px solid var(--default-color); + border-radius: 0.8rem; + font-size: 1.4rem; + line-height: 2.8rem; + height: 3.2rem; + padding: 0 1rem; + + &:hover { + background: var(--background-color-invert); + color: var(--default-color-invert); + opacity: 1; + + .icon { + filter: invert(1); + } + } + + &[disabled]:hover { + background: var(--background-color); + color: var(--default-color); + } + + .icon { + display: inline-block; + margin-right: 0.3em; + vertical-align: text-bottom; + width: 1.4em; + } +} + +.button--content-index { + @include media-breakpoint-up(md) { + margin-top: -0.5rem; + position: sticky; + top: 135px; + } + + @include media-breakpoint-up(sm) { + right: $container-padding-x; + } + + background: none; + border: 2px solid var(--white-500); + height: 3.2rem; + float: right; + padding: 0; + position: absolute; + right: $container-padding-x * 0.5; + top: -0.5rem; + width: 3.2rem; + z-index: 1; + + .icon { + background: #fff; + transition: filter 0.3s; + } + + .icon, + img { + height: 100%; + vertical-align: middle; + width: auto; + } + + &:hover { + .icon { + filter: invert(1); + } + } + + .expanded { + border-radius: 100%; + overflow: hidden; + + img { + height: auto; + margin-top: 0.8rem; + } + } +} + +.button--submit, +.button--outline { + font-size:2rem; + padding: 1.6rem 2rem; +} + +.button--outline { + background: none; + box-shadow: inset 0 0 0 2px #000; + color: #000; + + &:hover { + box-shadow: inset 0 0 0 2px var(--black-300); + } +} form { input[type='text'], @@ -818,12 +955,4 @@ iframe { svg { filter: invert(1); } -} - -.fixed { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1030; } \ No newline at end of file