mirror of
https://github.com/flarum/core.git
synced 2025-07-22 17:21:27 +02:00
Editor Driver Abstraction (#2594)
This will allow drop-in replacements of the editor with a more advanced WYSIWYG solution such as ProseMirror
This commit is contained in:
committed by
GitHub
parent
67306a9d34
commit
7d79912d36
5
js/package-lock.json
generated
5
js/package-lock.json
generated
@@ -4474,6 +4474,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"textarea-caret": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q=="
|
||||||
|
},
|
||||||
"through2": {
|
"through2": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
"mithril": "^2.0.4",
|
"mithril": "^2.0.4",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
"spin.js": "^3.1.0",
|
"spin.js": "^3.1.0",
|
||||||
|
"textarea-caret": "^3.1.0",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-merge": "^4.1.4"
|
"webpack-merge": "^4.1.4"
|
||||||
|
@@ -19,7 +19,6 @@ import extract from './utils/extract';
|
|||||||
import ScrollListener from './utils/ScrollListener';
|
import ScrollListener from './utils/ScrollListener';
|
||||||
import stringToColor from './utils/stringToColor';
|
import stringToColor from './utils/stringToColor';
|
||||||
import subclassOf from './utils/subclassOf';
|
import subclassOf from './utils/subclassOf';
|
||||||
import SuperTextarea from './utils/SuperTextarea';
|
|
||||||
import patchMithril from './utils/patchMithril';
|
import patchMithril from './utils/patchMithril';
|
||||||
import proxifyCompat from './utils/proxifyCompat';
|
import proxifyCompat from './utils/proxifyCompat';
|
||||||
import classList from './utils/classList';
|
import classList from './utils/classList';
|
||||||
@@ -92,7 +91,6 @@ export default {
|
|||||||
'utils/stringToColor': stringToColor,
|
'utils/stringToColor': stringToColor,
|
||||||
'utils/Stream': Stream,
|
'utils/Stream': Stream,
|
||||||
'utils/subclassOf': subclassOf,
|
'utils/subclassOf': subclassOf,
|
||||||
'utils/SuperTextarea': SuperTextarea,
|
|
||||||
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
||||||
'utils/patchMithril': patchMithril,
|
'utils/patchMithril': patchMithril,
|
||||||
'utils/proxifyCompat': proxifyCompat,
|
'utils/proxifyCompat': proxifyCompat,
|
||||||
|
@@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* A textarea wrapper with powerful helpers for text manipulation.
|
|
||||||
*
|
|
||||||
* This wraps a <textarea> DOM element and allows directly manipulating its text
|
|
||||||
* contents and cursor positions.
|
|
||||||
*
|
|
||||||
* I apologize for the pretentious name. :)
|
|
||||||
*/
|
|
||||||
export default class SuperTextarea {
|
|
||||||
/**
|
|
||||||
* @param {HTMLTextAreaElement} textarea
|
|
||||||
*/
|
|
||||||
constructor(textarea) {
|
|
||||||
this.el = textarea;
|
|
||||||
this.$ = $(textarea);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value of the text editor.
|
|
||||||
*
|
|
||||||
* @param {String} value
|
|
||||||
*/
|
|
||||||
setValue(value) {
|
|
||||||
this.$.val(value).trigger('input');
|
|
||||||
|
|
||||||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus the textarea and place the cursor at the given index.
|
|
||||||
*
|
|
||||||
* @param {number} position
|
|
||||||
*/
|
|
||||||
moveCursorTo(position) {
|
|
||||||
this.setSelectionRange(position, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the selected range of the textarea.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
getSelectionRange() {
|
|
||||||
return [this.el.selectionStart, this.el.selectionEnd];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the textarea at the position of the cursor.
|
|
||||||
*
|
|
||||||
* @param {String} text
|
|
||||||
*/
|
|
||||||
insertAtCursor(text) {
|
|
||||||
this.insertAt(this.el.selectionStart, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the textarea at the given position.
|
|
||||||
*
|
|
||||||
* @param {number} pos
|
|
||||||
* @param {String} text
|
|
||||||
*/
|
|
||||||
insertAt(pos, text) {
|
|
||||||
this.insertBetween(pos, pos, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert content into the textarea between the given positions.
|
|
||||||
*
|
|
||||||
* If the start and end positions are different, any text between them will be
|
|
||||||
* overwritten.
|
|
||||||
*
|
|
||||||
* @param start
|
|
||||||
* @param end
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
insertBetween(start, end, text) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace existing content from the start to the current cursor position.
|
|
||||||
*
|
|
||||||
* @param start
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
replaceBeforeCursor(start, text) {
|
|
||||||
this.insertBetween(start, this.el.selectionStart, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the selected range of the textarea.
|
|
||||||
*
|
|
||||||
* @param {number} start
|
|
||||||
* @param {number} end
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
setSelectionRange(start, end) {
|
|
||||||
this.el.setSelectionRange(start, end);
|
|
||||||
this.$.focus();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -73,6 +73,7 @@ import DiscussionListItem from './components/DiscussionListItem';
|
|||||||
import LoadingPost from './components/LoadingPost';
|
import LoadingPost from './components/LoadingPost';
|
||||||
import PostsUserPage from './components/PostsUserPage';
|
import PostsUserPage from './components/PostsUserPage';
|
||||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
||||||
|
import BasicEditorDriver from './utils/BasicEditorDriver';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import ForumApplication from './ForumApplication';
|
import ForumApplication from './ForumApplication';
|
||||||
|
|
||||||
@@ -85,6 +86,8 @@ export default Object.assign(compat, {
|
|||||||
'utils/alertEmailConfirmation': alertEmailConfirmation,
|
'utils/alertEmailConfirmation': alertEmailConfirmation,
|
||||||
'utils/UserControls': UserControls,
|
'utils/UserControls': UserControls,
|
||||||
'utils/Pane': Pane,
|
'utils/Pane': Pane,
|
||||||
|
'utils/BasicEditorDriver': BasicEditorDriver,
|
||||||
|
'utils/SuperTextarea': BasicEditorDriver, // @deprecated beta 16, remove beta 17
|
||||||
'states/ComposerState': ComposerState,
|
'states/ComposerState': ComposerState,
|
||||||
'states/DiscussionListState': DiscussionListState,
|
'states/DiscussionListState': DiscussionListState,
|
||||||
'states/GlobalSearchState': GlobalSearchState,
|
'states/GlobalSearchState': GlobalSearchState,
|
||||||
|
@@ -76,13 +76,13 @@ export default class Composer extends Component {
|
|||||||
|
|
||||||
// Whenever any of the inputs inside the composer are have focus, we want to
|
// Whenever any of the inputs inside the composer are have focus, we want to
|
||||||
// add a class to the composer to draw attention to it.
|
// add a class to the composer to draw attention to it.
|
||||||
this.$().on('focus blur', ':input', (e) => {
|
this.$().on('focus blur', ':input,.TextEditor-editorContainer', (e) => {
|
||||||
this.active = e.type === 'focusin';
|
this.active = e.type === 'focusin';
|
||||||
m.redraw();
|
m.redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
// When the escape key is pressed on any inputs, close the composer.
|
// When the escape key is pressed on any inputs, close the composer.
|
||||||
this.$().on('keydown', ':input', 'esc', () => this.state.close());
|
this.$().on('keydown', ':input,.TextEditor-editorContainer', 'esc', () => this.state.close());
|
||||||
|
|
||||||
this.handlers = {};
|
this.handlers = {};
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export default class Composer extends Component {
|
|||||||
* Draw focus to the first focusable content element (the text editor).
|
* Draw focus to the first focusable content element (the text editor).
|
||||||
*/
|
*/
|
||||||
focus() {
|
focus() {
|
||||||
this.$('.Composer-content :input:enabled:visible:first').focus();
|
this.$('.Composer-content :input:enabled:visible, .TextEditor-editor').first().focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import SuperTextarea from '../../common/utils/SuperTextarea';
|
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
|
|
||||||
|
import BasicEditorDriver from '../utils/BasicEditorDriver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `TextEditor` component displays a textarea with controls, including a
|
* The `TextEditor` component displays a textarea with controls, including a
|
||||||
* submit button.
|
* submit button.
|
||||||
@@ -22,25 +23,22 @@ export default class TextEditor extends Component {
|
|||||||
super.oninit(vnode);
|
super.oninit(vnode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the textarea.
|
* The value of the editor.
|
||||||
*
|
*
|
||||||
* @type {String}
|
* @type {String}
|
||||||
*/
|
*/
|
||||||
this.value = this.attrs.value || '';
|
this.value = this.attrs.value || '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is disabled.
|
||||||
|
*/
|
||||||
|
this.disabled = !!this.attrs.disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="TextEditor">
|
<div className="TextEditor">
|
||||||
<textarea
|
<div className="TextEditor-editorContainer"></div>
|
||||||
className="FormControl Composer-flexible"
|
|
||||||
oninput={(e) => {
|
|
||||||
this.oninput(e.target.value, e);
|
|
||||||
}}
|
|
||||||
placeholder={this.attrs.placeholder || ''}
|
|
||||||
disabled={!!this.attrs.disabled}
|
|
||||||
value={this.value}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ul className="TextEditor-controls Composer-footer">
|
<ul className="TextEditor-controls Composer-footer">
|
||||||
{listItems(this.controlItems().toArray())}
|
{listItems(this.controlItems().toArray())}
|
||||||
@@ -53,15 +51,35 @@ export default class TextEditor extends Component {
|
|||||||
oncreate(vnode) {
|
oncreate(vnode) {
|
||||||
super.oncreate(vnode);
|
super.oncreate(vnode);
|
||||||
|
|
||||||
const handler = () => {
|
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();
|
this.onsubmit();
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.$('textarea').bind('keydown', 'meta+return', handler);
|
buildEditor(dom) {
|
||||||
this.$('textarea').bind('keydown', 'ctrl+return', handler);
|
return new BasicEditorDriver(dom, this.buildEditorParams());
|
||||||
|
|
||||||
this.attrs.composer.editor = new SuperTextarea(this.$('textarea')[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,12 +133,10 @@ export default class TextEditor extends Component {
|
|||||||
*
|
*
|
||||||
* @param {String} value
|
* @param {String} value
|
||||||
*/
|
*/
|
||||||
oninput(value, e) {
|
oninput(value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|
||||||
this.attrs.onchange(this.value);
|
this.attrs.onchange(this.value);
|
||||||
|
|
||||||
e.redraw = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import subclassOf from '../../common/utils/subclassOf';
|
import subclassOf from '../../common/utils/subclassOf';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
import ReplyComposer from '../components/ReplyComposer';
|
import ReplyComposer from '../components/ReplyComposer';
|
||||||
|
import EditorDriverInterface from '../utils/EditorDriverInterface';
|
||||||
|
|
||||||
class ComposerState {
|
class ComposerState {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -29,7 +30,7 @@ class ComposerState {
|
|||||||
/**
|
/**
|
||||||
* A reference to the text editor that allows text manipulation.
|
* A reference to the text editor that allows text manipulation.
|
||||||
*
|
*
|
||||||
* @type {SuperTextArea|null}
|
* @type {EditorDriverInterface|null}
|
||||||
*/
|
*/
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
|
|
||||||
@@ -66,12 +67,16 @@ class ComposerState {
|
|||||||
clear() {
|
clear() {
|
||||||
this.position = ComposerState.Position.HIDDEN;
|
this.position = ComposerState.Position.HIDDEN;
|
||||||
this.body = { attrs: {} };
|
this.body = { attrs: {} };
|
||||||
this.editor = null;
|
|
||||||
this.onExit = null;
|
this.onExit = null;
|
||||||
|
|
||||||
this.fields = {
|
this.fields = {
|
||||||
content: Stream(''),
|
content: Stream(''),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy();
|
||||||
|
}
|
||||||
|
this.editor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
124
js/src/forum/utils/BasicEditorDriver.ts
Normal file
124
js/src/forum/utils/BasicEditorDriver.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
105
js/src/forum/utils/EditorDriverInterface.ts
Normal file
105
js/src/forum/utils/EditorDriverInterface.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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;
|
||||||
|
}
|
@@ -293,7 +293,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ComposerBody-editor {
|
.ComposerBody-editor {
|
||||||
.fullScreen & textarea {
|
.fullScreen & .TextEditor-editor {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +323,7 @@
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Text Editor
|
// Text Editor
|
||||||
|
|
||||||
.TextEditor textarea {
|
.TextEditor .TextEditor-editor {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0 0 10px;
|
padding: 0 0 10px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
Reference in New Issue
Block a user