mirror of
https://github.com/usememos/memos.git
synced 2024-09-21 14:55:59 +08:00
refactor: memo editor components (#1625)
This commit is contained in:
parent
8911ea1619
commit
e3496ac1a2
|
@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { RouterProvider } from "react-router-dom";
|
||||
import router from "./router";
|
||||
import { useGlobalStore } from "./store/module";
|
||||
import * as storage from "./helpers/storage";
|
||||
import storage from "./helpers/storage";
|
||||
import { getSystemColorScheme } from "./helpers/utils";
|
||||
import Loading from "./pages/Loading";
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { toLower } from "lodash-es";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
|
||||
import { useEditorStore, useGlobalStore } from "@/store/module";
|
||||
import Selector from "@/components/kit/Selector";
|
||||
|
||||
const MemoVisibilitySelector = () => {
|
||||
const { t } = useTranslation();
|
||||
const editorStore = useEditorStore();
|
||||
const {
|
||||
state: { systemStatus },
|
||||
} = useGlobalStore();
|
||||
const editorState = editorStore.state;
|
||||
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||
return {
|
||||
value: item.value,
|
||||
text: t(`memo.visibility.${toLower(item.value)}`),
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (systemStatus.disablePublicMemos) {
|
||||
editorStore.setMemoVisibility("PRIVATE");
|
||||
}
|
||||
}, [systemStatus.disablePublicMemos]);
|
||||
|
||||
const handleMemoVisibilityOptionChanged = async (value: string) => {
|
||||
const visibilityValue = value as Visibility;
|
||||
editorStore.setMemoVisibility(visibilityValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Selector
|
||||
className="visibility-selector"
|
||||
disabled={systemStatus.disablePublicMemos}
|
||||
tooltipTitle={t("memo.visibility.disabled")}
|
||||
value={editorState.memoVisibility}
|
||||
dataSource={memoVisibilityOptionSelectorItems}
|
||||
handleValueChanged={handleMemoVisibilityOptionChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoVisibilitySelector;
|
|
@ -0,0 +1,41 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useEditorStore } from "@/store/module";
|
||||
import Icon from "@/components/Icon";
|
||||
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
||||
import showResourcesSelectorDialog from "@/components/ResourcesSelectorDialog";
|
||||
|
||||
const ResourceSelector = () => {
|
||||
const { t } = useTranslation();
|
||||
const editorStore = useEditorStore();
|
||||
|
||||
const handleUploadFileBtnClick = () => {
|
||||
showCreateResourceDialog({
|
||||
onConfirm: (resourceList) => {
|
||||
editorStore.setResourceList([...editorStore.state.resourceList, ...resourceList]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="action-btn relative group">
|
||||
<Icon.FileText className="icon-img" />
|
||||
<div className="hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-auto overflow-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600 group-hover:flex">
|
||||
<div
|
||||
className="w-full flex text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0"
|
||||
onClick={handleUploadFileBtnClick}
|
||||
>
|
||||
<Icon.Plus className="w-4 mr-1" />
|
||||
<span>{t("common.create")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-full flex text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0"
|
||||
onClick={showResourcesSelectorDialog}
|
||||
>
|
||||
<Icon.Database className="w-4 mr-1" />
|
||||
<span>{t("editor.resources")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ResourceSelector;
|
39
web/src/components/MemoEditor/ActionButton/TagSelector.tsx
Normal file
39
web/src/components/MemoEditor/ActionButton/TagSelector.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { useTagStore } from "@/store/module";
|
||||
import Icon from "@/components/Icon";
|
||||
|
||||
interface Props {
|
||||
onTagSelectorClick: (tag: string) => void;
|
||||
}
|
||||
|
||||
const TagSelector = (props: Props) => {
|
||||
const { onTagSelectorClick } = props;
|
||||
const tagStore = useTagStore();
|
||||
const tags = tagStore.state.tags;
|
||||
|
||||
return (
|
||||
<div className="action-btn relative group">
|
||||
<Icon.Hash className="icon-img" />
|
||||
<div className="hidden flex-row justify-start items-start flex-wrap absolute top-6 left-0 mt-1 p-1 z-1 rounded w-52 h-auto max-h-48 overflow-y-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600 group-hover:flex">
|
||||
{tags.length > 0 ? (
|
||||
tags.map((tag) => {
|
||||
return (
|
||||
<span
|
||||
className="w-auto max-w-full truncate text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0"
|
||||
onClick={() => onTagSelectorClick(tag)}
|
||||
key={tag}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="italic text-sm ml-2" onClick={(e) => e.stopPropagation()}>
|
||||
No tags found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSelector;
|
35
web/src/components/MemoEditor/ResourceListView.tsx
Normal file
35
web/src/components/MemoEditor/ResourceListView.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useEditorStore } from "@/store/module";
|
||||
import { deleteMemoResource } from "@/helpers/api";
|
||||
import Icon from "../Icon";
|
||||
import ResourceIcon from "../ResourceIcon";
|
||||
|
||||
const ResourceListView = () => {
|
||||
const editorStore = useEditorStore();
|
||||
const editorState = editorStore.state;
|
||||
|
||||
const handleDeleteResource = async (resourceId: ResourceId) => {
|
||||
editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
|
||||
if (editorState.editMemoId) {
|
||||
await deleteMemoResource(editorState.editMemoId, resourceId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{editorState.resourceList && editorState.resourceList.length > 0 && (
|
||||
<div className="resource-list-wrapper">
|
||||
{editorState.resourceList.map((resource) => {
|
||||
return (
|
||||
<div key={resource.id} className="resource-container">
|
||||
<ResourceIcon resourceType="resource.type" className="icon-img" />
|
||||
<span className="name-text">{resource.filename}</span>
|
||||
<Icon.X className="close-icon" onClick={() => handleDeleteResource(resource.id)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ResourceListView;
|
|
@ -1,30 +1,26 @@
|
|||
import { isNumber, last, toLower, uniq } from "lodash-es";
|
||||
import { isNumber, last, uniq } from "lodash-es";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getMatchedNodes } from "@/labs/marked";
|
||||
import { deleteMemoResource, upsertMemoResource } from "@/helpers/api";
|
||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
|
||||
import { useEditorStore, useGlobalStore, useFilterStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module";
|
||||
import * as storage from "@/helpers/storage";
|
||||
import Icon from "./Icon";
|
||||
import Selector from "./kit/Selector";
|
||||
import Editor, { EditorRefActions } from "./Editor/Editor";
|
||||
import ResourceIcon from "./ResourceIcon";
|
||||
import showResourcesSelectorDialog from "./ResourcesSelectorDialog";
|
||||
import showCreateResourceDialog from "./CreateResourceDialog";
|
||||
import "@/less/memo-editor.less";
|
||||
import { upsertMemoResource } from "@/helpers/api";
|
||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
|
||||
import { useEditorStore, useFilterStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "@/store/module";
|
||||
import storage from "@/helpers/storage";
|
||||
import { clearContentQueryParam, getContentQueryParam } from "@/helpers/utils";
|
||||
import Icon from "../Icon";
|
||||
import Editor, { EditorRefActions } from "./Editor";
|
||||
import TagSelector from "./ActionButton/TagSelector";
|
||||
import ResourceSelector from "./ActionButton/ResourceSelector";
|
||||
import MemoVisibilitySelector from "./ActionButton/MemoVisibilitySelector";
|
||||
import "@/less/memo-editor.less";
|
||||
import ResourceListView from "./ResourceListView";
|
||||
|
||||
const listItemSymbolList = ["- [ ] ", "- [x] ", "- [X] ", "* ", "- "];
|
||||
const emptyOlReg = /^(\d+)\. $/;
|
||||
|
||||
const getEditorContentCache = (): string => {
|
||||
return storage.get(["editorContentCache"]).editorContentCache ?? "";
|
||||
};
|
||||
|
||||
const getInitialContent = (): string => {
|
||||
return getContentQueryParam() ?? getEditorContentCache();
|
||||
return getContentQueryParam() ?? storage.get(["editorContentCache"]).editorContentCache ?? "";
|
||||
};
|
||||
|
||||
const setEditorContentCache = (content: string) => {
|
||||
|
@ -47,9 +43,6 @@ const MemoEditor = () => {
|
|||
const memoStore = useMemoStore();
|
||||
const tagStore = useTagStore();
|
||||
const resourceStore = useResourceStore();
|
||||
const {
|
||||
state: { systemStatus },
|
||||
} = useGlobalStore();
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
fullscreen: false,
|
||||
|
@ -61,22 +54,8 @@ const MemoEditor = () => {
|
|||
const editorState = editorStore.state;
|
||||
const prevEditorStateRef = useRef(editorState);
|
||||
const editorRef = useRef<EditorRefActions>(null);
|
||||
const tagSelectorRef = useRef<HTMLDivElement>(null);
|
||||
const user = userStore.state.user as User;
|
||||
const setting = user.setting;
|
||||
const tags = tagStore.state.tags;
|
||||
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||
return {
|
||||
value: item.value,
|
||||
text: t(`memo.visibility.${toLower(item.value)}`),
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (systemStatus.disablePublicMemos) {
|
||||
editorStore.setMemoVisibility("PRIVATE");
|
||||
}
|
||||
}, [systemStatus.disablePublicMemos]);
|
||||
|
||||
useEffect(() => {
|
||||
const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]);
|
||||
|
@ -338,39 +317,19 @@ const MemoEditor = () => {
|
|||
editorRef.current?.scrollToCursor();
|
||||
};
|
||||
|
||||
const handleUploadFileBtnClick = () => {
|
||||
showCreateResourceDialog({
|
||||
onConfirm: (resourceList) => {
|
||||
editorStore.setResourceList([...editorState.resourceList, ...resourceList]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFullscreenBtnClick = () => {
|
||||
const handleFullscreenBtnClick = useCallback(() => {
|
||||
setState((state) => {
|
||||
return {
|
||||
...state,
|
||||
fullscreen: !state.fullscreen,
|
||||
};
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleTagSelectorClick = useCallback((tag: string) => {
|
||||
editorRef.current?.insertText(`#${tag} `);
|
||||
}, []);
|
||||
|
||||
const handleDeleteResource = async (resourceId: ResourceId) => {
|
||||
editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
|
||||
if (editorState.editMemoId) {
|
||||
await deleteMemoResource(editorState.editMemoId, resourceId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemoVisibilityOptionChanged = async (value: string) => {
|
||||
const visibilityValue = value as Visibility;
|
||||
editorStore.setMemoVisibility(visibilityValue);
|
||||
};
|
||||
|
||||
const handleEditorFocus = () => {
|
||||
editorRef.current?.focus();
|
||||
};
|
||||
|
@ -407,70 +366,22 @@ const MemoEditor = () => {
|
|||
<Editor ref={editorRef} {...editorConfig} />
|
||||
<div className="common-tools-wrapper">
|
||||
<div className="common-tools-container">
|
||||
<div className="action-btn tag-action">
|
||||
<Icon.Hash className="icon-img" />
|
||||
<div ref={tagSelectorRef} className="tag-list">
|
||||
{tags.length > 0 ? (
|
||||
tags.map((tag) => {
|
||||
return (
|
||||
<span className="item-container" onClick={() => handleTagSelectorClick(tag)} key={tag}>
|
||||
#{tag}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="tip-text italic" onClick={(e) => e.stopPropagation()}>
|
||||
No tags found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TagSelector onTagSelectorClick={(tag) => handleTagSelectorClick(tag)} />
|
||||
<button className="action-btn">
|
||||
<Icon.CheckSquare className="icon-img" onClick={handleCheckBoxBtnClick} />
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
|
||||
</button>
|
||||
<div className="action-btn resource-btn">
|
||||
<Icon.FileText className="icon-img" />
|
||||
<div className="resource-action-list">
|
||||
<div className="resource-action-item" onClick={handleUploadFileBtnClick}>
|
||||
<Icon.Plus className="icon-img" />
|
||||
<span>{t("common.create")}</span>
|
||||
</div>
|
||||
<div className="resource-action-item" onClick={showResourcesSelectorDialog}>
|
||||
<Icon.Database className="icon-img" />
|
||||
<span>{t("editor.resources")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceSelector />
|
||||
<button className="action-btn" onClick={handleFullscreenBtnClick}>
|
||||
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{editorState.resourceList && editorState.resourceList.length > 0 && (
|
||||
<div className="resource-list-wrapper">
|
||||
{editorState.resourceList.map((resource) => {
|
||||
return (
|
||||
<div key={resource.id} className="resource-container">
|
||||
<ResourceIcon resourceType="resource.type" className="icon-img" />
|
||||
<span className="name-text">{resource.filename}</span>
|
||||
<Icon.X className="close-icon" onClick={() => handleDeleteResource(resource.id)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<ResourceListView />
|
||||
<div className="editor-footer-container">
|
||||
<Selector
|
||||
className="visibility-selector"
|
||||
value={editorState.memoVisibility}
|
||||
tooltipTitle={t("memo.visibility.disabled")}
|
||||
dataSource={memoVisibilityOptionSelectorItems}
|
||||
disabled={systemStatus.disablePublicMemos}
|
||||
handleValueChanged={handleMemoVisibilityOptionChanged}
|
||||
/>
|
||||
<MemoVisibilitySelector />
|
||||
<div className="buttons-container">
|
||||
<button className={`action-btn cancel-btn ${isEditing ? "" : "!hidden"}`} onClick={handleCancelEdit}>
|
||||
{t("editor.cancel-edit")}
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { compare } from "semver";
|
||||
import * as api from "@/helpers/api";
|
||||
import * as storage from "@/helpers/storage";
|
||||
import storage from "@/helpers/storage";
|
||||
import { useGlobalStore } from "@/store/module";
|
||||
import Icon from "./Icon";
|
||||
|
||||
|
|
|
@ -18,44 +18,43 @@ interface StorageData {
|
|||
|
||||
type StorageKey = keyof StorageData;
|
||||
|
||||
/**
|
||||
* storage helper
|
||||
*/
|
||||
export function get(keys: StorageKey[]): Partial<StorageData> {
|
||||
const data: Partial<StorageData> = {};
|
||||
const storage = {
|
||||
get: (keys: StorageKey[]): Partial<StorageData> => {
|
||||
const data: Partial<StorageData> = {};
|
||||
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const stringifyValue = localStorage.getItem(key);
|
||||
if (stringifyValue !== null) {
|
||||
const val = JSON.parse(stringifyValue);
|
||||
data[key] = val;
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const stringifyValue = localStorage.getItem(key);
|
||||
if (stringifyValue !== null) {
|
||||
const val = JSON.parse(stringifyValue);
|
||||
data[key] = val;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Get storage failed in ", key, error);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Get storage failed in ", key, error);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function set(data: Partial<StorageData>) {
|
||||
for (const key in data) {
|
||||
try {
|
||||
const stringifyValue = JSON.stringify(data[key as StorageKey]);
|
||||
localStorage.setItem(key, stringifyValue);
|
||||
} catch (error: any) {
|
||||
console.error("Save storage failed in ", key, error);
|
||||
return data;
|
||||
},
|
||||
set: (data: Partial<StorageData>) => {
|
||||
for (const key in data) {
|
||||
try {
|
||||
const stringifyValue = JSON.stringify(data[key as StorageKey]);
|
||||
localStorage.setItem(key, stringifyValue);
|
||||
} catch (error: any) {
|
||||
console.error("Save storage failed in ", key, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function remove(keys: StorageKey[]) {
|
||||
for (const key of keys) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error: any) {
|
||||
console.error("Remove storage failed in ", key, error);
|
||||
},
|
||||
remove: (keys: StorageKey[]) => {
|
||||
for (const key of keys) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error: any) {
|
||||
console.error("Remove storage failed in ", key, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default storage;
|
||||
|
|
|
@ -39,50 +39,6 @@
|
|||
> .action-btn {
|
||||
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer text-gray-600 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow;
|
||||
|
||||
&.tag-action {
|
||||
@apply relative;
|
||||
|
||||
&:hover {
|
||||
> .tag-list {
|
||||
@apply flex;
|
||||
}
|
||||
}
|
||||
|
||||
> .tag-list {
|
||||
@apply hidden flex-row justify-start items-start flex-wrap absolute top-6 left-0 mt-1 p-1 z-1 rounded w-52 h-auto max-h-48 overflow-y-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600;
|
||||
|
||||
> .item-container {
|
||||
@apply w-auto max-w-full truncate text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0;
|
||||
}
|
||||
|
||||
> .tip-text {
|
||||
@apply w-auto text-sm text-gray-200 leading-6 px-2 cursor-default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.resource-btn {
|
||||
@apply relative;
|
||||
|
||||
&:hover {
|
||||
> .resource-action-list {
|
||||
@apply flex;
|
||||
}
|
||||
}
|
||||
|
||||
> .resource-action-list {
|
||||
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-auto overflow-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600;
|
||||
|
||||
> .resource-action-item {
|
||||
@apply w-full flex text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 mr-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as api from "@/helpers/api";
|
||||
import * as storage from "@/helpers/storage";
|
||||
import storage from "@/helpers/storage";
|
||||
import i18n from "@/i18n";
|
||||
import { findNearestLanguageMatch } from "@/utils/i18n";
|
||||
import store, { useAppSelector } from "../";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { camelCase } from "lodash-es";
|
||||
import * as api from "@/helpers/api";
|
||||
import * as storage from "@/helpers/storage";
|
||||
import storage from "@/helpers/storage";
|
||||
import { UNKNOWN_ID } from "@/helpers/consts";
|
||||
import { getSystemColorScheme } from "@/helpers/utils";
|
||||
import store, { useAppSelector } from "..";
|
||||
|
|
Loading…
Reference in a new issue