diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/data/cubicweb.widgets.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/web/data/cubicweb.widgets.js Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,555 @@ +/** + * Functions dedicated to widgets. + * + * :organization: Logilab + * :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. + * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr + * + * + */ + +// widget namespace +Widgets = {}; + +/** + * .. function:: buildWidget(wdgnode) + * + * this function takes a DOM node defining a widget and + * instantiates / builds the appropriate widget class + */ +function buildWidget(wdgnode) { + var wdgclass = Widgets[wdgnode.getAttribute('cubicweb:wdgtype')]; + if (wdgclass) { + return new wdgclass(wdgnode); + } + return null; +} + +function renderJQueryDatePicker(subject, button_image, date_format, min_date, max_date){ + $widget = cw.jqNode(subject); + $widget.datepicker({buttonImage: button_image, dateFormat: date_format, + firstDay: 1, showOn: "button", buttonImageOnly: true, + minDate: min_date, maxDate: max_date}); + $widget.change(function(ev) { + maxOfId = $(this).data('max-of'); + if (maxOfId) { + cw.jqNode(maxOfId).datepicker("option", "maxDate", this.value); + } + minOfId = $(this).data('min-of'); + if (minOfId) { + cw.jqNode(minOfId).datepicker("option", "minDate", this.value); + } + }); +} + +/** + * .. function:: buildWidgets(root) + * + * This function is called on load and is in charge to build + * JS widgets according to DOM nodes found in the page + */ +function buildWidgets(root) { + root = root || document; + jQuery(root).find('.widget').each(function() { + if (this.getAttribute('cubicweb:loadtype') == 'auto') { + buildWidget(this); + } + }); +} + +jQuery(document).ready(function() { + buildWidgets(); +}); + +function postJSON(url, data, callback) { + return jQuery.post(url, data, callback, AJAX_BASE_URL); +} + +function getJSON(url, data, callback) { + return jQuery.get(url, data, callback, AJAX_BASE_URL); +} + + +(function ($) { + var defaultSettings = { + initialvalue: '', + multiple: false, + mustMatch: false, + delay: 50, + limit: 50 + }; + function split(val) { return val.split( /\s*,\s*/ ); } + function extractLast(term) { return split(term).pop(); } + function allButLast(val) { + var terms = split(val); + terms.pop(); + return terms; + } + + var methods = { + __init__: function(suggestions, options) { + return this.each(function() { + // here, `this` refers to the DOM element (e.g. input) being wrapped + // by cwautomplete plugin + var instanceData = $(this).data('cwautocomplete'); + if (instanceData) { + // already initialized + return; + } + var settings = $.extend({}, defaultSettings, options); + instanceData = { + initialvalue: settings.initialvalue, + userInput: this, + hiddenInput: null + }; + var hiHandlers = methods.hiddenInputHandlers; + $(this).data('cwautocomplete', instanceData); + // in case of an existing value, the hidden input must be initialized even if + // the value is not changed + if (($(instanceData.userInput).attr('cubicweb:initialvalue') !== undefined) && !instanceData.hiddenInput){ + hiHandlers.initializeHiddenInput(instanceData); + } + $.ui.autocomplete.prototype._value = methods._value; + $.data(this, 'settings', settings); + if (settings.multiple) { + $.ui.autocomplete.filter = methods.multiple.makeFilter(this); + $(this).bind({ + autocompleteselect: methods.multiple.select, + autocompletefocus: methods.multiple.focus, + keydown: methods.multiple.keydown + }); + } + // XXX katia we dont need it if minLength == 0, but by setting minLength = 0 + // we probably break the backward compatibility + $(this).bind('blur', methods.blur); + if ($.isArray(suggestions)) { // precomputed list of suggestions + settings.source = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions); + } else { // url to call each time something is typed + settings.source = function(request, response) { + var d = loadRemote(suggestions, {q: request.term, limit: settings.limit}, 'POST'); + d.addCallback(function (suggestions) { + suggestions = hiHandlers.checkSuggestionsDataFormat(instanceData, suggestions); + response(suggestions); + if((suggestions.length) == 0){ + methods.resetValues(instanceData); + } + }); + }; + } + $(this).autocomplete(settings); + if (settings.mustMatch) { + $(this).keypress(methods.ensureExactMatch); + } + }); + }, + + _value: function() { + /* We extend the widget with the ability to lookup and + handle several terms at once ('multiple' option). E.g.: + toto, titi, tu.... The autocompletion must be + performed only on the last of such a list of terms. + */ + var settings = $(this.element).data('settings'); + var value = this.valueMethod.apply( this.element, arguments ); + if (settings.multiple & arguments.length === 0) { + return extractLast(value); + } + return value + }, + + multiple: { + focus: function() { + // prevent value inserted on focus + return false; + }, + select: function(event, ui) { + var terms = allButLast(this.value); + // add the selected item + terms.push(ui.item.value); + // add placeholder to get the comma-and-space at the end + terms.push(""); + this.value = terms.join( ", " ); + return false; + }, + keydown: function(evt) { + if (evt.keyCode == $.ui.keyCode.TAB) { + evt.preventDefault(); + } + }, + makeFilter: function(userInput) { + return function(array, term) { + // remove already entered terms from suggestion list + array = cw.utils.difference(array, allButLast(userInput.value)); + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + return $.grep( array, function(value) { + return matcher.test( value.label || value.value || value ); + }); + }; + } + }, + blur: function(evt){ + var instanceData = $(this).data('cwautocomplete'); + if($(instanceData.userInput).val().strip().length==0){ + methods.resetValues(instanceData); + } + }, + + ensureExactMatch: function(evt) { + var instanceData = $(this).data('cwautocomplete'); + if (evt.keyCode == $.ui.keyCode.ENTER || evt.keyCode == $.ui.keyCode.TAB) { + var validChoices = $.map($('ul.ui-autocomplete li'), + function(li) {return $(li).text();}); + if ($.inArray($(instanceData.userInput).val(), validChoices) == -1) { + $(instanceData.userInput).val(''); + $(instanceData.hiddenInput).val(instanceData.initialvalue || ''); + } + } + }, + + resetValues: function(instanceData){ + $(instanceData.userInput).val(''); + $(instanceData.hiddenInput).val(''); + }, + + + hiddenInputHandlers: { + /** + * `hiddenInputHandlers` defines all methods specific to handle the + * hidden input created along the standard text input. + * An hiddenInput is necessary when displayed suggestions are + * different from actual values to submit. + * Imagine an autocompletion widget to choose among a list of CWusers. + * Suggestions would be the list of logins, but actual values would + * be the corresponding eids. + * To handle such cases, suggestions list should be a list of JS objects + * with two `label` and `value` properties. + **/ + suggestionSelected: function(evt, ui) { + var instanceData = $(this).data('cwautocomplete'); + instanceData.hiddenInput.value = ui.item.value; + instanceData.value = ui.item.label; + return false; // stop propagation + }, + + suggestionFocusChanged: function(evt, ui) { + var instanceData = $(this).data('cwautocomplete'); + instanceData.userInput.value = ui.item.label; + return false; // stop propagation + }, + + needsHiddenInput: function(suggestions) { + return suggestions[0].label !== undefined; + }, + initializeHiddenInput: function(instanceData) { + var userInput = instanceData.userInput; + var hiddenInput = INPUT({ + type: "hidden", + name: userInput.name, + // XXX katia : this must be handeled in .SuggestField widget, but + // it seems not to be used anymore + value: $(userInput).attr('cubicweb:initialvalue') || userInput.value + }); + $(userInput).removeAttr('name').after(hiddenInput); + instanceData.hiddenInput = hiddenInput; + $(userInput).bind({ + autocompleteselect: methods.hiddenInputHandlers.suggestionSelected, + autocompletefocus: methods.hiddenInputHandlers.suggestionFocusChanged + }); + }, + + /* + * internal convenience function: old jquery plugin accepted to be fed + * with a list of couples (value, label). The new (jquery-ui) autocomplete + * plugin expects a list of objects with "value" and "label" properties. + * + * This function converts the old format to the new one. + */ + checkSuggestionsDataFormat: function(instanceData, suggestions) { + // check for old (value, label) format + if ($.isArray(suggestions) && suggestions.length && + $.isArray(suggestions[0])){ + if (suggestions[0].length == 2) { + cw.log('[3.10] autocomplete init func should return {label,value} dicts instead of lists'); + suggestions = $.map(suggestions, function(sugg) { + return {value: sugg[0], label: sugg[1]}; + }); + } else { + if(suggestions[0].length == 1){ + suggestions = $.map(suggestions, function(sugg) { + return {value: sugg[0], label: sugg[0]}; + }); + } + } + } + var hiHandlers = methods.hiddenInputHandlers; + if (suggestions.length && hiHandlers.needsHiddenInput(suggestions) + && !instanceData.hiddenInput) { + hiHandlers.initializeHiddenInput(instanceData); + hiHandlers.fixUserInputInitialValue(instanceData, suggestions); + } + // otherwise, assume data shape is correct + return suggestions; + }, + + fixUserInputInitialValue: function(instanceData, suggestions) { + // called when the data is loaded to reset the correct displayed + // value in the visible input field (typically replacing an eid + // by a displayable value) + var curvalue = instanceData.userInput.value; + if (!curvalue) { + return; + } + for (var i=0, length=suggestions.length; i < length; i++) { + var sugg = suggestions[i]; + if (sugg.value == curvalue) { + instanceData.userInput.value = sugg.label; + return; + } + } + } + } + }; + + $.fn.cwautocomplete = function(data, options) { + return methods.__init__.apply(this, [data, options]); + }; +})(jQuery); + + +Widgets.SuggestField = defclass('SuggestField', null, { + __init__: function(node, options) { + options = options || {}; + var multi = node.getAttribute('cubicweb:multi'); + options.multiple = (multi == "yes") ? true: false; + var d = loadRemote(node.getAttribute('cubicweb:dataurl')); + d.addCallback(function(data) { + $(node).cwautocomplete(data, options); + }); + } +}); + +Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], { + + __init__: function(node) { + Widgets.SuggestField.__init__(this, node, { + method: 'get' // XXX + }); + } + +}); + +Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], { + __init__: function(node) { + Widgets.SuggestField.__init__(this, node, { + mustMatch: true + }); + } +}); + +//remote version of RestrictedSuggestField +Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], { + __init__: function(node, options) { + var self = this; + options = options || {}; + options.delay = 50; + // multiple selection not supported yet (still need to formalize correctly + // initial values / display values) + var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null'); + if (!initialvalue) { + initialvalue = node.value; + } + options.initialvalue = initialvalue; + Widgets.SuggestField.__init__(this, node, options); + } +}); + +/** + * .. function:: toggleTree(event) + * + * called when the use clicks on a tree node + * - if the node has a `cubicweb:loadurl` attribute, replace the content of the node + * by the url's content. + * - else, there's nothing to do, let the jquery plugin handle it. + */ +function toggleTree(event) { + var linode = jQuery(this); + var url = linode.attr('cubicweb:loadurl'); + if (url) { + linode.find('ul.placeholder').remove(); + var d = linode.loadxhtml(url, null, 'post', 'append'); + d.addCallback(function(domnode) { + linode.removeAttr('cubicweb:loadurl'); + linode.find('> ul.treeview').treeview({ + toggle: toggleTree, + prerendered: true + }); + return null; + } + ); + } +} + +Widgets.TemplateTextField = defclass("TemplateTextField", null, { + + __init__: function(wdgnode) { + this.variables = jQuery(wdgnode).attr('cubicweb:variables').split(','); + this.options = { + name: wdgnode.getAttribute('cubicweb:inputid'), + rows: wdgnode.getAttribute('cubicweb:rows') || 40, + cols: wdgnode.getAttribute('cubicweb:cols') || 80 + }; + // this.variableRegexp = /%\((\w+)\)s/; + this.errorField = DIV({ + 'class': "errorMessage" + }); + this.textField = TEXTAREA(this.options); + jQuery(this.textField).bind('keyup', { + 'self': this + }, + this.highlightInvalidVariables); + jQuery('#substitutions').prepend(this.errorField); + jQuery('#substitutions .errorMessage').hide(); + wdgnode.appendChild(this.textField); + }, + + /* signal callbacks */ + + highlightInvalidVariables: function(event) { + var self = event.data.self; + var text = self.textField.value; + var unknownVariables = []; + var it = 0; + var group = null; + var variableRegexp = /%\((\w+)\)s/g; + // emulates rgx.findAll() + while ( (group = variableRegexp.exec(text)) ) { + if ($.inArray(group[1], self.variables) == -1) { + unknownVariables.push(group[1]); + } + it++; + if (it > 5) { + break; + } + } + var errText = ''; + if (unknownVariables.length) { + errText = "Detected invalid variables : " + unknownVariables.join(', '); + jQuery('#substitutions .errorMessage').show(); + } else { + jQuery('#substitutions .errorMessage').hide(); + } + self.errorField.innerHTML = errText; + } + +}); + +cw.widgets = { + /** + * .. function:: insertText(text, areaId) + * + * inspects textarea with id `areaId` and replaces the current selected text + * with `text`. Cursor is then set at the end of the inserted text. + */ + insertText: function (text, areaId) { + var textarea = jQuery('#' + areaId); + if (document.selection) { // IE + var selLength; + textarea.focus(); + var sel = document.selection.createRange(); + selLength = sel.text.length; + sel.text = text; + sel.moveStart('character', selLength - text.length); + sel.select(); + } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla + var startPos = textarea.selectionStart; + var endPos = textarea.selectionEnd; + // insert text so that it replaces the [startPos, endPos] part + textarea.value = textarea.value.substring(0, startPos) + text + textarea.value.substring(endPos, textarea.value.length); + // set cursor pos at the end of the inserted text + textarea.selectionStart = textarea.selectionEnd = startPos + text.length; + textarea.focus(); + } else { // safety belt for other browsers + textarea.value += text; + } + } +}; + + +// InOutWidget This contains specific InOutnWidget javascript +// IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets) + +(function ($) { + + var methods = { + __init__: function(fromSelect, toSelect) { + // closed over state + var state = {'$fromNode' : $(cw.escape('#' + fromSelect)), + '$toNode' : $(cw.escape('#' + toSelect)), + 'name' : this.attr('id')}; + + function sortoptions($optionlist) { + var $sorted = $optionlist.find('option').sort(function(opt1, opt2) { + return $(opt1).text() > $(opt2).text() ? 1 : -1; + }); + // this somehow translates to an inplace sort + $optionlist.append($sorted); + }; + sortoptions(state.$fromNode); + sortoptions(state.$toNode); + + // will move selected options from one list to the other + // and call an option handler on each option + function moveoptions ($fromlist, $tolist, opthandler) { + $fromlist.find('option:selected').each(function(index, option) { + var $option = $(option); + // add a new option to the target list + $tolist.append(OPTION({'value' : $option.val()}, + $option.text())); + // process callback on the option + opthandler.call(null, $option); + // remove option from the source list + $option.remove(); + }); + // re-sort both lists + sortoptions($fromlist); + sortoptions($tolist); + }; + + function addvalues () { + moveoptions(state.$fromNode, state.$toNode, function ($option) { + // add an hidden input for the edit controller + var hiddenInput = INPUT({ + type: 'hidden', name: state.name, + value : $option.val() + }); + state.$toNode.parent().append(hiddenInput); + }); + }; + + function removevalues () { + moveoptions(state.$toNode, state.$fromNode, function($option) { + // remove hidden inputs for the edit controller + var selector = 'input[name=' + cw.escape(state.name) + ']' + state.$toNode.parent().find(selector).each(function(index, input) { + if ($(input).val() == $option.val()) { + $(input).remove(); + } + }); + }); + }; + + var $this = $(this); + $this.find('.cwinoutadd').bind( // 'add >>>' symbol + 'click', {'state' : state}, addvalues); + $this.find('.cwinoutremove').bind( // 'remove <<<' symbol + 'click', {'state' : state}, removevalues); + + state.$fromNode.bind('dblclick', {'state': state}, addvalues); + state.$toNode.bind('dblclick', {'state': state}, removevalues); + + } + }; + $.fn.cwinoutwidget = function(fromSelect, toSelect) { + return methods.__init__.apply(this, [fromSelect, toSelect]); + }; +})(jQuery);