feat(toolbar): add toolbar popup
This commit is contained in:
parent
6ea0cd9256
commit
20b549643d
|
@ -116,7 +116,12 @@ export default defineComponent({
|
|||
outline: none;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.editor-toolbar.editor-toolbar-popup .toolbar-button {
|
||||
min-width: 24px;
|
||||
line-height: 24px;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.editor-toolbar:not(.editor-toolbar-mobile) .toolbar-button {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
|
|
@ -163,7 +163,7 @@ export default defineComponent({
|
|||
.toolbar-dropdown .toolbar-dropdown-trigger-arrow .data-icon-arrow {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 15px;
|
||||
top: calc(100% / 2 - 2px);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjhweCIgaGVpZ2h0PSI1cHgiIHZpZXdCb3g9IjAgMCA4IDUiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+DQogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1Mi41ICg2NzQ2OSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+DQogICAgPHRpdGxlPkdyb3VwIENvcHkgNjwvdGl0bGU+DQogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+DQogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIG9wYWNpdHk9IjAuNDUiPg0KICAgICAgICA8ZyBpZD0idG9vbGJhciIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMyOC4wMDAwMDAsIC0xOC4wMDAwMDApIj4NCiAgICAgICAgICAgIDxnIGlkPSJwYXJhZ3JhcGgtc3R5bGUiPg0KICAgICAgICAgICAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyNi4wMDAwMDAsIDQuMDAwMDAwKSI+DQogICAgICAgICAgICAgICAgICAgIDxnIGlkPSJHcm91cC1Db3B5LTYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMi4wMDAwMDAsIDEyLjAwMDAwMCkiPg0KICAgICAgICAgICAgICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjgiIGhlaWdodD0iOCI+PC9yZWN0Pg0KICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTAuNTk2MDkzNzUsMi41NTcwMzEyNSBMMy43NDUzMTI1LDYuMzc4MTI1IEMzLjg3NzM0Mzc1LDYuNTI1NzgxMjUgNC4xMDg1OTM3NSw2LjUyNTc4MTI1IDQuMjQwNjI1LDYuMzc4MTI1IEw3LjQwNTQ2ODc1LDIuNTU3MDMxMjUgQzcuNTk2MDkzNzUsMi4zNDI5Njg3NSA3LjQ0NDUzMTI1LDIuMDAzOTA2MjUgNy4xNTc4MTI1LDIuMDAzOTA2MjUgTDAuODQ0NTMxMjUsMi4wMDM5MDYyNSBDMC41NTcwMzEyNSwyLjAwMzkwNjI1IDAuNDA0Njg3NSwyLjM0Mjk2ODc1IDAuNTk2MDkzNzUsMi41NTcwMzEyNSBaIiBpZD0iU2hhcGUiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyI+PC9wYXRoPg0KICAgICAgICAgICAgICAgICAgICA8L2c+DQogICAgICAgICAgICAgICAgPC9nPg0KICAgICAgICAgICAgPC9nPg0KICAgICAgICA8L2c+DQogICAgPC9nPg0KPC9zdmc+);
|
||||
|
@ -191,14 +191,14 @@ export default defineComponent({
|
|||
left: 0px;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .toolbar-dropdown .toolbar-dropdown-list {
|
||||
.editor-toolbar-mobile .toolbar-dropdown .toolbar-dropdown-list,.editor-toolbar-popup .toolbar-dropdown .toolbar-dropdown-list{
|
||||
bottom: 32px;
|
||||
top: auto;
|
||||
max-height: calc(30vh);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .toolbar-dropdown.toolbar-dropdown-right .toolbar-dropdown-list {
|
||||
.editor-toolbar-mobile .toolbar-dropdown.toolbar-dropdown-right .toolbar-dropdown-list,.editor-toolbar-popup .toolbar-dropdown.toolbar-dropdown-right .toolbar-dropdown-list{
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
:placement="isMobile ? 'topRight' : 'bottom'"
|
||||
>
|
||||
<template #content>
|
||||
<div :class="['editor-toolbar', {'editor-toolbar-mobile': isMobile}]" data-element="ui">
|
||||
<div :class="['editor-toolbar', {'editor-toolbar-mobile': isMobile && !popup,
|
||||
'editor-toolbar-popup': popup,}]" data-element="ui">
|
||||
<template v-for="(item , index) in items" :key="index">
|
||||
<am-button v-if="item.type === 'button'" :key="index" v-bind="item" :engine="engine" />
|
||||
<am-dropdown v-if="item.type === 'dropdown'" :key="index" v-bind="item" :engine="engine" />
|
||||
|
|
|
@ -267,16 +267,29 @@ export {
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-toolbar.editor-toolbar-popup {
|
||||
position: initial;
|
||||
box-shadow: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .editor-toolbar-content {
|
||||
text-align: left;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .editor-toolbar-group {
|
||||
.editor-toolbar-mobile .editor-toolbar-group,.editor-toolbar-popup .editor-toolbar-group {
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-popup .editor-toolbar-content {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-popover .editor-toolbar {
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { h } from 'vue';
|
||||
import { CARD_SELECTOR, EngineInterface } from '@aomao/engine';
|
||||
import { CARD_SELECTOR, EngineInterface, isEngine, Range } from '@aomao/engine';
|
||||
import { ToolbarItemProps } from '../types';
|
||||
import TableSelector from '../components/table.vue';
|
||||
import fontfamily, { defaultData as fontFamilyDefaultData } from './fontfamily';
|
||||
|
@ -716,8 +716,11 @@ export const getToolbarDefaultConfig = (
|
|||
command: { name: 'link', args: ['_blank'] },
|
||||
title: language['link']['title'],
|
||||
onDisabled: () => {
|
||||
const { change, card } = engine;
|
||||
const range = change.range.get();
|
||||
const { card } = engine;
|
||||
const range = isEngine(engine)
|
||||
? engine.change.range.get()
|
||||
: Range.from(engine);
|
||||
if (!range) return !engine.command.queryEnabled('link');
|
||||
const cardComponent = card.find(range.startNode);
|
||||
return (
|
||||
(!!cardComponent &&
|
||||
|
|
|
@ -31,4 +31,14 @@
|
|||
z-index: 999;
|
||||
max-height: calc(80vh);
|
||||
overflow: auto;
|
||||
}
|
||||
/** ------------------- popup ---------------------- **/
|
||||
.data-toolbar-popup-wrapper {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
padding: 4px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee0e3;
|
||||
box-shadow: 0 4px 8px 0 rgba(31, 35, 41, 0.1);
|
||||
}
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '../../types';
|
||||
import { getToolbarDefaultConfig } from '../../config';
|
||||
import CollapseComponent, { CollapseComponentInterface } from './collapse';
|
||||
import ToolbarPopup from './popup';
|
||||
import './index.css';
|
||||
|
||||
type Data = Array<CollapseGroupProps>;
|
||||
|
@ -23,7 +24,7 @@ export interface ToolbarValue extends CardValue {
|
|||
data: Data;
|
||||
}
|
||||
|
||||
class ToolbarComponent<V extends ToolbarValue> extends Card<V> {
|
||||
class ToolbarComponent<V extends ToolbarValue = ToolbarValue> extends Card<V> {
|
||||
private keyword?: NodeInterface;
|
||||
private placeholder?: NodeInterface;
|
||||
private component?: CollapseComponentInterface;
|
||||
|
@ -265,3 +266,4 @@ class ToolbarComponent<V extends ToolbarValue> extends Card<V> {
|
|||
}
|
||||
|
||||
export default ToolbarComponent;
|
||||
export { ToolbarPopup };
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import { createApp, App } from 'vue';
|
||||
import { $, isEngine, isMobile, Range } from '@aomao/engine';
|
||||
import type { NodeInterface, EditorInterface } from '@aomao/engine';
|
||||
import Toolbar from '../../components/toolbar.vue';
|
||||
import type { GroupItemProps } from '../../types';
|
||||
|
||||
type PopupOptions = {
|
||||
items?: GroupItemProps[];
|
||||
};
|
||||
|
||||
export default class Popup {
|
||||
#editor: EditorInterface;
|
||||
#root: NodeInterface;
|
||||
#point: Record<'left' | 'top', number> = { left: 0, top: -9999 };
|
||||
#align: 'top' | 'bottom' = 'bottom';
|
||||
#options: PopupOptions = {};
|
||||
#vm?: App;
|
||||
|
||||
constructor(editor: EditorInterface, options: PopupOptions = {}) {
|
||||
this.#options = options;
|
||||
this.#editor = editor;
|
||||
this.#root = $(`<div class="data-toolbar-popup-wrapper">Test</div>`);
|
||||
document.body.append(this.#root[0]);
|
||||
if (isEngine(editor)) {
|
||||
this.#editor.on('select', this.onSelect);
|
||||
} else {
|
||||
document.addEventListener('selectionchange', this.onSelect);
|
||||
}
|
||||
if (!isMobile) window.addEventListener('scroll', this.onSelect);
|
||||
window.addEventListener('resize', this.onSelect);
|
||||
this.#editor.scrollNode?.on('scroll', this.onSelect);
|
||||
}
|
||||
|
||||
onSelect = () => {
|
||||
const range = Range.from(this.#editor)
|
||||
?.cloneRange()
|
||||
.shrinkToTextNode();
|
||||
const selection = window.getSelection();
|
||||
if (
|
||||
!range ||
|
||||
!selection ||
|
||||
!selection.focusNode ||
|
||||
range.collapsed ||
|
||||
(!range.commonAncestorNode.inEditor() &&
|
||||
!range.commonAncestorNode.isRoot())
|
||||
) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
const subRanges = range.getSubRanges();
|
||||
if (
|
||||
subRanges.length === 0 ||
|
||||
(this.#editor.card.active && !this.#editor.card.active.isEditable)
|
||||
) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
const topRange = subRanges[0];
|
||||
const bottomRange = subRanges[subRanges.length - 1];
|
||||
const topRect = topRange.getBoundingClientRect();
|
||||
const bottomRect = bottomRange.getBoundingClientRect();
|
||||
|
||||
let rootRect: DOMRect | undefined = undefined;
|
||||
this.showContent(() => {
|
||||
rootRect = this.#root.get<HTMLElement>()?.getBoundingClientRect();
|
||||
console.log(rootRect);
|
||||
if (!rootRect) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
this.#align =
|
||||
bottomRange.startNode.equal(selection.focusNode!) &&
|
||||
!topRange.startNode.equal(selection.focusNode!)
|
||||
? 'bottom'
|
||||
: 'top';
|
||||
const space = 12;
|
||||
let targetRect = this.#align === 'bottom' ? bottomRect : topRect;
|
||||
if (
|
||||
this.#align === 'top' &&
|
||||
targetRect.top - rootRect.height - space <
|
||||
window.innerHeight -
|
||||
(this.#editor.scrollNode?.height() || 0)
|
||||
) {
|
||||
this.#align = 'bottom';
|
||||
} else if (
|
||||
this.#align === 'bottom' &&
|
||||
targetRect.bottom + rootRect.height + space > window.innerHeight
|
||||
) {
|
||||
this.#align = 'top';
|
||||
}
|
||||
targetRect = this.#align === 'bottom' ? bottomRect : topRect;
|
||||
const top =
|
||||
this.#align === 'top'
|
||||
? targetRect.top - rootRect.height - space
|
||||
: targetRect.bottom + space;
|
||||
this.#point = {
|
||||
left: targetRect.left + targetRect.width - rootRect.width / 2,
|
||||
top,
|
||||
};
|
||||
this.#root.css({
|
||||
left: `${this.#point.left}px`,
|
||||
top: `${this.#point.top}px`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showContent(callback?: () => void) {
|
||||
const result = this.#editor.trigger('toolbar-render', this.#options);
|
||||
this.#vm = createApp(
|
||||
typeof result === 'object' ? result : Toolbar,
|
||||
this.#options,
|
||||
);
|
||||
this.#vm.mount(this.#root.get<HTMLDivElement>()!);
|
||||
setTimeout(() => {
|
||||
if (callback) callback();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.#root.css({
|
||||
left: '0px',
|
||||
top: '-9999px',
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#root.remove();
|
||||
if (isEngine(this.#editor)) {
|
||||
this.#editor.on('select', this.onSelect);
|
||||
} else {
|
||||
document.removeEventListener('selectionchange', this.onSelect);
|
||||
}
|
||||
if (!isMobile) window.removeEventListener('scroll', this.onSelect);
|
||||
window.removeEventListener('resize', this.onSelect);
|
||||
this.#editor.scrollNode?.off('scroll', this.onSelect);
|
||||
if (this.#vm) this.#vm.unmount();
|
||||
}
|
||||
}
|
||||
export type { GroupItemProps };
|
|
@ -6,9 +6,10 @@ import {
|
|||
Plugin,
|
||||
PluginOptions,
|
||||
} from '@aomao/engine';
|
||||
import { CollapseItemProps } from '../types';
|
||||
import { CollapseItemProps, GroupItemProps } from '../types';
|
||||
import locales from '../locales';
|
||||
import ToolbarComponent, { ToolbarValue } from './component';
|
||||
import ToolbarComponent, { ToolbarPopup } from './component';
|
||||
import type { ToolbarValue } from './component';
|
||||
|
||||
type Config = Array<{
|
||||
title: string;
|
||||
|
@ -16,6 +17,9 @@ type Config = Array<{
|
|||
}>;
|
||||
export interface ToolbarOptions extends PluginOptions {
|
||||
config: Config;
|
||||
popup?: {
|
||||
items: GroupItemProps[];
|
||||
};
|
||||
}
|
||||
|
||||
const defaultConfig = (editor: EditorInterface): Config => {
|
||||
|
@ -39,7 +43,9 @@ const defaultConfig = (editor: EditorInterface): Config => {
|
|||
];
|
||||
};
|
||||
|
||||
class ToolbarPlugin<T extends ToolbarOptions> extends Plugin<T> {
|
||||
class ToolbarPlugin<
|
||||
T extends ToolbarOptions = ToolbarOptions,
|
||||
> extends Plugin<T> {
|
||||
static get pluginName() {
|
||||
return 'toolbar';
|
||||
}
|
||||
|
@ -50,6 +56,9 @@ class ToolbarPlugin<T extends ToolbarOptions> extends Plugin<T> {
|
|||
this.editor.on('parse:value', (node) => this.paserValue(node));
|
||||
}
|
||||
this.editor.language.add(locales);
|
||||
if (this.options.popup) {
|
||||
new ToolbarPopup(this.editor);
|
||||
}
|
||||
}
|
||||
|
||||
paserValue(node: NodeInterface) {
|
||||
|
|
|
@ -293,6 +293,10 @@ export const groupProps = {
|
|||
>,
|
||||
default: [],
|
||||
},
|
||||
popup: {
|
||||
type: [Boolean, undefined] as PropType<boolean | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
icon: collapseItemProps.icon,
|
||||
content: buttonProps.content,
|
||||
};
|
||||
|
@ -326,7 +330,7 @@ export type GroupItemDataProps = {
|
|||
items: Array<ToolbarItemProps | string>;
|
||||
};
|
||||
|
||||
type GroupItemProps =
|
||||
export type GroupItemProps =
|
||||
| Array<
|
||||
| ToolbarItemProps
|
||||
| string
|
||||
|
|
|
@ -0,0 +1,333 @@
|
|||
import { merge, omit } from 'lodash-es';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import classnames from 'classnames-es-ts';
|
||||
import { EngineInterface, isMobile } from '@aomao/engine';
|
||||
import ToolbarGroup from './group';
|
||||
import {
|
||||
getToolbarDefaultConfig,
|
||||
fontFamilyDefaultData,
|
||||
fontfamily,
|
||||
} from './config/toolbar';
|
||||
import { ButtonProps, DropdownProps, ColorProps, CollapseProps } from './types';
|
||||
import ToolbarPlugin, { ToolbarComponent } from './plugin';
|
||||
import { CollapseGroupProps } from './collapse/group';
|
||||
import { CollapseItemProps } from './collapse/item';
|
||||
import locales from './locales';
|
||||
import './index.css';
|
||||
|
||||
export type ToolbarItemProps =
|
||||
| ButtonProps
|
||||
| DropdownProps
|
||||
| ColorProps
|
||||
| CollapseProps;
|
||||
|
||||
type GroupItemDataProps = {
|
||||
// 分组图标-mobile
|
||||
icon?: React.ReactNode;
|
||||
// 分组内容-mobile
|
||||
content?: React.ReactNode | (() => React.ReactNode);
|
||||
// 子项
|
||||
items: Array<ToolbarItemProps | string>;
|
||||
};
|
||||
|
||||
export type GroupItemProps =
|
||||
| Array<
|
||||
| ToolbarItemProps
|
||||
| string
|
||||
| (Omit<CollapseProps, 'groups'> & {
|
||||
groups: Array<
|
||||
Omit<CollapseGroupProps, 'items'> & {
|
||||
items: Array<
|
||||
Omit<CollapseItemProps, 'engine'> | 'string'
|
||||
>;
|
||||
}
|
||||
>;
|
||||
})
|
||||
>
|
||||
| GroupItemDataProps;
|
||||
|
||||
type GroupProps = Omit<GroupItemDataProps, 'items'> & {
|
||||
items: Array<ToolbarItemProps>;
|
||||
};
|
||||
|
||||
export type ToolbarProps = {
|
||||
engine: EngineInterface;
|
||||
items: GroupItemProps[];
|
||||
className?: string;
|
||||
popup?: boolean;
|
||||
};
|
||||
|
||||
const Toolbar: React.FC<ToolbarProps> = ({
|
||||
engine,
|
||||
className,
|
||||
popup,
|
||||
items = [],
|
||||
}) => {
|
||||
const [data, setData] = useState<Array<GroupProps>>([]);
|
||||
//移动端浏览器视图信息
|
||||
const toolbarRef = useRef<HTMLDivElement | null>(null);
|
||||
const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [mobileView, setMobileView] = useState({ top: 0 });
|
||||
const caluTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
//计算移动浏览器的视图变化
|
||||
const calcuMobileView = () => {
|
||||
if (!engine.isFocus() || engine.readonly) return;
|
||||
if (caluTimeoutRef.current) clearTimeout(caluTimeoutRef.current);
|
||||
caluTimeoutRef.current = setTimeout(() => {
|
||||
const rect = toolbarRef.current?.getBoundingClientRect();
|
||||
const height = rect?.height || 0;
|
||||
setMobileView({
|
||||
top:
|
||||
global.Math.max(
|
||||
document.body.scrollTop,
|
||||
document.documentElement.scrollTop,
|
||||
) +
|
||||
(window.visualViewport.height || 0) -
|
||||
height,
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*/
|
||||
const updateState = useCallback(() => {
|
||||
if (isMobile) calcuMobileView();
|
||||
const data: Array<GroupProps> = [];
|
||||
const defaultConfig = getToolbarDefaultConfig(engine);
|
||||
items.forEach((group) => {
|
||||
const dataGroup: GroupProps = { items: [] };
|
||||
if (!Array.isArray(group)) {
|
||||
dataGroup.icon = group.icon;
|
||||
dataGroup.content = group.content;
|
||||
|
||||
group = group.items;
|
||||
}
|
||||
group.forEach((item) => {
|
||||
let customItem:
|
||||
| ButtonProps
|
||||
| DropdownProps
|
||||
| ColorProps
|
||||
| CollapseProps
|
||||
| undefined = undefined;
|
||||
if (typeof item === 'string') {
|
||||
const defaultItem = defaultConfig.find((config) =>
|
||||
item === 'collapse'
|
||||
? config.type === item
|
||||
: config.type !== 'collapse' &&
|
||||
config.name === item,
|
||||
);
|
||||
if (defaultItem) customItem = defaultItem;
|
||||
} else {
|
||||
const defaultItem = defaultConfig.find((config) =>
|
||||
item.type === 'collapse'
|
||||
? config.type === item.type
|
||||
: config.type !== 'collapse' &&
|
||||
config.name === item.name,
|
||||
);
|
||||
// 解析collapse item 为字符串时
|
||||
if (item.type === 'collapse') {
|
||||
const customCollapse: CollapseProps = {
|
||||
...merge(
|
||||
omit({ ...defaultItem }, 'groups'),
|
||||
omit({ ...item }, 'groups'),
|
||||
),
|
||||
groups: [],
|
||||
};
|
||||
item.groups.forEach((group) => {
|
||||
const items: Array<
|
||||
Omit<CollapseItemProps, 'engine'>
|
||||
> = [];
|
||||
group.items.forEach((cItem) => {
|
||||
let targetItem = undefined;
|
||||
(defaultItem as CollapseProps).groups.some(
|
||||
(g) =>
|
||||
g.items.some((i) => {
|
||||
const isEqual =
|
||||
i.name ===
|
||||
(typeof cItem === 'string'
|
||||
? cItem
|
||||
: cItem.name);
|
||||
if (isEqual) {
|
||||
targetItem = {
|
||||
...i,
|
||||
...(typeof cItem ===
|
||||
'string'
|
||||
? {}
|
||||
: cItem),
|
||||
};
|
||||
}
|
||||
return isEqual;
|
||||
}),
|
||||
);
|
||||
if (targetItem) items.push(targetItem);
|
||||
else if (typeof cItem === 'object')
|
||||
items.push(cItem);
|
||||
});
|
||||
if (items.length > 0) {
|
||||
customCollapse.groups.push({
|
||||
...omit(group, 'itmes'),
|
||||
items,
|
||||
});
|
||||
}
|
||||
});
|
||||
customItem =
|
||||
customCollapse.groups.length > 0
|
||||
? customCollapse
|
||||
: undefined;
|
||||
} else if (item.type === 'dropdown') {
|
||||
customItem = defaultItem
|
||||
? merge(
|
||||
defaultItem,
|
||||
omit({ ...item }, 'type', 'items'),
|
||||
)
|
||||
: { ...item };
|
||||
(customItem as DropdownProps).items = item.items;
|
||||
} else {
|
||||
customItem = defaultItem
|
||||
? merge(defaultItem, omit({ ...item }, 'type'))
|
||||
: { ...item };
|
||||
}
|
||||
}
|
||||
if (customItem) {
|
||||
if (customItem.type === 'button') {
|
||||
if (customItem.onActive)
|
||||
customItem.active = customItem.onActive();
|
||||
else if (engine.command.queryEnabled(customItem.name))
|
||||
customItem.active = engine.command.queryState(
|
||||
customItem.name,
|
||||
);
|
||||
} else if (customItem.type === 'dropdown') {
|
||||
if (customItem.onActive)
|
||||
customItem.values = customItem.onActive(
|
||||
customItem.items,
|
||||
);
|
||||
else
|
||||
customItem.values = engine.command.queryState(
|
||||
customItem.name,
|
||||
);
|
||||
}
|
||||
if (customItem.type !== 'collapse')
|
||||
customItem.disabled = customItem.onDisabled
|
||||
? customItem.onDisabled()
|
||||
: !engine.command.queryEnabled(customItem.name);
|
||||
else {
|
||||
customItem.groups.forEach((group) =>
|
||||
group.items.forEach((item) => {
|
||||
item.disabled = item.onDisabled
|
||||
? item.onDisabled()
|
||||
: !engine.command.queryEnabled(item.name);
|
||||
}),
|
||||
);
|
||||
}
|
||||
dataGroup.items.push(customItem);
|
||||
}
|
||||
});
|
||||
if (dataGroup.items.length > 0) data.push(dataGroup);
|
||||
});
|
||||
setData(data);
|
||||
}, [engine, items]);
|
||||
|
||||
useMemo(() => {
|
||||
updateState();
|
||||
}, [engine, items]);
|
||||
|
||||
const update = () => {
|
||||
if (updateTimeoutRef.current) clearTimeout(updateTimeoutRef.current);
|
||||
updateTimeoutRef.current = setTimeout(() => {
|
||||
updateState();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
engine.language.add(locales);
|
||||
engine.on('select', update);
|
||||
engine.on('change', update);
|
||||
engine.on('blur', update);
|
||||
engine.on('focus', update);
|
||||
let scrollTimer: NodeJS.Timeout;
|
||||
|
||||
const hideMobileToolbar = () => {
|
||||
setMobileView({
|
||||
top: -120,
|
||||
});
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = setTimeout(() => {
|
||||
calcuMobileView();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleReadonly = () => {
|
||||
if (engine.readonly) {
|
||||
hideMobileToolbar();
|
||||
} else {
|
||||
calcuMobileView();
|
||||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
engine.on('readonly', handleReadonly);
|
||||
engine.on('blur', hideMobileToolbar);
|
||||
document.addEventListener('scroll', hideMobileToolbar);
|
||||
visualViewport.addEventListener('resize', calcuMobileView);
|
||||
visualViewport.addEventListener('scroll', calcuMobileView);
|
||||
} else {
|
||||
engine.on('readonly', update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
engine.off('select', update);
|
||||
engine.off('change', update);
|
||||
engine.off('blur', update);
|
||||
engine.off('focus', update);
|
||||
if (isMobile) {
|
||||
engine.off('readonly', handleReadonly);
|
||||
engine.off('blur', hideMobileToolbar);
|
||||
document.removeEventListener('scroll', hideMobileToolbar);
|
||||
visualViewport.removeEventListener('resize', calcuMobileView);
|
||||
visualViewport.removeEventListener('scroll', calcuMobileView);
|
||||
} else {
|
||||
engine.off('readonly', update);
|
||||
}
|
||||
};
|
||||
}, [engine]);
|
||||
|
||||
const onMouseDown = (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => {
|
||||
const nodeName = (event.target as Node).nodeName;
|
||||
if (nodeName !== 'INPUT' && nodeName !== 'TEXTAREA')
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
className={classnames('editor-toolbar', className, {
|
||||
'editor-toolbar-mobile': isMobile && !popup,
|
||||
'editor-toolbar-popup': popup,
|
||||
})}
|
||||
style={isMobile ? { top: `${mobileView.top}px` } : {}}
|
||||
data-element="ui"
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseOver={(event) => event.preventDefault()}
|
||||
onMouseMove={(event) => event.preventDefault()}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="editor-toolbar-content">
|
||||
{data.map((group, index) => (
|
||||
<ToolbarGroup key={index} engine={engine} {...group} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
export { ToolbarPlugin, ToolbarComponent, fontFamilyDefaultData, fontfamily };
|
|
@ -17,6 +17,13 @@
|
|||
line-height: 32px;
|
||||
}
|
||||
|
||||
.editor-toolbar.editor-toolbar-popup .toolbar-button {
|
||||
min-width: 24px;
|
||||
line-height: 24px;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.editor-toolbar:not(.editor-toolbar-mobile) .toolbar-button {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import React from 'react';
|
||||
import { CARD_SELECTOR, EngineInterface } from '@aomao/engine';
|
||||
import {
|
||||
import { CARD_SELECTOR, isEngine, Range } from '@aomao/engine';
|
||||
import type { EditorInterface } from '@aomao/engine';
|
||||
import type {
|
||||
ButtonProps,
|
||||
DropdownProps,
|
||||
ColorProps,
|
||||
CollapseProps,
|
||||
} from '../../types';
|
||||
import TableSelector from '../../table';
|
||||
import './index.css';
|
||||
import fontfamily, { defaultData as fontFamilyDefaultData } from './fontfamily';
|
||||
import './index.css';
|
||||
|
||||
export { fontfamily, fontFamilyDefaultData };
|
||||
|
||||
export const getToolbarDefaultConfig = (
|
||||
engine: EngineInterface,
|
||||
engine: EditorInterface,
|
||||
): Array<ButtonProps | DropdownProps | ColorProps | CollapseProps> => {
|
||||
const language = engine.language.get('toolbar');
|
||||
const headingLanguage = language['heading'];
|
||||
|
@ -867,8 +868,11 @@ export const getToolbarDefaultConfig = (
|
|||
command: { name: 'link', args: ['_blank'] },
|
||||
title: language['link']['title'],
|
||||
onDisabled: () => {
|
||||
const { change, card } = engine;
|
||||
const range = change.range.get();
|
||||
const { card } = engine;
|
||||
const range = isEngine(engine)
|
||||
? engine.change.range.get()
|
||||
: Range.from(engine);
|
||||
if (!range) return !engine.command.queryEnabled('link');
|
||||
const cardComponent = card.find(range.startNode);
|
||||
return (
|
||||
(!!cardComponent &&
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
.toolbar-dropdown .toolbar-dropdown-trigger-arrow .data-icon-arrow {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 15px;
|
||||
top: calc(100% / 2 - 2px);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjhweCIgaGVpZ2h0PSI1cHgiIHZpZXdCb3g9IjAgMCA4IDUiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+DQogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA1Mi41ICg2NzQ2OSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+DQogICAgPHRpdGxlPkdyb3VwIENvcHkgNjwvdGl0bGU+DQogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+DQogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIG9wYWNpdHk9IjAuNDUiPg0KICAgICAgICA8ZyBpZD0idG9vbGJhciIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMyOC4wMDAwMDAsIC0xOC4wMDAwMDApIj4NCiAgICAgICAgICAgIDxnIGlkPSJwYXJhZ3JhcGgtc3R5bGUiPg0KICAgICAgICAgICAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyNi4wMDAwMDAsIDQuMDAwMDAwKSI+DQogICAgICAgICAgICAgICAgICAgIDxnIGlkPSJHcm91cC1Db3B5LTYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMi4wMDAwMDAsIDEyLjAwMDAwMCkiPg0KICAgICAgICAgICAgICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjgiIGhlaWdodD0iOCI+PC9yZWN0Pg0KICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTAuNTk2MDkzNzUsMi41NTcwMzEyNSBMMy43NDUzMTI1LDYuMzc4MTI1IEMzLjg3NzM0Mzc1LDYuNTI1NzgxMjUgNC4xMDg1OTM3NSw2LjUyNTc4MTI1IDQuMjQwNjI1LDYuMzc4MTI1IEw3LjQwNTQ2ODc1LDIuNTU3MDMxMjUgQzcuNTk2MDkzNzUsMi4zNDI5Njg3NSA3LjQ0NDUzMTI1LDIuMDAzOTA2MjUgNy4xNTc4MTI1LDIuMDAzOTA2MjUgTDAuODQ0NTMxMjUsMi4wMDM5MDYyNSBDMC41NTcwMzEyNSwyLjAwMzkwNjI1IDAuNDA0Njg3NSwyLjM0Mjk2ODc1IDAuNTk2MDkzNzUsMi41NTcwMzEyNSBaIiBpZD0iU2hhcGUiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyI+PC9wYXRoPg0KICAgICAgICAgICAgICAgICAgICA8L2c+DQogICAgICAgICAgICAgICAgPC9nPg0KICAgICAgICAgICAgPC9nPg0KICAgICAgICA8L2c+DQogICAgPC9nPg0KPC9zdmc+);
|
||||
|
@ -47,14 +47,14 @@
|
|||
left: 0px;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .toolbar-dropdown .toolbar-dropdown-list {
|
||||
.editor-toolbar-mobile .toolbar-dropdown .toolbar-dropdown-list,.editor-toolbar-popup .toolbar-dropdown .toolbar-dropdown-list{
|
||||
bottom: 32px;
|
||||
top: auto;
|
||||
max-height: calc(30vh);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .toolbar-dropdown.toolbar-dropdown-right .toolbar-dropdown-list {
|
||||
.editor-toolbar-mobile .toolbar-dropdown.toolbar-dropdown-right .toolbar-dropdown-list,.editor-toolbar-popup .toolbar-dropdown.toolbar-dropdown-right .toolbar-dropdown-list{
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export type GroupProps = {
|
|||
>;
|
||||
icon?: React.ReactNode;
|
||||
content?: React.ReactNode | (() => React.ReactNode);
|
||||
popup?: boolean;
|
||||
};
|
||||
|
||||
const ToolbarGroup: React.FC<GroupProps> = ({
|
||||
|
@ -39,6 +40,7 @@ const ToolbarGroup: React.FC<GroupProps> = ({
|
|||
items,
|
||||
icon,
|
||||
content,
|
||||
popup,
|
||||
}) => {
|
||||
const renderItems = () => {
|
||||
return items.map((item, index) => {
|
||||
|
@ -76,7 +78,8 @@ const ToolbarGroup: React.FC<GroupProps> = ({
|
|||
content={
|
||||
<div
|
||||
className={classNames('editor-toolbar', {
|
||||
'editor-toolbar-mobile': isMobile,
|
||||
'editor-toolbar-mobile': isMobile && !popup,
|
||||
'editor-toolbar-popup': popup,
|
||||
})}
|
||||
data-element="ui"
|
||||
>
|
||||
|
|
|
@ -36,16 +36,29 @@
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-toolbar.editor-toolbar-popup {
|
||||
position: initial;
|
||||
box-shadow: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .editor-toolbar-content {
|
||||
text-align: left;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.editor-toolbar-mobile .editor-toolbar-group {
|
||||
.editor-toolbar-mobile .editor-toolbar-group,.editor-toolbar-popup .editor-toolbar-group {
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-popup .editor-toolbar-content {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar-popover .editor-toolbar {
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
|
|
|
@ -1,326 +1,9 @@
|
|||
import { merge, omit } from 'lodash-es';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import classnames from 'classnames-es-ts';
|
||||
import { EngineInterface, isMobile } from '@aomao/engine';
|
||||
import ToolbarGroup from './group';
|
||||
import {
|
||||
getToolbarDefaultConfig,
|
||||
fontFamilyDefaultData,
|
||||
fontfamily,
|
||||
} from './config/toolbar';
|
||||
import { ButtonProps, DropdownProps, ColorProps, CollapseProps } from './types';
|
||||
import { fontFamilyDefaultData, fontfamily } from './config/toolbar';
|
||||
import ToolbarPlugin, { ToolbarComponent } from './plugin';
|
||||
import { CollapseGroupProps } from './collapse/group';
|
||||
import { CollapseItemProps } from './collapse/item';
|
||||
import locales from './locales';
|
||||
import Toolbar from './Toolbar';
|
||||
import type { ToolbarProps, GroupItemProps, ToolbarItemProps } from './Toolbar';
|
||||
import './index.css';
|
||||
|
||||
type ToolbarItemProps =
|
||||
| ButtonProps
|
||||
| DropdownProps
|
||||
| ColorProps
|
||||
| CollapseProps;
|
||||
|
||||
type GroupItemDataProps = {
|
||||
// 分组图标-mobile
|
||||
icon?: React.ReactNode;
|
||||
// 分组内容-mobile
|
||||
content?: React.ReactNode | (() => React.ReactNode);
|
||||
// 子项
|
||||
items: Array<ToolbarItemProps | string>;
|
||||
};
|
||||
|
||||
type GroupItemProps =
|
||||
| Array<
|
||||
| ToolbarItemProps
|
||||
| string
|
||||
| (Omit<CollapseProps, 'groups'> & {
|
||||
groups: Array<
|
||||
Omit<CollapseGroupProps, 'items'> & {
|
||||
items: Array<
|
||||
Omit<CollapseItemProps, 'engine'> | 'string'
|
||||
>;
|
||||
}
|
||||
>;
|
||||
})
|
||||
>
|
||||
| GroupItemDataProps;
|
||||
|
||||
type GroupProps = Omit<GroupItemDataProps, 'items'> & {
|
||||
items: Array<ToolbarItemProps>;
|
||||
};
|
||||
|
||||
export type ToolbarProps = {
|
||||
engine: EngineInterface;
|
||||
items: Array<GroupItemProps>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Toolbar: React.FC<ToolbarProps> = ({ engine, className, items = [] }) => {
|
||||
const [data, setData] = useState<Array<GroupProps>>([]);
|
||||
//移动端浏览器视图信息
|
||||
const toolbarRef = useRef<HTMLDivElement | null>(null);
|
||||
const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [mobileView, setMobileView] = useState({ top: 0 });
|
||||
const caluTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
//计算移动浏览器的视图变化
|
||||
const calcuMobileView = () => {
|
||||
if (!engine.isFocus() || engine.readonly) return;
|
||||
if (caluTimeoutRef.current) clearTimeout(caluTimeoutRef.current);
|
||||
caluTimeoutRef.current = setTimeout(() => {
|
||||
const rect = toolbarRef.current?.getBoundingClientRect();
|
||||
const height = rect?.height || 0;
|
||||
setMobileView({
|
||||
top:
|
||||
global.Math.max(
|
||||
document.body.scrollTop,
|
||||
document.documentElement.scrollTop,
|
||||
) +
|
||||
(window.visualViewport.height || 0) -
|
||||
height,
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*/
|
||||
const updateState = useCallback(() => {
|
||||
if (isMobile) calcuMobileView();
|
||||
const data: Array<GroupProps> = [];
|
||||
const defaultConfig = getToolbarDefaultConfig(engine);
|
||||
items.forEach((group) => {
|
||||
const dataGroup: GroupProps = { items: [] };
|
||||
if (!Array.isArray(group)) {
|
||||
dataGroup.icon = group.icon;
|
||||
dataGroup.content = group.content;
|
||||
|
||||
group = group.items;
|
||||
}
|
||||
group.forEach((item) => {
|
||||
let customItem:
|
||||
| ButtonProps
|
||||
| DropdownProps
|
||||
| ColorProps
|
||||
| CollapseProps
|
||||
| undefined = undefined;
|
||||
if (typeof item === 'string') {
|
||||
const defaultItem = defaultConfig.find((config) =>
|
||||
item === 'collapse'
|
||||
? config.type === item
|
||||
: config.type !== 'collapse' &&
|
||||
config.name === item,
|
||||
);
|
||||
if (defaultItem) customItem = defaultItem;
|
||||
} else {
|
||||
const defaultItem = defaultConfig.find((config) =>
|
||||
item.type === 'collapse'
|
||||
? config.type === item.type
|
||||
: config.type !== 'collapse' &&
|
||||
config.name === item.name,
|
||||
);
|
||||
// 解析collapse item 为字符串时
|
||||
if (item.type === 'collapse') {
|
||||
const customCollapse: CollapseProps = {
|
||||
...merge(
|
||||
omit({ ...defaultItem }, 'groups'),
|
||||
omit({ ...item }, 'groups'),
|
||||
),
|
||||
groups: [],
|
||||
};
|
||||
item.groups.forEach((group) => {
|
||||
const items: Array<
|
||||
Omit<CollapseItemProps, 'engine'>
|
||||
> = [];
|
||||
group.items.forEach((cItem) => {
|
||||
let targetItem = undefined;
|
||||
(defaultItem as CollapseProps).groups.some(
|
||||
(g) =>
|
||||
g.items.some((i) => {
|
||||
const isEqual =
|
||||
i.name ===
|
||||
(typeof cItem === 'string'
|
||||
? cItem
|
||||
: cItem.name);
|
||||
if (isEqual) {
|
||||
targetItem = {
|
||||
...i,
|
||||
...(typeof cItem ===
|
||||
'string'
|
||||
? {}
|
||||
: cItem),
|
||||
};
|
||||
}
|
||||
return isEqual;
|
||||
}),
|
||||
);
|
||||
if (targetItem) items.push(targetItem);
|
||||
else if (typeof cItem === 'object')
|
||||
items.push(cItem);
|
||||
});
|
||||
if (items.length > 0) {
|
||||
customCollapse.groups.push({
|
||||
...omit(group, 'itmes'),
|
||||
items,
|
||||
});
|
||||
}
|
||||
});
|
||||
customItem =
|
||||
customCollapse.groups.length > 0
|
||||
? customCollapse
|
||||
: undefined;
|
||||
} else if (item.type === 'dropdown') {
|
||||
customItem = defaultItem
|
||||
? merge(
|
||||
defaultItem,
|
||||
omit({ ...item }, 'type', 'items'),
|
||||
)
|
||||
: { ...item };
|
||||
(customItem as DropdownProps).items = item.items;
|
||||
} else {
|
||||
customItem = defaultItem
|
||||
? merge(defaultItem, omit({ ...item }, 'type'))
|
||||
: { ...item };
|
||||
}
|
||||
}
|
||||
if (customItem) {
|
||||
if (customItem.type === 'button') {
|
||||
if (customItem.onActive)
|
||||
customItem.active = customItem.onActive();
|
||||
else if (engine.command.queryEnabled(customItem.name))
|
||||
customItem.active = engine.command.queryState(
|
||||
customItem.name,
|
||||
);
|
||||
} else if (customItem.type === 'dropdown') {
|
||||
if (customItem.onActive)
|
||||
customItem.values = customItem.onActive(
|
||||
customItem.items,
|
||||
);
|
||||
else
|
||||
customItem.values = engine.command.queryState(
|
||||
customItem.name,
|
||||
);
|
||||
}
|
||||
if (customItem.type !== 'collapse')
|
||||
customItem.disabled = customItem.onDisabled
|
||||
? customItem.onDisabled()
|
||||
: !engine.command.queryEnabled(customItem.name);
|
||||
else {
|
||||
customItem.groups.forEach((group) =>
|
||||
group.items.forEach((item) => {
|
||||
item.disabled = item.onDisabled
|
||||
? item.onDisabled()
|
||||
: !engine.command.queryEnabled(item.name);
|
||||
}),
|
||||
);
|
||||
}
|
||||
dataGroup.items.push(customItem);
|
||||
}
|
||||
});
|
||||
if (dataGroup.items.length > 0) data.push(dataGroup);
|
||||
});
|
||||
setData(data);
|
||||
}, [engine, items]);
|
||||
|
||||
useMemo(() => {
|
||||
updateState();
|
||||
}, [engine, items]);
|
||||
|
||||
const update = () => {
|
||||
if (updateTimeoutRef.current) clearTimeout(updateTimeoutRef.current);
|
||||
updateTimeoutRef.current = setTimeout(() => {
|
||||
updateState();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
engine.language.add(locales);
|
||||
engine.on('select', update);
|
||||
engine.on('change', update);
|
||||
engine.on('blur', update);
|
||||
engine.on('focus', update);
|
||||
let scrollTimer: NodeJS.Timeout;
|
||||
|
||||
const hideMobileToolbar = () => {
|
||||
setMobileView({
|
||||
top: -120,
|
||||
});
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = setTimeout(() => {
|
||||
calcuMobileView();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleReadonly = () => {
|
||||
if (engine.readonly) {
|
||||
hideMobileToolbar();
|
||||
} else {
|
||||
calcuMobileView();
|
||||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
engine.on('readonly', handleReadonly);
|
||||
engine.on('blur', hideMobileToolbar);
|
||||
document.addEventListener('scroll', hideMobileToolbar);
|
||||
visualViewport.addEventListener('resize', calcuMobileView);
|
||||
visualViewport.addEventListener('scroll', calcuMobileView);
|
||||
} else {
|
||||
engine.on('readonly', update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
engine.off('select', update);
|
||||
engine.off('change', update);
|
||||
engine.off('blur', update);
|
||||
engine.off('focus', update);
|
||||
if (isMobile) {
|
||||
engine.off('readonly', handleReadonly);
|
||||
engine.off('blur', hideMobileToolbar);
|
||||
document.removeEventListener('scroll', hideMobileToolbar);
|
||||
visualViewport.removeEventListener('resize', calcuMobileView);
|
||||
visualViewport.removeEventListener('scroll', calcuMobileView);
|
||||
} else {
|
||||
engine.off('readonly', update);
|
||||
}
|
||||
};
|
||||
}, [engine]);
|
||||
|
||||
const onMouseDown = (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => {
|
||||
const nodeName = (event.target as Node).nodeName;
|
||||
if (nodeName !== 'INPUT' && nodeName !== 'TEXTAREA')
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
className={classnames('editor-toolbar', className, {
|
||||
'editor-toolbar-mobile': isMobile,
|
||||
})}
|
||||
style={isMobile ? { top: `${mobileView.top}px` } : {}}
|
||||
data-element="ui"
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseOver={(event) => event.preventDefault()}
|
||||
onMouseMove={(event) => event.preventDefault()}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="editor-toolbar-content">
|
||||
{data.map((group, index) => (
|
||||
<ToolbarGroup key={index} engine={engine} {...group} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
export { ToolbarPlugin, ToolbarComponent, fontFamilyDefaultData, fontfamily };
|
||||
export type { ToolbarProps, GroupItemProps, ToolbarItemProps };
|
||||
|
|
|
@ -31,4 +31,14 @@
|
|||
z-index: 999;
|
||||
max-height: calc(80vh);
|
||||
overflow: auto;
|
||||
}
|
||||
/** ------------------- popup ---------------------- **/
|
||||
.data-toolbar-popup-wrapper {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
padding: 4px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee0e3;
|
||||
box-shadow: 0 4px 8px 0 rgba(31, 35, 41, 0.1);
|
||||
}
|
|
@ -11,8 +11,11 @@ import {
|
|||
import { CollapseGroupProps } from '../../collapse/group';
|
||||
import { CollapseItemProps } from '../../collapse/item';
|
||||
import { getToolbarDefaultConfig } from '../../config/toolbar';
|
||||
import { CollapseProps } from '../../types';
|
||||
import CollapseComponent, { CollapseComponentInterface } from './collapse';
|
||||
import type { CollapseProps } from '../../types';
|
||||
import CollapseComponent from './collapse';
|
||||
import type { CollapseComponentInterface } from './collapse';
|
||||
import ToolbarPopup from './popup';
|
||||
import type { GroupItemProps } from './popup';
|
||||
import './index.css';
|
||||
|
||||
type Data = Array<CollapseGroupProps>;
|
||||
|
@ -20,7 +23,7 @@ export interface ToolbarValue extends CardValue {
|
|||
data: Data;
|
||||
}
|
||||
|
||||
class ToolbarComponent<T extends ToolbarValue> extends Card<T> {
|
||||
class ToolbarComponent<T extends ToolbarValue = ToolbarValue> extends Card<T> {
|
||||
private keyword?: NodeInterface;
|
||||
private placeholder?: NodeInterface;
|
||||
private component?: CollapseComponentInterface;
|
||||
|
@ -259,3 +262,5 @@ class ToolbarComponent<T extends ToolbarValue> extends Card<T> {
|
|||
}
|
||||
|
||||
export default ToolbarComponent;
|
||||
export { ToolbarPopup };
|
||||
export type { GroupItemProps };
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { $, EngineInterface, isEngine, isMobile, Range } from '@aomao/engine';
|
||||
import type { NodeInterface, EditorInterface } from '@aomao/engine';
|
||||
import Toolbar from '../../Toolbar';
|
||||
import type { GroupItemProps } from '../../Toolbar';
|
||||
|
||||
type PopupOptions = {
|
||||
items?: GroupItemProps[];
|
||||
};
|
||||
|
||||
export default class Popup {
|
||||
#editor: EditorInterface;
|
||||
#root: NodeInterface;
|
||||
#point: Record<'left' | 'top', number> = { left: 0, top: -9999 };
|
||||
#align: 'top' | 'bottom' = 'bottom';
|
||||
#options: PopupOptions = {};
|
||||
|
||||
constructor(editor: EditorInterface, options: PopupOptions = {}) {
|
||||
this.#options = options;
|
||||
this.#editor = editor;
|
||||
this.#root = $(`<div class="data-toolbar-popup-wrapper">Test</div>`);
|
||||
document.body.append(this.#root[0]);
|
||||
if (isEngine(editor)) {
|
||||
this.#editor.on('select', this.onSelect);
|
||||
} else {
|
||||
document.addEventListener('selectionchange', this.onSelect);
|
||||
}
|
||||
if (!isMobile) window.addEventListener('scroll', this.onSelect);
|
||||
window.addEventListener('resize', this.onSelect);
|
||||
this.#editor.scrollNode?.on('scroll', this.onSelect);
|
||||
}
|
||||
|
||||
onSelect = () => {
|
||||
const range = Range.from(this.#editor)
|
||||
?.cloneRange()
|
||||
.shrinkToTextNode();
|
||||
const selection = window.getSelection();
|
||||
if (
|
||||
!range ||
|
||||
!selection ||
|
||||
!selection.focusNode ||
|
||||
range.collapsed ||
|
||||
(!range.commonAncestorNode.inEditor() &&
|
||||
!range.commonAncestorNode.isRoot())
|
||||
) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
const subRanges = range.getSubRanges();
|
||||
if (
|
||||
subRanges.length === 0 ||
|
||||
(this.#editor.card.active && !this.#editor.card.active.isEditable)
|
||||
) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
const topRange = subRanges[0];
|
||||
const bottomRange = subRanges[subRanges.length - 1];
|
||||
const topRect = topRange.getBoundingClientRect();
|
||||
const bottomRect = bottomRange.getBoundingClientRect();
|
||||
|
||||
let rootRect: DOMRect | undefined = undefined;
|
||||
this.showContent(() => {
|
||||
rootRect = this.#root.get<HTMLElement>()?.getBoundingClientRect();
|
||||
console.log(rootRect);
|
||||
if (!rootRect) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
this.#align =
|
||||
bottomRange.startNode.equal(selection.focusNode!) &&
|
||||
!topRange.startNode.equal(selection.focusNode!)
|
||||
? 'bottom'
|
||||
: 'top';
|
||||
const space = 12;
|
||||
let targetRect = this.#align === 'bottom' ? bottomRect : topRect;
|
||||
if (
|
||||
this.#align === 'top' &&
|
||||
targetRect.top - rootRect.height - space <
|
||||
window.innerHeight -
|
||||
(this.#editor.scrollNode?.height() || 0)
|
||||
) {
|
||||
this.#align = 'bottom';
|
||||
} else if (
|
||||
this.#align === 'bottom' &&
|
||||
targetRect.bottom + rootRect.height + space > window.innerHeight
|
||||
) {
|
||||
this.#align = 'top';
|
||||
}
|
||||
targetRect = this.#align === 'bottom' ? bottomRect : topRect;
|
||||
const top =
|
||||
this.#align === 'top'
|
||||
? targetRect.top - rootRect.height - space
|
||||
: targetRect.bottom + space;
|
||||
this.#point = {
|
||||
left: targetRect.left + targetRect.width - rootRect.width / 2,
|
||||
top,
|
||||
};
|
||||
this.#root.css({
|
||||
left: `${this.#point.left}px`,
|
||||
top: `${this.#point.top}px`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showContent(callback?: () => void) {
|
||||
const result = this.#editor.trigger('toolbar-render', this.#options);
|
||||
ReactDOM.render(
|
||||
typeof result === 'object' ? (
|
||||
result
|
||||
) : (
|
||||
<Toolbar
|
||||
popup={true}
|
||||
engine={this.#editor as EngineInterface}
|
||||
items={this.#options.items || []}
|
||||
/>
|
||||
),
|
||||
this.#root.get<HTMLDivElement>()!,
|
||||
() => {
|
||||
if (callback) callback();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.#root.css({
|
||||
left: '0px',
|
||||
top: '-9999px',
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#root.remove();
|
||||
if (isEngine(this.#editor)) {
|
||||
this.#editor.on('select', this.onSelect);
|
||||
} else {
|
||||
document.removeEventListener('selectionchange', this.onSelect);
|
||||
}
|
||||
if (!isMobile) window.removeEventListener('scroll', this.onSelect);
|
||||
window.removeEventListener('resize', this.onSelect);
|
||||
this.#editor.scrollNode?.off('scroll', this.onSelect);
|
||||
}
|
||||
}
|
||||
export type { GroupItemProps };
|
|
@ -1,15 +1,18 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
DATA_TRANSIENT_ELEMENT,
|
||||
EditorInterface,
|
||||
isEngine,
|
||||
isSafari,
|
||||
NodeInterface,
|
||||
Plugin,
|
||||
} from '@aomao/engine';
|
||||
import type {
|
||||
EditorInterface,
|
||||
NodeInterface,
|
||||
PluginOptions,
|
||||
} from '@aomao/engine';
|
||||
import { CollapseItemProps } from '../collapse/item';
|
||||
import ToolbarComponent, { ToolbarValue } from './component';
|
||||
import type { CollapseItemProps } from '../collapse/item';
|
||||
import ToolbarComponent, { ToolbarPopup } from './component';
|
||||
import type { ToolbarValue, GroupItemProps } from './component';
|
||||
import locales from '../locales';
|
||||
|
||||
type Config = Array<{
|
||||
|
@ -18,6 +21,9 @@ type Config = Array<{
|
|||
}>;
|
||||
export interface ToolbarOptions extends PluginOptions {
|
||||
config: Config;
|
||||
popup?: {
|
||||
items: GroupItemProps[];
|
||||
};
|
||||
}
|
||||
|
||||
const defaultConfig = (editor: EditorInterface): Config => {
|
||||
|
@ -36,7 +42,6 @@ const defaultConfig = (editor: EditorInterface): Config => {
|
|||
'video-uploader',
|
||||
'math',
|
||||
'status',
|
||||
//'mind'
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@ -51,20 +56,13 @@ class ToolbarPlugin<T extends ToolbarOptions> extends Plugin<T> {
|
|||
if (isEngine(this.editor)) {
|
||||
this.editor.on('keydown:slash', (event) => this.onSlash(event));
|
||||
this.editor.on('parse:value', (node) => this.paserValue(node));
|
||||
//this.editor.on('select', this.onSelect)
|
||||
}
|
||||
this.editor.language.add(locales);
|
||||
if (this.options.popup) {
|
||||
new ToolbarPopup(this.editor);
|
||||
}
|
||||
}
|
||||
|
||||
// onSelect = () => {
|
||||
// if (!isEngine(this.editor)) return;
|
||||
// const { change, card } = this.editor;
|
||||
// if(card.active) return
|
||||
// const range = change.range.get().cloneRange().shrinkToTextNode();
|
||||
// if(range.collapsed) return
|
||||
// const { startNode, endNode } = range
|
||||
// }
|
||||
|
||||
paserValue(node: NodeInterface) {
|
||||
if (
|
||||
node.isCard() &&
|
||||
|
@ -117,6 +115,6 @@ class ToolbarPlugin<T extends ToolbarOptions> extends Plugin<T> {
|
|||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
export { ToolbarComponent };
|
||||
export { ToolbarComponent, ToolbarPopup };
|
||||
export type { ToolbarValue };
|
||||
export default ToolbarPlugin;
|
||||
|
|
|
@ -40,7 +40,9 @@ const MODE_ALIAS: { [key: string]: string } = {
|
|||
'c++': 'cpp',
|
||||
};
|
||||
|
||||
export default class<T extends CodeblockOptions> extends Plugin<T> {
|
||||
export default class<
|
||||
T extends CodeblockOptions = CodeblockOptions,
|
||||
> extends Plugin<T> {
|
||||
static get pluginName() {
|
||||
return 'codeblock';
|
||||
}
|
||||
|
@ -375,3 +377,4 @@ export default class<T extends CodeblockOptions> extends Plugin<T> {
|
|||
}
|
||||
}
|
||||
export { CodeBlockComponent };
|
||||
export type { CodeBlockValue };
|
||||
|
|
|
@ -376,3 +376,4 @@ export default class<T extends CodeBlockOptions> extends Plugin<T> {
|
|||
}
|
||||
}
|
||||
export { CodeBlockComponent };
|
||||
export type { CodeBlockValue };
|
||||
|
|
|
@ -131,3 +131,4 @@ class Embed<T extends EmbedOptions> extends Plugin<T> {
|
|||
|
||||
export default Embed;
|
||||
export { EmbedComponent };
|
||||
export type { EmbedValue };
|
||||
|
|
|
@ -15,7 +15,9 @@ export interface LinkOptions extends PluginOptions {
|
|||
hotkey?: string | Array<string>;
|
||||
markdown?: string;
|
||||
}
|
||||
export default class<T extends LinkOptions> extends InlinePlugin<T> {
|
||||
export default class<
|
||||
T extends LinkOptions = LinkOptions,
|
||||
> extends InlinePlugin<T> {
|
||||
private toolbar?: Toolbar;
|
||||
|
||||
static get pluginName() {
|
||||
|
|
Loading…
Reference in New Issue