Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.

ActionBar #376

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ target.innerHTML = template;

const editor = new BasicEditor({ editable: target });
editor.load(FontAwesome);
// editor.load(DevTools);
editor.load(DevTools);
editor.configure(DomLayout, {
location: [target, 'replace'],
components: [
Expand Down
1 change: 1 addition & 0 deletions examples/demo/layout.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<jw-editor>
<t-range><t t-zone="tools"/></t-range>
<jw-header><t t-zone="tools"/></jw-header>
<t-actionbar/>
<jw-body><t t-zone="main"/></jw-body>
<jw-footer>
<t t-zone="status"/>
Expand Down
17 changes: 17 additions & 0 deletions packages/bundle-basic-editor/BasicEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { FontSize } from '../plugin-font-size/src/FontSize';
import { Button } from '../plugin-button/src/Button';
import { HorizontalRule } from '../plugin-horizontal-rule/src/HorizontalRule';
import { Strikethrough } from '../plugin-strikethrough/src/Strikethrough';
import { ActionBar } from '../plugin-commandbar/src/ActionBar';

export class BasicEditor extends JWEditor {
constructor(params?: { editable?: HTMLElement }) {
Expand Down Expand Up @@ -163,5 +164,21 @@ export class BasicEditor extends JWEditor {
['UndoButton', 'RedoButton'],
],
});
this.configure(ActionBar, {
actions: [
{
name: 'action1',
},
{
name: 'action2',
},
{
name: 'foo',
},
{
name: 'bar',
},
],
});
}
}
131 changes: 131 additions & 0 deletions packages/plugin-commandbar/src/ActionBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { JWPluginConfig, JWPlugin } from '../../core/src/JWPlugin';
import { Parser } from '../../plugin-parser/src/Parser';
import { Loadables } from '../../core/src/JWEditor';
import { Renderer } from '../../plugin-renderer/src/Renderer';
import { InsertTextParams } from '../../plugin-char/src/Char';
import { CharNode } from '../../plugin-char/src/CharNode';
import { VNode } from '../../core/src/VNodes/VNode';
import JWEditor from '../../core/src/JWEditor';
import { ReactiveValue } from '../../utils/src/ReactiveValue';
import { ActionBarXmlDomParser } from './ActionBarXmlDomParser';
import { ActionBarDomObjectRenderer } from './ActionBarDomObjectRenderer';
import { CommandParams } from '../../core/src/Dispatcher';
import { setSelection } from '../../utils/src/testUtils';
import { MarkerNode } from '../../core/src/VNodes/MarkerNode';

export interface ActionItem {
name: string;
}

interface CommandBarConfig extends JWPluginConfig {
// /**
// * Take a function that will receive a filtering term and should return true
// * if it has at least one match.
// */
// filter: (term: string) => boolean;
// /**
// * Callback to be executed to open the external command bar.
// */
// open: () => void;
// /**
// * Callback to be executed to close the external command bar.
// */
// close: () => void;
actions?: ActionItem[];
}
// export class Color<T extends ColorConfig = ColorConfig> extends JWPlugin<T> {

export class ActionBar<T extends CommandBarConfig = CommandBarConfig> extends JWPlugin<T> {
// static dependencies = [Inline];
readonly loadables: Loadables<Parser & Renderer> = {
parsers: [ActionBarXmlDomParser],
renderers: [ActionBarDomObjectRenderer],
};
commands = {
// insertText: {
// handler: this.insertText,
// },
};
commandHooks = {
insertText: this._insertText.bind(this),
deleteWord: this._check.bind(this),
deleteBackward: this._check.bind(this),
deleteForward: this._check.bind(this),
setSelection: this._check.bind(this),
};
availableActions = new ReactiveValue<ActionItem[]>([]);
private _opened = false;
private _actions: T['actions'];
private _initialNode: MarkerNode;
private _lastNode: MarkerNode;

constructor(public editor: JWEditor, public configuration: Partial<T> = {}) {
super(editor, configuration);
if (!configuration.actions) {
throw new Error(
'Impossible to load the ActionBar without actions in the configuration.',
);
}
this._actions = configuration.actions;
}

_insertText(params: InsertTextParams): void {
if (this._opened) {
this._check(params);
} else {
const range = params.context.range;
const beforeStart = range.start.previousSibling();
const isSlash = beforeStart instanceof CharNode && beforeStart.char === '/';
// If there is no previous sibling before start, it means beforeStart is
// the first node of the container.
if (range.isCollapsed() && isSlash) {
console.log('open');
const firstMarker = new MarkerNode();
beforeStart.parent.insertBefore(firstMarker, beforeStart);
this._initialNode = firstMarker;
const lastMarker = new MarkerNode();
beforeStart.parent.insertAfter(lastMarker, beforeStart);
this._lastNode = lastMarker;
this._opened = true;
this.availableActions.set(this._actions);
}
}
}

private _check(params: CommandParams): void {
if (this._opened) {
console.warn('check');
// const range = this.editor.selection.range;

const chars: string[] = [];
let index = this._initialNode.parent.childVNodes.indexOf(this._initialNode) + 1;
const allNodes = this._initialNode.parent.childVNodes;
let node: VNode;
while ((node = allNodes[index])) {
if (node instanceof CharNode) chars.push(node.char);
index++;
}
const term = chars.slice(1).join('');
const termFuzzyRegex = term.split('').join('.*');

const actions = this._actions.filter(a => a.name.match(termFuzzyRegex));

if (!actions.length) {
this.close();
console.log('close');
} else {
this.availableActions.set(actions);
}
}
}

close(): void {
this._opened = false;
this._initialNode.remove();
this._lastNode.remove();
this.availableActions.set([]);
}
// _openCommandBar(): void {
// this._opened = true;
// }
}
84 changes: 84 additions & 0 deletions packages/plugin-commandbar/src/ActionBarDomObjectRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
DomObjectRenderingEngine,
DomObject,
} from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine';
import { NodeRenderer } from '../../plugin-renderer/src/NodeRenderer';
import { Predicate } from '../../core/src/VNodes/VNode';
import { ActionBarNode } from './ActionBarNode';
import { ActionBar, ActionItem } from './ActionBar';
import { Color } from '../../plugin-color/src/Color';
import { RenderingEngine } from '../../plugin-renderer/src/RenderingEngine';
import { ReactiveValue } from '../../utils/src/ReactiveValue';
import { Positionable, PositionableVerticalAlignment } from '../../utils/src/Positionable';
import { DomLayout } from '../../plugin-dom-layout/src/DomLayout';
import { Layout } from '../../plugin-layout/src/Layout';
import { DomLayoutEngine } from '../../plugin-dom-layout/src/DomLayoutEngine';

export class ActionBarDomObjectRenderer extends NodeRenderer<DomObject> {
static id = DomObjectRenderingEngine.id;
engine: DomObjectRenderingEngine;
predicate: Predicate = ActionBarNode;
availableActions: ReactiveValue<ActionItem[]>;

constructor(engine: RenderingEngine<DomObject>) {
super(engine);
this.engine.editor.plugins.get(Color);
this.availableActions = this.engine.editor.plugins.get(ActionBar).availableActions;
}

async render(): Promise<DomObject> {
const actionBar = document.createElement('jw-actionbar');
let positionable: Positionable;

const updateActions = (): void => {
const container = this.engine.editor.selection.range.startContainer;
const layout = this.engine.editor.plugins.get(Layout);
const domLayoutEngine = layout.engines.dom as DomLayoutEngine;
const selectedDomNode = domLayoutEngine.getDomNodes(container)[0] as HTMLElement;

console.log('selectedDomNode:', selectedDomNode);

positionable.resetRelativeElement(selectedDomNode);
positionable.resetPositionedElement();

const actionItems = this.availableActions.get();
if (actionItems.length) {
actionBar.style.display = 'block';
actionBar.innerHTML = '';
for (const node of this._renderActionNode(actionItems)) {
actionBar.appendChild(node);
}
} else {
actionBar.style.display = 'none';
}
};
const attach = (): void => {
console.log('attach');
positionable = new Positionable({
positionedElement: actionBar,
verticalAlignment: PositionableVerticalAlignment.BOTTOM,
});
positionable.bind();
this.availableActions.on('set', updateActions);
setTimeout(() => updateActions(), 1000);
};
const detach = (): void => {
console.log('detach');
positionable.destroy();
this.availableActions.off('set', updateActions);
};
return { dom: [actionBar], attach, detach };
}

_positionNode() {}

_renderActionNode(actionItems: ActionItem[]): HTMLElement[] {
return actionItems.map(item => {
const element = document.createElement('jw-action-item');
element.innerHTML = `
<jw-action-name>${item.name}</jw-action-name>
`;
return element;
});
}
}
15 changes: 15 additions & 0 deletions packages/plugin-commandbar/src/ActionBarNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AtomicNode } from '../../core/src/VNodes/AtomicNode';
import { ReactiveValue } from '../../utils/src/ReactiveValue';
import { ActionItem } from './ActionBar';

// interface ActionBarNodeParams {
// actionsItems: ReactiveValue<ActionItem[]>;
// }

export class ActionBarNode extends AtomicNode {
// actionsItems: ActionBarNodeParams['actionsItems'];
// constructor(params: ActionBarNodeParams) {
// super();
// this.actionsItems = params.actionsItems;
// }
}
19 changes: 19 additions & 0 deletions packages/plugin-commandbar/src/ActionBarXmlDomParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { removeFormattingSpace } from '../../utils/src/formattingSpace';
import { AbstractParser } from '../../plugin-parser/src/AbstractParser';
import { XmlDomParsingEngine } from '../../plugin-xml/src/XmlDomParsingEngine';
import { VNode } from '../../core/src/VNodes/VNode';
import { nodeName } from '../../utils/src/utils';
import { ActionBarNode } from './ActionBarNode';

export class ActionBarXmlDomParser extends AbstractParser<Node> {
static id = XmlDomParsingEngine.id;
engine: XmlDomParsingEngine;

predicate = (item: Node): boolean => {
return item instanceof Element && nodeName(item) === 'T-ACTIONBAR';
};

async parse(item: Node): Promise<VNode[]> {
return [new ActionBarNode()];
}
}
Empty file.
62 changes: 62 additions & 0 deletions packages/utils/src/EventMixin2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Abstract class to add event mechanism.
*/
export class EventMixin2 {
_eventCallbacks: Record<string, Function[]>;
_callbackWorking: Set<Function>;

/**
* Subscribe to an event with a callback.
*
* @param eventName
* @param callback
*/
on(eventName: string, callback: Function): void {
if (!this._eventCallbacks) {
this._eventCallbacks = {};
}
if (!this._eventCallbacks[eventName]) {
this._eventCallbacks[eventName] = [];
}
this._eventCallbacks[eventName].push(callback);
}

/**
* Stop listening to an event for a specific callback. If no callback is
* provided, remove stop listening all event `eventName`.
*
* @param eventName
* @param callback
*/
off(eventName: string, callback?: Function): void {
if (callback) {
const index = this._eventCallbacks[eventName].indexOf(callback);
if (index !== -1) {
this._eventCallbacks[eventName].splice(index, 1);
}
} else {
this._eventCallbacks[eventName].splice(0);
}
}

/**
* Fire an event for of this object and all ancestors.
*
* @param eventName
* @param args
*/
trigger<A>(eventName: string, args?: A): void {
if (this._eventCallbacks?.[eventName]) {
if (!this._callbackWorking) {
this._callbackWorking = new Set();
}
for (const callback of this._eventCallbacks[eventName]) {
if (!this._callbackWorking.has(callback)) {
this._callbackWorking.add(callback);
callback(args);
this._callbackWorking.delete(callback);
}
}
}
}
}
Loading