%3 ::xowiki::formfield::richtext ::xowiki::formfield::richtext check=safe_html editor initialize preset_conf pretty_value ::xowiki::formfield::textarea ::xowiki::formfield::textarea add_statistics clear_editor_mixins initialize render_as_div render_input set_feedback ::xowiki::formfield::richtext->::xowiki::formfield::textarea ::xowiki::formfield::FormField ::xowiki::formfield::FormField ::xowiki::formfield::textarea->::xowiki::formfield::FormField ::xowiki::formfield::richtext::tinymce ::xowiki::formfield::richtext::tinymce compute_config initialize render_input ::xowiki::formfield::richtext::tinymce->::xowiki::formfield::richtext

Class ::xowiki::formfield::richtext::tinymce

::xowiki::formfield::richtext::tinymce[i] create ... \
           [ -additionalConfig (default "") ] \
           [ -customConfig (default "") ] \
           [ -extraPlugins (default "") ]

TinyMCE XoWiki richtext-editor integration.
Documented Parameters:
extraPlugins
a list of couples 'pluginId' 'pluginURL' specifying additional plugins.
customConfig
a configuration dict that will completely override configuration coming from the parameters.
additionalConfig
a configuration dict that will be merged with configuration coming from the parameters. Values specified here will take precedence on the parameter values.
See Also:
https://www.tiny.cloud/docs/tinymce/latest/
Defined in /var/www/openacs.org/packages/xowiki/tcl/form-field-procs.tcl

Class Relations

  • class: ::xotcl::Class[i]
  • superclass: ::xowiki::formfield::richtext[i]
::xotcl::Class create ::xowiki::formfield::richtext::tinymce \
     -superclass ::xowiki::formfield::richtext

Methods (to be applied on instances)

  • additionalConfig (setter)

  • compute_config (scripted)

    #
    # Here we compute the editor config, merging requested one with
    # systems configurations and presets.
    #
    # @return a dict
    #
    if {${:customConfig} ne ""} {
      set default_config ${:customConfig}
    } else {
      set default_config [::richtext::tinymce::default_config]
    }
    
    set config [list]
    
    if {${:extraPlugins} ne ""} {
      set extra_plugins [::richtext::tinymce::serialize_options ${:extraPlugins}]
      lappend config external_plugins "{$extra_plugins}"
    }
    
    #
    # Inline means the editor will not be displayed unless we click on
    # its content. We also supported an inplace mode, where the editor
    # does something similar, but with explicit save and cancel
    # buttons underneath.
    #
    # As the difference is subtle and there is currently not a
    # requirement for inplace mode, we treat both mode the same as
    # "inline".
    #
    set inline_p [expr {${:displayMode} in {"inplace" "inline"} ? true : false}]
    lappend config inline $inline_p
    
    #
    # Inject a reference to the current object, useful e.g. for
    # plugins to know where to point to.
    #
    lappend config object_id [${:object} item_id]
    
    set config [dict merge  [list language [ad_conn language]]  $default_config  [:preset_conf]  ${:additionalConfig}  $config]
  • customConfig (setter)

  • extraPlugins (setter)

  • initialize (scripted)

    next
    set :widget_type richtext
  • render_input (scripted)

    set disabled [:is_disabled]
    set is_repeat_template [:is_repeat_template_p]
    
    #
    # Field is disabled. We simply render as a div.
    #
    if {$disabled && !$is_repeat_template} {
      :render_as_div
      return
    }
    
    set config [:compute_config]
    set inline_p [dict get $config inline]
    
    set config [::richtext::tinymce::serialize_options $config]
    
    #
    # Include the relevant javascript.
    #
    ::richtext::tinymce::add_editor -init=false
    
    if {$is_repeat_template} {
      #
      # A repeated field. We use a MutationObserver to detect whenever
      # a new field has been appended and we enhance it on the fly.
      #
      ::template::add_body_handler  -identifier richtext_tinymce_editor_init_repeat  -event load  -script {
            function richtext_tinymce_editor_init_repeat(id, containerId, editorConfig) {
              // Via this pattern we recognize fields that are
              // appended to the DOM that are relevant to our repeated
              // formfield.
              const namePattern = id.replace(/^F\.[^\.]+\./g, '').replaceAll(/\.[0-9]+/g, '.[1-9][0-9]*');
              const targetNode = document.getElementById(containerId);
              const config = { childList: true, subtree: true };
              const callback = (mutationList, observer) => {
                for (const mutation of mutationList) {
                  for (const node of mutation.addedNodes) {
                     // Skip text nodes
                     if (!node.querySelectorAll) { continue; }
                     for (const inputField of node.querySelectorAll('[name]')) {
                      // Skip things that are not instances of our field.
                      if (!inputField.getAttribute('name').match(`^${namePattern}\$`)) { continue; }
                      editorConfig.selector = `[id='${inputField.id}']`;
                      tinyMCE.init(editorConfig);
                    }
                  }
                }
              };
              const observer = new MutationObserver(callback);
              observer.observe(targetNode, config);
            }
          }
    
      #
      # The repeat container is the topmost ancestor of this
      # formfield. This is true both for regular and compound repeated
      # fields.
      #
      set obj [self]
      while {[$obj exists parent_field]} {
        set parent_field [$obj set parent_field]
        set obj $parent_field
      }
      set repeat_container_id [$parent_field id]
    
      ::template::add_body_handler -event load -script [subst -nocommands {
        richtext_tinymce_editor_init_repeat('${:id}', '${repeat_container_id}', {$config});
      }]
    } else {
      #
      # A regular non-repeated field.
      #
      ::template::add_body_handler  -identifier richtext_tinymce_editor_init  -event load  -script {
            function richtext_tinymce_editor_init(elementId, fieldName, editorConfig) {
              editorConfig.selector = `[id='${elementId}']`;
              tinyMCE.init(editorConfig).then((editors) => {
                const replacedField = document.querySelector(`input[type=hidden][name='${elementId}']`);
                replacedField?.form.addEventListener('submit', (evt) => {
                  replacedField.name = fieldName;
                  replacedField.value = editors[0].getContent();
                });
              });
            }
          }
    
      ::template::add_body_handler -event load -script [subst -nocommands {
        richtext_tinymce_editor_init('${:id}', '${:name}', {$config});
      }]
    }
    
    if {$inline_p} {
      #
      # In inline mode, the markup we send is a div, that TinyMCE
      # replaces with an editor + a hidden input field with our same id,
      # but no name attribute.
      #
      # Before the form is submitted, we get the content from the
      # editor and store it as the hidden field value, then set the
      # name attribute as XoWiki expects it. This logic is found in
      # both the promise handler for TinyMCE.init up in the js
      # functions.
      #
      # We do not want to stop inheritance here, because we want to be
      # able to plug behavior in subclasses of richtext.
      #
      set :render_as_div_p true
    }
    
    next