mirror of
https://github.com/flarum/framework.git
synced 2025-06-23 02:51:22 +08:00
Editor Consolidation (#28)
- Move insertText to core - Move styles and apply to core - Simplify shortcut and button system - Drop mdarea for now. In the future, we could implement the features we use (list continue, indent) in core. - Remove admin dist (only admin setting was mdarea) - Move inline style to css
This commit is contained in:

committed by
GitHub

parent
4ec8b87cdc
commit
7ea9db7426
@ -11,9 +11,6 @@ use Flarum\Extend;
|
|||||||
use s9e\TextFormatter\Configurator;
|
use s9e\TextFormatter\Configurator;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(new Extend\Frontend('admin'))
|
|
||||||
->js(__DIR__.'/js/dist/admin.js'),
|
|
||||||
|
|
||||||
(new Extend\Frontend('forum'))
|
(new Extend\Frontend('forum'))
|
||||||
->js(__DIR__.'/js/dist/forum.js')
|
->js(__DIR__.'/js/dist/forum.js')
|
||||||
->css(__DIR__.'/less/forum.less'),
|
->css(__DIR__.'/less/forum.less'),
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from './src/admin';
|
|
2
extensions/markdown/js/dist/admin.js
vendored
2
extensions/markdown/js/dist/admin.js
vendored
@ -1,2 +0,0 @@
|
|||||||
module.exports=function(e){var r={};function t(n){if(r[n])return r[n].exports;var a=r[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,t),a.l=!0,a.exports}return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var a in e)t.d(n,a,function(r){return e[r]}.bind(null,a));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=8)}({2:function(e,r){e.exports=flarum.core.compat.app},8:function(e,r,t){"use strict";t.r(r);var n=t(2),a=t.n(n);a.a.initializers.add("flarum-markdown",(function(){a.a.extensionData.for("flarum-markdown").registerSetting({setting:"flarum-markdown.mdarea",type:"boolean",help:a.a.translator.trans("flarum-markdown.admin.settings.mdarea_help"),label:a.a.translator.trans("flarum-markdown.admin.settings.mdarea_label")})}))}});
|
|
||||||
//# sourceMappingURL=admin.js.map
|
|
1
extensions/markdown/js/dist/admin.js.map
vendored
1
extensions/markdown/js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,12 +0,0 @@
|
|||||||
import app from 'flarum/app';
|
|
||||||
|
|
||||||
app.initializers.add('flarum-markdown', () => {
|
|
||||||
app.extensionData
|
|
||||||
.for('flarum-markdown')
|
|
||||||
.registerSetting({
|
|
||||||
setting: 'flarum-markdown.mdarea',
|
|
||||||
type: 'boolean',
|
|
||||||
help: app.translator.trans('flarum-markdown.admin.settings.mdarea_help'),
|
|
||||||
label: app.translator.trans('flarum-markdown.admin.settings.mdarea_label')
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,8 +1,5 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from 'flarum/Component';
|
||||||
import icon from 'flarum/helpers/icon';
|
import icon from 'flarum/helpers/icon';
|
||||||
import apply from '../util/apply';
|
|
||||||
|
|
||||||
const modifierKey = navigator.userAgent.match(/Macintosh/) ? '⌘' : 'ctrl';
|
|
||||||
|
|
||||||
export default class MarkdownButton extends Component {
|
export default class MarkdownButton extends Component {
|
||||||
oncreate(vnode) {
|
oncreate(vnode) {
|
||||||
@ -13,8 +10,8 @@ export default class MarkdownButton extends Component {
|
|||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<button className="Button Button--icon Button--link" title={this.title()} data-hotkey={this.attrs.hotkey}
|
<button className="Button Button--icon Button--link" title={this.attrs.title} data-hotkey={this.attrs.hotkey}
|
||||||
onclick={this.click.bind(this)} onkeydown={this.keydown.bind(this)}>
|
onkeydown={this.keydown.bind(this)} onclick={this.attrs.onclick}>
|
||||||
{icon(this.attrs.icon)}
|
{icon(this.attrs.icon)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@ -23,19 +20,7 @@ export default class MarkdownButton extends Component {
|
|||||||
keydown(event) {
|
keydown(event) {
|
||||||
if (event.key === ' ' || event.key === 'Enter') {
|
if (event.key === ' ' || event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.click();
|
this.element.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
click() {
|
|
||||||
return apply(this.element, this.attrs.style);
|
|
||||||
}
|
|
||||||
|
|
||||||
title() {
|
|
||||||
let tooltip = this.attrs.title;
|
|
||||||
|
|
||||||
if (this.attrs.hotkey) tooltip += ` <${modifierKey}-${this.attrs.hotkey}>`;
|
|
||||||
|
|
||||||
return tooltip;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,9 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from 'flarum/Component';
|
||||||
|
|
||||||
const modifierKey = navigator.userAgent.match(/Macintosh/) ? 'Meta' : 'Control';
|
|
||||||
|
|
||||||
export default class MarkdownToolbar extends Component {
|
export default class MarkdownToolbar extends Component {
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.attrs.setShortcutHandler(this.shortcut.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode) {
|
||||||
return <div id="MarkdownToolbar" data-for={this.attrs.for} style={{ display: 'inline-block' }}>
|
return <div class="MarkdownToolbar">
|
||||||
{vnode.children}
|
{vnode.children}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcut(event) {
|
|
||||||
if ((event.metaKey && modifierKey === 'Meta') || (event.ctrlKey && modifierKey === 'Control')) {
|
|
||||||
const button = this.element.querySelector(`[data-hotkey="${event.key}"]`);
|
|
||||||
|
|
||||||
if (button) {
|
|
||||||
button.click();
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,45 +7,68 @@
|
|||||||
* https://github.com/github/markdown-toolbar-element/blob/master/LICENSE
|
* https://github.com/github/markdown-toolbar-element/blob/master/LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extend, override } from 'flarum/extend';
|
import { extend } from 'flarum/extend';
|
||||||
import TextEditor from 'flarum/components/TextEditor';
|
import TextEditor from 'flarum/components/TextEditor';
|
||||||
|
import BasicEditorDriver from 'flarum/common/utils/BasicEditorDriver';
|
||||||
|
import styleSelectedText from 'flarum/common/utils/styleSelectedText';
|
||||||
|
|
||||||
import MarkdownToolbar from './components/MarkdownToolbar';
|
import MarkdownToolbar from './components/MarkdownToolbar';
|
||||||
import MarkdownButton from './components/MarkdownButton';
|
import MarkdownButton from './components/MarkdownButton';
|
||||||
import MarkdownEditorDriver from './util/MarkdownEditorDriver';
|
|
||||||
|
|
||||||
let shortcutHandler = () => { };
|
const modifierKey = navigator.userAgent.match(/Macintosh/) ? '⌘' : 'ctrl';
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
'header': { prefix: '### ' },
|
||||||
|
'bold': { prefix: '**', suffix: '**', trimFirst: true },
|
||||||
|
'italic': { prefix: '_', suffix: '_', trimFirst: true },
|
||||||
|
'quote': { prefix: '> ', multiline: true, surroundWithNewlines: true },
|
||||||
|
'code': { prefix: '`', suffix: '`', blockPrefix: '```', blockSuffix: '```' },
|
||||||
|
'link': { prefix: '[', suffix: '](https://)', replaceNext: 'https://', scanFor: 'https?://' },
|
||||||
|
'image': { prefix: '', replaceNext: 'https://', scanFor: 'https?://' },
|
||||||
|
'unordered_list': { prefix: '- ', multiline: true, surroundWithNewlines: true },
|
||||||
|
'ordered_list': { prefix: '1. ', multiline: true, orderedList: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyStyle = (id) => {
|
||||||
|
// This is a nasty hack that breaks encapsulation of the editor.
|
||||||
|
// In future releases, we'll need to tweak the editor driver interface
|
||||||
|
// to support triggering events like this.
|
||||||
|
styleSelectedText(app.composer.editor.el, styles[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeShortcut(id, key) {
|
||||||
|
return function (e) {
|
||||||
|
if (e.key === key && (e.metaKey && modifierKey === '⌘' || e.ctrlKey && modifierKey === 'ctrl')) {
|
||||||
|
applyStyle(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.initializers.add('flarum-markdown', function (app) {
|
app.initializers.add('flarum-markdown', function (app) {
|
||||||
let index = 1;
|
extend(BasicEditorDriver.prototype, 'keyHandlers', function (items) {
|
||||||
|
items.add('bold', makeShortcut('bold', 'b'));
|
||||||
extend(TextEditor.prototype, 'oninit', function () {
|
items.add('italic', makeShortcut('italic', 'i'));
|
||||||
this.textareaId = 'textarea' + (index++);
|
|
||||||
});
|
|
||||||
|
|
||||||
override(TextEditor.prototype, 'buildEditor', function (_, dom) {
|
|
||||||
return new MarkdownEditorDriver(dom, this.buildEditorParams());
|
|
||||||
});
|
|
||||||
|
|
||||||
extend(TextEditor.prototype, 'buildEditorParams', function (params) {
|
|
||||||
params.textareaId = this.textareaId;
|
|
||||||
params.shortcutHandler = shortcutHandler;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
extend(TextEditor.prototype, 'toolbarItems', function (items) {
|
extend(TextEditor.prototype, 'toolbarItems', function (items) {
|
||||||
const tooltip = name => app.translator.trans(`flarum-markdown.forum.composer.${name}_tooltip`);
|
const tooltip = (name, hotkey) => {
|
||||||
|
return app.translator.trans(`flarum-markdown.forum.composer.${name}_tooltip`) + (hotkey ? ` <${modifierKey}-${hotkey}>` : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeApplyStyle = (id) => {
|
||||||
|
return () => applyStyle(id);
|
||||||
|
}
|
||||||
|
|
||||||
items.add('markdown', (
|
items.add('markdown', (
|
||||||
<MarkdownToolbar for={this.textareaId} setShortcutHandler={handler => shortcutHandler = handler}>
|
<MarkdownToolbar for={this.textareaId} setShortcutHandler={handler => shortcutHandler = handler}>
|
||||||
<MarkdownButton title={tooltip('header')} icon="fas fa-heading" style={{ prefix: '### ' }} />
|
<MarkdownButton title={tooltip('header')} icon="fas fa-heading" onclick={makeApplyStyle('header')} />
|
||||||
<MarkdownButton title={tooltip('bold')} icon="fas fa-bold" style={{ prefix: '**', suffix: '**', trimFirst: true }} hotkey="b" />
|
<MarkdownButton title={tooltip('bold', 'b')} icon="fas fa-bold" onclick={makeApplyStyle('bold')} />
|
||||||
<MarkdownButton title={tooltip('italic')} icon="fas fa-italic" style={{ prefix: '_', suffix: '_', trimFirst: true }} hotkey="i" />
|
<MarkdownButton title={tooltip('italic', 'i')} icon="fas fa-italic" onclick={makeApplyStyle('italic')} />
|
||||||
<MarkdownButton title={tooltip('quote')} icon="fas fa-quote-left" style={{ prefix: '> ', multiline: true, surroundWithNewlines: true }} />
|
<MarkdownButton title={tooltip('quote')} icon="fas fa-quote-left" onclick={makeApplyStyle('quote')} />
|
||||||
<MarkdownButton title={tooltip('code')} icon="fas fa-code" style={{ prefix: '`', suffix: '`', blockPrefix: '```', blockSuffix: '```' }} />
|
<MarkdownButton title={tooltip('code')} icon="fas fa-code" onclick={makeApplyStyle('code')} />
|
||||||
<MarkdownButton title={tooltip('link')} icon="fas fa-link" style={{ prefix: '[', suffix: '](https://)', replaceNext: 'https://', scanFor: 'https?://' }} />
|
<MarkdownButton title={tooltip('image')} icon="fas fa-image" onclick={makeApplyStyle('image')} />
|
||||||
<MarkdownButton title={tooltip('image')} icon="fas fa-image" style={{ prefix: '', replaceNext: 'https://', scanFor: 'https?://' }} />
|
<MarkdownButton title={tooltip('unordered_list')} icon="fas fa-list-ul" onclick={makeApplyStyle('unordered_list')} />
|
||||||
<MarkdownButton title={tooltip('unordered_list')} icon="fas fa-list-ul" style={{ prefix: '- ', multiline: true, surroundWithNewlines: true }} />
|
<MarkdownButton title={tooltip('ordered_list')} icon="fas fa-list-ol" onclick={makeApplyStyle('ordered_list')} />
|
||||||
<MarkdownButton title={tooltip('ordered_list')} icon="fas fa-list-ol" style={{ prefix: '1. ', multiline: true, orderedList: true }} />
|
|
||||||
</MarkdownToolbar>
|
</MarkdownToolbar>
|
||||||
), 100);
|
), 100);
|
||||||
});
|
});
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
import MarkdownArea from 'mdarea';
|
|
||||||
import BasicEditorDriver from 'flarum/utils/BasicEditorDriver';
|
|
||||||
|
|
||||||
export class MarkdownEditorFlarumExtension {
|
|
||||||
constructor(oninput, callInputListeners, onsubmit) {
|
|
||||||
this.oninput = oninput;
|
|
||||||
this.callInputListeners = callInputListeners;
|
|
||||||
this.onsubmit = onsubmit;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKey(
|
|
||||||
prefix,
|
|
||||||
selection,
|
|
||||||
postfix,
|
|
||||||
evt
|
|
||||||
) {
|
|
||||||
// setTimeout executes after the call stack has cleared,
|
|
||||||
// so any DOM changes originating from mdarea (e.g. executing an undo)
|
|
||||||
// will be finished by then. At that time, `e.target.value` will represent
|
|
||||||
// the updated value of the textarea in response to the keypress.
|
|
||||||
// Unfortunately, this doesn't work without a value for mobile safari,
|
|
||||||
// so we need to set 25ms as an arbitrary timeout.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.oninput(evt.target.value);
|
|
||||||
|
|
||||||
if ((evt.metaKey || evt.ctrlKey) && evt.key === 'Enter') {
|
|
||||||
return this.onsubmit();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callInputListeners(evt);
|
|
||||||
}, 25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MarkdownEditorDriver extends BasicEditorDriver {
|
|
||||||
build(dom, params) {
|
|
||||||
if (app.forum.attribute('flarum-markdown.mdarea')) {
|
|
||||||
this.el.className = params.classNames.join(' ');
|
|
||||||
this.el.disabled = params.disabled;
|
|
||||||
this.el.placeholder = params.placeholder;
|
|
||||||
this.el.value = params.value;
|
|
||||||
|
|
||||||
dom.append(this.el);
|
|
||||||
|
|
||||||
const callInputListeners = (e) => {
|
|
||||||
params.inputListeners.forEach((listener) => {
|
|
||||||
listener();
|
|
||||||
});
|
|
||||||
|
|
||||||
e.redraw = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Our mdarea extension won't detect programmatic changes via
|
|
||||||
// the `app.composer.editor api.
|
|
||||||
this.el.addEventListener('input', function (e) {
|
|
||||||
if (e instanceof CustomEvent) {
|
|
||||||
params.oninput(e.target.value);
|
|
||||||
callInputListeners(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// This one can't be run through mdarea, but that doesn't matter
|
|
||||||
// because mdarea doesn't change value in response to clicks.
|
|
||||||
this.el.addEventListener('click', callInputListeners);
|
|
||||||
|
|
||||||
this.mdarea = new MarkdownArea(this.el, {
|
|
||||||
keyMap: {
|
|
||||||
indent: ['Ctrl+m'],
|
|
||||||
outdent: ['Ctrl+M'],
|
|
||||||
inline: []
|
|
||||||
},
|
|
||||||
extensions: [
|
|
||||||
new MarkdownEditorFlarumExtension(params.oninput, callInputListeners, params.onsubmit)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
super.build(dom, params);
|
|
||||||
}
|
|
||||||
this.el.id = params.textareaId;
|
|
||||||
|
|
||||||
// We can't bind shortcutHandler directly in case `build`
|
|
||||||
// runs before MarkdownToolbar's `oninit`.
|
|
||||||
this.el.addEventListener('keydown', function (e) {
|
|
||||||
return params.shortcutHandler(...arguments);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if (app.forum.attribute('flarum-markdown.mdarea')) {
|
|
||||||
this.mdarea.destroy();
|
|
||||||
}
|
|
||||||
super.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Original Copyright GitHub, Inc. Licensed under the MIT License.
|
|
||||||
* See license text at https://github.com/github/markdown-toolbar-element/blob/master/LICENSE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import insertText from './insertText';
|
|
||||||
import { blockStyle, isMultipleLines, multilineStyle, orderedList } from './styles';
|
|
||||||
|
|
||||||
export const styleSelectedText = (textarea, styleArgs) => {
|
|
||||||
const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
|
|
||||||
let result;
|
|
||||||
if (styleArgs.orderedList) {
|
|
||||||
result = orderedList(textarea);
|
|
||||||
}
|
|
||||||
else if (styleArgs.multiline && isMultipleLines(text)) {
|
|
||||||
result = multilineStyle(textarea, styleArgs);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
result = blockStyle(textarea, styleArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertText(textarea, result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (button, stylesToApply) => {
|
|
||||||
const toolbar = button.parentElement;
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
prefix: '',
|
|
||||||
suffix: '',
|
|
||||||
blockPrefix: '',
|
|
||||||
blockSuffix: '',
|
|
||||||
multiline: false,
|
|
||||||
replaceNext: '',
|
|
||||||
prefixSpace: false,
|
|
||||||
scanFor: '',
|
|
||||||
surroundWithNewlines: false,
|
|
||||||
orderedList: false,
|
|
||||||
trimFirst: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const style = Object.assign({}, defaults, stylesToApply);
|
|
||||||
|
|
||||||
const field = document.getElementById(toolbar.dataset.for);
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
field.focus();
|
|
||||||
styleSelectedText(field, style);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* Original Copyright GitHub, Inc. Licensed under the MIT License.
|
|
||||||
* See license text at https://github.com/github/markdown-toolbar-element/blob/master/LICENSE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export let canInsertText = null;
|
|
||||||
|
|
||||||
export default (textarea, { text, selectionStart, selectionEnd }) => {
|
|
||||||
const originalSelectionStart = textarea.selectionStart;
|
|
||||||
const before = textarea.value.slice(0, originalSelectionStart);
|
|
||||||
const after = textarea.value.slice(textarea.selectionEnd);
|
|
||||||
|
|
||||||
if (canInsertText === null || canInsertText === true) {
|
|
||||||
textarea.contentEditable = 'true';
|
|
||||||
try {
|
|
||||||
canInsertText = document.execCommand('insertText', false, text);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
canInsertText = false;
|
|
||||||
}
|
|
||||||
textarea.contentEditable = 'false';
|
|
||||||
}
|
|
||||||
if (canInsertText && !textarea.value.slice(0, textarea.selectionStart).endsWith(text)) {
|
|
||||||
canInsertText = false;
|
|
||||||
}
|
|
||||||
if (!canInsertText) {
|
|
||||||
try {
|
|
||||||
document.execCommand('ms-beginUndoUnit');
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
textarea.value = before + text + after;
|
|
||||||
try {
|
|
||||||
document.execCommand('ms-endUndoUnit');
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
// fire custom event, works on IE
|
|
||||||
const event = document.createEvent('Event');
|
|
||||||
|
|
||||||
event.initEvent('input', true, true);
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
if (selectionStart != null && selectionEnd != null) {
|
|
||||||
textarea.setSelectionRange(selectionStart, selectionEnd);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
textarea.setSelectionRange(originalSelectionStart, textarea.selectionEnd);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,230 +0,0 @@
|
|||||||
/*
|
|
||||||
* Original Copyright GitHub, Inc. Licensed under the MIT License.
|
|
||||||
* See license text at https://github.com/github/markdown-toolbar-element/blob/master/LICENSE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function isMultipleLines(string) {
|
|
||||||
return string.trim().split('\n').length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function repeat(string, n) {
|
|
||||||
return Array(n + 1).join(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wordSelectionStart(text, i) {
|
|
||||||
let index = i;
|
|
||||||
|
|
||||||
while (text[index] && text[index - 1] != null && !text[index - 1].match(/\s/)) {
|
|
||||||
index--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wordSelectionEnd(text, i, multiline) {
|
|
||||||
let index = i;
|
|
||||||
const breakpoint = multiline ? /\n/ : /\s/;
|
|
||||||
|
|
||||||
while (text[index] && !text[index].match(breakpoint)) {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expandSelectedText(textarea, prefixToUse, suffixToUse) {
|
|
||||||
let multiline = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
|
|
||||||
|
|
||||||
if (textarea.selectionStart === textarea.selectionEnd) {
|
|
||||||
textarea.selectionStart = wordSelectionStart(textarea.value, textarea.selectionStart);
|
|
||||||
textarea.selectionEnd = wordSelectionEnd(textarea.value, textarea.selectionEnd, multiline);
|
|
||||||
} else {
|
|
||||||
const expandedSelectionStart = textarea.selectionStart - prefixToUse.length;
|
|
||||||
const expandedSelectionEnd = textarea.selectionEnd + suffixToUse.length;
|
|
||||||
const beginsWithPrefix = textarea.value.slice(expandedSelectionStart, textarea.selectionStart) === prefixToUse;
|
|
||||||
const endsWithSuffix = textarea.value.slice(textarea.selectionEnd, expandedSelectionEnd) === suffixToUse;
|
|
||||||
|
|
||||||
if (beginsWithPrefix && endsWithSuffix) {
|
|
||||||
textarea.selectionStart = expandedSelectionStart;
|
|
||||||
textarea.selectionEnd = expandedSelectionEnd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function newlinesToSurroundSelectedText(textarea) {
|
|
||||||
const beforeSelection = textarea.value.slice(0, textarea.selectionStart);
|
|
||||||
const afterSelection = textarea.value.slice(textarea.selectionEnd);
|
|
||||||
const breaksBefore = beforeSelection.match(/\n*$/);
|
|
||||||
const breaksAfter = afterSelection.match(/^\n*/);
|
|
||||||
const newlinesBeforeSelection = breaksBefore ? breaksBefore[0].length : 0;
|
|
||||||
const newlinesAfterSelection = breaksAfter ? breaksAfter[0].length : 0;
|
|
||||||
let newlinesToAppend;
|
|
||||||
let newlinesToPrepend;
|
|
||||||
|
|
||||||
if (beforeSelection.match(/\S/) && newlinesBeforeSelection < 2) {
|
|
||||||
newlinesToAppend = repeat('\n', 2 - newlinesBeforeSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (afterSelection.match(/\S/) && newlinesAfterSelection < 2) {
|
|
||||||
newlinesToPrepend = repeat('\n', 2 - newlinesAfterSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newlinesToAppend == null) {
|
|
||||||
newlinesToAppend = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newlinesToPrepend == null) {
|
|
||||||
newlinesToPrepend = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
newlinesToAppend,
|
|
||||||
newlinesToPrepend
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const blockStyle = (textarea, arg) => {
|
|
||||||
let newlinesToAppend;
|
|
||||||
let newlinesToPrepend;
|
|
||||||
const { prefix, suffix, blockPrefix, blockSuffix, replaceNext, prefixSpace, scanFor, surroundWithNewlines } = arg;
|
|
||||||
const originalSelectionStart = textarea.selectionStart;
|
|
||||||
const originalSelectionEnd = textarea.selectionEnd;
|
|
||||||
let selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
|
|
||||||
let prefixToUse = isMultipleLines(selectedText) && blockPrefix.length > 0 ? `${blockPrefix}\n` : prefix;
|
|
||||||
let suffixToUse = isMultipleLines(selectedText) && blockSuffix.length > 0 ? `\n${blockSuffix}` : suffix;
|
|
||||||
|
|
||||||
if (prefixSpace) {
|
|
||||||
const beforeSelection = textarea.value[textarea.selectionStart - 1];
|
|
||||||
if (textarea.selectionStart !== 0 && beforeSelection != null && !beforeSelection.match(/\s/)) {
|
|
||||||
prefixToUse = ` ${prefixToUse}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedText = expandSelectedText(textarea, prefixToUse, suffixToUse, arg.multiline);
|
|
||||||
let selectionStart = textarea.selectionStart;
|
|
||||||
let selectionEnd = textarea.selectionEnd;
|
|
||||||
const hasReplaceNext = replaceNext.length > 0 && suffixToUse.indexOf(replaceNext) > -1 && selectedText.length > 0;
|
|
||||||
|
|
||||||
if (surroundWithNewlines) {
|
|
||||||
const ref = newlinesToSurroundSelectedText(textarea);
|
|
||||||
newlinesToAppend = ref.newlinesToAppend;
|
|
||||||
newlinesToPrepend = ref.newlinesToPrepend;
|
|
||||||
prefixToUse = newlinesToAppend + prefix;
|
|
||||||
suffixToUse += newlinesToPrepend;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedText.startsWith(prefixToUse) && selectedText.endsWith(suffixToUse)) {
|
|
||||||
const replacementText = selectedText.slice(prefixToUse.length, selectedText.length - suffixToUse.length);
|
|
||||||
if (originalSelectionStart === originalSelectionEnd) {
|
|
||||||
let position = originalSelectionStart - prefixToUse.length;
|
|
||||||
position = Math.max(position, selectionStart);
|
|
||||||
position = Math.min(position, selectionStart + replacementText.length);
|
|
||||||
selectionStart = selectionEnd = position;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectionEnd = selectionStart + replacementText.length;
|
|
||||||
}
|
|
||||||
return { text: replacementText, selectionStart, selectionEnd };
|
|
||||||
}
|
|
||||||
else if (!hasReplaceNext) {
|
|
||||||
let replacementText = prefixToUse + selectedText + suffixToUse;
|
|
||||||
selectionStart = originalSelectionStart + prefixToUse.length;
|
|
||||||
selectionEnd = originalSelectionEnd + prefixToUse.length;
|
|
||||||
const whitespaceEdges = selectedText.match(/^\s*|\s*$/g);
|
|
||||||
if (arg.trimFirst && whitespaceEdges) {
|
|
||||||
const leadingWhitespace = whitespaceEdges[0] || '';
|
|
||||||
const trailingWhitespace = whitespaceEdges[1] || '';
|
|
||||||
replacementText = leadingWhitespace + prefixToUse + selectedText.trim() + suffixToUse + trailingWhitespace;
|
|
||||||
selectionStart += leadingWhitespace.length;
|
|
||||||
selectionEnd -= trailingWhitespace.length;
|
|
||||||
}
|
|
||||||
return { text: replacementText, selectionStart, selectionEnd };
|
|
||||||
}
|
|
||||||
else if (scanFor.length > 0 && selectedText.match(scanFor)) {
|
|
||||||
suffixToUse = suffixToUse.replace(replaceNext, selectedText);
|
|
||||||
const replacementText = prefixToUse + suffixToUse;
|
|
||||||
selectionStart = selectionEnd = selectionStart + prefixToUse.length;
|
|
||||||
return { text: replacementText, selectionStart, selectionEnd };
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const replacementText = prefixToUse + selectedText + suffixToUse;
|
|
||||||
selectionStart = selectionStart + prefixToUse.length + selectedText.length + suffixToUse.indexOf(replaceNext);
|
|
||||||
selectionEnd = selectionStart + replaceNext.length;
|
|
||||||
return { text: replacementText, selectionStart, selectionEnd };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const multilineStyle = (textarea, arg) => {
|
|
||||||
const { prefix, suffix, surroundWithNewlines } = arg;
|
|
||||||
let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
|
|
||||||
let selectionStart = textarea.selectionStart;
|
|
||||||
let selectionEnd = textarea.selectionEnd;
|
|
||||||
const lines = text.split('\n');
|
|
||||||
const undoStyle = lines.every(line => line.startsWith(prefix) && line.endsWith(suffix));
|
|
||||||
if (undoStyle) {
|
|
||||||
text = lines.map(line => line.slice(prefix.length, line.length - suffix.length)).join('\n');
|
|
||||||
selectionEnd = selectionStart + text.length;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
text = lines.map(line => prefix + line + suffix).join('\n');
|
|
||||||
if (surroundWithNewlines) {
|
|
||||||
const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea);
|
|
||||||
selectionStart += newlinesToAppend.length;
|
|
||||||
selectionEnd = selectionStart + text.length;
|
|
||||||
text = newlinesToAppend + text + newlinesToPrepend;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { text, selectionStart, selectionEnd };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const orderedList = (textarea) => {
|
|
||||||
const orderedListRegex = /^\d+\.\s+/;
|
|
||||||
const noInitialSelection = textarea.selectionStart === textarea.selectionEnd;
|
|
||||||
let selectionEnd;
|
|
||||||
let selectionStart;
|
|
||||||
let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
|
|
||||||
let textToUnstyle = text;
|
|
||||||
let lines = text.split('\n');
|
|
||||||
let startOfLine, endOfLine;
|
|
||||||
if (noInitialSelection) {
|
|
||||||
const linesBefore = textarea.value.slice(0, textarea.selectionStart).split(/\n/);
|
|
||||||
startOfLine = textarea.selectionStart - linesBefore[linesBefore.length - 1].length;
|
|
||||||
endOfLine = wordSelectionEnd(textarea.value, textarea.selectionStart, true);
|
|
||||||
textToUnstyle = textarea.value.slice(startOfLine, endOfLine);
|
|
||||||
}
|
|
||||||
const linesToUnstyle = textToUnstyle.split('\n');
|
|
||||||
const undoStyling = linesToUnstyle.every(line => orderedListRegex.test(line));
|
|
||||||
if (undoStyling) {
|
|
||||||
lines = linesToUnstyle.map(line => line.replace(orderedListRegex, ''));
|
|
||||||
text = lines.join('\n');
|
|
||||||
if (noInitialSelection && startOfLine && endOfLine) {
|
|
||||||
const lengthDiff = linesToUnstyle[0].length - lines[0].length;
|
|
||||||
selectionStart = selectionEnd = textarea.selectionStart - lengthDiff;
|
|
||||||
textarea.selectionStart = startOfLine;
|
|
||||||
textarea.selectionEnd = endOfLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lines = (function () {
|
|
||||||
let i;
|
|
||||||
let len;
|
|
||||||
let index;
|
|
||||||
const results = [];
|
|
||||||
for (index = i = 0, len = lines.length; i < len; index = ++i) {
|
|
||||||
const line = lines[index];
|
|
||||||
results.push(`${index + 1}. ${line}`);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
})();
|
|
||||||
text = lines.join('\n');
|
|
||||||
const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea);
|
|
||||||
selectionStart = textarea.selectionStart + newlinesToAppend.length;
|
|
||||||
selectionEnd = selectionStart + text.length;
|
|
||||||
if (noInitialSelection)
|
|
||||||
selectionStart = selectionEnd;
|
|
||||||
text = newlinesToAppend + text + newlinesToPrepend;
|
|
||||||
}
|
|
||||||
return { text, selectionStart, selectionEnd };
|
|
||||||
}
|
|
@ -1,9 +1,6 @@
|
|||||||
/*
|
.MarkdownToolbar {
|
||||||
* This file is part of Flarum.
|
display: 'inline-block';
|
||||||
*
|
}
|
||||||
* For detailed copyright and license information, please view the
|
|
||||||
* LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
span.spoiler {
|
span.spoiler {
|
||||||
// Style the inline spoiler itself: a dark block, invisible font
|
// Style the inline spoiler itself: a dark block, invisible font
|
||||||
@ -26,3 +23,4 @@ span.spoiler {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
flarum-markdown:
|
flarum-markdown:
|
||||||
admin:
|
|
||||||
settings:
|
|
||||||
mdarea_help: mdarea is a textarea util that auto-continues lists, helps with code formatting, and assists with indentation.
|
|
||||||
mdarea_label: Enable mdarea?
|
|
||||||
|
|
||||||
forum:
|
forum:
|
||||||
composer:
|
composer:
|
||||||
bold_tooltip: Add bold text
|
bold_tooltip: Add bold text
|
||||||
|
Reference in New Issue
Block a user