Skip to content

Commit

Permalink
feat: only enable indent, outdent toolbar item in the list (#2294)
Browse files Browse the repository at this point in the history
* wip: enhance toolbar state(only enable `indent`, `outdent` item in list item)

* feat: only enable indent, outdent toolbar item in the list

* chore: fix that toolbar buttons activate in preview mode

* chore: fix broken test case

Co-authored-by: jajugoguma <daeyeon.kim@nhn.com>
  • Loading branch information
js87zz and jajugoguma authored Feb 10, 2022
1 parent f04c7dd commit 30743f6
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 53 deletions.
4 changes: 2 additions & 2 deletions apps/editor/src/__test__/integration/ui/toolbar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,11 +620,11 @@ describe('custom toolbar element', () => {
});

it('should toggle active state properly when toolbar state is changed', () => {
em.emit('changeToolbarState', { toolbarState: { strong: true } });
em.emit('changeToolbarState', { toolbarState: { strong: { active: true } } });

expect(onUpdatedSpy).toHaveBeenCalledWith(expect.objectContaining({ active: true }));

em.emit('changeToolbarState', { toolbarState: { strong: false } });
em.emit('changeToolbarState', { toolbarState: { strong: { active: false } } });

expect(onUpdatedSpy).toHaveBeenCalledWith(expect.objectContaining({ active: false }));
});
Expand Down
49 changes: 42 additions & 7 deletions apps/editor/src/markdown/plugins/previewHighlight.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,75 @@
import { MdNode, MdPos } from '@toast-ui/toastmark';
import { Plugin } from 'prosemirror-state';
import { MdContext } from '@t/spec';
import { ToolbarState, ToolbarStateKeys } from '@t/ui';
import { ToolbarStateMap, ToolbarStateKeys } from '@t/ui';
import { traverseParentNodes, isListNode } from '@/utils/markdown';
import { includes } from '@/utils/common';

const defaultToolbarStateKeys: ToolbarStateKeys[] = [
'taskList',
'orderedList',
'bulletList',
'table',
'strong',
'emph',
'strike',
'heading',
'thematicBreak',
'blockQuote',
'code',
'codeBlock',
'indent',
'outdent',
];

function getToolbarStateType(mdNode: MdNode) {
const { type } = mdNode;

if (isListNode(mdNode)) {
if (mdNode.listData.task) {
return 'taskList';
}
return mdNode.listData.type === 'ordered' ? 'orderedList' : 'bulletList';
}
if (mdNode.type.indexOf('table') !== -1) {

if (type.indexOf('table') !== -1) {
return 'table';
}
return mdNode.type;

if (!includes(defaultToolbarStateKeys, type)) {
return null;
}

return type as ToolbarStateKeys;
}

function getToolbarState(targetNode: MdNode) {
const toolbarState = {} as ToolbarState;
const toolbarState = {
indent: { active: false, disabled: true },
outdent: { active: false, disabled: true },
} as ToolbarStateMap;

let listEnabled = true;

traverseParentNodes(targetNode, (mdNode) => {
const type = getToolbarStateType(mdNode);

if (type === 'customBlock' || type === 'image' || type === 'link') {
if (!type) {
return;
}

if (type === 'bulletList' || type === 'orderedList') {
// to apply the nearlist list state in the nested list
if (listEnabled) {
toolbarState[type] = true;
toolbarState[type] = { active: true };

toolbarState.indent.disabled = false;
toolbarState.outdent.disabled = false;

listEnabled = false;
}
} else {
toolbarState[type as ToolbarStateKeys] = true;
toolbarState[type as ToolbarStateKeys] = { active: true };
}
});

Expand Down
12 changes: 7 additions & 5 deletions apps/editor/src/ui/components/toolbar/buttonHoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SetItemWidth,
ComponentClass,
ToolbarButtonInfo,
ToolbarState,
ToolbarStateMap,
} from '@t/ui';
import { Emitter } from '@t/event';
import html from '@/ui/vdom/template';
Expand All @@ -24,11 +24,12 @@ interface Props {
}

interface Payload {
toolbarState: ToolbarState;
toolbarState: ToolbarStateMap;
}

interface State {
active: boolean;
disabled: boolean;
}

const TOOLTIP_INDENT = 6;
Expand All @@ -37,7 +38,7 @@ export function connectHOC(WrappedComponent: ComponentClass) {
return class ButtonHOC extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { active: false };
this.state = { active: false, disabled: props.disabled };
this.addEvent();
}

Expand All @@ -46,9 +47,9 @@ export function connectHOC(WrappedComponent: ComponentClass) {

if (item.state) {
eventEmitter.listen('changeToolbarState', ({ toolbarState }: Payload) => {
const active = !!toolbarState[item.state!];
const { active, disabled } = toolbarState[item.state!] ?? {};

this.setState({ active });
this.setState({ active: !!active, disabled: disabled ?? this.props.disabled });
});
}
}
Expand Down Expand Up @@ -87,6 +88,7 @@ export function connectHOC(WrappedComponent: ComponentClass) {
showTooltip=${this.showTooltip}
hideTooltip=${this.hideTooltip}
getBound=${this.getBound}
disabled=${this.state.disabled || this.props.disabled}
/>
`;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/editor/src/ui/toolbarItemFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ function createDefaultToolbarItemInfo(type: string) {
className: 'indent',
command: 'indent',
tooltip: i18n.get('Indent'),
state: 'indent',
};
break;
case 'outdent':
Expand All @@ -198,6 +199,7 @@ function createDefaultToolbarItemInfo(type: string) {
className: 'outdent',
command: 'outdent',
tooltip: i18n.get('Outdent'),
state: 'outdent',
};
break;
case 'scrollSync':
Expand Down
39 changes: 22 additions & 17 deletions apps/editor/src/wysiwyg/plugins/toolbarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Plugin, Selection } from 'prosemirror-state';

import { includes } from '@/utils/common';

import { ToolbarState, ToolbarStateKeys } from '@t/ui';
import { ToolbarStateMap, ToolbarStateKeys } from '@t/ui';
import { Emitter } from '@t/event';

type ListType = 'bulletList' | 'orderedList' | 'taskList';
Expand All @@ -26,8 +26,8 @@ function getToolbarStateType(node: Node, parentNode: Node) {
return type;
}

function setListNodeToolbarState(type: ToolbarStateKeys, nodeTypeState: ToolbarState) {
nodeTypeState[type] = true;
function setListNodeToolbarState(type: ToolbarStateKeys, nodeTypeState: ToolbarStateMap) {
nodeTypeState[type] = { active: true };

LIST_TYPES.filter((listName) => listName !== type).forEach((listType) => {
if (nodeTypeState[listType]) {
Expand All @@ -36,26 +36,29 @@ function setListNodeToolbarState(type: ToolbarStateKeys, nodeTypeState: ToolbarS
});
}

function getMarkTypeStates(from: ResolvedPos, to: ResolvedPos, schema: Schema) {
const markTypeState = {} as ToolbarState;

function setMarkTypeStates(
from: ResolvedPos,
to: ResolvedPos,
schema: Schema,
toolbarState: ToolbarStateMap
) {
MARK_TYPES.forEach((type) => {
const mark = schema.marks[type];
const marksAtPos = from.marksAcross(to) || [];
const foundMark = !!mark.isInSet(marksAtPos);

if (foundMark) {
markTypeState[type as ToolbarStateKeys] = true;
toolbarState[type as ToolbarStateKeys] = { active: true };
}
});

return markTypeState;
}

function getToolbarState(selection: Selection, doc: Node, schema: Schema) {
const { $from, $to, from, to } = selection;
const nodeTypeState = {} as ToolbarState;
let markTypeState = {} as ToolbarState;
const toolbarState = {
indent: { active: false, disabled: true },
outdent: { active: false, disabled: true },
} as ToolbarStateMap;

doc.nodesBetween(from, to, (node, _, parentNode) => {
const type = getToolbarStateType(node, parentNode);
Expand All @@ -65,18 +68,20 @@ function getToolbarState(selection: Selection, doc: Node, schema: Schema) {
}

if (includes(LIST_TYPES, type)) {
setListNodeToolbarState(type as ToolbarStateKeys, nodeTypeState);
setListNodeToolbarState(type as ToolbarStateKeys, toolbarState);

toolbarState.indent.disabled = false;
toolbarState.outdent.disabled = false;
} else if (type === 'paragraph' || type === 'text') {
markTypeState = getMarkTypeStates($from, $to, schema);
setMarkTypeStates($from, $to, schema, toolbarState);
} else {
nodeTypeState[type as ToolbarStateKeys] = true;
toolbarState[type as ToolbarStateKeys] = { active: true };
}
});

return { ...nodeTypeState, ...markTypeState };
return toolbarState;
}

export function toolbarState(eventEmitter: Emitter) {
export function toolbarStateHighlight(eventEmitter: Emitter) {
return new Plugin({
view() {
return {
Expand Down
4 changes: 2 additions & 2 deletions apps/editor/src/wysiwyg/wwEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { emitImageBlobHook, pasteImageOnly } from '@/helper/image';
import { tableSelection } from './plugins/selection/tableSelection';
import { tableContextMenu } from './plugins/tableContextMenu';
import { task } from './plugins/task';
import { toolbarState } from './plugins/toolbarState';
import { toolbarStateHighlight } from './plugins/toolbarState';

import { CustomBlockView } from './nodeview/customBlockView';
import { ImageView } from './nodeview/imageView';
Expand Down Expand Up @@ -111,7 +111,7 @@ export default class WysiwygEditor extends EditorBase {
tableSelection(),
tableContextMenu(this.eventEmitter),
task(),
toolbarState(this.eventEmitter),
toolbarStateHighlight(this.eventEmitter),
...this.createPluginProps(),
]);
}
Expand Down
42 changes: 22 additions & 20 deletions apps/editor/types/ui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ export interface ToolbarButtonOptions {
state?: ToolbarStateKeys;
}

interface ToolbarCustomItemState {
active: boolean;
disabled: boolean;
}

export interface ToolbarCustomOptions {
name: string;
tooltip?: string;
Expand All @@ -28,7 +23,7 @@ export interface ToolbarCustomOptions {
hidden?: boolean;
state?: ToolbarStateKeys;
onMounted?: (execCommand: ExecCommand) => void;
onUpdated?: (toolbarState: ToolbarCustomItemState) => void;
onUpdated?: (toolbarState: ToolbarItemState) => void;
}

export type ToolbarButtonInfo = {
Expand Down Expand Up @@ -115,21 +110,28 @@ export interface TabInfo {
text: string;
}

interface ToolbarState {
taskList: boolean;
orderedList: boolean;
bulletList: boolean;
table: boolean;
strong: boolean;
emph: boolean;
strike: boolean;
heading: boolean;
thematicBreak: boolean;
blockQuote: boolean;
code: boolean;
codeBlock: boolean;
interface ToolbarItemState {
active: boolean;
disabled?: boolean;
}

interface ToolbarStateMap {
taskList: ToolbarItemState;
orderedList: ToolbarItemState;
bulletList: ToolbarItemState;
table: ToolbarItemState;
strong: ToolbarItemState;
emph: ToolbarItemState;
strike: ToolbarItemState;
heading: ToolbarItemState;
thematicBreak: ToolbarItemState;
blockQuote: ToolbarItemState;
code: ToolbarItemState;
codeBlock: ToolbarItemState;
indent: ToolbarItemState;
outdent: ToolbarItemState;
}
export type ToolbarStateKeys = keyof ToolbarState;
export type ToolbarStateKeys = keyof ToolbarStateMap;

export type ToolbarItemInfo = ToolbarCustomOptions | ToolbarButtonInfo;
export type ToolbarGroupInfo = ToolbarItemInfo[] & { hidden?: boolean };
Expand Down

0 comments on commit 30743f6

Please sign in to comment.