@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
172 lines (152 loc) • 5.2 kB
text/typescript
import { analyticsService } from '../../analytics';
import {
EditorState,
PluginKey,
EditorView,
Schema,
NodeViewDesc,
TextSelection,
Plugin,
Node,
} from '../../prosemirror';
import { panelNodeView } from '../../nodeviews';
import inputRulePlugin from './input-rules';
export interface PanelType {
panelType: 'info' | 'note' | 'tip' | 'warning';
}
export const availablePanelType = [
{ panelType: 'info' },
{ panelType: 'note' },
{ panelType: 'tip' },
{ panelType: 'warning' }
];
export class PanelState {
private state: EditorState<any>;
private activeNode: Node | undefined;
private changeHandlers: PanelStateSubscriber[] = [];
element?: HTMLElement | undefined;
activePanelType?: string | undefined;
toolbarVisible?: boolean | undefined;
editorFocused: boolean = false;
constructor(state: EditorState<any>) {
this.changeHandlers = [];
this.state = state;
this.toolbarVisible = false;
}
updateEditorFocused(editorFocused: boolean) {
this.editorFocused = editorFocused;
}
changePanelType(view: EditorView, panelType: PanelType) {
analyticsService.trackEvent(`atlassian.editor.format.${panelType.panelType}.button`);
const { state, dispatch } = view;
let { tr } = state;
const { panel } = state.schema.nodes;
const { $from, $to } = state.selection;
let newFrom = tr.doc.resolve($from.start($from.depth - 1));
let newTo = tr.doc.resolve($to.end($to.depth - 1));
let range = newFrom.blockRange(newTo)!;
tr.lift(range, $from.depth - 2);
newFrom = tr.doc.resolve(tr.mapping.map(newFrom.pos));
newTo = tr.doc.resolve(tr.mapping.map(newTo.pos));
range = newFrom.blockRange(newTo)!;
tr = tr.wrap(range, [{ type: panel, attrs: panelType }]);
dispatch(tr);
}
removePanel(view: EditorView) {
const { dispatch, state } = view;
let { tr } = state;
let { $from, $to } = state.selection;
let newFrom = tr.doc.resolve($from.start($from.depth - 1));
let newTo = tr.doc.resolve($to.end($to.depth - 1));
let range = newFrom.blockRange(newTo)!;
tr = tr.delete(range!.start - 1, range!.end + 1);
dispatch(tr);
}
subscribe(cb: PanelStateSubscriber) {
this.changeHandlers.push(cb);
cb(this);
}
unsubscribe(cb: PanelStateSubscriber) {
this.changeHandlers = this.changeHandlers.filter(ch => ch !== cb);
}
update(state: EditorState<any>, docView: NodeViewDesc, domEvent: boolean = false) {
this.state = state;
const newPanel = this.getActivePanel(docView);
if ((domEvent && newPanel) || this.activeNode !== newPanel) {
const newElement = newPanel && this.getDomElement(docView);
this.activeNode = newPanel;
this.toolbarVisible = this.editorFocused && !!newPanel && (domEvent || this.element !== newElement);
this.element = newElement;
this.activePanelType = newPanel && newPanel.attrs['panelType'];
this.changeHandlers.forEach(cb => cb(this));
}
}
private getActivePanel(docView: NodeViewDesc): Node | undefined {
const { state } = this;
if (state.selection instanceof TextSelection) {
const { $from } = state.selection;
const node = $from.node($from.depth - 1);
if (node && node.type === state.schema.nodes.panel) {
return node;
}
}
}
private getDomElement(docView: NodeViewDesc): HTMLElement | undefined {
const { state: { selection } } = this;
if (selection instanceof TextSelection) {
const { node } = docView.domFromPos(selection.$from.pos);
let currentNode = node;
while (currentNode) {
if (currentNode.attributes && currentNode.attributes['data-panel-type']) {
return currentNode as HTMLElement;
}
currentNode = currentNode.parentNode!;
}
}
}
}
export type PanelStateSubscriber = (state: PanelState) => any;
export const stateKey = new PluginKey('panelPlugin');
const plugin = new Plugin({
state: {
init(config, state: EditorState<any>) {
return new PanelState(state);
},
apply(tr, pluginState: PanelState, oldState, newState) {
const stored = tr.getMeta(stateKey);
if (stored) {
pluginState.update(newState, stored.docView, stored.domEvent);
}
return pluginState;
}
},
key: stateKey,
view: (view: EditorView) => {
return {
update: (view: EditorView, prevState: EditorState<any>) => {
stateKey.getState(view.state).update(view.state, view.docView);
}
};
},
props: {
nodeViews: {
panel: panelNodeView,
},
handleClick(view: EditorView, event) {
stateKey.getState(view.state).update(view.state, view.docView, true);
return false;
},
onFocus(view: EditorView, event) {
stateKey.getState(view.state).updateEditorFocused(true);
},
onBlur(view: EditorView, event) {
const pluginState = stateKey.getState(view.state);
pluginState.updateEditorFocused(false);
pluginState.update(view.state, view.docView, true);
},
},
});
const plugins = (schema: Schema<any, any>) => {
return [plugin, inputRulePlugin(schema)].filter((plugin) => !!plugin) as Plugin[];
};
export default plugins;