From 50a4530cfc33adc384dddfd83db4e225da1f09fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sevilla=20Marti=CC=81n?= Date: Wed, 10 Jul 2019 12:30:35 -0400 Subject: [PATCH 1/4] Use custom JSX implementation of GitHub's markdown toolbar that works in IE 11 --- extensions/markdown/js/package-lock.json | 92 ++++--- extensions/markdown/js/package.json | 2 - .../js/src/forum/components/MarkdownButton.js | 47 ++++ .../src/forum/components/MarkdownToolbar.js | 30 +++ extensions/markdown/js/src/forum/index.js | 39 ++- .../markdown/js/src/forum/pollyfills.js | 17 ++ .../markdown/js/src/forum/util/apply.js | 45 ++++ .../markdown/js/src/forum/util/insertText.js | 49 ++++ .../markdown/js/src/forum/util/styles.js | 225 ++++++++++++++++++ 9 files changed, 469 insertions(+), 77 deletions(-) create mode 100644 extensions/markdown/js/src/forum/components/MarkdownButton.js create mode 100644 extensions/markdown/js/src/forum/components/MarkdownToolbar.js create mode 100644 extensions/markdown/js/src/forum/pollyfills.js create mode 100644 extensions/markdown/js/src/forum/util/apply.js create mode 100644 extensions/markdown/js/src/forum/util/insertText.js create mode 100644 extensions/markdown/js/src/forum/util/styles.js diff --git a/extensions/markdown/js/package-lock.json b/extensions/markdown/js/package-lock.json index 1730006b1..87e2959c4 100644 --- a/extensions/markdown/js/package-lock.json +++ b/extensions/markdown/js/package-lock.json @@ -979,11 +979,6 @@ "to-fast-properties": "^2.0.0" } }, - "@github/markdown-toolbar-element": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-0.1.1.tgz", - "integrity": "sha512-P+wEginh59MeqKwk7oBOsFotZ1cWGdafH+lcipbaBbGN2jS58vNvHJ9UMCOGlt049Xtl7Cm607+hH11dmoBinw==" - }, "@webassemblyjs/ast": { "version": "1.7.11", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", @@ -1138,11 +1133,6 @@ "@xtuc/long": "4.2.1" } }, - "@webcomponents/custom-elements": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.2.1.tgz", - "integrity": "sha512-flmTp4rVbBkcUIF3eBO3LNoAaYvleTdhPZKzdzr6iztWLLrxCctcK+7MAQeC3/SPjc3JDdC3jYLMRF4R6C3f9g==" - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -2322,13 +2312,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" }, "dependencies": { "abbrev": { @@ -2347,7 +2337,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "optional": true, "requires": { @@ -2370,7 +2360,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "optional": true }, @@ -2395,15 +2385,15 @@ "optional": true }, "debug": { - "version": "2.6.9", + "version": "4.1.1", "bundled": true, "optional": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "optional": true }, @@ -2446,7 +2436,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "optional": true, "requires": { @@ -2464,11 +2454,11 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -2525,16 +2515,16 @@ "optional": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "optional": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "optional": true, "requires": { @@ -2550,32 +2540,32 @@ } }, "ms": { - "version": "2.0.0", + "version": "2.1.1", "bundled": true, "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.3.0", "bundled": true, "optional": true, "requires": { - "debug": "^2.1.2", + "debug": "^4.1.0", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.12.0", "bundled": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -2591,12 +2581,12 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.6", "bundled": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.4.1", "bundled": true, "optional": true, "requires": { @@ -2663,11 +2653,11 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -2695,15 +2685,15 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "optional": true }, @@ -2718,7 +2708,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.7.0", "bundled": true, "optional": true }, @@ -2764,16 +2754,16 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -2783,11 +2773,11 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -2796,7 +2786,7 @@ "optional": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "optional": true } @@ -3501,9 +3491,9 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "optional": true }, "nanomatch": { diff --git a/extensions/markdown/js/package.json b/extensions/markdown/js/package.json index b76b407f6..b32885d0f 100644 --- a/extensions/markdown/js/package.json +++ b/extensions/markdown/js/package.json @@ -2,8 +2,6 @@ "private": true, "name": "@flarum/markdown", "dependencies": { - "@github/markdown-toolbar-element": "^0.1.1", - "@webcomponents/custom-elements": "^1.2.1", "flarum-webpack-config": "0.1.0-beta.10", "mdarea": "^0.0.10", "webpack": "^4.26.0", diff --git a/extensions/markdown/js/src/forum/components/MarkdownButton.js b/extensions/markdown/js/src/forum/components/MarkdownButton.js new file mode 100644 index 000000000..bff1fc194 --- /dev/null +++ b/extensions/markdown/js/src/forum/components/MarkdownButton.js @@ -0,0 +1,47 @@ +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 { + config(isInitialized) { + if (isInitialized) return; + + this.$().tooltip(); + } + + view() { + return ; + } + + keydown(event) { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + this.click(); + } + } + + click() { + return apply(this.element, this.styles()); + } + + title() { + let tooltip = this.props.title; + + if (this.props.hotkey) tooltip += ` <${modifierKey}-${this.props.hotkey}>`; + + return tooltip; + } + + icon() { + return this.props.icon; + } + + styles() { + return this.props.style; + } +} diff --git a/extensions/markdown/js/src/forum/components/MarkdownToolbar.js b/extensions/markdown/js/src/forum/components/MarkdownToolbar.js new file mode 100644 index 000000000..525fdc2bc --- /dev/null +++ b/extensions/markdown/js/src/forum/components/MarkdownToolbar.js @@ -0,0 +1,30 @@ +import Component from 'flarum/Component'; + +const modifierKey = navigator.userAgent.match(/Macintosh/) ? 'Meta' : 'Control'; + +export default class MarkdownToolbar extends Component { + config(isInitialized) { + if (isInitialized) return; + + const field = document.getElementById(this.props.for); + + field.addEventListener('keydown', this.shortcut.bind(this)); + } + + view() { + return
+ {this.props.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 047fefcc8..6f5f95c04 100644 --- a/extensions/markdown/js/src/forum/index.js +++ b/extensions/markdown/js/src/forum/index.js @@ -1,17 +1,12 @@ import { extend } from 'flarum/extend'; import TextEditor from 'flarum/components/TextEditor'; -import icon from 'flarum/helpers/icon'; +import MarkdownArea from 'mdarea'; -let MarkdownArea; - -if (window.Reflect) { - require('@webcomponents/custom-elements'); - require('@github/markdown-toolbar-element'); - MarkdownArea = require('mdarea/mdarea.js'); -} +import './pollyfills'; +import MarkdownToolbar from './components/MarkdownToolbar'; +import MarkdownButton from './components/MarkdownButton'; app.initializers.add('flarum-markdown', function(app) { - if (!MarkdownArea) return; let index = 1; @@ -28,6 +23,7 @@ app.initializers.add('flarum-markdown', function(app) { const editor = new MarkdownArea(element); editor.disableInline(); + editor.ignoreTab(); context.onunload = function() { editor.destroy(); @@ -35,24 +31,19 @@ app.initializers.add('flarum-markdown', function(app) { }); extend(TextEditor.prototype, 'toolbarItems', function(items) { - const attrs = { - className: 'Button Button--icon Button--link', - config: elm => $(elm).tooltip() - }; - const tooltip = name => app.translator.trans(`flarum-markdown.forum.composer.${name}_tooltip`); items.add('markdown', ( - - {icon('fas fa-heading')} - '} {...attrs}>{icon('fas fa-bold')} - '} {...attrs}>{icon('fas fa-italic')} - {icon('fas fa-quote-left')} - {icon('fas fa-code')} - '} {...attrs}>{icon('fas fa-link')} - {icon('fas fa-list-ul')} - {icon('fas fa-list-ol')} - + + + + + ', multiline: true, surroundWithNewlines: true }} /> + + + + + ), 100); }); }); diff --git a/extensions/markdown/js/src/forum/pollyfills.js b/extensions/markdown/js/src/forum/pollyfills.js new file mode 100644 index 000000000..918827b58 --- /dev/null +++ b/extensions/markdown/js/src/forum/pollyfills.js @@ -0,0 +1,17 @@ +if (!String.prototype.startsWith) { + Object.defineProperty(String.prototype, 'startsWith', { + value: function(search, pos) { + pos = !pos || pos < 0 ? 0 : +pos; + return this.substring(pos, pos + search.length) === search; + } + }); +} + +if (!String.prototype.endsWith) { + String.prototype.endsWith = function(search, this_len) { + if (this_len === undefined || this_len > this.length) { + this_len = this.length; + } + return this.substring(this_len - search.length, this_len) === search; + }; +} diff --git a/extensions/markdown/js/src/forum/util/apply.js b/extensions/markdown/js/src/forum/util/apply.js new file mode 100644 index 000000000..534463e2a --- /dev/null +++ b/extensions/markdown/js/src/forum/util/apply.js @@ -0,0 +1,45 @@ +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 new file mode 100644 index 000000000..c659a40b9 --- /dev/null +++ b/extensions/markdown/js/src/forum/util/insertText.js @@ -0,0 +1,49 @@ +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 new file mode 100644 index 000000000..5da1dfa04 --- /dev/null +++ b/extensions/markdown/js/src/forum/util/styles.js @@ -0,0 +1,225 @@ +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 }; +} From f4e250f514fc0b342c36a11cb9ad9499b7b32fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sevilla=20Marti=CC=81n?= Date: Wed, 10 Jul 2019 12:41:05 -0400 Subject: [PATCH 2/4] Simplify some code in MarkdownButton component --- .../js/src/forum/components/MarkdownButton.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/extensions/markdown/js/src/forum/components/MarkdownButton.js b/extensions/markdown/js/src/forum/components/MarkdownButton.js index bff1fc194..6ad05e094 100644 --- a/extensions/markdown/js/src/forum/components/MarkdownButton.js +++ b/extensions/markdown/js/src/forum/components/MarkdownButton.js @@ -14,7 +14,7 @@ export default class MarkdownButton extends Component { view() { return ; } @@ -26,7 +26,7 @@ export default class MarkdownButton extends Component { } click() { - return apply(this.element, this.styles()); + return apply(this.element, this.props.style); } title() { @@ -36,12 +36,4 @@ export default class MarkdownButton extends Component { return tooltip; } - - icon() { - return this.props.icon; - } - - styles() { - return this.props.style; - } } From eb561e6343678135a3ec3b86b578a68b5382c1cc Mon Sep 17 00:00:00 2001 From: David Sevilla Martin Date: Sat, 3 Aug 2019 09:14:06 -0400 Subject: [PATCH 3/4] Fix 'polyfills' incorrect spelling, add LICENSE text for startsWith polyfill --- extensions/markdown/js/src/forum/index.js | 2 +- .../markdown/js/src/forum/{pollyfills.js => polyfills.js} | 1 + extensions/markdown/js/src/forum/util/apply.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename extensions/markdown/js/src/forum/{pollyfills.js => polyfills.js} (90%) diff --git a/extensions/markdown/js/src/forum/index.js b/extensions/markdown/js/src/forum/index.js index 6f5f95c04..582b31538 100644 --- a/extensions/markdown/js/src/forum/index.js +++ b/extensions/markdown/js/src/forum/index.js @@ -2,7 +2,7 @@ import { extend } from 'flarum/extend'; import TextEditor from 'flarum/components/TextEditor'; import MarkdownArea from 'mdarea'; -import './pollyfills'; +import './polyfills'; import MarkdownToolbar from './components/MarkdownToolbar'; import MarkdownButton from './components/MarkdownButton'; diff --git a/extensions/markdown/js/src/forum/pollyfills.js b/extensions/markdown/js/src/forum/polyfills.js similarity index 90% rename from extensions/markdown/js/src/forum/pollyfills.js rename to extensions/markdown/js/src/forum/polyfills.js index 918827b58..1649bbcd0 100644 --- a/extensions/markdown/js/src/forum/pollyfills.js +++ b/extensions/markdown/js/src/forum/polyfills.js @@ -1,3 +1,4 @@ +/*! https://mths.be/startswith v0.2.0 by @mathias */ if (!String.prototype.startsWith) { Object.defineProperty(String.prototype, 'startsWith', { value: function(search, pos) { diff --git a/extensions/markdown/js/src/forum/util/apply.js b/extensions/markdown/js/src/forum/util/apply.js index 534463e2a..7f206060c 100644 --- a/extensions/markdown/js/src/forum/util/apply.js +++ b/extensions/markdown/js/src/forum/util/apply.js @@ -1,5 +1,5 @@ import insertText from './insertText'; -import {blockStyle, isMultipleLines, multilineStyle, orderedList} from "./styles"; +import { blockStyle, isMultipleLines, multilineStyle, orderedList } from './styles'; export const styleSelectedText = (textarea, styleArgs) => { const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd); From 8277b54651fd465a166c9a249a66cfdfa3ad86cb Mon Sep 17 00:00:00 2001 From: David Sevilla Martin Date: Sun, 11 Aug 2019 16:06:30 -0400 Subject: [PATCH 4/4] Add image button (flarum/core#1679) --- extensions/markdown/js/src/forum/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/markdown/js/src/forum/index.js b/extensions/markdown/js/src/forum/index.js index 582b31538..cb95b93a1 100644 --- a/extensions/markdown/js/src/forum/index.js +++ b/extensions/markdown/js/src/forum/index.js @@ -41,6 +41,7 @@ app.initializers.add('flarum-markdown', function(app) { ', multiline: true, surroundWithNewlines: true }} /> +