Index: openacs-4/packages/xowiki/tcl/form-field-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowiki/tcl/form-field-procs.tcl,v diff -u -r1.284.2.244 -r1.284.2.245 --- openacs-4/packages/xowiki/tcl/form-field-procs.tcl 29 Apr 2024 16:05:27 -0000 1.284.2.244 +++ openacs-4/packages/xowiki/tcl/form-field-procs.tcl 2 May 2024 12:47:47 -0000 1.284.2.245 @@ -3570,6 +3570,532 @@ ########################################################### # + # ::xowiki::formfield::richtext::ckeditor4 + # + # mode: wysiwyg, source + # skin: moono, kama + # extraPlugins: tcl-list, is converted to comma list for js + # + ########################################################### + Class create richtext::ckeditor4 -superclass richtext -parameter { + {mode wysiwyg} + {skin "bootstrapck,/resources/xowiki/ckeditor4/skins/bootstrapck/"} + {toolbar Full} + {CSSclass xowiki-ckeditor} + {uiColor ""} + {allowedContent ""} + {CSSclass xowiki-ckeditor} + {customConfig "config.js"} + {callback "/* callback code */"} + {destroy_callback "/* callback code */"} + {submit_callback ""} + {extraPlugins ""} + {extraAllowedContent {*(*)}} + {ck_package standard} + {templatesFiles ""} + {templates ""} + {contentsCss /resources/xowiki/ck_contents.css} + {imageSelectorDialog /xowiki/ckeditor-images/} + {additionalConfigOptions ""} + } + richtext::ckeditor4 set editor_mixin 1 + + richtext::ckeditor4 instproc initialize {} { + switch -- ${:displayMode} { + inplace { append :help_text " #xowiki.ckeip_help#" } + } + next + set :widget_type richtext + # Mangle the id to make it compatible with jQuery; most probably + # not optimal and just a temporary solution + regsub -all -- {[.:-]} ${:id} "" id + :id $id + } + + richtext::ckeditor4 instproc js_image_helper {} { + set path [${:object} pretty_link] + append js \ + [subst -novariables { + function xowiki_image_callback(editor) { + if (typeof editor != "undefined") { + $(editor.element.$.form).submit(function(e) { + calc_image_tags_to_wiki_image_links(this); + }); + editor.setData(calc_wiki_image_links_to_image_tags('[set path]',editor.getData())); + } + } + }] { + function calc_image_tags_to_wiki_image_links(form) { + var calc = function() { + var wiki_link = $(this).attr('alt'); + $(this).replaceWith('[['+wiki_link+']]'); + } + $(form).find('iframe').each(function() { + $(this).contents().find('img[type="wikilink"]').each(calc); + }); + + $(form).find('textarea.ckeip').each(function() { + var contents = $('
'+this.value+'
'); + contents.find('img[type="wikilink"]').each(calc); + this.value = contents.html(); + }); + return true; + } + + function calc_image_tags_to_wiki_image_links_inline(e) { + var data = $('
'+CKEDITOR.instances[e].getData()+'
'); + data.find('img[type="wikilink"]').each( function() { + var wiki_link = $(this).attr('alt'); + $(this).replaceWith('[['+wiki_link+']]'); + }); + CKEDITOR.instances[e].setData(data.html()); + CKEDITOR.instances[e].updateElement(); + } + + function calc_wiki_image_links_to_image_tags(path, text) { + // console.log('path = <' + path + '>'); + var regex_wikilink = new RegExp('(\\[\\[.SELF./image:)(.*?)(\\]\\])', 'g'); + text = text.replace(regex_wikilink,'.SELF./image:$2'); + return text; + } + } + ::xo::Page requireJS $js + } + + richtext::ckeditor4 instproc pathNames {fileNames} { + set result [list] + foreach fn $fileNames { + if {[regexp {^[./]} $fn]} { + append result $fn + } else { + append result "/resources/xowiki/$fn" + } + } + return $result + } + + richtext::ckeditor4 instproc render_input {} { + set disabled [:is_disabled] + set is_repeat_template [:is_repeat_template_p] + + # :msg "${:id} ${:name} - $is_repeat_template" + + if {$is_repeat_template} { + set :data-repeat-template-id ${:id} + } + + # if value is empty, we need something to be clickable for display mode inplace + if {[:value] eq "" && ${:displayMode} eq "inplace"} { + :value " " + } + + if {![:istype ::xowiki::formfield::richtext] || ($disabled && !$is_repeat_template)} { + :render_richtext_as_div + } else { + + template::head::add_javascript -src urn:ad:js:jquery + try { + # + # Try to use the ckeditor from the richtext-ckeditor4 + # installation. + # + # There seems to be something broken on 4.9.2 version on the + # CDN. If we do not use standard-all, then we see an error + # about a missing + # ".../4.9.2/full/plugins/iframedialog/plugin.js". There + # exists a default "iframe" and a "iframedialog" plugin for + # ckeditor4, the latter is not included in the standard builds + # (only in "-all"). + # + # UPDATE July 2021: The "*-all" ckpackages are gone for newer + # versions of CKEditor (e.g. 4.16.*) and it is unlikely that + # it will be revived for the standard packages. One can + # download the "iframedialog" plugin still from the addons + # + # https://ckeditor.com/cke4/addon/iframedialog + # + # and add it manually to the downloaded tree in e.g. + # + # richtext-ckeditor4/www/resources/4.16.1/standard/plugins/ + # + # For the time being, we remove the "xowikiimage" plugin from + # the extraPlugins to make it working out of the box. This + # plugin should be rewritten using the current dialogs of + # ckeditor. + # + ::richtext::ckeditor4::add_editor \ + -order 90 \ + -ck_package ${:ck_package} \ + -adapters "jquery.js" + + } trap {TCL LOOKUP COMMAND} {errorMsg} { + # + # If for whatever reason, richtext-ckeditor4 is not available, + # bail out and tell the user to install. + # + + error "Please install the package: richtext-ckeditor4" + } + + # + # In contrary to the documentation, ckeditor4 names instances + # after the id, not the name. + # + set id ${:id} + set name ${:name} + set package_id [${:object} package_id] + + # Earlier versions required the plugin "sourcedialog" in + # "inline" mode. Not sure why. This plugin was removed from + # CKEditor. + #if {${:displayMode} eq "inline"} { + # lappend :extraPlugins sourcedialog + #} + + if {"xowikiimage" in ${:extraPlugins}} { + :js_image_helper + set ready_callback "xowiki_image_callback(CKEDITOR.instances\['$id'\]);" + set ready_callback2 {xowiki_image_callback(e.editor);} + } else { + set ready_callback "/*none*/;" + set ready_callback2 $ready_callback + set submit_callback "/*none*/;" + } + + # + # Append dimensions (when available) in JSON notation. + # + set dimensions {} + if {[info exists :height]} { + lappend dimensions [subst {"height": "${:height}"}] + } + if {[info exists :width]} { + lappend dimensions [subst {"width": "${:width}"}] + } + if {[llength $dimensions] > 0} { + set dimensions [join $dimensions ,], + } + + set options [subst { + $dimensions + ${:additionalConfigOptions} + toolbar : '${:toolbar}', + uiColor: '${:uiColor}', + language: '[::xo::cc lang]', + skin: '${:skin}', + startupMode: '${:mode}', + disableNativeSpellChecker: false, + parent_id: '[${:object} item_id]', + package_url: '[::$package_id package_url]', + extraPlugins: '[join ${:extraPlugins} ,]', + extraAllowedContent: '${:extraAllowedContent}', + contentsCss: '${:contentsCss}', + imageSelectorDialog: '[:imageSelectorDialog]?parent_id=[${:object} item_id]', + ready_callback: '$ready_callback2', + customConfig: '${:customConfig}', + textarea_id: id + }] + if {${:allowedContent} ne ""} { + # + # Syntax rules: + # https://ckeditor.com/docs/ckeditor4/latest/guide/dev_allowed_content_rules.html#string-format + # + if {${:allowedContent} in {true false}} { + append options " , allowedContent: ${:allowedContent}\n" + } else { + append options " , allowedContent: '${:allowedContent}'\n" + } + } + if {${:templatesFiles} ne ""} { + append options " , templates_files: \['[join [:pathNames ${:templatesFiles}] ',' ]' \]\n" + } + if {${:templates} ne ""} { + append options " , templates: '${:templates}'\n" + } + + #set parent [[${:object} package_id] get_page_from_item_or_revision_id [${:object} parent_id]];# ??? + + if {${:displayMode} eq "inplace"} { + + lappend :CSSclass ckeip + ::xo::Page requireJS "/resources/xowiki/ckeip.js" + + ::xo::Page requireJS [subst -nocommands { + function load_$id (id) { + // must use id provided as argument + \$('#' + id).ckeip(function() { ${:callback}}, { + name: '$name', + ckeditor_config: { + $options, + destroy_callback: function() { ${:destroy_callback} } + } + }); + } + }] + if {!$is_repeat_template} { + ::xo::Page requireJS [subst -nocommands { + \$(document).ready(function() { + CKEDITOR.plugins.addExternal( 'xowikiimage', '/resources/xowiki/ckeditor4/plugins/xowikiimage/', 'plugin.js' ); + if (\$('#$id').parents('.repeatable').length != 0) { + if (\$('#$id').is(':visible')) { + load_$id ('$id'); + } + } else { + //this is not inside a repeatable container, load normally + load_$id ('$id'); + } + } ); + }] + } + :render_richtext_as_div + } elseif {${:displayMode} eq "inline"} { + if {"xowikiimage" in ${:extraPlugins}} { + set ready_callback "xowiki_image_callback(CKEDITOR.instances\['$id'\]);" + set submit_callback "calc_image_tags_to_wiki_image_links_inline('$id');" + } + + set submit_callback "$submit_callback ${:submit_callback}" + ::xo::Page requireJS [subst { + function load_$id (id) { + CKEDITOR.inline(id, { + on: { + instanceReady: function(e) { + \$(e.editor.element.\$).attr('title', '${:label}'); + \$(e.editor.element.\$.form).submit(function(e) { + $submit_callback + }); + } + }, + $options + }); + } + }] + if {!$is_repeat_template} { + ::xo::Page requireJS [subst { + \$(document).ready(function() { + CKEDITOR.plugins.addExternal( 'xowikiimage', '/resources/xowiki/ckeditor4/plugins/xowikiimage/', 'plugin.js' ); + if (\$('#$id').parents('.repeatable').length != 0) { + if (\$('#$id').is(':visible')) { + load_$id ('$id'); + } + } else { + //this is not inside a repeatable container, load normally + load_$id ('$id'); + } + $ready_callback + }); + }] + } + next + } else { + ::xo::Page requireJS [subst -nocommands { + function load_$id (id) { + // must use id provided as argument + \$('#' + id).ckeditor(function() { ${:callback} }, {$options}); + } + }] + if {!$is_repeat_template} { + ::xo::Page requireJS [subst -nocommands { + \$(document).ready(function() { + CKEDITOR.plugins.addExternal( 'xowikiimage', '/resources/xowiki/ckeditor4/plugins/xowikiimage/', 'plugin.js' ); + load_$id ('$id'); + $ready_callback + //CKEDITOR.instances['$id'].on('instanceReady',function(e) {$ready_callback}); + }); + }] + } + next + } + } + } + + ########################################################### + # + # ::xowiki::formfield::richtext::wym + # + ########################################################### + Class create richtext::wym -superclass richtext -parameter { + {CSSclass wymeditor} + width + height + {skin silver} + {plugins "hovertools resizable fullscreen"} + } + richtext::wym set editor_mixin 1 + richtext::wym instproc initialize {} { + next + set :widget_type richtext + } + richtext::wym instproc render_input {} { + set disabled [:is_disabled] + if {![:istype ::xowiki::formfield::richtext] || $disabled } { + :render_richtext_as_div + } else { + ::xo::Page requireCSS "/resources/xowiki/wymeditor/skins/default/screen.css" + ::xo::Page requireJS urn:ad:js:jquery + ::xo::Page requireJS "/resources/xowiki/wymeditor/jquery.wymeditor.pack.js" + set postinit "" + foreach plugin {hovertools resizable fullscreen embed} { + if {$plugin in [:plugins]} { + switch -- $plugin { + embed {} + resizable { + ::xo::Page requireJS urn:ad:js:jquery-ui + append postinit "wym.${plugin}();\n" + } + default {append postinit "wym.${plugin}();\n"} + } + ::xo::Page requireJS "/resources/xowiki/wymeditor/plugins/$plugin/jquery.wymeditor.$plugin.js" + } + } + regsub -all -- {[.:]} ${:id} {\\\\&} JID + + # possible skins are per in the distribution: "default", "sliver", "minimal" and "twopanels" + set config [list "skin: '[:skin]'"] + + #:msg "wym, h [info exists :height] || w [info exists :width]" + if {[info exists :height] || [info exists :width]} { + set height_cmd "" + set width_cmd "" + if {[info exists :height]} {set height_cmd "jQuery(wym._box).find(wym._options.iframeSelector).css('height','[:height]');"} + if {[info exists :width]} {set width_cmd "wym_box.css('width', '[:width]');"} + set postInit [subst -nocommand -nobackslash { + postInit: function(wym) { + wym_box = jQuery(".wym_box"); + $height_cmd + $width_cmd + $postinit + }}] + lappend config $postInit + } + if {$config ne ""} { + set config \{[join $config ,]\} + } + ::xo::Page requireJS [subst -nocommand -nobackslash { + jQuery(function() { + jQuery("#$JID").wymeditor($config); + }); + }] + + next + } + } + + ########################################################### + # + # ::xowiki::formfield::richtext::xinha + # + ########################################################### + + Class create richtext::xinha -superclass richtext -parameter { + javascript + {height} + {style} + {wiki_p true} + {slim false} + {CSSclass xinha} + extraPlugins + } + richtext::xinha set editor_mixin 1 + richtext::xinha instproc initialize {} { + switch -- ${:displayMode} { + inplace { + ::xo::Page requireJS "/resources/xowiki/xinha-inplace.js" + if {![info exists ::__xinha_inplace_init_done]} { + template::add_body_handler -event onload -script "xinha.inplace.init();" + set ::__xinha_inplace_init_done 1 + } + } + inline { error "inline is not supported for xinha"} + } + + next + set :widget_type richtext + if {![info exists :extraPlugins]} { + set :plugins \ + [parameter::get -parameter "XowikiXinhaDefaultPlugins" \ + -default [parameter::get_from_package_key \ + -package_key "acs-templating" \ + -parameter "XinhaDefaultPlugins"]] + } else { + set :plugins ${:extraPlugins} + } + set :options [:get_attributes editor plugins width height folder_id script_dir javascript wiki_p] + # for the time being, we can't set the defaults via parameter, + # but only manually, since the editor is used as a mixin, the parameter + # would have precedence over the defaults of subclasses + if {![info exists :slim]} {set :slim false} + if {![info exists :style]} {set :style "width: 100%;"} + if {![info exists :height]} {set :height 350px} + if {![info exists :wiki_p]} {set :wiki_p 1} + if {${:slim}} { + lappend :options javascript { + xinha_config.toolbar = [['popupeditor', 'formatblock', 'bold','italic','createlink','insertimage'], + ['separator','insertorderedlist','insertunorderedlist','outdent','indent'], + ['separator','killword','removeformat','htmlmode'] + ]; + } + } + } + + richtext::xinha instproc render_input {} { + set disabled [:is_disabled] + if {![:istype ::xowiki::formfield::richtext] || $disabled} { + :render_richtext_as_div + } else { + # + # required CSP directives for Xinha + # + security::csp::require script-src 'unsafe-eval' + security::csp::require script-src 'unsafe-inline' + + # we use for the time being the initialization of xinha based on + # the blank master + set ::acs_blank_master(xinha) 1 + set quoted [list] + foreach e [:plugins] {lappend quoted '$e'} + set ::acs_blank_master(xinha.plugins) [join $quoted ", "] + + array set o ${:options} + set xinha_options "" + foreach e {width height folder_id fs_package_id file_types attach_parent_id wiki_p package_id} { + if {[info exists o($e)]} { + append xinha_options "xinha_config.$e = '$o($e)';\n" + } + } + append xinha_options "xinha_config.package_id = '[::xo::cc package_id]';\n" + if {[info exists o(javascript)]} { + append xinha_options $o(javascript) \n + } + set ::acs_blank_master(xinha.options) $xinha_options + lappend ::acs_blank_master__htmlareas ${:id} + + if {${:displayMode} eq "inplace"} { + ::html::div [:get_attributes id name {CSSclass class} disabled] { + set href \# + ::html::a -style "float: right;" -class edit-item-button -href $href -id ${:id}-edit { + ::html::t -disableOutputEscaping   + } + template::add_event_listener \ + -id ${:id}-edit \ + -script [subst {xinha.inplace.openEditor('${:id}');}] + + ::html::div -id "${:id}__CONTENT__" { + ::html::t -disableOutputEscaping [:value] + } + } + set :hiddenid ${:id}__HIDDEN__ + set :type hidden + ::html::input [:get_attributes {hiddenid id} name type value] {} + } else { + #::html::div [:get_attributes id name cols rows style {CSSclass class} disabled] {} + next + } + } + } + + ########################################################### + # # ::xowiki::formfield::ShuffleField # ###########################################################