update: 优化toolbar显示

This commit is contained in:
yanmao 2022-01-16 16:09:31 +08:00
parent b5f805654a
commit a234f94ecf
15 changed files with 242 additions and 212 deletions

View File

@ -114,17 +114,10 @@ export const pluginConfig: { [key: string]: PluginOptions } = {
[ToolbarPlugin.pluginName]: {
popup: {
items: [
['undo', 'redo'],
['bold', 'strikethrough', 'fontcolor'],
{
icon: 'text',
items: [
'bold',
'italic',
'strikethrough',
'underline',
'backcolor',
'moremark',
],
items: ['italic', 'underline', 'backcolor', 'moremark'],
},
[
{

View File

@ -17,6 +17,7 @@ import Selection from '../selection';
import { $ } from '../node';
import NativeEvent from './native-event';
import ChangeRange from './range';
import { Range } from 'src';
class ChangeModel implements ChangeInterface {
private engine: EngineInterface;
@ -249,8 +250,8 @@ class ChangeModel implements ChangeInterface {
);
}
getOriginValue() {
const { container, schema, conversion } = this.engine;
getOriginValue(container: NodeInterface = this.engine.container) {
const { schema, conversion } = this.engine;
return new Parser(
container.get<HTMLElement>()?.outerHTML || '',
this.engine,
@ -268,12 +269,16 @@ class ChangeModel implements ChangeInterface {
if (options.ignoreCursor || this.isComposing()) {
value = this.getOriginValue();
} else {
const range = this.range.get();
let range = this.range.get();
let selection;
const container = this.engine.container.clone(true);
if (!range.inCard()) {
const path = range.toPath(true);
if (!path) return this.getOriginValue();
range = Range.fromPath(this.engine, path, true, container);
selection = range.createSelection();
}
value = this.getOriginValue();
value = this.getOriginValue(container);
selection?.move();
}
return value;

View File

@ -830,8 +830,7 @@ class Mark implements MarkModelInterface {
nodeApi.unwrap(children);
}
});
nodeApi.wrap(targetNode, mark);
return targetNode;
return nodeApi.wrap(targetNode, mark);
} else if (node.name !== mark.name) {
node.remove();
}
@ -1099,6 +1098,12 @@ class Mark implements MarkModelInterface {
) {
started = false;
return false;
} else if (
selection?.focus &&
result.next()?.equal(selection.focus)
) {
started = false;
return false;
}
}
if (typeof result !== 'undefined') return true;

View File

@ -6,6 +6,7 @@
</template>
<button
:class="['toolbar-button',className,{'toolbar-button-active': active,'toolbar-button-disabled':disabled}]"
ref="element"
@click="triggerClick"
@mousedown="triggerMouseDown"
@mouseenter="triggerMouseEnter"
@ -33,6 +34,7 @@ export default defineComponent({
},
props:buttonProps,
setup(props){
const element = ref<HTMLButtonElement | undefined>()
let hotkey = props.hotkey;
//
if (props.engine && (hotkey === true || hotkey === undefined)) {
@ -51,7 +53,8 @@ export default defineComponent({
iconIsHtml:/^<.*>/.test(props.icon?.trim() || ""),
isMobile,
visible,
hotkeyText:hotkey
hotkeyText:hotkey,
element
}
},
data(){

View File

@ -22,6 +22,7 @@
:on-click="toggleDropdown"
:disabled="disabled"
:placement="placement"
ref="targetRef"
>
<template #icon>
<span class="colorpicker-button-dropdown-empty" />
@ -43,7 +44,6 @@
</template>
<script lang="ts">
import { defineComponent, onUnmounted, ref, watch } from 'vue'
import { $ } from '@aomao/engine'
import { colorProps } from '../../types'
import { useRight } from '../../hooks';
import AmButton from '../button.vue'
@ -62,7 +62,7 @@ export default defineComponent({
const buttonRef = ref<HTMLDivElement | null>(null)
const isRight = useRight(buttonRef)
const targetRef = ref<typeof AmButton | undefined>(undefined)
const currentColor = ref(props.defaultActiveColor);
const getContent = () => {
@ -88,24 +88,25 @@ export default defineComponent({
};
const showDropdown = () => {
setTimeout(() => {
document.addEventListener('click', hideDropdown);
}, 10);
visible.value = true
};
const hideDropdown = (event?: MouseEvent) => {
if (event?.target && $(event.target).closest('.toolbar-dropdown-list').length > 0)
return;
document.removeEventListener('click', hideDropdown);
if(event && targetRef.value?.element && targetRef.value.element.contains(event.target as Node)) return;
visible.value = false
};
watch(() => visible.value, (value,oldValue) => {
if(value) document.addEventListener('click', hideDropdown);
else document.removeEventListener('click', hideDropdown);
})
const triggerClick = (event:MouseEvent) => {
triggerSelect(currentColor.value,event)
}
const triggerSelect = (color: string, event: MouseEvent) => {
hideDropdown()
currentColor.value = color;
buttonContent.value = typeof props.content === 'string'
? props.content
@ -139,7 +140,8 @@ export default defineComponent({
currentColor,
triggerSelect,
triggerClick,
toggleDropdown
toggleDropdown,
targetRef
}
}
})

View File

@ -41,7 +41,6 @@ export default defineComponent({
let { hotkey } = item
//
if (props.engine && (hotkey === true || hotkey === undefined)) {
console.log(item)
hotkey = autoGetHotkey(
props.engine,
command && !Array.isArray(command) ? command.name : props.name,

View File

@ -17,6 +17,7 @@
:active="visible"
:disabled="disabled"
:placement="placement"
ref="targetRef"
>
<template #default>
<slot :item="content">
@ -56,7 +57,8 @@ export default defineComponent({
setup(props,cxt){
const valuesVar = ref<string | number | string[]>("")
let buttonContent = ref<DropdownListItem | {icon?:string,content?:string} | undefined>(undefined)
const visible = ref(false)
const targetRef = ref<typeof AmButton | undefined>(undefined)
const buttonRef = ref<HTMLDivElement | null>(null)
const isRight = useRight(buttonRef)
@ -95,50 +97,52 @@ export default defineComponent({
valuesVar.value = values ||
(props.icon || props.content ? '' : defaultItem?.key || '')
}
const triggerMouseDown = (event: MouseEvent) => {
event.preventDefault();
}
const triggerClick = (event: MouseEvent) =>{
event.preventDefault();
if (props.disabled) {
return;
}
if (visible.value) {
hide();
} else {
show();
}
}
const show = () => {
visible.value = true
}
const hide = (event?: MouseEvent) => {
if(event && targetRef.value?.element && targetRef.value.element.contains(event.target as Node)) return;
visible.value = false
}
const triggerSelect = (event: MouseEvent, key: string) => {
hide()
if (props.onSelect) props.onSelect(event, key);
}
update(props.values)
watch(() => ({...props}), (newProps) => update(newProps.values))
watch(() => visible.value, (value,oldValue) => {
if(value) document.addEventListener('click', hide);
else document.removeEventListener('click', hide);
})
return {
buttonRef,
isRight,
buttonContent,
valuesVar
valuesVar,
triggerMouseDown,
triggerClick,
show,
hide,
triggerSelect,
visible,
targetRef
}
},
data(){
return {
visible:false
}
},
methods:{
triggerMouseDown(event: MouseEvent){
event.preventDefault();
},
triggerClick(event: MouseEvent){
event.preventDefault();
if (this.disabled) {
return;
}
if (this.visible) {
this.hide();
} else {
this.show();
}
},
show(){
setTimeout(() => {
document.addEventListener('click', this.hide);
}, 10);
this.visible = true
},
hide(){
document.removeEventListener('click', this.hide);
this.visible = false
},
triggerSelect(event: MouseEvent, key: string){
this.hide()
if (this.onSelect) this.onSelect(event, key);
}
}
})
</script>
@ -148,8 +152,9 @@ export default defineComponent({
}
.toolbar-dropdown .toolbar-dropdown-trigger {
align-items: center;
display: flex;
align-items: stretch;
height: 100%;
}
.toolbar-dropdown .toolbar-dropdown-trigger .toolbar-dropdown-button-text {

View File

@ -2,7 +2,6 @@
<div v-if="!!icon || !!content" class="editor-toolbar-group">
<am-popover
:get-popup-container="getPopupContainer"
trigger="click"
overlay-class-name="editor-toolbar-popover"
:arrow-point-at-center="true"
:placement="isMobile ? 'topRight' : undefined"
@ -64,11 +63,11 @@ export default defineComponent({
</script>
<style>
.editor-toolbar-group {
padding: 4px 8px;
padding: 4px;
width: auto;
border-left: 1px solid #e8e8e8;
display: flex;
align-items: center;
align-items: stretch;
}
.editor-toolbar .editor-toolbar-group:nth-child(1) {

View File

@ -30,7 +30,6 @@ export default class Popup {
window.addEventListener('resize', this.onSelect);
this.#editor.scrollNode?.on('scroll', this.onSelect);
document.addEventListener('mousedown', this.hide);
this.#editor.on('blur', this.hide);
}
onSelect = () => {
@ -137,13 +136,20 @@ export default class Popup {
showContent(callback?: () => void) {
const result = this.#editor.trigger('toolbar-render', this.#options);
if (this.#vm) this.#vm.unmount();
this.#vm = createApp(typeof result === 'object' ? result : Toolbar, {
...this.#options,
engine: this.#editor,
popup: true,
});
this.#vm.mount(this.#root.get<HTMLDivElement>()!);
let content = Toolbar;
if (typeof result === 'object') {
this.#vm?.unmount();
this.#vm = undefined;
content = result;
}
if (!this.#vm) {
this.#vm = createApp(content, {
...this.#options,
engine: this.#editor,
popup: true,
});
this.#vm.mount(this.#root.get<HTMLDivElement>()!);
}
setTimeout(() => {
if (callback) callback();
}, 200);
@ -174,8 +180,10 @@ export default class Popup {
window.removeEventListener('resize', this.onSelect);
this.#editor.scrollNode?.off('scroll', this.onSelect);
document.removeEventListener('mousedown', this.hide);
this.#editor.off('blur', this.hide);
if (this.#vm) this.#vm.unmount();
if (this.#vm) {
this.#vm.unmount();
this.#vm = undefined;
}
}
}
export type { GroupItemProps };

View File

@ -26,115 +26,118 @@ export type ButtonProps = {
onMouseLeave?: (event: React.MouseEvent) => void;
};
const ToolbarButton: React.FC<ButtonProps> = (props) => {
const { name, engine, command } = props;
const ToolbarButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { name, engine, command } = props;
const [tooltipVisible, setTooltipVisible] = useState(false);
const [tooltipVisible, setTooltipVisible] = useState(false);
const onClick = (event: React.MouseEvent) => {
const { command, onClick, disabled, autoExecute } = props;
const onClick = (event: React.MouseEvent) => {
const { command, onClick, disabled, autoExecute } = props;
const nodeName = (event.target as Node).nodeName;
if (nodeName !== 'INPUT' && nodeName !== 'TEXTAREA')
event.preventDefault();
const nodeName = (event.target as Node).nodeName;
if (nodeName !== 'INPUT' && nodeName !== 'TEXTAREA')
event.preventDefault();
if (disabled) return;
if (onClick && onClick(event) === false) return;
if (autoExecute !== false) {
let commandName = name;
let commandArgs = [];
if (command) {
if (!Array.isArray(command)) {
commandName = command.name;
commandArgs = command.args;
} else {
commandArgs = command;
if (disabled) return;
if (onClick && onClick(event) === false) return;
if (autoExecute !== false) {
let commandName = name;
let commandArgs = [];
if (command) {
if (!Array.isArray(command)) {
commandName = command.name;
commandArgs = command.args;
} else {
commandArgs = command;
}
}
engine?.command.execute(commandName, ...commandArgs);
}
engine?.command.execute(commandName, ...commandArgs);
};
const onMouseDown = (event: React.MouseEvent) => {
event.preventDefault();
const { onMouseDown, disabled } = props;
if (disabled) return;
if (onMouseDown) onMouseDown(event);
setTooltipVisible(false);
};
const onMouseEnter = (event: React.MouseEvent) => {
const { onMouseEnter } = props;
if (onMouseEnter) {
onMouseEnter(event);
}
setTooltipVisible(true);
};
const onMouseLeave = (event: React.MouseEvent) => {
const { onMouseLeave } = props;
if (onMouseLeave) {
onMouseLeave(event);
}
setTooltipVisible(false);
};
const renderButton = () => {
const { icon, content, className, active, disabled } = props;
return (
<button
className={classnames('toolbar-button', className, {
'toolbar-button-active': active,
'toolbar-button-disabled': disabled,
})}
ref={ref}
onClick={onClick}
onMouseDown={onMouseDown}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{typeof icon === 'string' ? (
<span className={`data-icon data-icon-${icon}`} />
) : (
icon
)}
{typeof content === 'function' ? content() : content}
</button>
);
};
let title = props.title ? (
<div className="toolbar-tooltip-title">{props.title}</div>
) : null;
let hotkey = props.hotkey;
//默认获取插件的热键
if (engine && (hotkey === true || hotkey === undefined)) {
hotkey = autoGetHotkey(
engine,
command && !Array.isArray(command) ? command.name : name,
);
}
};
const onMouseDown = (event: React.MouseEvent) => {
event.preventDefault();
const { onMouseDown, disabled } = props;
if (disabled) return;
if (onMouseDown) onMouseDown(event);
setTooltipVisible(false);
};
const onMouseEnter = (event: React.MouseEvent) => {
const { onMouseEnter } = props;
if (onMouseEnter) {
onMouseEnter(event);
if (typeof hotkey === 'string' && hotkey !== '') {
title = (
<>
{title}
<div className="toolbar-tooltip-hotkey">
{formatHotkey(hotkey)}
</div>
</>
);
}
setTooltipVisible(true);
};
const onMouseLeave = (event: React.MouseEvent) => {
const { onMouseLeave } = props;
if (onMouseLeave) {
onMouseLeave(event);
}
setTooltipVisible(false);
};
const renderButton = () => {
const { icon, content, className, active, disabled } = props;
return (
<button
className={classnames('toolbar-button', className, {
'toolbar-button-active': active,
'toolbar-button-disabled': disabled,
})}
onClick={onClick}
onMouseDown={onMouseDown}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
return title && !isMobile ? (
<Tooltip
placement={props.placement || 'bottom'}
title={title}
visible={tooltipVisible}
>
{typeof icon === 'string' ? (
<span className={`data-icon data-icon-${icon}`} />
) : (
icon
)}
{typeof content === 'function' ? content() : content}
</button>
{renderButton()}
</Tooltip>
) : (
renderButton()
);
};
let title = props.title ? (
<div className="toolbar-tooltip-title">{props.title}</div>
) : null;
let hotkey = props.hotkey;
//默认获取插件的热键
if (engine && (hotkey === true || hotkey === undefined)) {
hotkey = autoGetHotkey(
engine,
command && !Array.isArray(command) ? command.name : name,
);
}
if (typeof hotkey === 'string' && hotkey !== '') {
title = (
<>
{title}
<div className="toolbar-tooltip-hotkey">
{formatHotkey(hotkey)}
</div>
</>
);
}
return title && !isMobile ? (
<Tooltip
placement={props.placement || 'bottom'}
title={title}
visible={tooltipVisible}
>
{renderButton()}
</Tooltip>
) : (
renderButton()
);
};
},
);
export default ToolbarButton;

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import classNames from 'classnames-es-ts';
import { $ } from '@aomao/engine';
import type { EngineInterface, Placement } from '@aomao/engine';
import { useRight } from '../hooks';
import Button from '../button';
@ -52,16 +51,10 @@ const ColorButton: React.FC<ColorButtonProps> = ({
),
);
const [currentColor, setCurrentColor] = useState(defaultActiveColor);
const targetRef = useRef<HTMLButtonElement | null>(null);
const buttonRef = useRef<HTMLDivElement | null>(null);
const isRight = useRight(buttonRef);
useEffect(() => {
return () => {
document.removeEventListener('click', hideDropdown);
};
}, []);
useEffect(() => {
setButtonContent(
typeof content === 'string'
@ -76,7 +69,6 @@ const ColorButton: React.FC<ColorButtonProps> = ({
const toggleDropdown = (event: React.MouseEvent) => {
event.preventDefault();
if (pickerVisible) {
hideDropdown();
} else {
@ -84,26 +76,30 @@ const ColorButton: React.FC<ColorButtonProps> = ({
}
};
const showDropdown = () => {
setTimeout(() => {
document.addEventListener('click', hideDropdown);
}, 10);
setPickerVisible(true);
};
useEffect(() => {
if (pickerVisible) document.addEventListener('click', hideDropdown);
return () => {
document.removeEventListener('click', hideDropdown);
};
}, [pickerVisible]);
const hideDropdown = (event?: MouseEvent) => {
const showDropdown = useCallback(() => {
setPickerVisible(true);
}, []);
const hideDropdown = useCallback((event?: MouseEvent) => {
if (
event?.target &&
$(event.target).closest('.toolbar-dropdown-list').length > 0
event &&
targetRef.current &&
targetRef.current.contains(event.target as Node)
)
return;
document.removeEventListener('click', hideDropdown);
setPickerVisible(false);
};
}, []);
const triggerSelect = (color: string, event: React.MouseEvent) => {
setCurrentColor(color);
hideDropdown();
if (autoExecute !== false) {
let commandName = name;
let commandArgs = [color, defaultColor];
@ -153,6 +149,7 @@ const ColorButton: React.FC<ColorButtonProps> = ({
content={<span className="data-icon data-icon-arrow" />}
onClick={toggleDropdown}
placement={placement}
ref={targetRef}
/>
</div>
{pickerVisible && (

View File

@ -3,8 +3,9 @@
}
.toolbar-dropdown .toolbar-dropdown-trigger {
align-items: center;
display: flex;
align-items: stretch;
height: 100%;
}
.toolbar-dropdown .toolbar-dropdown-trigger .toolbar-dropdown-button-text {

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import classnames from 'classnames-es-ts';
import type { EngineInterface, Placement } from '@aomao/engine';
import Button from '../button';
@ -46,6 +46,7 @@ const Dropdown: React.FC<DropdownProps> = ({
const [visible, setVisible] = useState(false);
const buttonRef = useRef<HTMLDivElement | null>(null);
const targetRef = useRef<HTMLButtonElement | null>(null);
const isRight = useRight(buttonRef);
const toggle = (event: React.MouseEvent) => {
@ -61,19 +62,29 @@ const Dropdown: React.FC<DropdownProps> = ({
}
};
useEffect(() => {
if (visible) document.addEventListener('click', hide);
return () => {
document.removeEventListener('click', hide);
};
}, [visible]);
const show = () => {
setTimeout(() => {
document.addEventListener('click', hide);
}, 10);
setVisible(true);
};
const hide = () => {
document.removeEventListener('click', hide);
const hide = useCallback((event?: MouseEvent) => {
if (
event &&
targetRef.current &&
targetRef.current.contains(event.target as Node)
)
return;
setVisible(false);
};
}, []);
const triggerSelect = (event: React.MouseEvent, key: string) => {
hide();
if (onSelect) onSelect(event, key);
};
@ -147,6 +158,7 @@ const Dropdown: React.FC<DropdownProps> = ({
active={visible}
disabled={disabled}
placement={placement}
ref={targetRef}
/>
</div>
{visible && (

View File

@ -1,9 +1,9 @@
.editor-toolbar-group {
padding: 4px 8px;
padding: 4px;
width: auto;
border-left: 1px solid #e8e8e8;
display: flex;
align-items: center;
align-items: stretch;
}
.editor-toolbar .editor-toolbar-group:nth-child(1) {

View File

@ -30,7 +30,6 @@ export default class Popup {
window.addEventListener('resize', this.onSelect);
this.#editor.scrollNode?.on('scroll', this.onSelect);
document.addEventListener('mousedown', this.hide);
this.#editor.on('blur', this.hide);
}
onSelect = () => {
@ -180,7 +179,6 @@ export default class Popup {
window.removeEventListener('resize', this.onSelect);
this.#editor.scrollNode?.off('scroll', this.onSelect);
document.removeEventListener('mousedown', this.hide);
this.#editor.off('blur', this.hide);
}
}
export type { GroupItemProps };