Feature #43950 » 0002-Add-support-for-pasting-spreadsheet-tables-as-CommonMark-Textile-tables-in-wiki-textareas.patch
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1392 | 1392 |
end |
| 1393 | 1393 |
end |
| 1394 | 1394 | |
| 1395 |
def list_autofill_data_attributes
|
|
| 1395 |
def wiki_textarea_stimulus_attributes
|
|
| 1396 | 1396 |
return {} if Setting.text_formatting.blank?
|
| 1397 | 1397 | |
| 1398 | 1398 |
{
|
| 1399 |
controller: 'list-autofill', |
|
| 1400 |
action: 'beforeinput->list-autofill#handleBeforeInput', |
|
| 1401 |
list_autofill_text_formatting_param: Setting.text_formatting |
|
| 1399 |
controller: 'list-autofill table-paste', |
|
| 1400 |
action: 'beforeinput->list-autofill#handleBeforeInput paste->table-paste#handlePaste', |
|
| 1401 |
list_autofill_text_formatting_param: Setting.text_formatting, |
|
| 1402 |
table_paste_text_formatting_param: Setting.text_formatting |
|
| 1402 | 1403 |
} |
| 1403 | 1404 |
end |
| 1404 | 1405 | |
| app/helpers/custom_fields_helper.rb | ||
|---|---|---|
| 87 | 87 |
css += ' wiki-edit' |
| 88 | 88 |
data = {
|
| 89 | 89 |
:auto_complete => true |
| 90 |
}.merge(list_autofill_data_attributes)
|
|
| 90 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 91 | 91 |
end |
| 92 | 92 |
cf.format.edit_tag( |
| 93 | 93 |
self, |
| ... | ... | |
| 137 | 137 |
css += ' wiki-edit' |
| 138 | 138 |
data = {
|
| 139 | 139 |
:auto_complete => true |
| 140 |
}.merge(list_autofill_data_attributes)
|
|
| 140 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 141 | 141 |
end |
| 142 | 142 |
custom_field.format.bulk_edit_tag( |
| 143 | 143 |
self, |
| app/javascript/controllers/table_paste_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from '@hotwired/stimulus'
|
|
| 2 | ||
| 3 |
class CommonMarkTableFormatter {
|
|
| 4 |
format(rows) {
|
|
| 5 |
if (rows.length === 0) return null |
|
| 6 | ||
| 7 |
const output = [] |
|
| 8 |
output.push(this.#formatRow(rows[0])) |
|
| 9 | ||
| 10 |
const separator = rows[0].map(() => '--').join(' | ')
|
|
| 11 |
output.push(`| ${separator} |`)
|
|
| 12 | ||
| 13 |
for (let i = 1; i < rows.length; i++) {
|
|
| 14 |
output.push(this.#formatRow(rows[i])) |
|
| 15 |
} |
|
| 16 | ||
| 17 |
return output.join('\n')
|
|
| 18 |
} |
|
| 19 | ||
| 20 |
#formatRow(row) {
|
|
| 21 |
return `| ${row.map(cell => this.#formatCell(cell)).join(' | ')} |`
|
|
| 22 |
} |
|
| 23 | ||
| 24 |
#formatCell(cell) {
|
|
| 25 |
return cell |
|
| 26 |
.replaceAll('|', '\\|')
|
|
| 27 |
.replaceAll('\n', '<br>')
|
|
| 28 |
} |
|
| 29 |
} |
|
| 30 | ||
| 31 |
class TextileTableFormatter {
|
|
| 32 |
format(rows) {
|
|
| 33 |
if (rows.length === 0) return null |
|
| 34 | ||
| 35 |
const output = [] |
|
| 36 |
output.push(this.#formatHeader(rows[0])) |
|
| 37 | ||
| 38 |
for (let i = 1; i < rows.length; i++) {
|
|
| 39 |
output.push(this.#formatRow(rows[i])) |
|
| 40 |
} |
|
| 41 | ||
| 42 |
return output.join('\n')
|
|
| 43 |
} |
|
| 44 | ||
| 45 |
#formatHeader(row) {
|
|
| 46 |
return `|_. ${row.map(cell => this.#formatCell(cell)).join(' |_. ')} |`
|
|
| 47 |
} |
|
| 48 | ||
| 49 |
#formatRow(row) {
|
|
| 50 |
return `| ${row.map(cell => this.#formatCell(cell)).join(' | ')} |`
|
|
| 51 |
} |
|
| 52 | ||
| 53 |
#formatCell(cell) {
|
|
| 54 |
return cell.replaceAll('|', '|')
|
|
| 55 |
} |
|
| 56 |
} |
|
| 57 | ||
| 58 |
export default class extends Controller {
|
|
| 59 |
handlePaste(event) {
|
|
| 60 |
const formatter = this.#tableFormatterFor(event.params.textFormatting) |
|
| 61 |
if (!formatter) return |
|
| 62 | ||
| 63 |
const rows = this.#extractTableFromClipboard(event) |
|
| 64 |
if (!rows) return |
|
| 65 | ||
| 66 |
const table = formatter.format(rows) |
|
| 67 |
if (!table) return |
|
| 68 | ||
| 69 |
event.preventDefault() |
|
| 70 |
this.#insertTextAtCursor(event.currentTarget, table) |
|
| 71 |
} |
|
| 72 | ||
| 73 |
#tableFormatterFor(textFormatting) {
|
|
| 74 |
switch (textFormatting) {
|
|
| 75 |
case 'common_mark': |
|
| 76 |
return new CommonMarkTableFormatter() |
|
| 77 |
case 'textile': |
|
| 78 |
return new TextileTableFormatter() |
|
| 79 |
default: |
|
| 80 |
return null |
|
| 81 |
} |
|
| 82 |
} |
|
| 83 | ||
| 84 |
#extractTableFromClipboard(event) {
|
|
| 85 |
const clipboardData = event.clipboardData |
|
| 86 |
if (!clipboardData) return null |
|
| 87 | ||
| 88 |
const htmlData = clipboardData.getData('text/html')
|
|
| 89 |
if (!htmlData) return null |
|
| 90 | ||
| 91 |
return this.#extractTableFromHtml(htmlData) |
|
| 92 |
} |
|
| 93 | ||
| 94 |
#extractTableFromHtml(html) {
|
|
| 95 |
const temp = document.createElement('div')
|
|
| 96 |
temp.innerHTML = html.replace(/\r?\n/g, '') |
|
| 97 | ||
| 98 |
const table = temp.querySelector('table')
|
|
| 99 |
if (!table) return null |
|
| 100 | ||
| 101 |
const rows = [] |
|
| 102 |
table.querySelectorAll('tr').forEach(tr => {
|
|
| 103 |
const cells = [] |
|
| 104 |
tr.querySelectorAll('td, th').forEach(cell => {
|
|
| 105 |
cells.push(this.#extractCellText(cell).trim()) |
|
| 106 |
}) |
|
| 107 |
if (cells.length > 0) {
|
|
| 108 |
rows.push(cells) |
|
| 109 |
} |
|
| 110 |
}) |
|
| 111 | ||
| 112 |
return this.#normalizeRows(rows) |
|
| 113 |
} |
|
| 114 | ||
| 115 |
#normalizeRows(rows) {
|
|
| 116 |
if (rows.length === 0) return null |
|
| 117 | ||
| 118 |
const maxColumns = rows.reduce((currentMax, row) => Math.max(currentMax, row.length), 0) |
|
| 119 |
if (maxColumns < 2) return null |
|
| 120 | ||
| 121 |
rows.forEach(row => {
|
|
| 122 |
while (row.length < maxColumns) {
|
|
| 123 |
row.push('')
|
|
| 124 |
} |
|
| 125 |
}) |
|
| 126 | ||
| 127 |
return rows |
|
| 128 |
} |
|
| 129 | ||
| 130 |
#extractCellText(cell) {
|
|
| 131 |
const clone = cell.cloneNode(true) |
|
| 132 | ||
| 133 |
// Treat <br> as an in-cell line break and keep it as an internal newline |
|
| 134 |
// so each formatter can render it appropriately. |
|
| 135 |
clone.querySelectorAll('br').forEach(br => {
|
|
| 136 |
br.replaceWith('\n')
|
|
| 137 |
}) |
|
| 138 | ||
| 139 |
return clone.textContent |
|
| 140 |
} |
|
| 141 | ||
| 142 |
#insertTextAtCursor(input, text) {
|
|
| 143 |
const { selectionStart, selectionEnd } = input
|
|
| 144 | ||
| 145 |
const replacement = `${text}\n\n`
|
|
| 146 | ||
| 147 |
input.setRangeText(replacement, selectionStart, selectionEnd, 'end') |
|
| 148 |
const newCursorPos = selectionStart + replacement.length |
|
| 149 |
input.setSelectionRange(newCursorPos, newCursorPos) |
|
| 150 | ||
| 151 |
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
| 152 |
} |
|
| 153 |
} |
|
| app/views/documents/_form.html.erb | ||
|---|---|---|
| 5 | 5 |
<p><%= f.text_field :title, :required => true, :size => 60 %></p> |
| 6 | 6 |
<p><%= f.textarea :description, :cols => 60, :rows => 15, :class => 'wiki-edit', |
| 7 | 7 |
:data => {
|
| 8 |
:auto_complete => true
|
|
| 9 |
}.merge(list_autofill_data_attributes)
|
|
| 8 |
:auto_complete => true |
|
| 9 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 10 | 10 |
%></p> |
| 11 | 11 | |
| 12 | 12 |
<% @document.custom_field_values.each do |value| %> |
| app/views/issues/_edit.html.erb | ||
|---|---|---|
| 32 | 32 |
<fieldset id="add_notes"><legend><%= l(:field_notes) %></legend> |
| 33 | 33 |
<%= f.textarea :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', |
| 34 | 34 |
:data => {
|
| 35 |
:auto_complete => true
|
|
| 36 |
}.merge(list_autofill_data_attributes),
|
|
| 35 |
:auto_complete => true |
|
| 36 |
}.merge(wiki_textarea_stimulus_attributes),
|
|
| 37 | 37 |
:no_label => true %> |
| 38 | 38 |
<%= wikitoolbar_for 'issue_notes', preview_issue_path(:project_id => @project, :issue_id => @issue) %> |
| 39 | 39 | |
| app/views/issues/_form.html.erb | ||
|---|---|---|
| 36 | 36 |
<%= f.textarea :description, :cols => 60, :accesskey => accesskey(:edit), :class => 'wiki-edit', |
| 37 | 37 |
:rows => [[10, @issue.description.to_s.length / 50].max, 20].min, |
| 38 | 38 |
:data => {
|
| 39 |
:auto_complete => true
|
|
| 40 |
}.merge(list_autofill_data_attributes),
|
|
| 39 |
:auto_complete => true |
|
| 40 |
}.merge(wiki_textarea_stimulus_attributes),
|
|
| 41 | 41 |
:no_label => true %> |
| 42 | 42 |
<% end %> |
| 43 | 43 |
<%= link_to_function content_tag(:span, sprite_icon('edit', l(:button_edit))), '$(this).hide(); $("#issue_description_and_toolbar").show()', :class => 'icon icon-edit' unless @issue.new_record? %>
|
| app/views/issues/bulk_edit.html.erb | ||
|---|---|---|
| 222 | 222 |
<legend><%= l(:field_notes) %></legend> |
| 223 | 223 |
<%= textarea_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit', |
| 224 | 224 |
:data => {
|
| 225 |
:auto_complete => true
|
|
| 226 |
}.merge(list_autofill_data_attributes)
|
|
| 225 |
:auto_complete => true |
|
| 226 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 227 | 227 |
%> |
| 228 | 228 |
<%= wikitoolbar_for 'notes' %> |
| 229 | 229 | |
| app/views/journals/_notes_form.html.erb | ||
|---|---|---|
| 6 | 6 |
<%= textarea_tag 'journal[notes]', @journal.notes, :id => "journal_#{@journal.id}_notes", :class => 'wiki-edit',
|
| 7 | 7 |
:rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min), |
| 8 | 8 |
:data => {
|
| 9 |
:auto_complete => true
|
|
| 10 |
}.merge(list_autofill_data_attributes)
|
|
| 9 |
:auto_complete => true |
|
| 10 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 11 | 11 |
%> |
| 12 | 12 |
<% if @journal.safe_attribute? 'private_notes' %> |
| 13 | 13 |
<%= hidden_field_tag 'journal[private_notes]', '0' %> |
| app/views/messages/_form.html.erb | ||
|---|---|---|
| 26 | 26 |
<%= f.textarea :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content', |
| 27 | 27 |
:accesskey => accesskey(:edit), |
| 28 | 28 |
:data => {
|
| 29 |
:auto_complete => true
|
|
| 30 |
}.merge(list_autofill_data_attributes)
|
|
| 29 |
:auto_complete => true |
|
| 30 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 31 | 31 |
%></p> |
| 32 | 32 |
<%= wikitoolbar_for 'message_content', preview_board_message_path(:board_id => @board, :id => @message) %> |
| 33 | 33 |
<!--[eoform:message]--> |
| app/views/news/_form.html.erb | ||
|---|---|---|
| 12 | 12 |
<p><%= f.textarea :summary, :cols => 60, :rows => 2 %></p> |
| 13 | 13 |
<p><%= f.textarea :description, :required => true, :cols => 60, :rows => 15, :class => 'wiki-edit', |
| 14 | 14 |
:data => {
|
| 15 |
:auto_complete => true
|
|
| 16 |
}.merge(list_autofill_data_attributes)
|
|
| 15 |
:auto_complete => true |
|
| 16 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 17 | 17 |
%></p> |
| 18 | 18 |
<p id="attachments_form"><label><%= l(:label_attachment_plural) %></label><%= render :partial => 'attachments/form', :locals => {:container => @news} %></p>
|
| 19 | 19 |
</div> |
| app/views/news/show.html.erb | ||
|---|---|---|
| 70 | 70 |
<%= textarea 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit', |
| 71 | 71 |
:data => {
|
| 72 | 72 |
:auto_complete => true |
| 73 |
}.merge(list_autofill_data_attributes)
|
|
| 73 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 74 | 74 |
%> |
| 75 | 75 |
<%= wikitoolbar_for 'comment_comments', preview_news_path(:project_id => @project, :id => @news) %> |
| 76 | 76 |
</div> |
| app/views/projects/_form.html.erb | ||
|---|---|---|
| 4 | 4 |
<!--[form:project]--> |
| 5 | 5 |
<p><%= f.text_field :name, :required => true, :size => 60 %></p> |
| 6 | 6 | |
| 7 |
<p><%= f.textarea :description, :rows => 8, :class => 'wiki-edit', :data => list_autofill_data_attributes %></p>
|
|
| 7 |
<p><%= f.textarea :description, :rows => 8, :class => 'wiki-edit', :data => wiki_textarea_stimulus_attributes %></p>
|
|
| 8 | 8 |
<p><%= f.text_field :identifier, :required => true, :size => 60, :disabled => @project.identifier_frozen?, :maxlength => Project::IDENTIFIER_MAX_LENGTH %> |
| 9 | 9 |
<% unless @project.identifier_frozen? %> |
| 10 | 10 |
<em class="info"><%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> <%= l(:text_project_identifier_info).html_safe %></em> |
| app/views/settings/_general.html.erb | ||
|---|---|---|
| 3 | 3 |
<div class="box tabular settings"> |
| 4 | 4 |
<p><%= setting_text_field :app_title, :size => 30 %></p> |
| 5 | 5 | |
| 6 |
<p><%= setting_textarea :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit', :data => list_autofill_data_attributes %></p>
|
|
| 6 |
<p><%= setting_textarea :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit', :data => wiki_textarea_stimulus_attributes %></p>
|
|
| 7 | 7 |
<%= wikitoolbar_for 'settings_welcome_text' %> |
| 8 | 8 | |
| 9 | 9 | |
| app/views/settings/_notifications.html.erb | ||
|---|---|---|
| 19 | 19 |
</fieldset> |
| 20 | 20 | |
| 21 | 21 |
<fieldset class="box"><legend><%= l(:setting_emails_header) %></legend> |
| 22 |
<%= setting_textarea :emails_header, :label => false, :class => 'wiki-edit', :rows => 5, :data => list_autofill_data_attributes %>
|
|
| 22 |
<%= setting_textarea :emails_header, :label => false, :class => 'wiki-edit', :rows => 5, :data => wiki_textarea_stimulus_attributes %>
|
|
| 23 | 23 |
<%= wikitoolbar_for 'settings_emails_header' %> |
| 24 | 24 |
</fieldset> |
| 25 | 25 | |
| 26 | 26 |
<fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend> |
| 27 |
<%= setting_textarea :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5, :data => list_autofill_data_attributes %>
|
|
| 27 |
<%= setting_textarea :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5, :data => wiki_textarea_stimulus_attributes %>
|
|
| 28 | 28 |
<%= wikitoolbar_for 'settings_emails_footer' %> |
| 29 | 29 |
</fieldset> |
| 30 | 30 | |
| app/views/wiki/edit.html.erb | ||
|---|---|---|
| 16 | 16 |
<%= textarea_tag 'content[text]', @text, :cols => 100, :rows => 25, :accesskey => accesskey(:edit), |
| 17 | 17 |
:class => 'wiki-edit', |
| 18 | 18 |
:data => {
|
| 19 |
:auto_complete => true
|
|
| 20 |
}.merge(list_autofill_data_attributes)
|
|
| 19 |
:auto_complete => true |
|
| 20 |
}.merge(wiki_textarea_stimulus_attributes)
|
|
| 21 | 21 |
%> |
| 22 | 22 | |
| 23 | 23 |
<% if @page.safe_attribute_names.include?('parent_id') && @wiki.pages.any? %>
|
| test/helpers/application_helper_test.rb | ||
|---|---|---|
| 2258 | 2258 |
} |
| 2259 | 2259 |
end |
| 2260 | 2260 | |
| 2261 |
def test_list_autofill_data_attributes
|
|
| 2261 |
def test_wiki_textarea_stimulus_attributes
|
|
| 2262 | 2262 |
with_settings :text_formatting => 'textile' do |
| 2263 | 2263 |
expected = {
|
| 2264 |
controller: "list-autofill", |
|
| 2265 |
action: "keydown->list-autofill#handleEnter",
|
|
| 2266 |
list_autofill_target: "input",
|
|
| 2267 |
list_autofill_text_formatting_param: "textile"
|
|
| 2264 |
controller: "list-autofill table-paste",
|
|
| 2265 |
action: "beforeinput->list-autofill#handleBeforeInput paste->table-paste#handlePaste",
|
|
| 2266 |
list_autofill_text_formatting_param: "textile",
|
|
| 2267 |
table_paste_text_formatting_param: "textile"
|
|
| 2268 | 2268 |
} |
| 2269 | 2269 | |
| 2270 |
assert_equal expected, list_autofill_data_attributes
|
|
| 2270 |
assert_equal expected, wiki_textarea_stimulus_attributes
|
|
| 2271 | 2271 |
end |
| 2272 | 2272 |
end |
| 2273 | 2273 | |
| 2274 |
def test_list_autofill_data_attributes_with_blank_text_formatting
|
|
| 2274 |
def test_wiki_textarea_stimulus_attributes_with_blank_text_formatting
|
|
| 2275 | 2275 |
with_settings :text_formatting => '' do |
| 2276 |
assert_equal({}, list_autofill_data_attributes)
|
|
| 2276 |
assert_equal({}, wiki_textarea_stimulus_attributes)
|
|
| 2277 | 2277 |
end |
| 2278 | 2278 |
end |
| 2279 | 2279 |
end |
| test/system/table_paste_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../application_system_test_case' |
|
| 21 | ||
| 22 |
class TablePasteSystemTest < ApplicationSystemTestCase |
|
| 23 |
HTML_TABLE = <<~'HTML' |
|
| 24 |
<table> |
|
| 25 |
<tr><th>Item</th><th>Notes</th></tr> |
|
| 26 |
<tr><td>Redmine 6.1</td><td>Supports <wiki> tags</td></tr> |
|
| 27 |
<tr><td>Multi-line</td><td>First line<br>Second line | escaped</td></tr> |
|
| 28 |
<tr><td>Path value</td><td>C:\Temp\redmine & logs</td></tr> |
|
| 29 |
</table> |
|
| 30 |
HTML |
|
| 31 | ||
| 32 |
def test_paste_html_table_as_commonmark_table_in_issue_description |
|
| 33 |
with_settings text_formatting: 'common_mark' do |
|
| 34 |
log_user('jsmith', 'jsmith')
|
|
| 35 |
visit '/projects/ecookbook/issues/new' |
|
| 36 | ||
| 37 |
result = dispatch_paste(find('#issue_description'), html: HTML_TABLE)
|
|
| 38 | ||
| 39 |
assert_equal <<~'TEXT', result |
|
| 40 |
| Item | Notes | |
|
| 41 |
| -- | -- | |
|
| 42 |
| Redmine 6.1 | Supports <wiki> tags | |
|
| 43 |
| Multi-line | First line<br>Second line \| escaped | |
|
| 44 |
| Path value | C:\Temp\redmine & logs | |
|
| 45 | ||
| 46 |
TEXT |
|
| 47 |
end |
|
| 48 |
end |
|
| 49 | ||
| 50 |
def test_paste_html_table_as_textile_table_in_wiki_edit |
|
| 51 |
with_settings text_formatting: 'textile' do |
|
| 52 |
log_user('jsmith', 'jsmith')
|
|
| 53 |
visit '/projects/ecookbook/wiki/CookBook_documentation/edit' |
|
| 54 | ||
| 55 |
result = dispatch_paste(find('#content_text'), html: HTML_TABLE)
|
|
| 56 | ||
| 57 |
assert_equal <<~'TEXT', result |
|
| 58 |
|_. Item |_. Notes | |
|
| 59 |
| Redmine 6.1 | Supports <wiki> tags | |
|
| 60 |
| Multi-line | First line |
|
| 61 |
Second line | escaped | |
|
| 62 |
| Path value | C:\Temp\redmine & logs | |
|
| 63 | ||
| 64 |
TEXT |
|
| 65 |
end |
|
| 66 |
end |
|
| 67 | ||
| 68 |
private |
|
| 69 | ||
| 70 |
def dispatch_paste(field, html:) |
|
| 71 |
page.evaluate_script(<<~JS, field, html) |
|
| 72 |
((element, htmlText) => {
|
|
| 73 |
element.value = '' |
|
| 74 |
element.setSelectionRange(0, 0) |
|
| 75 | ||
| 76 |
const clipboardData = {
|
|
| 77 |
getData() {
|
|
| 78 |
return htmlText |
|
| 79 |
} |
|
| 80 |
} |
|
| 81 | ||
| 82 |
const event = new Event('paste', { bubbles: true, cancelable: true })
|
|
| 83 |
Object.defineProperty(event, 'clipboardData', { value: clipboardData })
|
|
| 84 |
element.dispatchEvent(event) |
|
| 85 | ||
| 86 |
return element.value |
|
| 87 |
})(arguments[0], arguments[1]) |
|
| 88 |
JS |
|
| 89 |
end |
|
| 90 |
end |
|
- « Previous
- 1
- …
- 5
- 6
- 7
- Next »