From 7ea9db7426b37cf78aa94c68d5fa9e128dc0e67d Mon Sep 17 00:00:00 2001 From: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com> Date: Mon, 10 May 2021 17:35:22 -0400 Subject: [PATCH] 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 --- extensions/markdown/extend.php | 3 - extensions/markdown/js/admin.js | 1 - extensions/markdown/js/dist/admin.js | 2 - extensions/markdown/js/dist/admin.js.map | 1 - extensions/markdown/js/src/admin/index.js | 12 - .../js/src/forum/components/MarkdownButton.js | 21 +- .../src/forum/components/MarkdownToolbar.js | 21 +- extensions/markdown/js/src/forum/index.js | 75 ++++-- .../js/src/forum/util/MarkdownEditorDriver.js | 94 ------- .../markdown/js/src/forum/util/apply.js | 50 ---- .../markdown/js/src/forum/util/insertText.js | 54 ---- .../markdown/js/src/forum/util/styles.js | 230 ------------------ extensions/markdown/less/forum.less | 10 +- extensions/markdown/locale/en.yml | 5 - 14 files changed, 57 insertions(+), 522 deletions(-) delete mode 100644 extensions/markdown/js/admin.js delete mode 100644 extensions/markdown/js/dist/admin.js delete mode 100644 extensions/markdown/js/dist/admin.js.map delete mode 100644 extensions/markdown/js/src/admin/index.js delete mode 100644 extensions/markdown/js/src/forum/util/MarkdownEditorDriver.js delete mode 100644 extensions/markdown/js/src/forum/util/apply.js delete mode 100644 extensions/markdown/js/src/forum/util/insertText.js delete mode 100644 extensions/markdown/js/src/forum/util/styles.js diff --git a/extensions/markdown/extend.php b/extensions/markdown/extend.php index 77ba694a1..0fe79cf98 100644 --- a/extensions/markdown/extend.php +++ b/extensions/markdown/extend.php @@ -11,9 +11,6 @@ use Flarum\Extend; use s9e\TextFormatter\Configurator; return [ - (new Extend\Frontend('admin')) - ->js(__DIR__.'/js/dist/admin.js'), - (new Extend\Frontend('forum')) ->js(__DIR__.'/js/dist/forum.js') ->css(__DIR__.'/less/forum.less'), diff --git a/extensions/markdown/js/admin.js b/extensions/markdown/js/admin.js deleted file mode 100644 index 3e69ff3b9..000000000 --- a/extensions/markdown/js/admin.js +++ /dev/null @@ -1 +0,0 @@ -export * from './src/admin'; diff --git a/extensions/markdown/js/dist/admin.js b/extensions/markdown/js/dist/admin.js deleted file mode 100644 index ff4ae69d2..000000000 --- a/extensions/markdown/js/dist/admin.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/extensions/markdown/js/dist/admin.js.map b/extensions/markdown/js/dist/admin.js.map deleted file mode 100644 index 985be2629..000000000 --- a/extensions/markdown/js/dist/admin.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack://@flarum/markdown/webpack/bootstrap","webpack://@flarum/markdown/external \"flarum.core.compat['app']\"","webpack://@flarum/markdown/./src/admin/index.js"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","flarum","core","compat","app","initializers","add","extensionData","for","registerSetting","setting","type","help","translator","trans","label"],"mappings":"2BACE,IAAIA,EAAmB,GAGvB,SAASC,EAAoBC,GAG5B,GAAGF,EAAiBE,GACnB,OAAOF,EAAiBE,GAAUC,QAGnC,IAAIC,EAASJ,EAAiBE,GAAY,CACzCG,EAAGH,EACHI,GAAG,EACHH,QAAS,IAUV,OANAI,EAAQL,GAAUM,KAAKJ,EAAOD,QAASC,EAAQA,EAAOD,QAASF,GAG/DG,EAAOE,GAAI,EAGJF,EAAOD,QA0Df,OArDAF,EAAoBQ,EAAIF,EAGxBN,EAAoBS,EAAIV,EAGxBC,EAAoBU,EAAI,SAASR,EAASS,EAAMC,GAC3CZ,EAAoBa,EAAEX,EAASS,IAClCG,OAAOC,eAAeb,EAASS,EAAM,CAAEK,YAAY,EAAMC,IAAKL,KAKhEZ,EAAoBkB,EAAI,SAAShB,GACX,oBAAXiB,QAA0BA,OAAOC,aAC1CN,OAAOC,eAAeb,EAASiB,OAAOC,YAAa,CAAEC,MAAO,WAE7DP,OAAOC,eAAeb,EAAS,aAAc,CAAEmB,OAAO,KAQvDrB,EAAoBsB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQrB,EAAoBqB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKX,OAAOY,OAAO,MAGvB,GAFA1B,EAAoBkB,EAAEO,GACtBX,OAAOC,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOrB,EAAoBU,EAAEe,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRzB,EAAoB6B,EAAI,SAAS1B,GAChC,IAAIS,EAAST,GAAUA,EAAOqB,WAC7B,WAAwB,OAAOrB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAH,EAAoBU,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRZ,EAAoBa,EAAI,SAASiB,EAAQC,GAAY,OAAOjB,OAAOkB,UAAUC,eAAe1B,KAAKuB,EAAQC,IAGzG/B,EAAoBkC,EAAI,GAIjBlC,EAAoBA,EAAoBmC,EAAI,G,kBClFrDhC,EAAOD,QAAUkC,OAAOC,KAAKC,OAAY,K,0DCEzCC,IAAIC,aAAaC,IAAI,mBAAmB,WACpCF,IAAIG,cACCC,IAAI,mBACJC,gBAAgB,CACbC,QAAS,yBACTC,KAAM,UACNC,KAAMR,IAAIS,WAAWC,MAAM,8CAC3BC,MAAOX,IAAIS,WAAWC,MAAM","file":"admin.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 8);\n","module.exports = flarum.core.compat['app'];","import app from 'flarum/app';\n\napp.initializers.add('flarum-markdown', () => {\n app.extensionData\n .for('flarum-markdown')\n .registerSetting({\n setting: 'flarum-markdown.mdarea',\n type: 'boolean',\n help: app.translator.trans('flarum-markdown.admin.settings.mdarea_help'),\n label: app.translator.trans('flarum-markdown.admin.settings.mdarea_label')\n });\n});\n"],"sourceRoot":""} \ No newline at end of file diff --git a/extensions/markdown/js/src/admin/index.js b/extensions/markdown/js/src/admin/index.js deleted file mode 100644 index c312dfc22..000000000 --- a/extensions/markdown/js/src/admin/index.js +++ /dev/null @@ -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') - }); -}); diff --git a/extensions/markdown/js/src/forum/components/MarkdownButton.js b/extensions/markdown/js/src/forum/components/MarkdownButton.js index cd88699af..0ac984d91 100644 --- a/extensions/markdown/js/src/forum/components/MarkdownButton.js +++ b/extensions/markdown/js/src/forum/components/MarkdownButton.js @@ -1,8 +1,5 @@ import Component from 'flarum/Component'; import icon from 'flarum/helpers/icon'; -import apply from '../util/apply'; - -const modifierKey = navigator.userAgent.match(/Macintosh/) ? '⌘' : 'ctrl'; export default class MarkdownButton extends Component { oncreate(vnode) { @@ -13,8 +10,8 @@ export default class MarkdownButton extends Component { view() { return ( - ); @@ -23,19 +20,7 @@ export default class MarkdownButton extends Component { keydown(event) { if (event.key === ' ' || event.key === 'Enter') { 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; - } } diff --git a/extensions/markdown/js/src/forum/components/MarkdownToolbar.js b/extensions/markdown/js/src/forum/components/MarkdownToolbar.js index 2e7217c5a..b63fa7334 100644 --- a/extensions/markdown/js/src/forum/components/MarkdownToolbar.js +++ b/extensions/markdown/js/src/forum/components/MarkdownToolbar.js @@ -1,28 +1,9 @@ import Component from 'flarum/Component'; -const modifierKey = navigator.userAgent.match(/Macintosh/) ? 'Meta' : 'Control'; - export default class MarkdownToolbar extends Component { - oncreate(vnode) { - super.oncreate(vnode); - - this.attrs.setShortcutHandler(this.shortcut.bind(this)); - } - view(vnode) { - return
+ return
{vnode.children}
; } - - 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() - } - } - } } diff --git a/extensions/markdown/js/src/forum/index.js b/extensions/markdown/js/src/forum/index.js index 2a11b82d3..7a1b99423 100644 --- a/extensions/markdown/js/src/forum/index.js +++ b/extensions/markdown/js/src/forum/index.js @@ -7,45 +7,68 @@ * 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 BasicEditorDriver from 'flarum/common/utils/BasicEditorDriver'; +import styleSelectedText from 'flarum/common/utils/styleSelectedText'; import MarkdownToolbar from './components/MarkdownToolbar'; 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: '![', suffix: '](https://)', 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) { - let index = 1; - - extend(TextEditor.prototype, 'oninit', function () { - 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(BasicEditorDriver.prototype, 'keyHandlers', function (items) { + items.add('bold', makeShortcut('bold', 'b')); + items.add('italic', makeShortcut('italic', 'i')); }); 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', ( shortcutHandler = handler}> - - - - ', multiline: true, surroundWithNewlines: true }} /> - - - - - + + + + + + + + ), 100); }); diff --git a/extensions/markdown/js/src/forum/util/MarkdownEditorDriver.js b/extensions/markdown/js/src/forum/util/MarkdownEditorDriver.js deleted file mode 100644 index a13621ccd..000000000 --- a/extensions/markdown/js/src/forum/util/MarkdownEditorDriver.js +++ /dev/null @@ -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(); - } -} diff --git a/extensions/markdown/js/src/forum/util/apply.js b/extensions/markdown/js/src/forum/util/apply.js deleted file mode 100644 index 92f00575a..000000000 --- a/extensions/markdown/js/src/forum/util/apply.js +++ /dev/null @@ -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); - } -} diff --git a/extensions/markdown/js/src/forum/util/insertText.js b/extensions/markdown/js/src/forum/util/insertText.js deleted file mode 100644 index c2de8a1bd..000000000 --- a/extensions/markdown/js/src/forum/util/insertText.js +++ /dev/null @@ -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); - } -}; diff --git a/extensions/markdown/js/src/forum/util/styles.js b/extensions/markdown/js/src/forum/util/styles.js deleted file mode 100644 index 4b6715e1d..000000000 --- a/extensions/markdown/js/src/forum/util/styles.js +++ /dev/null @@ -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 }; -} diff --git a/extensions/markdown/less/forum.less b/extensions/markdown/less/forum.less index 36f5f6522..9fb25de3e 100644 --- a/extensions/markdown/less/forum.less +++ b/extensions/markdown/less/forum.less @@ -1,9 +1,6 @@ -/* - * This file is part of Flarum. - * - * For detailed copyright and license information, please view the - * LICENSE file that was distributed with this source code. - */ +.MarkdownToolbar { + display: 'inline-block'; +} span.spoiler { // Style the inline spoiler itself: a dark block, invisible font @@ -26,3 +23,4 @@ span.spoiler { visibility: hidden; } } + diff --git a/extensions/markdown/locale/en.yml b/extensions/markdown/locale/en.yml index 802de9e1f..3ece63f7d 100644 --- a/extensions/markdown/locale/en.yml +++ b/extensions/markdown/locale/en.yml @@ -1,9 +1,4 @@ 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: composer: bold_tooltip: Add bold text