--- /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);