ruby-on-rails Rails JavaScript delegation for tinymce-rails in a nested fields partial

g52tjvyc  于 2023-11-20  发布在  Ruby
关注(0)|答案(1)|浏览(90)

在Rails 7.1引擎中使用tinymce-rails gem,我有一个form partial和一个动态添加的嵌套字段form _page_section_fields partial,嵌套字段form有一个文本区域,它使用了tinymce WYSIWYG编辑器,这适用于页面部分的现有字段,但不适用于动态添加的部分。表单有一个添加字段链接,可以动态添加新的嵌套字段表单,正是这个功能无法显示编辑器。我需要找到一种方法来将<%=tinymce :try%> erb JavaScript委托给容器表单中的嵌套字段div。我可能会将tinymce erb标记和相应的yml config替换为脚本标记,例如。

<script type="text/javascript">

  tinymce.init({
    selector: 'tinymce' //etc...
  });
</script>

字符串
但还是不知道这有什么好处。
_form.html.erb表单的基本部分是

<%= content_for :admin_head do %>
      <%= tinymce_assets %>
      <%= javascript_import_module_tag 'ccs_cms/custom_page/nested_fields/addFields' %>
      <%= javascript_import_module_tag 'ccs_cms/custom_page/nested_fields/removeFields' %>
    <% end %>

<fieldset>
  <legend>Page sections:</legend>
  <div id="nested-fields"> // I need to delegate tinymce to this div somehow
    <%=form.fields_for :page_sections do |page_section_form|%>
      <%= render 'page_section_fields', form: page_section_form %>
    <%end%>
    <%= link_to_add_fields "Add Section", form, :page_sections %>
  </div>
</fieldset>


部分named _page_section_fields.html.erb的字段

<div id="nested-fields">
  <p></p>
  <section>
    <fieldset>
      <legend> Page Section </legend>
      <%= form.hidden_field :_destroy %>

      <div class="cms-admin-field">
        <%= form.label :content %>:
        <%= form.text_area :content, class: "tinymce" %>
      </div>

      <%=tinymce :try%> //How do I delegate this to nested-fields div?

      <div class="cms-admin-field">
        <%= form.label :collapsed_header_text %>:
        <%= form.text_field :collapsed_header_text, editor: { template: :classic, type: :classic } %>
      </div>
      <div class="cms-admin-field">
        <%= form.label :include_contact_form %>:
        <%= form.check_box :include_contact_form %>
      </div>
      <div class="cms-admin-field">
        <%= form.label :collapsible %>:
        <%= form.check_box :collapsible %>
      </div>
      <div class="cms-admin-field">
        <%= form.label :has_borders %>:
        <%= form.check_box :has_borders %>
      </div>
      <div class="cms-admin-field">
        <%= form.label :full_width %>:
        <%= form.check_box :full_width %>
      </div>
      <div>
        <%= link_to "Remove", '#', class: "remove_fields" %>
      </div>
    </fieldset>
  </section>
</div>


link_to_add_fields rails帮助器

def link_to_add_fields(name, f, association)
    new_object = f.object.send(association).klass.new

    # Saves the unique ID of the object into a variable.
    # This is needed to ensure the key of the associated array is unique. This is makes parsing the content in the `data-fields` attribute easier through Javascript.
    # We could use another method to achive this.
    id = new_object.object_id

    # https://api.rubyonrails.org/ fields_for(record_name, record_object = nil, fields_options = {}, &block)
    # record_name = :page_sections
    # record_object = new_object
    # fields_options = { child_index: id }
    # child_index` is used to ensure the key of the associated array is unique, and that it matched the value in the `data-id` attribute.
    # `page[page_sections_attributes][child_index_value][_destroy]`
    fields =
      f.fields_for(association, new_object, child_index: id) do |builder|
        # `association.to_s.singularize + "_fields"` ends up evaluating to `page_sections_fields`
        # The render function will then look for `views/pages/_page_sections_fields.html.erb`
        # The render function also needs to be passed the value of 'builder', because `views/pages/_page_sections_fields.html.erb` needs this to render the form tags.
        render(association.to_s.singularize + "_fields", form: builder)
      end

    # This renders a simple link, but passes information into `data` attributes.
    # This info can be named anything we want, but in this case we chose `data-id:` and `data-fields:`.
    # The `id:` is from `new_object.object_id`.
    # The `fields:` are rendered from the `fields` blocks.
    # We use `gsub("\n", "")` to remove anywhite space from the rendered partial.
    # The `id:` value needs to match the value used in `child_index: id`.
    link_to(
      name,
      "#",
      class: "add_fields",
      data: {
        id: id,
        fields: fields.gsub("\n", ""),
      },
    )
  end


添加部分的JavaScript,也许这就是委托所属的地方,也许不是!

class addFields {
  // This executes when the function is instantiated.
  constructor() {
    this.links = document.querySelectorAll(".add_fields");
    this.iterateLinks();
  }

  iterateLinks() {
    // If there are no links on the page, stop the function from executing.
    if (this.links.length === 0) return;
    // Loop over each link on the page. A page could have multiple nested forms.
    this.links.forEach((link) => {
      link.addEventListener("click", (e) => {
        this.handleClick(link, e);
      });
    });
  }

  handleClick(link, e) {
    // Stop the function from executing if a link or event were not passed into the function.
    if (!link || !e) return;
    // Prevent the browser from following the URL.
    e.preventDefault();
    // Save a unique timestamp to ensure the key of the associated array is unique.
    let time = new Date().getTime();
    // Save the data id attribute into a variable. This corresponds to `new_object.object_id`.
    let linkId = link.dataset.id;
    // Create a new regular expression needed to find any instance of the `new_object.object_id` used in the fields data attribute if there's a value in `linkId`.
    let regexp = linkId ? new RegExp(linkId, "g") : null;
    // Replace all instances of the `new_object.object_id` with `time`, and save markup into a variable if there's a value in `regexp`.
    let newFields = regexp ? link.dataset.fields.replace(regexp, time) : null;
    // Add the new markup to the form if there are fields to add.
    newFields ? link.insertAdjacentHTML("beforebegin", newFields) : null;

  }
}

document.addEventListener('DOMContentLoaded', function() {
  new addFields();
});


tinymce有一个event_root选项,它应该做我需要的事情,但它只适用于我不使用的行编辑模式
tinymce,yml配置如下所示

try:
  event_root: '#nested-fields'
  menubar: file edit view insert format tools table help

  toolbar:
    - undo redo | accordion accordionremove | blocks fontfamily fontsize | bold italic underline strikethrough | align numlist bullist
    - link image | table media | lineheight outdent indent| forecolor backcolor removeformat charmap emoticons code fullscreen preview save print | pagebreak codesample | ltr rtl
  toolbar_mode: sliding

  contextmenu: link image table
  quickbars_selection_toolbar: bold italic | quicklink h2 h3 blockquote quickimage quicktable

  plugins:
    - preview importcss searchreplace autolink autosave save directionality code
    - visualblocks visualchars fullscreen image link media template codesample
    - table charmap pagebreak nonbreaking insertdatetime advlist lists
    - wordcount help charmap quickbars emoticons accordion

  promotion: false

#  useDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
#  isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches
#  skin: useDarkMode ? 'oxide-dark' : 'oxide'
#  content_css: useDarkMode ? 'dark' : 'default'

  autosave_ask_before_unload: true
  autosave_interval: 30s
  autosave_prefix: 'tinymce-autosave-{path}{query}-{id}-'
  autosave_restore_when_empty: true
  autosave_retention: 30m

  image_caption: true
  image_advtab: true
  image_class_list: [
    { title: 'None', value: '' },
    { title: 'Drop shadow', value: 'shadow' }
  ]


如果重组任何这些使解决方案更容易实现,那么没关系。我总是愿意学习更好的做事方法。
我应该补充说,这个功能是在一个引擎,但这不应该影响问题或解决方案。

lndjwyie

lndjwyie1#

# app/models/page.rb
class Page < ApplicationRecord
  has_many :page_sections
  accepts_nested_attributes_for :page_sections
end

# app/models/page_section.rb
class PageSection < ApplicationRecord
  belongs_to :page
end

x

# app/helpers/application_helper.rb

module ApplicationHelper
  def link_to_add_fields(name, f, association)
    association_class = f.object.class.reflect_on_association(association).klass

    template = f.fields_for association, association_class.new, child_index: "__CHILD_INDEX__" do |ff|
      # UPDATE: you're right, fields partial should be based on
      #         the association name, not the class name.
      # render "#{association_class.model_name.singular}_fields", f: ff
      render "#{association.to_s.singularize}_fields", f: ff
    end

    link_to name, "#", class: "add_fields", data: {template:}
  end
end
# _form.html.erb

<%= tinymce_assets %> # this gem isn't really required 

<%= form_with model: Page.new do |f| %>
  <%= f.fields_for :page_sections do |ff| %>
    <%= render "page_section_fields", f: ff %>
  <% end %>
  <%= link_to_add_fields "Add Section", f, :page_sections %>

  <%= f.submit %>
<% end %>
# _page_section_fields.html.erb

<%= f.text_area :content, class: "tinymce" %>

的数据
你并不是真正的委派,你是在元素上初始化事件监听器,这样你的点击就可以添加新的字段。委派时,你设置一个事件监听器,然后弄清楚当事件被分派时是否需要做一些事情。

// app/javascript/application.js

document.addEventListener("click", function(event) {
  // this is delegation
  if (event.target.matches(".add_fields")) {
    addFields(event);
  }
});

// forget yaml config, just do it here
const tinyConfig = {
  event_root: null,
  selector: ".tinymce",
  menubar: "file edit view insert format tools table help",
  toolbar: ["undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | align numlist bullist","link image | accordion accordionremove | table media | lineheight outdent indent| forecolor backcolor removeformat charmap emoticons code fullscreen preview save print | pagebreak codesample | ltr rtl"],
  toolbar_mode: "sliding",
  contextmenu: "link image table",
  quickbars_selection_toolbar: "bold italic | quicklink h2 h3 blockquote quickimage quicktable",
  plugins: "preview importcss searchreplace autolink autosave save directionality code,visualblocks visualchars fullscreen image link media codesample,table charmap pagebreak nonbreaking insertdatetime advlist lists,wordcount help charmap quickbars emoticons accordion",
  promotion: false,
  autosave_ask_before_unload: true,
  autosave_interval: "30s",
  autosave_prefix: "tinymce-autosave-{path}{query}-{id}-",
  autosave_restore_when_empty: true,
  autosave_retention: "30m",
  image_caption: true,
  image_advtab: true,
  image_class_list: [{"title":"None","value":""},{"title":"Drop shadow","value":"shadow"}]
}

function addFields(event) {
  event.preventDefault();
  const { target } = event;
  const template = target.dataset.template.replace(/__CHILD_INDEX__/g, new Date().getTime().toString())
  target.insertAdjacentHTML("beforebegin", template)

  // initialize tinymce
  tinymce.init(tinyConfig);
}


另一种方法是使用观察者,当事情变得复杂时,只有这么多的地方可以插入各种init函数来保持事情“活着”。这可能是这种情况下的过度使用:

// app/javascript/application.js

const observer = new MutationObserver((mutationList) => {
  mutationList.forEach((mutation) => {
    if (mutation.type == "childList") {
      mutation.addedNodes.forEach((node) => {
        if (node instanceof Element) {

          if (node.matches(".tinymce")) {
            tinymce.init(tinyConfig);
          }

        }
      });
    }
  });
});

observer.observe(document.body, { childList: true, subtree: true });

要在页面加载时初始化编辑器,您必须单独执行:

// app/javascript/application.js

document.addEventListener("DOMContentLoaded", function() {
  tinymce.init(tinyConfig);
});


如果你使用的是importmaps,你可以将配置设置为全局的:

// app/javascript/application.js

window.tinyConfig = {
  event_root: null,
  selector: ".tinymce",
  // ...
}


然后从内联脚本中使用它:

<script type="module">
  tinymce.init(tinyConfig);
</script>

相关问题