mirror of
https://github.com/flarum/core.git
synced 2025-10-25 05:36:07 +02:00
Move TextEditor to common (#2649)
This commit is contained in:
committed by
GitHub
parent
08ba2599d7
commit
ddac07d991
@@ -36,8 +36,8 @@ import HeaderSecondary from './components/HeaderSecondary';
|
||||
import ComposerButton from './components/ComposerButton';
|
||||
import DiscussionList from './components/DiscussionList';
|
||||
import ReplyPlaceholder from './components/ReplyPlaceholder';
|
||||
import TextEditor from './components/TextEditor';
|
||||
import TextEditorButton from './components/TextEditorButton';
|
||||
import TextEditor from '../common/components/TextEditor'; // @deprecated beta 16, remove beta 17. Moved to common.
|
||||
import TextEditorButton from '../common/components/TextEditorButton'; // @deprecated beta 16, remove beta 17. Moved to common.
|
||||
import AvatarEditor from './components/AvatarEditor';
|
||||
import Post from './components/Post';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
@@ -73,7 +73,7 @@ import DiscussionListItem from './components/DiscussionListItem';
|
||||
import LoadingPost from './components/LoadingPost';
|
||||
import PostsUserPage from './components/PostsUserPage';
|
||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
||||
import BasicEditorDriver from './utils/BasicEditorDriver';
|
||||
import BasicEditorDriver from '../common/utils/BasicEditorDriver';
|
||||
import routes from './routes';
|
||||
import ForumApplication from './ForumApplication';
|
||||
|
||||
@@ -116,8 +116,8 @@ export default Object.assign(compat, {
|
||||
'components/ComposerButton': ComposerButton,
|
||||
'components/DiscussionList': DiscussionList,
|
||||
'components/ReplyPlaceholder': ReplyPlaceholder,
|
||||
'components/TextEditor': TextEditor,
|
||||
'components/TextEditorButton': TextEditorButton,
|
||||
'components/TextEditor': TextEditor, // @deprecated beta 16, remove beta 17. Moved to common.
|
||||
'components/TextEditorButton': TextEditorButton, // @deprecated beta 16, remove beta 17. Moved to common.
|
||||
'components/AvatarEditor': AvatarEditor,
|
||||
'components/Post': Post,
|
||||
'components/SettingsPage': SettingsPage,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Component from '../../common/Component';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
|
||||
import TextEditor from './TextEditor';
|
||||
import TextEditor from '../../common/components/TextEditor';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import Component from '../../common/Component';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import Button from '../../common/components/Button';
|
||||
|
||||
import BasicEditorDriver from '../utils/BasicEditorDriver';
|
||||
|
||||
/**
|
||||
* The `TextEditor` component displays a textarea with controls, including a
|
||||
* submit button.
|
||||
*
|
||||
* ### Attrs
|
||||
*
|
||||
* - `composer`
|
||||
* - `submitLabel`
|
||||
* - `value`
|
||||
* - `placeholder`
|
||||
* - `disabled`
|
||||
* - `preview`
|
||||
*/
|
||||
export default class TextEditor extends Component {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
/**
|
||||
* The value of the editor.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.value = this.attrs.value || '';
|
||||
|
||||
/**
|
||||
* Whether the editor is disabled.
|
||||
*/
|
||||
this.disabled = !!this.attrs.disabled;
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="TextEditor">
|
||||
<div className="TextEditor-editorContainer"></div>
|
||||
|
||||
<ul className="TextEditor-controls Composer-footer">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
<li className="TextEditor-toolbar">{this.toolbarItems().toArray()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.attrs.composer.editor = this.buildEditor(this.$('.TextEditor-editorContainer')[0]);
|
||||
}
|
||||
|
||||
onupdate() {
|
||||
const newDisabled = !!this.attrs.disabled;
|
||||
|
||||
if (this.disabled !== newDisabled) {
|
||||
this.disabled = newDisabled;
|
||||
this.attrs.composer.editor.disabled(newDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
buildEditorParams() {
|
||||
return {
|
||||
classNames: ['FormControl', 'Composer-flexible', 'TextEditor-editor'],
|
||||
disabled: this.disabled,
|
||||
placeholder: this.attrs.placeholder || '',
|
||||
value: this.value,
|
||||
oninput: this.oninput.bind(this),
|
||||
inputListeners: [],
|
||||
onsubmit: () => {
|
||||
this.onsubmit();
|
||||
m.redraw();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
buildEditor(dom) {
|
||||
return new BasicEditorDriver(dom, this.buildEditorParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the text editor controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add(
|
||||
'submit',
|
||||
Button.component(
|
||||
{
|
||||
icon: 'fas fa-paper-plane',
|
||||
className: 'Button Button--primary',
|
||||
itemClassName: 'App-primaryControl',
|
||||
onclick: this.onsubmit.bind(this),
|
||||
},
|
||||
this.attrs.submitLabel
|
||||
)
|
||||
);
|
||||
|
||||
if (this.attrs.preview) {
|
||||
items.add(
|
||||
'preview',
|
||||
Button.component({
|
||||
icon: 'far fa-eye',
|
||||
className: 'Button Button--icon',
|
||||
onclick: this.attrs.preview,
|
||||
title: app.translator.trans('core.forum.composer.preview_tooltip'),
|
||||
oncreate: (vnode) => $(vnode.dom).tooltip(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the toolbar controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
toolbarItems() {
|
||||
return new ItemList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input into the textarea.
|
||||
*
|
||||
* @param {String} value
|
||||
*/
|
||||
oninput(value) {
|
||||
this.value = value;
|
||||
|
||||
this.attrs.onchange(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submit button being clicked.
|
||||
*/
|
||||
onsubmit() {
|
||||
this.attrs.onsubmit(this.value);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Button from '../../common/components/Button';
|
||||
|
||||
/**
|
||||
* The `TextEditorButton` component displays a button suitable for the text
|
||||
* editor toolbar.
|
||||
*/
|
||||
export default class TextEditorButton extends Button {
|
||||
static initAttrs(attrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = attrs.className || 'Button Button--icon Button--link';
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.$().tooltip();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import subclassOf from '../../common/utils/subclassOf';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import ReplyComposer from '../components/ReplyComposer';
|
||||
import EditorDriverInterface from '../utils/EditorDriverInterface';
|
||||
import EditorDriverInterface from '../../common/utils/EditorDriverInterface';
|
||||
|
||||
class ComposerState {
|
||||
constructor() {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import getCaretCoordinates from 'textarea-caret';
|
||||
import EditorDriverInterface, { EditorDriverParams } from './EditorDriverInterface';
|
||||
|
||||
export default class BasicEditorDriver implements EditorDriverInterface {
|
||||
el: HTMLTextAreaElement;
|
||||
|
||||
constructor(dom: HTMLElement, params: EditorDriverParams) {
|
||||
this.el = document.createElement('textarea');
|
||||
|
||||
this.build(dom, params);
|
||||
}
|
||||
|
||||
build(dom: HTMLElement, params: EditorDriverParams) {
|
||||
this.el.className = params.classNames.join(' ');
|
||||
this.el.disabled = params.disabled;
|
||||
this.el.placeholder = params.placeholder;
|
||||
this.el.value = params.value;
|
||||
|
||||
const callInputListeners = (e) => {
|
||||
params.inputListeners.forEach((listener) => {
|
||||
listener();
|
||||
});
|
||||
|
||||
e.redraw = false;
|
||||
};
|
||||
|
||||
this.el.oninput = (e) => {
|
||||
params.oninput(this.el.value);
|
||||
callInputListeners(e);
|
||||
};
|
||||
|
||||
this.el.onclick = callInputListeners;
|
||||
this.el.onkeyup = callInputListeners;
|
||||
|
||||
this.el.addEventListener('keydown', function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
params.onsubmit();
|
||||
}
|
||||
});
|
||||
|
||||
dom.append(this.el);
|
||||
}
|
||||
|
||||
protected setValue(value: string) {
|
||||
$(this.el).val(value).trigger('input');
|
||||
|
||||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
moveCursorTo(position: number) {
|
||||
this.setSelectionRange(position, position);
|
||||
}
|
||||
|
||||
getSelectionRange(): Array<number> {
|
||||
return [this.el.selectionStart, this.el.selectionEnd];
|
||||
}
|
||||
|
||||
getLastNChars(n: number): string {
|
||||
const value = this.el.value;
|
||||
|
||||
return value.slice(Math.max(0, this.el.selectionStart - n), this.el.selectionStart);
|
||||
}
|
||||
|
||||
insertAtCursor(text: string) {
|
||||
this.insertAt(this.el.selectionStart, text);
|
||||
}
|
||||
|
||||
insertAt(pos: number, text: string) {
|
||||
this.insertBetween(pos, pos, text);
|
||||
}
|
||||
|
||||
insertBetween(start: number, end: number, text: string) {
|
||||
const value = this.el.value;
|
||||
|
||||
const before = value.slice(0, start);
|
||||
const after = value.slice(end);
|
||||
|
||||
this.setValue(`${before}${text}${after}`);
|
||||
|
||||
// Move the textarea cursor to the end of the content we just inserted.
|
||||
this.moveCursorTo(start + text.length);
|
||||
}
|
||||
|
||||
replaceBeforeCursor(start: number, text: string) {
|
||||
this.insertBetween(start, this.el.selectionStart, text);
|
||||
}
|
||||
|
||||
protected setSelectionRange(start: number, end: number) {
|
||||
this.el.setSelectionRange(start, end);
|
||||
this.focus();
|
||||
}
|
||||
|
||||
getCaretCoordinates(position: number) {
|
||||
const relCoords = getCaretCoordinates(this.el, position);
|
||||
|
||||
return {
|
||||
top: relCoords.top - this.el.scrollTop,
|
||||
left: relCoords.left,
|
||||
};
|
||||
}
|
||||
|
||||
// DOM Interactions
|
||||
|
||||
/**
|
||||
* Set the disabled status of the editor.
|
||||
*/
|
||||
disabled(disabled: boolean) {
|
||||
this.el.disabled = disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus on the editor.
|
||||
*/
|
||||
focus() {
|
||||
this.el.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the editor
|
||||
*/
|
||||
destroy() {
|
||||
this.el.remove();
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
export interface EditorDriverParams {
|
||||
/**
|
||||
* An array of HTML class names to apply to the editor's main DOM element.
|
||||
*/
|
||||
classNames: string[];
|
||||
|
||||
/**
|
||||
* Whether the editor should be initially disabled.
|
||||
*/
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
* An optional placeholder for the editor.
|
||||
*/
|
||||
placeholder: string;
|
||||
|
||||
/**
|
||||
* An optional initial value for the editor.
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* This is separate from inputListeners since the full serialized content will be passed to it.
|
||||
* It is considered private API, and should not be used/modified by extensions not implementing
|
||||
* EditorDriverInterface.
|
||||
*/
|
||||
oninput: Function;
|
||||
|
||||
/**
|
||||
* Each of these functions will be called on click, input, and keyup.
|
||||
* No arguments will be passed.
|
||||
*/
|
||||
inputListeners: Function[];
|
||||
|
||||
/**
|
||||
* This function will be called if submission is triggered programmatically via keybind.
|
||||
* No arguments should be passed.
|
||||
*/
|
||||
onsubmit: Function;
|
||||
}
|
||||
|
||||
export default interface EditorDriverInterface {
|
||||
/**
|
||||
* Focus the editor and place the cursor at the given position.
|
||||
*/
|
||||
moveCursorTo(position: number): void;
|
||||
|
||||
/**
|
||||
* Get the selected range of the editor.
|
||||
*/
|
||||
getSelectionRange(): Array<number>;
|
||||
|
||||
/**
|
||||
* Get the last N characters from the current "text block".
|
||||
*
|
||||
* A textarea-based driver would just return the last N characters,
|
||||
* but more advanced implementations might restrict to the current block.
|
||||
*
|
||||
* This is useful for monitoring recent user input to trigger autocomplete.
|
||||
*/
|
||||
getLastNChars(n: number): string;
|
||||
|
||||
/**
|
||||
* Insert content into the editor at the position of the cursor.
|
||||
*/
|
||||
insertAtCursor(text: string, escape: boolean): void;
|
||||
|
||||
/**
|
||||
* Insert content into the editor at the given position.
|
||||
*/
|
||||
insertAt(pos: number, text: string, escape: boolean): void;
|
||||
|
||||
/**
|
||||
* Insert content into the editor between the given positions.
|
||||
*
|
||||
* If the start and end positions are different, any text between them will be
|
||||
* overwritten.
|
||||
*/
|
||||
insertBetween(start: number, end: number, text: string, escape: boolean): void;
|
||||
|
||||
/**
|
||||
* Replace existing content from the start to the current cursor position.
|
||||
*/
|
||||
replaceBeforeCursor(start: number, text: string, escape: boolean): void;
|
||||
|
||||
/**
|
||||
* Get left and top coordinates of the caret relative to the editor viewport.
|
||||
*/
|
||||
getCaretCoordinates(position: number): { left: number; top: number };
|
||||
|
||||
/**
|
||||
* Set the disabled status of the editor.
|
||||
*/
|
||||
disabled(disabled: boolean): void;
|
||||
|
||||
/**
|
||||
* Focus on the editor.
|
||||
*/
|
||||
focus(): void;
|
||||
|
||||
/**
|
||||
* Destroy the editor
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user