Project

General

Profile

Feature #43259 » 0001-Define-the-wiki-toolbar-markup-in-JavaScript-for-eac.patch

Marius BĂLTEANU, 2026-10-28 22:52

View differences:

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: '![](', suffix: ')'},
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

  
(4-4/8)