Feature #43259 » 0001-Define-the-wiki-toolbar-markup-in-JavaScript-for-eac.patch
| app/javascript/controllers/wiki_toolbar_controller.js | ||
|---|---|---|
| 11 | 11 |
// |
| 12 | 12 |
// The toolbar is rendered after the textarea and moves itself to the |
| 13 | 13 |
// textarea's position on connect, so that existing views only need to call |
| 14 |
// wikitoolbar_for. The text formatting syntax is given by the button |
|
| 15 |
// parameters, so this controller is shared by all text formatters. |
|
| 14 |
// wikitoolbar_for. The markup inserted by each button is defined by the text |
|
| 15 |
// formatter in app/javascript/wiki_formatting/<format>.js, which is loaded on |
|
| 16 |
// connect. |
|
| 16 | 17 |
export default class extends Controller {
|
| 17 | 18 |
static targets = ['editor', 'preview', 'editTab', 'previewTab', 'toolbar', 'precodeMenu', 'tableMenu'] |
| 18 | 19 |
static values = {
|
| 20 |
format: String, |
|
| 19 | 21 |
fieldId: String, |
| 20 | 22 |
previewUrl: String, |
| 21 | 23 |
helpUrl: String |
| ... | ... | |
| 34 | 36 |
this.field.before(this.element) |
| 35 | 37 |
this.editorTarget.prepend(this.field) |
| 36 | 38 |
} |
| 39 |
this.syntaxLoaded ??= import(`wiki_formatting/${this.formatValue}`)
|
|
| 40 |
.then((module) => { this.syntax = module.default })
|
|
| 41 |
.catch((error) => console.error(`Wiki toolbar syntax not found for ${this.formatValue}`, error))
|
|
| 37 | 42 |
this.addShortcutsToTitles() |
| 38 | 43 |
this.element.hidden = false |
| 39 | 44 |
} |
| ... | ... | |
| 45 | 50 | |
| 46 | 51 |
// Actions |
| 47 | 52 | |
| 48 |
wrap({ params: { prefix, suffix } }) {
|
|
| 49 |
this.encloseSelection(String(prefix), String(suffix ?? prefix)) |
|
| 53 |
async apply({ params: { button } }) {
|
|
| 54 |
await this.syntaxLoaded |
|
| 55 |
this.syntax?.[button]?.(this) |
|
| 50 | 56 |
} |
| 51 | 57 | |
| 52 |
wrapLines({ params: { prefix = '', suffix = '', strip, pattern, replacement = '' } }) {
|
|
| 53 |
this.encloseLineSelection(String(prefix), String(suffix), (str) => {
|
|
| 54 |
if (strip) {
|
|
| 55 |
str = str.replace(new RegExp(strip), '') |
|
| 56 |
} |
|
| 57 |
if (pattern) {
|
|
| 58 |
str = str.replace(/\r/g, '').replace(new RegExp(`(\\n|^)${pattern}`, 'g'), `$1${replacement}`)
|
|
| 59 |
} |
|
| 60 |
return str |
|
| 61 |
}) |
|
| 58 |
async insertPrecode({ params: { lang } }) {
|
|
| 59 |
await this.syntaxLoaded |
|
| 60 |
this.syntax?.precode?.(this, String(lang)) |
|
| 61 |
} |
|
| 62 | ||
| 63 |
async insertTable({ params: { cols, rows } }) {
|
|
| 64 |
await this.syntaxLoaded |
|
| 65 |
this.syntax?.table?.(this, cols, rows) |
|
| 62 | 66 |
} |
| 63 | 67 | |
| 64 | 68 |
help() {
|
| ... | ... | |
| 66 | 70 |
} |
| 67 | 71 | |
| 68 | 72 |
shortcut(event) {
|
| 69 |
if (event.target !== this.field || !(isMac ? event.metaKey : event.ctrlKey)) return |
|
| 73 |
if (event.target !== this.field || event.shiftKey || event.altKey) return |
|
| 74 |
if (!(isMac ? event.metaKey : event.ctrlKey)) return |
|
| 70 | 75 | |
| 71 | 76 |
const button = this.toolbarTarget.querySelector(`[data-wiki-toolbar-shortcut="${event.key.toLowerCase()}"]`)
|
| 72 | 77 |
if (button) {
|
| ... | ... | |
| 127 | 132 |
const template = this[`${params.menu}MenuTarget`]
|
| 128 | 133 |
this.menu = template.content.firstElementChild.cloneNode(true) |
| 129 | 134 |
this.menu.dataset.name = params.menu |
| 130 |
this.menuParams = params |
|
| 131 | 135 |
this.element.append(this.menu) |
| 132 | 136 |
this.positionMenu(currentTarget) |
| 133 | 137 | |
| ... | ... | |
| 147 | 151 |
this.closeMenuOnOutsideClick = null |
| 148 | 152 |
} |
| 149 | 153 | |
| 150 |
insertTable({ params: { cols, rows } }) {
|
|
| 151 |
const { headerCell, separatorCell } = this.menuParams
|
|
| 152 |
const names = 'ABCDEFGHIJ'.slice(0, cols).split('')
|
|
| 153 |
const line = (cells) => `|${cells.join('|')}|\n`
|
|
| 154 | ||
| 155 |
let table = line(names.map((name) => String(headerCell).replace('%{name}', name)))
|
|
| 156 |
if (separatorCell) table += line(Array(cols).fill(separatorCell)) |
|
| 157 |
table += line(Array(cols).fill(' ')).repeat(rows)
|
|
| 158 |
this.encloseLineSelection(table, '') |
|
| 159 |
} |
|
| 160 | ||
| 161 | 154 |
highlightTable({ target }) {
|
| 162 | 155 |
const button = target.closest('button')
|
| 163 | 156 |
if (!button) return |
| ... | ... | |
| 170 | 163 |
}) |
| 171 | 164 |
} |
| 172 | 165 | |
| 166 |
// Editing helpers used by the text formatter syntaxes |
|
| 167 | ||
| 168 |
// Wraps the selection, e.g. wrap('**') or wrap('<u>', '</u>')
|
|
| 169 |
wrap(prefix, suffix = prefix) {
|
|
| 170 |
this.encloseSelection(prefix, suffix) |
|
| 171 |
} |
|
| 172 | ||
| 173 |
// Wraps the selected lines, optionally transforming them |
|
| 174 |
wrapLines(prefix, suffix = '', transform = (text) => text) {
|
|
| 175 |
this.encloseLineSelection(prefix, suffix, transform) |
|
| 176 |
} |
|
| 177 | ||
| 178 |
// Replaces in the selected lines, e.g. to turn them into a list |
|
| 179 |
replaceLines(pattern, replacement) {
|
|
| 180 |
this.wrapLines('', '', (text) => text.replace(/\r/g, '').replace(pattern, replacement))
|
|
| 181 |
} |
|
| 182 | ||
| 183 |
columnNames(count) {
|
|
| 184 |
return 'ABCDEFGHIJ'.slice(0, count).split('')
|
|
| 185 |
} |
|
| 186 | ||
| 173 | 187 |
// Private |
| 174 | 188 | |
| 175 | 189 |
get previewing() {
|
| app/javascript/wiki_formatting/common_mark.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 | ||
| 7 |
// CommonMark markup inserted by the wiki toolbar buttons. |
|
| 8 |
// Each function receives the editor (the wiki-toolbar controller). |
|
| 9 | ||
| 10 |
const heading = (level) => (editor) => |
|
| 11 |
editor.wrapLines(`${'#'.repeat(level)} `, '', (text) => text.replace(/^#+\s+/, ''))
|
|
| 12 | ||
| 13 |
export default {
|
|
| 14 |
strong: (editor) => editor.wrap('**'),
|
|
| 15 |
em: (editor) => editor.wrap('*'),
|
|
| 16 |
ins: (editor) => editor.wrap('<u>', '</u>'),
|
|
| 17 |
del: (editor) => editor.wrap('~~'),
|
|
| 18 |
code: (editor) => editor.wrap('`'),
|
|
| 19 | ||
| 20 |
h1: heading(1), |
|
| 21 |
h2: heading(2), |
|
| 22 |
h3: heading(3), |
|
| 23 | ||
| 24 |
ul: (editor) => editor.replaceLines(/(\n|^)[#-]?\s*/g, '$1* '), |
|
| 25 |
ol: (editor) => editor.replaceLines(/(\n|^)[*-]?\s*/g, '$11. '), |
|
| 26 |
tl: (editor) => editor.replaceLines(/(\n|^)[*-]?\s*/g, '$1* [ ] '), |
|
| 27 | ||
| 28 |
bq: (editor) => editor.replaceLines(/(\n|^)( *)([^\n]*)/g, '$1> $2$3'), |
|
| 29 |
unbq: (editor) => editor.replaceLines(/(\n|^) *(> ?)?( *)([^\n]*)/g, '$1$3$4'), |
|
| 30 |
pre: (editor) => editor.wrapLines('```\n', '\n```'),
|
|
| 31 |
precode: (editor, lang) => editor.wrapLines(`\`\`\` ${lang}\n`, '\n```\n'),
|
|
| 32 |
table: (editor, cols, rows) => {
|
|
| 33 |
const header = editor.columnNames(cols).map((name) => `${name} `)
|
|
| 34 |
editor.wrapLines(`|${header.join('|')}|\n` + `|${'--|'.repeat(cols)}\n` + `|${' |'.repeat(cols)}\n`.repeat(rows))
|
|
| 35 |
}, |
|
| 36 | ||
| 37 |
link: (editor) => editor.wrap('[[', ']]'),
|
|
| 38 |
img: (editor) => editor.wrap('')
|
|
| 39 |
} |
|
| app/javascript/wiki_formatting/textile.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 | ||
| 7 |
// Textile markup inserted by the wiki toolbar buttons. |
|
| 8 |
// Each function receives the editor (the wiki-toolbar controller). |
|
| 9 | ||
| 10 |
const heading = (level) => (editor) => |
|
| 11 |
editor.wrapLines(`h${level}. `, '', (text) => text.replace(/^h\d+\.\s+/, ''))
|
|
| 12 | ||
| 13 |
export default {
|
|
| 14 |
strong: (editor) => editor.wrap('*'),
|
|
| 15 |
em: (editor) => editor.wrap('_'),
|
|
| 16 |
ins: (editor) => editor.wrap('+'),
|
|
| 17 |
del: (editor) => editor.wrap('-'),
|
|
| 18 |
code: (editor) => editor.wrap('@'),
|
|
| 19 | ||
| 20 |
h1: heading(1), |
|
| 21 |
h2: heading(2), |
|
| 22 |
h3: heading(3), |
|
| 23 | ||
| 24 |
ul: (editor) => editor.replaceLines(/(\n|^)[#-]?\s*/g, '$1* '), |
|
| 25 |
ol: (editor) => editor.replaceLines(/(\n|^)[*-]?\s*/g, '$1# '), |
|
| 26 | ||
| 27 |
bq: (editor) => editor.replaceLines(/(\n|^)( *)([^\n]*)/g, '$1> $2$3'), |
|
| 28 |
unbq: (editor) => editor.replaceLines(/(\n|^) *(> ?)?( *)([^\n]*)/g, '$1$3$4'), |
|
| 29 |
pre: (editor) => editor.wrapLines('<pre>\n', '\n</pre>'),
|
|
| 30 |
precode: (editor, lang) => editor.wrapLines(`<pre><code class="${lang}">\n`, '\n</code></pre>\n'),
|
|
| 31 |
table: (editor, cols, rows) => {
|
|
| 32 |
const header = editor.columnNames(cols).map((name) => `_.${name}`)
|
|
| 33 |
editor.wrapLines(`|${header.join('|')}|\n` + `|${' |'.repeat(cols)}\n`.repeat(rows))
|
|
| 34 |
}, |
|
| 35 | ||
| 36 |
link: (editor) => editor.wrap('[[', ']]'),
|
|
| 37 |
img: (editor) => editor.wrap('!', '!')
|
|
| 38 |
} |
|
| config/importmap.rb | ||
|---|---|---|
| 7 | 7 |
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" |
| 8 | 8 |
pin "turndown" # @7.2.0 |
| 9 | 9 |
pin_all_from "app/javascript/controllers", under: "controllers" |
| 10 |
pin_all_from "app/javascript/wiki_formatting", under: "wiki_formatting", preload: false |
|
| 10 | 11 |
pin "tablesort", to: "tablesort.min.js" |
| 11 | 12 |
pin "tablesort.number", to: "tablesort.number.min.js" |
| 12 | 13 |
pin "chart.js", preload: false, to: "chart.min.js" # @4.5.1 |
| lib/redmine/wiki_formatting/common_mark/helper.rb | ||
|---|---|---|
| 33 | 33 |
end |
| 34 | 34 | |
| 35 | 35 |
class WikiToolbar < Redmine::WikiToolbar |
| 36 |
def buttons |
|
| 36 |
def format |
|
| 37 |
'common_mark' |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def button_groups |
|
| 37 | 41 |
[ |
| 38 |
{name: 'strong', action: 'wrap', prefix: '**', shortcut: 'b'},
|
|
| 39 |
{name: 'em', action: 'wrap', prefix: '*', shortcut: 'i'},
|
|
| 40 |
{name: 'ins', action: 'wrap', prefix: '<u>', suffix: '</u>', shortcut: 'u'},
|
|
| 41 |
{name: 'del', action: 'wrap', prefix: '~~'},
|
|
| 42 |
{name: 'code', action: 'wrap', prefix: '`'},
|
|
| 43 |
SPACER, |
|
| 44 |
{name: 'h1', action: 'wrapLines', prefix: '# ', strip: '^#+\s+'},
|
|
| 45 |
{name: 'h2', action: 'wrapLines', prefix: '## ', strip: '^#+\s+'},
|
|
| 46 |
{name: 'h3', action: 'wrapLines', prefix: '### ', strip: '^#+\s+'},
|
|
| 47 |
SPACER, |
|
| 48 |
{name: 'ul', action: 'wrapLines', pattern: '[#-]?\s*', replacement: '* '},
|
|
| 49 |
{name: 'ol', action: 'wrapLines', pattern: '[*-]?\s*', replacement: '1. '},
|
|
| 50 |
{name: 'tl', action: 'wrapLines', pattern: '[*-]?\s*', replacement: '* [ ] '},
|
|
| 51 |
SPACER, |
|
| 52 |
{name: 'bq', action: 'wrapLines', pattern: '( *)([^\n]*)', replacement: '> $2$3'},
|
|
| 53 |
{name: 'unbq', action: 'wrapLines', pattern: ' *(> ?)?( *)([^\n]*)', replacement: '$3$4'},
|
|
| 54 |
{name: 'table', menu: :table, header_cell: '%{name} ', separator_cell: '--'},
|
|
| 55 |
{name: 'pre', action: 'wrapLines', prefix: "```\n", suffix: "\n```"},
|
|
| 56 |
{name: 'precode', menu: :precode, prefix: "``` %{lang}\n", suffix: "\n```\n"},
|
|
| 57 |
SPACER, |
|
| 58 |
{name: 'link', action: 'wrap', prefix: '[[', suffix: ']]'},
|
|
| 59 |
{name: 'img', action: 'wrap', prefix: ''},
|
|
| 60 |
SPACER, |
|
| 61 |
{name: 'help', action: 'help'}
|
|
| 42 |
%w[strong em ins del code], |
|
| 43 |
%w[h1 h2 h3], |
|
| 44 |
%w[ul ol tl], |
|
| 45 |
%w[bq unbq table pre precode], |
|
| 46 |
%w[link img], |
|
| 47 |
%w[help] |
|
| 62 | 48 |
] |
| 63 | 49 |
end |
| 64 | 50 |
end |
| lib/redmine/wiki_formatting/textile/helper.rb | ||
|---|---|---|
| 33 | 33 |
end |
| 34 | 34 | |
| 35 | 35 |
class WikiToolbar < Redmine::WikiToolbar |
| 36 |
def buttons |
|
| 36 |
def format |
|
| 37 |
'textile' |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def button_groups |
|
| 37 | 41 |
[ |
| 38 |
{name: 'strong', action: 'wrap', prefix: '*', shortcut: 'b'},
|
|
| 39 |
{name: 'em', action: 'wrap', prefix: '_', shortcut: 'i'},
|
|
| 40 |
{name: 'ins', action: 'wrap', prefix: '+', shortcut: 'u'},
|
|
| 41 |
{name: 'del', action: 'wrap', prefix: '-'},
|
|
| 42 |
{name: 'code', action: 'wrap', prefix: '@'},
|
|
| 43 |
SPACER, |
|
| 44 |
{name: 'h1', action: 'wrapLines', prefix: 'h1. ', strip: '^h\d+\.\s+'},
|
|
| 45 |
{name: 'h2', action: 'wrapLines', prefix: 'h2. ', strip: '^h\d+\.\s+'},
|
|
| 46 |
{name: 'h3', action: 'wrapLines', prefix: 'h3. ', strip: '^h\d+\.\s+'},
|
|
| 47 |
SPACER, |
|
| 48 |
{name: 'ul', action: 'wrapLines', pattern: '[#-]?\s*', replacement: '* '},
|
|
| 49 |
{name: 'ol', action: 'wrapLines', pattern: '[*-]?\s*', replacement: '# '},
|
|
| 50 |
SPACER, |
|
| 51 |
{name: 'bq', action: 'wrapLines', pattern: '( *)([^\n]*)', replacement: '> $2$3'},
|
|
| 52 |
{name: 'unbq', action: 'wrapLines', pattern: ' *(> ?)?( *)([^\n]*)', replacement: '$3$4'},
|
|
| 53 |
{name: 'table', menu: :table, header_cell: '_.%{name}'},
|
|
| 54 |
{name: 'pre', action: 'wrapLines', prefix: "<pre>\n", suffix: "\n</pre>"},
|
|
| 55 |
{name: 'precode', menu: :precode, prefix: "<pre><code class=\"%{lang}\">\n", suffix: "\n</code></pre>\n"},
|
|
| 56 |
SPACER, |
|
| 57 |
{name: 'link', action: 'wrap', prefix: '[[', suffix: ']]'},
|
|
| 58 |
{name: 'img', action: 'wrap', prefix: '!', suffix: '!'},
|
|
| 59 |
SPACER, |
|
| 60 |
{name: 'help', action: 'help'}
|
|
| 42 |
%w[strong em ins del code], |
|
| 43 |
%w[h1 h2 h3], |
|
| 44 |
%w[ul ol], |
|
| 45 |
%w[bq unbq table pre precode], |
|
| 46 |
%w[link img], |
|
| 47 |
%w[help] |
|
| 61 | 48 |
] |
| 62 | 49 |
end |
| 63 | 50 |
end |
| lib/redmine/wiki_toolbar.rb | ||
|---|---|---|
| 20 | 20 |
module Redmine |
| 21 | 21 |
# Renders the wiki toolbar handled by the wiki-toolbar Stimulus controller. |
| 22 | 22 |
# |
| 23 |
# Text formatters subclass it and describe their buttons in #buttons. Each |
|
| 24 |
# button is a Hash with: |
|
| 25 |
# * :name - the button name, used for the "jstb_<name>" CSS class |
|
| 26 |
# * :action - the controller action: wrap, wrapLines or help |
|
| 27 |
# * :menu - :precode or :table, opens a menu instead of running an action |
|
| 28 |
# * :shortcut - optional key used with Ctrl (Cmd on macOS) |
|
| 29 |
# * any other key is passed to the action as a Stimulus action parameter |
|
| 30 |
# |
|
| 31 |
# Use SPACER to separate groups of buttons. |
|
| 23 |
# The toolbar only describes the layout: which buttons are displayed and in |
|
| 24 |
# which groups. What each button inserts in the text is defined for each text |
|
| 25 |
# formatter in app/javascript/wiki_formatting/<format>.js. |
|
| 32 | 26 |
class WikiToolbar |
| 33 |
SPACER = :spacer |
|
| 34 | ||
| 35 | 27 |
LABELS = {
|
| 36 | 28 |
'strong' => :label_wiki_toolbar_strong, |
| 37 | 29 |
'em' => :label_wiki_toolbar_italic, |
| ... | ... | |
| 54 | 46 |
'help' => :label_wiki_toolbar_help |
| 55 | 47 |
}.freeze |
| 56 | 48 | |
| 57 |
BUTTON_KEYS = [:name, :action, :menu, :shortcut, :label].freeze |
|
| 58 |
# Button options used to render the menu instead of being action parameters |
|
| 59 |
MENU_KEYS = {precode: [:prefix, :suffix]}.freeze
|
|
| 49 |
SHORTCUTS = {'strong' => 'b', 'em' => 'i', 'ins' => 'u'}.freeze
|
|
| 50 |
MENUS = %w[table precode].freeze |
|
| 60 | 51 |
TABLE_MENU_ROWS = 5 |
| 61 | 52 |
TABLE_MENU_COLS = 10 |
| 53 |
KEYBOARD_ACTIONS = 'keydown->wiki-toolbar#shortcut ' \ |
|
| 54 |
'keydown.ctrl+shift+p@window->wiki-toolbar#togglePreview ' \ |
|
| 55 |
'keydown.meta+shift+p@window->wiki-toolbar#togglePreview' |
|
| 62 | 56 | |
| 63 | 57 |
# Helper methods shared by the text formatters that use the toolbar |
| 64 | 58 |
module Helper |
| ... | ... | |
| 81 | 75 |
@view = view |
| 82 | 76 |
end |
| 83 | 77 | |
| 84 |
def buttons |
|
| 85 |
[] |
|
| 78 |
# Name of the text formatter, used to load its syntax in JavaScript |
|
| 79 |
def format |
|
| 80 |
raise NotImplementedError |
|
| 81 |
end |
|
| 82 | ||
| 83 |
# Groups of buttons, separated by spacers |
|
| 84 |
def button_groups |
|
| 85 |
raise NotImplementedError |
|
| 86 | 86 |
end |
| 87 | 87 | |
| 88 | 88 |
def render(field_id, preview_url) |
| 89 | 89 |
tag.div(class: 'jstBlock', hidden: true, |
| 90 | 90 |
data: {
|
| 91 | 91 |
controller: 'wiki-toolbar', |
| 92 |
action: keyboard_actions, |
|
| 92 |
action: KEYBOARD_ACTIONS, |
|
| 93 |
wiki_toolbar_format_value: format, |
|
| 93 | 94 |
wiki_toolbar_field_id_value: field_id, |
| 94 | 95 |
wiki_toolbar_preview_url_value: preview_url, |
| 95 | 96 |
wiki_toolbar_help_url_value: @view.help_wiki_syntax_path |
| ... | ... | |
| 132 | 133 |
end |
| 133 | 134 | |
| 134 | 135 |
def rendered_buttons |
| 135 |
@rendered_buttons ||= @view.safe_join(buttons.map {|button| render_button(button)})
|
|
| 136 |
end |
|
| 137 | ||
| 138 |
def render_button(button) |
|
| 139 |
return tag.span(" ", class: 'jstSpacer') if button == SPACER
|
|
| 140 | ||
| 141 |
name = button[:name] |
|
| 142 |
title = l(button[:label] || LABELS.fetch(name)) |
|
| 143 |
data = button_params(button) |
|
| 144 |
if button[:menu] |
|
| 145 |
data[:action] = 'wiki-toolbar#toggleMenu' |
|
| 146 |
data[:wiki_toolbar_menu_param] = button[:menu] |
|
| 147 |
else |
|
| 148 |
data[:action] = "wiki-toolbar##{button[:action]}"
|
|
| 149 |
end |
|
| 150 |
data[:wiki_toolbar_shortcut] = button[:shortcut] if button[:shortcut] |
|
| 136 |
@rendered_buttons ||= @view.safe_join( |
|
| 137 |
button_groups.map {|names| @view.safe_join(names.map {|name| render_button(name)})},
|
|
| 138 |
tag.span("\u00a0", class: 'jstSpacer')
|
|
| 139 |
) |
|
| 140 |
end |
|
| 141 | ||
| 142 |
def render_button(name) |
|
| 143 |
title = l(LABELS.fetch(name)) |
|
| 144 |
data = |
|
| 145 |
if MENUS.include?(name) |
|
| 146 |
{action: 'wiki-toolbar#toggleMenu', wiki_toolbar_menu_param: name}
|
|
| 147 |
elsif name == 'help' |
|
| 148 |
{action: 'wiki-toolbar#help'}
|
|
| 149 |
else |
|
| 150 |
{action: 'wiki-toolbar#apply', wiki_toolbar_button_param: name}
|
|
| 151 |
end |
|
| 152 |
data[:wiki_toolbar_shortcut] = SHORTCUTS[name] if SHORTCUTS[name] |
|
| 151 | 153 | |
| 152 | 154 |
tag.button(tag.span(title), type: 'button', class: "jstb_#{name}", title: title, tabindex: 200, data: data)
|
| 153 | 155 |
end |
| 154 | 156 | |
| 155 |
# Converts the button options to Stimulus action parameters |
|
| 156 |
def button_params(button) |
|
| 157 |
button.except(*BUTTON_KEYS, *MENU_KEYS[button[:menu]]).transform_keys {|key| :"wiki_toolbar_#{key}_param"}
|
|
| 158 |
end |
|
| 159 | ||
| 160 | 157 |
def menus |
| 161 |
@menus ||= begin |
|
| 162 |
templates = buttons.filter_map do |button| |
|
| 163 |
next unless button.is_a?(Hash) && button[:menu] |
|
| 164 | ||
| 165 |
tag.template(data: {wiki_toolbar_target: "#{button[:menu]}Menu"}) do
|
|
| 166 |
send(:"#{button[:menu]}_menu", button)
|
|
| 167 |
end |
|
| 158 |
@menus ||= @view.safe_join( |
|
| 159 |
MENUS.map do |name| |
|
| 160 |
tag.template(data: {wiki_toolbar_target: "#{name}Menu"}) {send(:"#{name}_menu")}
|
|
| 168 | 161 |
end |
| 169 |
@view.safe_join(templates) |
|
| 170 |
end |
|
| 162 |
) |
|
| 171 | 163 |
end |
| 172 | 164 | |
| 173 |
# Lists the languages for code highlighting. Each item wraps the selected |
|
| 174 |
# lines using the :prefix and :suffix of the button, where %{lang} is
|
|
| 175 |
# replaced by the language. |
|
| 176 |
def precode_menu(button) |
|
| 165 |
def precode_menu |
|
| 177 | 166 |
tag.ul(class: 'jstMenu', role: 'menu', |
| 178 | 167 |
data: {action: 'click->wiki-toolbar#closeMenu keydown.esc->wiki-toolbar#closeMenu'}) do
|
| 179 | 168 |
@view.safe_join( |
| 180 | 169 |
code_languages.map do |lang| |
| 181 | 170 |
tag.li(role: 'none') do |
| 182 | 171 |
tag.button(lang, type: 'button', role: 'menuitem', |
| 183 |
data: {
|
|
| 184 |
action: 'wiki-toolbar#wrapLines', |
|
| 185 |
wiki_toolbar_prefix_param: format(button[:prefix], lang: lang), |
|
| 186 |
wiki_toolbar_suffix_param: format(button[:suffix], lang: lang) |
|
| 187 |
}) |
|
| 172 |
data: {action: 'wiki-toolbar#insertPrecode', wiki_toolbar_lang_param: lang})
|
|
| 188 | 173 |
end |
| 189 | 174 |
end |
| 190 | 175 |
) |
| 191 | 176 |
end |
| 192 | 177 |
end |
| 193 | 178 | |
| 194 |
def table_menu(_button)
|
|
| 179 |
def table_menu |
|
| 195 | 180 |
tag.table(class: 'table-generator', role: 'grid', |
| 196 | 181 |
data: {
|
| 197 | 182 |
action: 'click->wiki-toolbar#closeMenu keydown.esc->wiki-toolbar#closeMenu ' \ |
| ... | ... | |
| 221 | 206 |
def code_languages |
| 222 | 207 |
User.current.pref.toolbar_language_options.to_s.split(',')
|
| 223 | 208 |
end |
| 224 | ||
| 225 |
def keyboard_actions |
|
| 226 |
@keyboard_actions ||= begin |
|
| 227 |
keys = buttons.filter_map {|button| button[:shortcut] if button.is_a?(Hash)}
|
|
| 228 |
actions = keys.flat_map do |key| |
|
| 229 |
%w[ctrl meta].map {|modifier| "keydown.#{modifier}+#{key}->wiki-toolbar#shortcut"}
|
|
| 230 |
end |
|
| 231 |
actions += %w[ctrl meta].map {|modifier| "keydown.#{modifier}+shift+p@window->wiki-toolbar#togglePreview"}
|
|
| 232 |
actions.join(' ')
|
|
| 233 |
end |
|
| 234 |
end |
|
| 235 | 209 |
end |
| 236 | 210 |
end |
| test/integration/layout_test.rb | ||
|---|---|---|
| 62 | 62 |
assert_include "var wikiImageMimeTypes = #{Redmine::MimeType.by_type('image').to_json};", response.body
|
| 63 | 63 |
assert_select 'div.jstBlock[data-wiki-toolbar-field-id-value=issue_description]' do |
| 64 | 64 |
UserPreference::DEFAULT_TOOLBAR_LANGUAGE_OPTIONS.each do |lang| |
| 65 |
assert_select 'template[data-wiki-toolbar-target=precodeMenu]', :html => /data-wiki-toolbar-prefix-param="``` #{lang}/
|
|
| 65 |
assert_select 'template[data-wiki-toolbar-target=precodeMenu]', :html => /data-wiki-toolbar-lang-param="#{lang}"/
|
|
| 66 | 66 |
end |
| 67 | 67 |
end |
| 68 | 68 |
end |
| test/unit/lib/redmine/wiki_toolbar_test.rb | ||
|---|---|---|
| 50 | 50 |
with_settings :text_formatting => 'common_mark' do |
| 51 | 51 |
html = wikitoolbar_for('issue_description')
|
| 52 | 52 | |
| 53 |
assert_select_in html, 'div.jstBlock[data-wiki-toolbar-format-value=common_mark]' |
|
| 53 | 54 |
assert_select_in html, 'div.jstElements' do |
| 54 | 55 |
assert_select 'button', 19 |
| 55 | 56 |
assert_select 'span.jstSpacer', 5 |
| 56 |
assert_select 'button.jstb_strong[title=?][data-action=?][data-wiki-toolbar-prefix-param=?][data-wiki-toolbar-shortcut=b]', |
|
| 57 |
'Strong', 'wiki-toolbar#wrap', '**' |
|
| 58 |
assert_select 'button.jstb_ins[data-wiki-toolbar-prefix-param=?][data-wiki-toolbar-suffix-param=?]', '<u>', '</u>' |
|
| 59 |
assert_select 'button.jstb_h1[data-action=?][data-wiki-toolbar-prefix-param=?][data-wiki-toolbar-strip-param=?]', |
|
| 60 |
'wiki-toolbar#wrapLines', '# ', '^#+\s+' |
|
| 61 |
assert_select 'button.jstb_tl[data-wiki-toolbar-replacement-param=?]', '* [ ] ' |
|
| 62 |
assert_select 'button.jstb_table[data-action=?][data-wiki-toolbar-menu-param=table][data-wiki-toolbar-separator-cell-param=?]', |
|
| 63 |
'wiki-toolbar#toggleMenu', '--' |
|
| 64 |
assert_select 'button.jstb_precode[data-wiki-toolbar-menu-param=precode]:not([data-wiki-toolbar-prefix-param])' |
|
| 57 |
assert_select 'button.jstb_strong[title=?][data-action=?][data-wiki-toolbar-button-param=strong][data-wiki-toolbar-shortcut=b]', |
|
| 58 |
'Strong', 'wiki-toolbar#apply' |
|
| 59 |
assert_select 'button.jstb_tl[data-wiki-toolbar-button-param=tl]' |
|
| 60 |
assert_select 'button.jstb_table[data-action=?][data-wiki-toolbar-menu-param=table]', 'wiki-toolbar#toggleMenu' |
|
| 61 |
assert_select 'button.jstb_precode[data-action=?][data-wiki-toolbar-menu-param=precode]', 'wiki-toolbar#toggleMenu' |
|
| 65 | 62 |
assert_select 'button.jstb_help[data-action=?]', 'wiki-toolbar#help' |
| 66 | 63 |
end |
| 67 | 64 |
end |
| ... | ... | |
| 71 | 68 |
with_settings :text_formatting => 'textile' do |
| 72 | 69 |
html = wikitoolbar_for('issue_description')
|
| 73 | 70 | |
| 71 |
assert_select_in html, 'div.jstBlock[data-wiki-toolbar-format-value=textile]' |
|
| 74 | 72 |
assert_select_in html, 'div.jstElements' do |
| 75 | 73 |
assert_select 'button', 18 |
| 76 | 74 |
assert_select 'button.jstb_tl', 0 |
| 77 |
assert_select 'button.jstb_strong[data-wiki-toolbar-prefix-param=?]', '*' |
|
| 78 |
assert_select 'button.jstb_ol[data-wiki-toolbar-replacement-param=?]', '# ' |
|
| 79 |
assert_select 'button.jstb_table[data-wiki-toolbar-header-cell-param=?]:not([data-wiki-toolbar-separator-cell-param])', '_.%{name}'
|
|
| 80 | 75 |
end |
| 81 | 76 |
end |
| 82 | 77 |
end |
| ... | ... | |
| 96 | 91 |
html = wikitoolbar_for('issue_description')
|
| 97 | 92 |
actions = Nokogiri::HTML.fragment(html).at_css('div.jstBlock')['data-action'].split
|
| 98 | 93 | |
| 99 |
%w[b i u].each do |key| |
|
| 100 |
assert_include "keydown.ctrl+#{key}->wiki-toolbar#shortcut", actions
|
|
| 101 |
assert_include "keydown.meta+#{key}->wiki-toolbar#shortcut", actions
|
|
| 102 |
end |
|
| 103 |
assert_include 'keydown.ctrl+shift+p@window->wiki-toolbar#togglePreview', actions |
|
| 104 |
assert_include 'keydown.meta+shift+p@window->wiki-toolbar#togglePreview', actions |
|
| 94 |
assert_equal ['keydown->wiki-toolbar#shortcut', |
|
| 95 |
'keydown.ctrl+shift+p@window->wiki-toolbar#togglePreview', |
|
| 96 |
'keydown.meta+shift+p@window->wiki-toolbar#togglePreview'], actions |
|
| 97 |
assert_select_in html, 'button[data-wiki-toolbar-shortcut]', 3 |
|
| 105 | 98 |
end |
| 106 | 99 |
end |
| 107 | 100 | |
| ... | ... | |
| 113 | 106 |
html = wikitoolbar_for('issue_description')
|
| 114 | 107 | |
| 115 | 108 |
assert_select_in html, 'template[data-wiki-toolbar-target=precodeMenu] ul.jstMenu[role=menu]' do |
| 116 |
assert_select 'button[role=menuitem][data-action=?]', 'wiki-toolbar#wrapLines', :count => 2
|
|
| 109 |
assert_select 'button[role=menuitem]', 2
|
|
| 117 | 110 |
end |
| 118 |
item = Nokogiri::HTML.fragment(html).at_css('ul.jstMenu button')
|
|
| 119 |
assert_equal 'ruby', item.text |
|
| 120 |
assert_equal %(<pre><code class="ruby">\n), item['data-wiki-toolbar-prefix-param'] |
|
| 121 |
assert_equal "\n</code></pre>\n", item['data-wiki-toolbar-suffix-param'] |
|
| 111 |
assert_select_in html, 'ul.jstMenu button[data-action=?][data-wiki-toolbar-lang-param=ruby]', 'wiki-toolbar#insertPrecode', :text => 'ruby' |
|
| 122 | 112 |
end |
| 123 | 113 |
end |
| 124 | 114 | |