tippy-floating-fix

This commit is contained in:
Untone 2024-10-11 23:49:34 +03:00
parent 73f78823e0
commit 424af47b38
5 changed files with 231 additions and 148 deletions

View File

@ -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 })

View File

@ -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<HTMLTextAreaElement | undefined>()
const [prevForm, setPrevForm] = createStore<ShoutForm>(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 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: shout.slug || '',
shoutId: shout.id || 0,
title: shout.title || '',
lead: shout.lead || '',
description: shout.description || '',
subtitle: shout.subtitle || '',
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: shout.body || '',
coverImageUrl: shout.cover || '',
media: shout.media || '',
layout: shout.layout
body: loadedShout.body || '',
coverImageUrl: loadedShout.cover || '',
media: loadedShout.media || '',
layout: loadedShout.layout
}
setForm((_) => draftForm)
console.debug('draft from props data: ', draftForm)
setForm(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)

View File

@ -17,7 +17,7 @@ type Props = {
textAreaRef?: (el: HTMLTextAreaElement) => void
}
const GrowingTextarea = (props: Props) => {
export const GrowingTextarea = (props: Props) => {
const [value, setValue] = createSignal<string>('')
const [isFocused, setIsFocused] = createSignal(false)

View File

@ -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<Editor | undefined>
isCollabMode: Accessor<boolean>
setIsCollabMode: SetStoreFunction<boolean>
handleInputChange: (key: keyof ShoutForm, value: string) => void
saving: Accessor<boolean>
hasChanges: Accessor<boolean>
}
export const EditorContext = createContext<EditorContextType>({} 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<boolean>(false)
const [form, setForm] = createStore<ShoutForm>({
body: '',
slug: '',
shoutId: 0,
title: '',
selectedTopics: []
})
const [form, setForm] = createStore<ShoutForm>(defaultForm)
const [formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
characters: 0,
words: 0
})
const [wordCounter, setWordCounter] = createSignal<WordCounter>({ characters: 0, words: 0 })
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
const [isCollabMode, setIsCollabMode] = createSignal<boolean>(false)
// current publishing editor instance to connect settings, panel and editor
const [editing, setEditing] = createSignal<Editor | undefined>(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
}
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<Editor | undefined>(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 = {

View File

@ -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'],
@ -819,11 +956,3 @@ iframe {
filter: invert(1);
}
}
.fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
}