Index: openacs-4/packages/xowiki/tcl/repeat-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowiki/tcl/repeat-procs.tcl,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/xowiki/tcl/repeat-procs.tcl 24 Jun 2013 16:15:55 -0000 1.1 @@ -0,0 +1,183 @@ +::xo::library doc { + Form-field "repeat" + + @author Gustaf Neumann + @creation-date 2013-02-27 +} + +::xo::library require -package xowiki form-field-procs + +namespace eval ::xowiki::formfield { + + # TODO: + # - improve styling (e.g. remove/deactivate controls for + # addition/deletion, when min/max is reached) + # - allow max to be open-ended (see also "addItem" in .js) + # - test for more input types + # - maybe deactivate container display for "repeat=1..1" + + ::xowiki::formfield::FormField instproc repeat {range} { + if {[my exists __initialized_repeat]} return + + set oldClass [my info class] + my class ::xowiki::formfield::repeatContainer + + if {$oldClass ne [my info class]} { + my reset_parameter + my set __state reset + } + + if {$range ne ""} { + my instvar min max + if {[regexp {^(\d*)[.][.](\d*)$} $range _ low high]} { + if {$low ne ""} {set min $low} + if {$high ne ""} {set max $high} + if {$min > $max} { + error "invalid range '$range' specified (lower limit $min must not be larger than higher limit $max)" + } + if {$min < 0 || $max < 1} { + error "invalid range '$range' specified (max $max must be at least 1) " + } + } else { + error "invalid range '$range' specified (must be of form 'min..max')" + } + } + my initialize + } + + ########################################################### + # + # ::xowiki::formfield::repeatContainer + # + ########################################################### + Class repeatContainer -superclass ::xowiki::formfield::CompoundField -parameter { + {min 1} + {max 5} + } + repeatContainer instproc item_spec {} { + # + # Return the spec of a contained item, which is a subset of the + # container spec. + # + set result {} + set is_required false + foreach s [split [my spec] ,] { + # don't propagate "repeat" and "label" properties + if { [string match repeat=* $s] || [string match label=* $s] } continue + if { [string match required $s]} {set is_required true; continue} + lappend result $s + } + return [list $is_required [join $result ,]] + } + repeatContainer instproc initialize {} { + ::xo::Page requireJS "/resources/xowiki/repeat.js" + ::xo::Page requireJS "/resources/xowiki/jquery/jquery.min.js" + + if {[my exists __initialized_repeat]} {return} + next + my set __initialized_repeat 1 + # + # Derive the spec of the contained items from the spec of the + # container. + # + set itemSpec [lindex [my item_spec] 1] + set is_required [lindex [my item_spec] 0] + + # + # Use item .0 as template for other items in .js (e.g. blank an + # item with the template, when it is deleted. By using a + # potentially compound item as template, we are able to preserve + # default values for subfields without knowing the detailed + # structure). + # + set components [list [list 0 $itemSpec]] + + # + # Add max content items (1 .. max) and build form fields + # + for {set i 1} {$i <= [my max]} {incr i} { + if {$i <= [my min] && $is_required} { + lappend components [list $i $itemSpec,required,label=$i] + } else { + lappend components [list $i $itemSpec,label=$i] + } + } + my create_components $components + + # + # Deactivate template item + # + set componentList [my components] + if {[llength $componentList] > 0} { + [lindex $componentList 0] set_disabled true + [lindex $componentList 0] set_is_repeat_template true + } + } + + #repeatContainer instproc convert_to_internal {} { + # next + # my msg name=[my name],value=[my get_compound_value] + #} + + repeatContainer instproc count_values {values} { + set count 1 + set highestCount 1 + if {![my required]} {set highestCount [my min]} + # The first pair is the default from the template field (.0) + set default [lindex $values 1] + foreach f [lrange [my components] 1 end] {name value} [lrange $values 2 end] { + if {[$f required] || ($value ne "" && ![$f same_value $value $default])} {set highestCount $count} + incr count + } + return $highestCount + } + + repeatContainer instproc render_input {} { + # + # Render content of the container within in a fieldset, + # without labels for the contained items. + # + html::fieldset [my get_attributes id {CSSclass class}] { + set i 1 + my instvar min max name + set clientData "{'min':$min,'max':$max, 'name':'$name'}" + set CSSclass "[my form_widget_CSSclass] repeatable" + set providedValues [my count_values [my value]] + if {$min > $providedValues} { + set nrItems $min + } else { + set nrItems $providedValues + } + incr nrItems + set containerDisabled [expr {[my exists disabled] && [my disabled] ne "false"}] + foreach c [my components] { + set atts [list class $CSSclass] + if {$i > $nrItems || [string match *.0 [$c name]]} { + lappend atts style "display: none;" + } + ::html::div $atts { + $c render_input + # compound fields - link not shown if we are not rendering for the template and copy the template afterwards + # if {!$containerDisabled} { + ::html::a -href "#" -onclick "return xowiki.repeat.delItem(this,\"$clientData\")" { html::t "\[x\]" } + # } + } + incr i + } + # if {!$containerDisabled} { + html::a -href "#" -onclick "return xowiki.repeat.addItem(this,\"$clientData\");" { html::t "add another" } + # } + } + } + + repeatContainer instproc validate {obj} { + foreach c [lrange [my components] 1 [my count_values [my value]]] { + set result [$c validate $obj] + if {$result ne ""} { + return $result + } + } + return "" + } + +} \ No newline at end of file Index: openacs-4/packages/xowiki/www/resources/repeat.js =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/xowiki/www/resources/repeat.js,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/xowiki/www/resources/repeat.js 24 Jun 2013 16:15:56 -0000 1.1 @@ -0,0 +1,281 @@ +var xowiki = xowiki || {}; +xowiki.repeat = {}; +/* + * addItem + * + * Add an item to the container if nrItems is below maximum. Actually, + * this function just invisible items visible. + */ +xowiki.repeat.addItem = function(e, json) { + var data = eval("(" + json + ')'); + var items = e.parentNode.children; + for (var j = 1; j < items.length; j++) { + if (items[j].nodeName != 'DIV') { continue; } + if (items[j].style.display == 'none') { + // We can make an existing but invisible item visible. + items[j].style.display = 'block'; + + // IPAD HACK START + // for ipad we have to set the contenteditiable to true for the ckeditor inline if it is false + var ck_editors = $(items[j]).find('.xowiki-ckeditor.cke_editable.cke_editable_inline.cke_contents_ltr'); + for (var k = 0; k < ck_editors.length; k++) { + if ($(ck_editors[k]).attr('contenteditable') == 'false') { + console.log('we have to set the contenteditable to true'); + $(ck_editors[k]).attr('contenteditable','true'); + } + } + // IPAD HACK ENDE + + return false; + } + } + // We could add another item here by adding a copy of the template + // and renaming the field like in delItems. We have to care as + // well in RepeatContainer.initialize() to check, how many + // subcomponents must be generated in advance (not max as now). + console.log('could add one more, j ' + j); + console.info(data); + return false; +}; + +/* + * itemStats + * + * Collect statistics of a repeatable container. This function computs + * the number of visible items, total items, the current item index + * and the div-nodes containing the items. + */ +xowiki.repeat.itemStats = function(item) { + var items = item.parentNode.children; + var visible = 0; + var nr = 0; + var divs = new Array(); + var current = -1; + for (var j = 0; j < items.length; j++) { + if (items[j].nodeName != 'DIV') { continue; } + if (items[j].style.display != 'none') { visible ++; } + if (items[j] == item) {current = nr;}; + divs[nr] = items[j]; + nr ++; + } + return {'visible' : visible, 'nr' : nr, 'current': current, 'divs' : divs}; +} + +/* + * renameItem + * + * Search in the dom tree for input names and rename it based on the + * provided stem. + */ +xowiki.repeat.renameItem = function(top, e, from, to) { + if (e == undefined) {return;} + //console.log('renameItem: work on ' + e); + //console.info(e); + var items = e.children; + if (items.length == 0 || e.nodeName == 'SELECT') { + var name = e.name; + if (typeof name != "undefined" && name != "") { + //console.log('renameItem: compare ' + name + ' from ' + from); + var compareLength = from.length; + if (name.substring(0,compareLength) == from) { + if (compareLength != name.length) { + to += name.substring(compareLength, name.length); + } + e.name = to; + e.disabled = false; + // we have also to remove the disabled attribute for options of a select field + if (e.nodeName == 'SELECT') { + $(e).find('option:disabled').each(function() { + $(this).attr('disabled', false); + }); + } + + console.log('renameItem: renamed ' + name + ' base ' + from + ' to ' + to); + this.renameItem(top, top, + '__old_value_' + from, + '__old_value_' + to); + } + } + } else if (e.nodeName == 'DIV' || e.nodeName == 'FIELDSET') { + for (var j = 0; j < items.length; j++) { + this.renameItem(top, items[j], from, to); + } + } else { + console.log('rename ignores ' + e); + } +} + +/* + * delItem + * + * Delete the current item. Actually, this implementation overwrites the + * current item with the template item, moves it to the end and renames + * the fields. + */ +xowiki.repeat.delItem = function(e, json) { + var data = eval("(" + json + ')'); + var item = e.parentNode; + var stats = this.itemStats(item); + //console.info(item); + console.info(stats); + //console.info(data); + + var current = stats['current']; + var last = stats['visible']; + var items = item.parentNode.children; + var divs = stats['divs']; + //console.info(divs); + var display = 'none'; + + if (stats['visible'] < data['min']+1) { + // we have reached the minimum + // so we simulate that the current item is the last one -> it is reset by the template + // the only difference is that we shouldn't hide it + last = current; + display = 'block'; + } + + console.log('delete ' + current); + + if (current == last) { + //console.log('delete the last item'); + } else { + for (var j = current; j < last; j++) { + var k = j + 1; + + // before moving we are storing the input values --> so that the values are being moved + // normal input fields + $(divs[k]).find(':input[type=text]').each(function() { + $(this).attr('value',$(this).val()); + }); + + // radio and checkbox input fields + $(divs[k]).find(':input[type=radio|checkbox]:checked').each(function() { + $(this).attr('checked',$(this).attr('checked')); + }); + + // selected options of select fields + $(divs[k]).find(':selected').each(function() { + $(this).attr('selected','on'); + }); + + // textarea + $(divs[k]).find('textarea').each(function() { + $(this).html($(this).val()); + }); + + var oldid = item.parentNode.id + '.' + k; + var newid = item.parentNode.id + '.' + j; + + + // before we can move the items we have to remove the ckeditor instance if available + // otherwise it will shown twice after moving (because we are reloading it) + // we have to reload because the ckeditor will not work after moving + // additionally we have to set the content of the ckeditor in the textarea + if (typeof CKEDITOR != "undefined") { + // we are selecting all ckeditor intances which are at the same level and below of the current item + for (var l in CKEDITOR.instances) { + // console.log('instance name: ' + CKEDITOR.instances[l].name); + var searchString = item.parentNode.id + k; + // the instance names of the ckeditor are without '. : -' --> see also initialize of ckeditor + searchString = searchString.replace(/[.:-]/g,''); + + // console.log('searchString: '+searchString); + if (CKEDITOR.instances[l].name.search(searchString) == 0) { + // console.log('data to copy: '+CKEDITOR.instances[l].getData()); + + CKEDITOR.instances[l].updateElement(); // should update the textarea but it doesn't -> so we have to do that manually + document.getElementById(CKEDITOR.instances[l].name).innerHTML=CKEDITOR.instances[l].getData(); + + CKEDITOR.instances[l].destroy(true); + } + } + } + divs[j].innerHTML = divs[k].innerHTML; + + + // due to the fact that the ckeditor are using the ids for reloading we have to recycle them (and for the other cases it doesn't hurt) + this.renameIds(divs[j],oldid,newid); + + this.renameItem(divs[j], divs[j], + data['name'] + '.' + (k), data['name'] + '.' + (j)); + } + }; + // We add an empty item at the end to force back-reporting of + // empty content to the instance variables. Otherwise the old + // content would stay. This means, that we should never physically + // delete items. + divs[last].innerHTML = divs[0].innerHTML; + + var templateid = item.parentNode.id + '.0'; + var newid = item.parentNode.id + '.' + last; + + // due to the fact that the ckeditor are using the ids for reloading we have to recycle them (and for the other cases it doesn't hurt) + this.renameIds(divs[last],templateid,newid); + + // ckeditor releoding + // we are selecting all ckeditor intances which are at the same level and below of the current item + // so we can be sure that in case of a compound field all editors are reloaded correctly + // .xowiki-ckeditor --> normaler ckeditor + // .xowiki-ckeditor.ckeip --> inplace editor + // .xowiki-ckeditor.cke_editable.cke_editable_inline.cke_contents_ltr --> inline editing + var ckclasses = [".xowiki-ckeditor",".xowiki-ckeditor.ckeip",".xowiki-ckeditor.cke_editable.cke_editable_inline.cke_contents_ltr"]; + for (var i = 0; i < ckclasses.length; i++) { + var ck_editors = $(item.parentNode).find(ckclasses[i]); + for (var j = 0; j < ck_editors.length; j++) { + var idofeditor = ck_editors[j].id; + console.log('reloading ckeditor for id: '+idofeditor); + var functionname = 'load_' + idofeditor; + try { + window[functionname](); + } catch(err) { + console.log('function: ' + functionname + ' not found maybe it is a template'); + } + } + } + + + this.renameItem(divs[last], divs[last], + data['name'] + '.0', data['name'] + '.' + (last)); + + + divs[last].style.display = display; + + // force refresh of tree + item.parentNode.style.display = 'none'; + item.parentNode.style.display = 'block'; + + console.log('final html ' + item.parentNode.innerHTML); + return false; +}; + + +/* + * renameIds + * + * Rename all ids (also children) from the current element + * which matches somewhere the searchString and replace the parts + * with the replaceString + * example: + * e: id = "ckeip_Fendefvar40" + * searchString: Fendefvar40 + * replaceString: Fendefvar41 + * result: ckeip_Fendefvar40 -> ckeip_Fendefvar41 + */ +xowiki.repeat.renameIds = function(e, searchString, replaceString) { + $(e).find('[id*="'+searchString+'"]').each(function() { + var tmpid = $(this).attr("id"); + tmpid = tmpid.replace(searchString,replaceString); + $(this).attr("id", tmpid); + }); + + // the instance names of the ckeditor are without '. : -' --> see also initialize of ckeditor + searchString = searchString.replace(/[.:-]/g,''); + replaceString = replaceString.replace(/[.:-]/g,''); + + $(e).find('[id*="'+searchString+'"]').each(function() { + var tmpid = $(this).attr("id"); + tmpid = tmpid.replace(searchString,replaceString); + $(this).attr("id", tmpid); + }); +} \ No newline at end of file