Advertisement
Guest User

form.js

a guest
Mar 23rd, 2016
140
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /* This file is part of Tryton.  The COPYRIGHT file at the top level of
  2.    this repository contains the full copyright notices and license terms. */
  3. (function() {
  4.     'use strict';
  5.  
  6.     Sao.View.Form = Sao.class_(Sao.View, {
  7.         editable: true,
  8.         init: function(screen, xml) {
  9.             Sao.View.Form._super.init.call(this, screen, xml);
  10.             this.view_type = 'form';
  11.             this.el = jQuery('<div/>', {
  12.                 'class': 'form'
  13.             });
  14.             this.widgets = {};
  15.             this.widget_id = 0;
  16.             this.state_widgets = [];
  17.             this.containers = [];
  18.             this.notebooks = [];
  19.             var root = xml.children()[0];
  20.             var container = this.parse(screen.model, root);
  21.             this.el.append(container.el);
  22.         },
  23.         _parse_node: function(model, child, container, attributes, labels) {
  24.             var widget;
  25.             switch (child.tagName) {
  26.                 case 'image':
  27.                     this._parse_image(
  28.                             model, child, container, attributes);
  29.                     break;
  30.                 case 'separator':
  31.                     this._parse_separator(
  32.                             model, child, container, attributes);
  33.                     break;
  34.                 case 'label':
  35.                     widget = this._parse_label(
  36.                             model, child, container, attributes);
  37.                     if (attributes.name && widget) {
  38.                         labels[attributes.name] = widget;
  39.                     }
  40.                     break;
  41.                 case 'newline':
  42.                     container.add_row();
  43.                     break;
  44.                 case 'button':
  45.                     this._parse_button(child, container, attributes);
  46.                     break;
  47.                 case 'notebook':
  48.                     this._parse_notebook(
  49.                             model, child, container, attributes);
  50.                     break;
  51.                 case 'page':
  52.                     this._parse_page(model, child, container, attributes);
  53.                     break;
  54.                 case 'field':
  55.                     widget = this._parse_field(
  56.                             model, child, container, attributes);
  57.                     if ((attributes.name in labels) &&
  58.                             widget &&
  59.                             widget.labelled) {
  60.                         var label = labels[attributes.name];
  61.                         label.el.uniqueId();
  62.                         widget.labelled.uniqueId();
  63.                         widget.labelled.attr(
  64.                                 'aria-labelledby', label.el.attr('id'));
  65.                         label.el.attr('for', widget.labelled.attr('id'));
  66.                     }
  67.                     break;
  68.                 case 'group':
  69.                     this._parse_group(model, child, container, attributes);
  70.                     break;
  71.                 case 'hpaned':
  72.                     this._parse_paned(model, child, container, attributes,
  73.                             'horizontal');
  74.                     break;
  75.                 case 'vpaned':
  76.                     this._parse_paned(model, child, container, attributes,
  77.                             'vertical');
  78.                     break;
  79.                 case 'child':
  80.                     this._parse_child(model, child, container, attributes);
  81.                     break;
  82.             }
  83.         },
  84.         parse: function(model, node, container) {
  85.             if (container === undefined) {
  86.                 container = new Sao.View.Form.Container(
  87.                     Number(node.getAttribute('col') || 4));
  88.                 this.containers.push(container);
  89.             }
  90.             var labels = {};
  91.             var _parse = function(index, child) {
  92.                 var attributes = {};
  93.                 for (var i = 0, len = child.attributes.length; i < len; i++) {
  94.                     var attribute = child.attributes[i];
  95.                     attributes[attribute.name] = attribute.value;
  96.                 }
  97.                 ['readonly', 'invisible'].forEach(function(name) {
  98.                     if (attributes[name]) {
  99.                         attributes[name] = attributes[name] == 1;
  100.                     }
  101.                 });
  102.                 ['yexpand', 'yfill', 'xexpand', 'xfill', 'colspan'].forEach(
  103.                         function(name) {
  104.                             if (attributes[name]) {
  105.                                 attributes[name] = Number(attributes[name]);
  106.                             }
  107.                         });
  108.                 this._parse_node(model, child, container, attributes, labels);
  109.             };
  110.             jQuery(node).children().each(_parse.bind(this));
  111.             return container;
  112.         },
  113.         _parse_image: function(model, node, container, attributes) {
  114.             var image = new Sao.View.Form.Image_(attributes);
  115.             this.state_widgets.push(image);
  116.             container.add(attributes, image);
  117.         },
  118.         _parse_separator: function(model, node, container, attributes) {
  119.             var name = attributes.name;
  120.             var text = attributes.string;
  121.             if (name in model.fields) {
  122.                 if (!attributes.states && (name in model.fields)) {
  123.                     attributes.states = model.fields[name].description.states;
  124.                 }
  125.                 if (!text) {
  126.                     text = model.fields[name].description.string;
  127.                 }
  128.             }
  129.             var separator = new Sao.View.Form.Separator(text, attributes);
  130.             this.state_widgets.push(separator);
  131.             container.add(attributes, separator);
  132.         },
  133.         _parse_label: function(model, node, container, attributes) {
  134.             var name = attributes.name;
  135.             var text = attributes.string;
  136.             if (attributes.xexpand === undefined) {
  137.                 attributes.xexpand = 0;
  138.             }
  139.             if (name in model.fields) {
  140.                 if (name == this.screen.exclude_field) {
  141.                     container.add(attributes);
  142.                     return;
  143.                 }
  144.                 if (!attributes.states && (name in model.fields)) {
  145.                     attributes.states = model.fields[name].description.states;
  146.                 }
  147.                 if (!text) {
  148.                     // TODO RTL and translation
  149.                     text = model.fields[name]
  150.                         .description.string + ': ';
  151.                 }
  152.                 if (attributes.xalign === undefined) {
  153.                     attributes.xalign = 1.0;
  154.                 }
  155.             }
  156.             var label;
  157.             if (text) {
  158.                 label = new Sao.View.Form.Label(text, attributes);
  159.                 this.state_widgets.push(label);
  160.             }
  161.             container.add(attributes, label);
  162.             return label;
  163.         },
  164.         _parse_button: function(node, container, attributes) {
  165.             var button = new Sao.common.Button(attributes);
  166.             this.state_widgets.push(button);
  167.             container.add(attributes, button);
  168.             button.el.click(button, this.button_clicked.bind(this));
  169.         },
  170.         _parse_notebook: function(model, node, container, attributes) {
  171.             if (attributes.colspan === undefined) {
  172.                 attributes.colspan = 4;
  173.             }
  174.             var notebook = new Sao.View.Form.Notebook(attributes);
  175.             this.notebooks.push(notebook);
  176.             this.state_widgets.push(notebook);
  177.             container.add(attributes, notebook);
  178.             this.parse(model, node, notebook);
  179.         },
  180.         _parse_page: function(model, node, container, attributes) {
  181.             var text = attributes.string;
  182.             if (attributes.name in model.fields) {
  183.                 var field = model.fields[attributes.name];
  184.                 if (attributes.name == this.screen.exclude_field) {
  185.                     return;
  186.                 }
  187.                 ['states', 'string'].forEach(function(attr) {
  188.                     if ((attributes[attr] === undefined) &&
  189.                             (field.description[attr] !== undefined)) {
  190.                         attributes[attr] = field.description[attr];
  191.                     }
  192.                 });
  193.             }
  194.             var page = this.parse(model, node);
  195.             page = new Sao.View.Form.Page(
  196.                     container.add(page.el, attributes.string), attributes);
  197.             this.state_widgets.push(page);
  198.         },
  199.         _parse_field: function(model, node, container, attributes) {
  200.             var name = attributes.name;
  201.             if (!(name in model.fields) || name == this.screen.exclude_field) {
  202.                 container.add(attributes);
  203.                 return;
  204.             }
  205.             if (!attributes.widget) {
  206.                 attributes.widget = model.fields[name]
  207.                     .description.type;
  208.             }
  209.             var attribute_names = ['relation', 'domain', 'selection', 'help',
  210.                 'relation_field', 'string', 'views', 'add_remove', 'sort',
  211.                 'context', 'size', 'filename', 'autocomplete', 'translate',
  212.                 'create', 'delete', 'selection_change_with', 'schema_model'];
  213.             for (var i in attribute_names) {
  214.                 var attr = attribute_names[i];
  215.                 if ((attr in model.fields[name].description) &&
  216.                         (node.getAttribute(attr) === null)) {
  217.                     attributes[attr] = model.fields[name]
  218.                         .description[attr];
  219.                 }
  220.             }
  221.             var WidgetFactory = Sao.View.form_widget_get(
  222.                     attributes.widget);
  223.             if (!WidgetFactory) {
  224.                 container.add(attributes);
  225.                 return;
  226.             }
  227.             var widget = new WidgetFactory(name, model, attributes);
  228.             widget.position = this.widget_id += 1;
  229.             widget.view = this;
  230.             // TODO expand, fill, height, width
  231.             container.add(attributes, widget);
  232.             if (this.widgets[name] === undefined) {
  233.                 this.widgets[name] = [];
  234.             }
  235.             this.widgets[name].push(widget);
  236.             this.fields[name] = true;
  237.             return widget;
  238.         },
  239.         _parse_group: function(model, node, container, attributes) {
  240.             var group = new Sao.View.Form.Group(attributes);
  241.             group.add(this.parse(model, node));
  242.             this.state_widgets.push(group);
  243.             container.add(attributes, group);
  244.         },
  245.         _parse_paned: function(model, node, container, attributes,
  246.                               orientation) {
  247.             if (attributes.yexpand === undefined) {
  248.                 attributes.yexpand = true;
  249.             }
  250.             if (attributes.yfill === undefined) {
  251.                 attributes.yfill = true;
  252.             }
  253.             var paned = new Sao.common.Paned(orientation);
  254.             // TODO position
  255.             container.add(attributes, paned);
  256.             this.parse(model, node, paned);
  257.         },
  258.         _parse_child: function(model, node, paned, attributes) {
  259.             var container = this.parse(model, node);
  260.             var child;
  261.             if (!paned.get_child1().children().length) {
  262.                 child = paned.get_child1();
  263.             } else {
  264.                 child = paned.get_child2();
  265.             }
  266.             child.append(container.el);
  267.         },
  268.         get_buttons: function() {
  269.             var buttons = [];
  270.             for (var j in this.state_widgets) {
  271.                 var widget = this.state_widgets[j];
  272.                 if (widget instanceof Sao.common.Button) {
  273.                     buttons.push(widget);
  274.                 }
  275.             }
  276.             return buttons;
  277.         },
  278.         display: function() {
  279.             var record = this.screen.current_record;
  280.             var field;
  281.             var name;
  282.             var promesses = {};
  283.             if (record) {
  284.                 // Force to set fields in record
  285.                 // Get first the lazy one to reduce number of requests
  286.                 var fields = [];
  287.                 for (name in record.model.fields) {
  288.                     field = record.model.fields[name];
  289.                     fields.push([name, field.description.loading || 'eager']);
  290.                 }
  291.                 fields.sort(function(a, b) {
  292.                     return a[1].localeCompare(b[1]);
  293.                 });
  294.                 fields.forEach(function(e) {
  295.                     var name = e[0];
  296.                     promesses[name] = record.load(name);
  297.                 });
  298.             }
  299.             var set_state = function(record, field, name) {
  300.                 var prm = jQuery.when();
  301.                 if (name in promesses) {
  302.                     prm = promesses[name];
  303.                 }
  304.                 prm.done(function() {
  305.                     field.set_state(record);
  306.                 });
  307.             };
  308.             var display = function(record, field, name) {
  309.                 return function(widget) {
  310.                     var prm = jQuery.when();
  311.                     if (name in promesses) {
  312.                         prm = promesses[name];
  313.                     }
  314.                     prm.done(function() {
  315.                         widget.display(record, field);
  316.                     });
  317.                 };
  318.             };
  319.             for (name in this.widgets) {
  320.                 var widgets = this.widgets[name];
  321.                 field = null;
  322.                 if (record) {
  323.                     field = record.model.fields[name];
  324.                 }
  325.                 if (field) {
  326.                     set_state(record, field, name);
  327.                 }
  328.                 widgets.forEach(display(record, field, name));
  329.             }
  330.             return jQuery.when.apply(jQuery,
  331.                     jQuery.map(promesses, function(p) {
  332.                         return p;
  333.                     })
  334.                 ).done(function() {
  335.                     var j;
  336.                     for (j in this.state_widgets) {
  337.                         var state_widget = this.state_widgets[j];
  338.                         state_widget.set_state(record);
  339.                     }
  340.                     for (j in this.containers) {
  341.                         var container = this.containers[j];
  342.                         container.resize();
  343.                     }
  344.                     Sao.View.resize(this.el);
  345.                 }.bind(this));
  346.         },
  347.         set_value: function() {
  348.             var record = this.screen.current_record;
  349.             if (record) {
  350.                 var set_value = function(widget) {
  351.                     widget.set_value(record, this);
  352.                 };
  353.                 for (var name in this.widgets) {
  354.                     if (name in record.model.fields) {
  355.                         var widgets = this.widgets[name];
  356.                         var field = record.model.fields[name];
  357.                         widgets.forEach(set_value, field);
  358.                     }
  359.                 }
  360.             }
  361.         },
  362.         button_clicked: function(event) {
  363.             var button = event.data;
  364.             button.el.prop('disabled', true);
  365.             try {
  366.                 this.screen.button(button.attributes);
  367.             } finally {
  368.                 button.el.prop('disabled', false);
  369.             }
  370.         },
  371.         selected_records: function() {
  372.             if (this.screen.current_record) {
  373.                 return [this.screen.current_record];
  374.             }
  375.             return [];
  376.         },
  377.         set_cursor: function(new_, reset_view) {
  378.             var i, name, j;
  379.             var focus_el, notebook, child;
  380.             var widgets, error_el, pages, is_ancestor;
  381.  
  382.             var currently_focused = jQuery(document.activeElement);
  383.             var has_focus = currently_focused.closest(this.el) > 0;
  384.             if (reset_view || has_focus) {
  385.                 if (reset_view) {
  386.                     for (i = 0; i < this.notebooks.length; i++) {
  387.                         notebook = this.notebooks[i];
  388.                         notebook.set_current_page(0);
  389.                     }
  390.                 }
  391.                 if (this.attributes.cursor in this.widgets) {
  392.                     focus_el = Sao.common.find_focusable_child(
  393.                             this.widgets[this.attributes.cursor][0].el);
  394.                 } else {
  395.                     child = Sao.common.find_focusable_child(this.el);
  396.                     if (child) {
  397.                         child.focus();
  398.                     }
  399.                 }
  400.             }
  401.  
  402.             var record = this.screen.current_record;
  403.             if (record) {
  404.                 var invalid_widgets = [];
  405.                 // We use the has-error class to find the invalid elements
  406.                 // because Sao.common.find_focusable_child use the :visible
  407.                 // selector which acts differently than GTK's get_visible
  408.                 var error_els = this.el.find('.has-error');
  409.                 var invalid_fields = record.invalid_fields();
  410.                 for (name in invalid_fields) {
  411.                     widgets = this.widgets[name];
  412.                     for (i = 0; i < error_els.length; i++) {
  413.                         error_el = jQuery(error_els[i]);
  414.                         for (j = 0; j < widgets.length; j++) {
  415.                             if (error_el.closest(widgets[j].el).length > 0) {
  416.                                 invalid_widgets.push(error_el);
  417.                                 break;
  418.                             }
  419.                         }
  420.                     }
  421.                 }
  422.                 if (invalid_widgets.length > 0) {
  423.                     focus_el = Sao.common.find_first_focus_widget(this.el,
  424.                             invalid_widgets);
  425.                 }
  426.             }
  427.  
  428.             if (focus_el) {
  429.                 for (i = 0; i < this.notebooks.length; i++) {
  430.                     notebook = this.notebooks[i];
  431.                     pages = notebook.get_n_pages();
  432.                     for (j = 0; j < pages; j++) {
  433.                         child = notebook.get_nth_page(j);
  434.                         is_ancestor = (
  435.                                 jQuery(focus_el).closest(child).length > 0);
  436.                         if (is_ancestor) {
  437.                             notebook.set_current_page(j);
  438.                             break;
  439.                         }
  440.                     }
  441.                 }
  442.                 // Only input & textarea can grab the focus
  443.                 jQuery(focus_el).find('input,select,textarea').focus();
  444.             }
  445.         }
  446.     });
  447.  
  448.     Sao.View.Form.Container = Sao.class_(Object, {
  449.         init: function(col) {
  450.             if (col === undefined) col = 4;
  451.             this.col = col;
  452.             this.el = jQuery('<table/>', {
  453.                 'class': 'form-container responsive responsive-noheader'
  454.             });
  455.             this.add_row();
  456.         },
  457.         add_row: function() {
  458.             this.el.append(jQuery('<tr/>'));
  459.         },
  460.         rows: function() {
  461.             return this.el.children().children('tr');
  462.         },
  463.         row: function() {
  464.             return this.rows().last();
  465.         },
  466.         add: function(attributes, widget) {
  467.             var colspan = attributes.colspan;
  468.             if (colspan === undefined) colspan = 1;
  469.             var xfill = attributes.xfill;
  470.             if (xfill === undefined) xfill = 1;
  471.             var xexpand = attributes.xexpand;
  472.             if (xexpand === undefined) xexpand = 1;
  473.             var len = 0;
  474.             var row = this.row();
  475.             row.children().map(function(i, e) {
  476.                 len += Number(jQuery(e).attr('colspan') || 1);
  477.             });
  478.             if (len + colspan > this.col) {
  479.                 this.add_row();
  480.                 row = this.row();
  481.             }
  482.             var el;
  483.             if (widget) {
  484.                 el = widget.el;
  485.             }
  486.             var cell = jQuery('<td/>', {
  487.                 'colspan': colspan,
  488.                 'class': widget ? widget.class_ || '' : ''
  489.             }).append(el);
  490.             row.append(cell);
  491.  
  492.             if (!widget) {
  493.                 return;
  494.             }
  495.  
  496.             // TODO yexpand
  497.             if (attributes.yfill) {
  498.                 cell.css('vertical-align', 'top');
  499.             }
  500.  
  501.             if (attributes.xalign !== undefined) {
  502.                 // TODO replace by start/end when supported
  503.                 cell.css('text-align', attributes.xalign >= 0.5? 'right': 'left');
  504.             }
  505.             if (xexpand) {
  506.                 cell.addClass('xexpand');
  507.                 cell.css('width', '100%');
  508.             }
  509.             if (xfill) {
  510.                 cell.addClass('xfill');
  511.                 if (xexpand) {
  512.                     el.css('width', '100%');
  513.                 }
  514.             }
  515.  
  516.             if (attributes.help) {
  517.                 widget.el.data('toggle', 'tooltip');
  518.                 widget.el.attr('title', attributes.help);
  519.                 widget.el.tooltip();
  520.             }
  521.         },
  522.         resize: function() {
  523.             var rows = this.rows().toArray();
  524.             var widths = [];
  525.             var col = this.col;
  526.             var has_expand = false;
  527.             var i, j;
  528.             var get_xexpands = function(row) {
  529.                 row = jQuery(row);
  530.                 var xexpands = [];
  531.                 i = 0;
  532.                 row.children().map(function() {
  533.                     var cell = jQuery(this);
  534.                     var colspan = Math.min(Number(cell.attr('colspan')), col);
  535.                     if (cell.hasClass('xexpand') &&
  536.                         (!jQuery.isEmptyObject(cell.children())) &&
  537.                         (cell.children(':not(.tooltip)').css('display') != 'none')) {
  538.                         xexpands.push([cell, i]);
  539.                     }
  540.                     i += colspan;
  541.                 });
  542.                 return xexpands;
  543.             };
  544.             // Sort rows to compute first the most constraining row
  545.             // which are the one with the more xexpand cells
  546.             // and with the less colspan
  547.             rows.sort(function(a, b) {
  548.                 a = get_xexpands(a);
  549.                 b = get_xexpands(b);
  550.                 if (a.length == b.length) {
  551.                     var reduce = function(previous, current) {
  552.                         var cell = current[0];
  553.                         var colspan = Math.min(
  554.                             Number(cell.attr('colspan')), col);
  555.                         return previous + colspan;
  556.                     };
  557.                     return a.reduce(reduce, 0) - b.reduce(reduce, 0);
  558.                 } else {
  559.                     return b.length - a.length;
  560.                 }
  561.             });
  562.             rows.forEach(function(row) {
  563.                 row = jQuery(row);
  564.                 var xexpands = get_xexpands(row);
  565.                 var width = 100 / xexpands.length;
  566.                 xexpands.forEach(function(e) {
  567.                     var cell = e[0];
  568.                     i = e[1];
  569.                     var colspan = Math.min(Number(cell.attr('colspan')), col);
  570.                     var current_width = 0;
  571.                     for (j = 0; j < colspan; j++) {
  572.                         current_width += widths[i + j] || 0;
  573.                     }
  574.                     for (j = 0; j < colspan; j++) {
  575.                         if (!current_width) {
  576.                             widths[i + j] = width / colspan;
  577.                         } else if (current_width > width) {
  578.                             // Split proprotionally the difference over all cells
  579.                             // following their current width
  580.                             var diff = current_width - width;
  581.                             if (widths[i + j]) {
  582.                                 widths[i + j] -= (diff /
  583.                                     (current_width / widths[i + j]));
  584.                             }
  585.                         }
  586.                     }
  587.                 });
  588.                 if (!jQuery.isEmptyObject(xexpands)) {
  589.                     has_expand = true;
  590.                 }
  591.             });
  592.             rows.forEach(function(row) {
  593.                 row = jQuery(row);
  594.                 i = 0;
  595.                 row.children().map(function() {
  596.                     var cell = jQuery(this);
  597.                     var colspan = Math.min(Number(cell.attr('colspan')), col);
  598.                     if (cell.hasClass('xexpand') &&
  599.                         (cell.children(':not(.tooltip)').css('display') !=
  600.                          'none')) {
  601.                         var width = 0;
  602.                         for (j = 0; j < colspan; j++) {
  603.                             width += widths[i + j] || 0;
  604.                         }
  605.                         cell.css('width', width + '%');
  606.                     } else {
  607.                         cell.css('width', '');
  608.                     }
  609.                     if (cell.children().css('display') == 'none') {
  610.                         cell.css('visibility', 'collapse');
  611.                     } else {
  612.                         cell.css('visibility', 'visible');
  613.                     }
  614.                     i += colspan;
  615.                 });
  616.             });
  617.             if (has_expand) {
  618.                 this.el.css('width', '100%');
  619.             } else {
  620.                 this.el.css('width', '');
  621.             }
  622.         }
  623.     });
  624.  
  625.     Sao.View.Form.StateWidget = Sao.class_(Object, {
  626.         init: function(attributes) {
  627.             this.attributes = attributes;
  628.         },
  629.         set_state: function(record) {
  630.             var state_changes;
  631.             if (record) {
  632.                 state_changes = record.expr_eval(this.attributes.states || {});
  633.             } else {
  634.                 state_changes = {};
  635.             }
  636.             var invisible = state_changes.invisible;
  637.             if (invisible === undefined) {
  638.                 invisible = this.attributes.invisible;
  639.             }
  640.             if (invisible) {
  641.                 this.hide();
  642.             } else {
  643.                 this.show();
  644.             }
  645.         },
  646.         show: function() {
  647.             this.el.show();
  648.         },
  649.         hide: function() {
  650.             this.el.hide();
  651.         }
  652.     });
  653.  
  654.     Sao.View.Form.LabelMixin = Sao.class_(Sao.View.Form.StateWidget, {
  655.         init: function(text, attributes) {
  656.             Sao.View.Form.LabelMixin._super.init.call(this, attributes);
  657.         },
  658.         set_state: function(record) {
  659.             Sao.View.Form.LabelMixin._super.set_state.call(this, record);
  660.             var field;
  661.             if (this.attributes.name && record) {
  662.                 field = record.model.fields[this.attributes.name];
  663.             }
  664.             if ((this.attributes.string === undefined) && field) {
  665.                 var text = '';
  666.                 if (record) {
  667.                     text = field.get_client(record) || '';
  668.                 }
  669.                 this.el.val(text);
  670.             }
  671.             var state_changes;
  672.             if (record) {
  673.                 state_changes = record.expr_eval(attributes && (this.attributes.states || {}));
  674.             } else {
  675.                 state_changes = {};
  676.             }
  677.             if ((field && field.description.required) ||
  678.                     state_changes.required) {
  679.                 this.el.addClass('required');
  680.             } else {
  681.                 this.el.removeClass('required');
  682.             }
  683.             if ((field && field.description.readonly) ||
  684.                     state_changes.readonly) {
  685.                 this.el.removeClass('editable');
  686.                 this.el.removeClass('required');
  687.             } else {
  688.                 this.el.addClass('editable');
  689.             }
  690.         }
  691.     });
  692.  
  693.     Sao.View.Form.Separator = Sao.class_(Sao.View.Form.StateWidget, {
  694.         init: function(text, attributes) {
  695.             Sao.View.Form.Separator._super.init.call(this, attributes);
  696.             this.el = jQuery('<div/>', {
  697.                 'class': 'form-separator'
  698.             });
  699.             if (text) {
  700.                 this.el.append(jQuery('<label/>', {
  701.                     text: text
  702.                 }));
  703.             }
  704.             this.el.append(jQuery('<hr/>'));
  705.         }
  706.     });
  707.  
  708.     Sao.View.Form.Label = Sao.class_(Sao.View.Form.LabelMixin, {
  709.         class_: 'form-label',
  710.         init: function(text, attributes) {
  711.             Sao.View.Form.Label._super.init.call(this, attributes);
  712.             this.el = jQuery('<label/>', {
  713.                 text: text,
  714.                 'class': this.class_ + ' form-label'
  715.             });
  716.         },
  717.         set_state: function(record) {
  718.             console.log(record);
  719.             Sao.View.Form.Label._super.set_state.call(this, record);
  720.         }
  721.     });
  722.  
  723.     Sao.View.Form.Notebook = Sao.class_(Sao.View.Form.StateWidget, {
  724.         class_: 'form-notebook',
  725.         init: function(attributes) {
  726.             Sao.View.Form.Notebook._super.init.call(this, attributes);
  727.             this.el = jQuery('<div/>', {
  728.                 'class': this.class_
  729.             });
  730.             this.nav = jQuery('<ul/>', {
  731.                 'class': 'nav nav-tabs',
  732.                 role: 'tablist'
  733.             }).appendTo(this.el);
  734.             this.panes = jQuery('<div/>', {
  735.                 'class': 'tab-content'
  736.             }).appendTo(this.el);
  737.             this.selected = false;
  738.         },
  739.         add: function(tab, text) {
  740.             var pane = jQuery('<div/>', {
  741.                 'role': 'tabpanel',
  742.                 'class': 'tab-pane',
  743.             }).uniqueId();
  744.             var tab_id = pane.attr('id');
  745.             var page = jQuery('<li/>', {
  746.                 'role': 'presentation'
  747.             }).append(
  748.                 jQuery('<a/>', {
  749.                     'aria-controls': tab_id,
  750.                     'role': 'tab',
  751.                     'data-toggle': 'tab',
  752.                     'href': '#' + tab_id
  753.                 }).append(text)
  754.                 .on('shown.bs.tab', function() {
  755.                     Sao.View.resize(tab);
  756.                 })).appendTo(this.nav);
  757.             pane.html(tab).appendTo(this.panes);
  758.             if (!this.selected) {
  759.                 // Can not use .tab('show')
  760.                 page.addClass('active');
  761.                 pane.addClass('active');
  762.                 this.selected = true;
  763.             }
  764.             return page;
  765.         },
  766.         set_current_page: function(page_index) {
  767.             var tab = this.nav.find(
  768.                     'li[role="presentation"]:eq(' + page_index + ') a');
  769.             tab.tab('show');
  770.         },
  771.         get_n_pages: function() {
  772.             return this.nav.find("li[role='presentation']").length;
  773.         },
  774.         get_nth_page: function(page_index) {
  775.             return jQuery(this.panes.find("div[role='tabpanel']")[page_index]);
  776.         }
  777.     });
  778.  
  779.     Sao.View.Form.Page = Sao.class_(Sao.View.Form.StateWidget, {
  780.         init: function(el, attributes) {
  781.             Sao.View.Form.Page._super.init.call(this, attributes);
  782.             this.el = el;
  783.         }
  784.     });
  785.  
  786.     Sao.View.Form.Group = Sao.class_(Sao.View.Form.StateWidget, {
  787.         class_: 'form-group_',
  788.         init: function(attributes) {
  789.             Sao.View.Form.Group._super.init.call(this, attributes);
  790.             this.el = jQuery('<div/>', {
  791.                 'class': this.class_
  792.             });
  793.         },
  794.         add: function(widget) {
  795.             this.el.append(widget.el);
  796.         }
  797.     });
  798.  
  799.     Sao.View.Form.Image_ = Sao.class_(Sao.View.Form.StateWidget, {
  800.         class_: 'form-image_',
  801.         init: function(attributes) {
  802.             Sao.View.Form.Image_._super.init.call(this, attributes);
  803.             this.el = jQuery('<div/>', {
  804.                 'class_': this.class_
  805.             });
  806.             this.img = jQuery('<img/>', {
  807.                 'class': 'center-block'
  808.             }).appendTo(this.el);
  809.             Sao.common.ICONFACTORY.register_icon(attributes.name)
  810.                 .done(function(url) {
  811.                     this.img.attr('src', url);
  812.                 }.bind(this));
  813.         }
  814.     });
  815.  
  816.     Sao.View.form_widget_get = function(type) {
  817.         switch (type) {
  818.             case 'char':
  819.                 return Sao.View.Form.Char;
  820.             case 'password':
  821.                 return Sao.View.Form.Password;
  822.             case 'date':
  823.                 return Sao.View.Form.Date;
  824.             case 'datetime':
  825.                 return Sao.View.Form.DateTime;
  826.             case 'time':
  827.                 return Sao.View.Form.Time;
  828.             case 'timedelta':
  829.                 return Sao.View.Form.TimeDelta;
  830.             case 'integer':
  831.             case 'biginteger':
  832.                 return Sao.View.Form.Integer;
  833.             case 'float':
  834.             case 'numeric':
  835.                 return Sao.View.Form.Float;
  836.             case 'selection':
  837.                 return Sao.View.Form.Selection;
  838.             case 'boolean':
  839.                 return Sao.View.Form.Boolean;
  840.             case 'text':
  841.                 return Sao.View.Form.Text;
  842.             case 'richtext':
  843.                 return Sao.View.Form.RichText;
  844.             case 'many2one':
  845.                 return Sao.View.Form.Many2One;
  846.             case 'one2one':
  847.                 return Sao.View.Form.One2One;
  848.             case 'reference':
  849.                 return Sao.View.Form.Reference;
  850.             case 'one2many':
  851.                 return Sao.View.Form.One2Many;
  852.             case 'many2many':
  853.                 return Sao.View.Form.Many2Many;
  854.             case 'binary':
  855.                 return Sao.View.Form.Binary;
  856.             case 'multiselection':
  857.                 return Sao.View.Form.MultiSelection;
  858.             case 'image':
  859.                 return Sao.View.Form.Image;
  860.             case 'url':
  861.                 return Sao.View.Form.URL;
  862.             case 'email':
  863.                 return Sao.View.Form.Email;
  864.             case 'callto':
  865.                 return Sao.View.Form.CallTo;
  866.             case 'sip':
  867.                 return Sao.View.Form.SIP;
  868.             case 'progressbar':
  869.                 return Sao.View.Form.ProgressBar;
  870.             case 'dict':
  871.                 return Sao.View.Form.Dict;
  872.         }
  873.     };
  874.  
  875.  
  876.     Sao.View.Form.Widget = Sao.class_(Object, {
  877.         init: function(field_name, model, attributes) {
  878.             this.field_name = field_name;
  879.             this.model = model;
  880.             this.view = null;  // Filled later
  881.             this.attributes = attributes;
  882.             this.el = null;
  883.             this.position = 0;
  884.             this.visible = true;
  885.             this.labelled = null;  // Element which received the labelledby
  886.         },
  887.         display: function(record, field) {
  888.             var readonly = this.attributes.readonly;
  889.             var invisible = this.attributes.invisible;
  890.             if (!field) {
  891.                 if (readonly === undefined) {
  892.                     readonly = true;
  893.                 }
  894.                 if (invisible === undefined) {
  895.                     invisible = false;
  896.                 }
  897.                 this.set_readonly(readonly);
  898.                 this.set_invisible(invisible);
  899.                 return;
  900.             }
  901.             var state_attrs = field.get_state_attrs(record);
  902.             if (readonly === undefined) {
  903.                 readonly = state_attrs.readonly;
  904.                 if (readonly === undefined) {
  905.                     readonly = false;
  906.                 }
  907.             }
  908.             if (this.view.screen.attributes.readonly) {
  909.                 readonly = true;
  910.             }
  911.             this.set_readonly(readonly);
  912.             var invalid = state_attrs.invalid;
  913.             if (!readonly && invalid) {
  914.                 this.el.addClass('has-error');
  915.             } else {
  916.                 this.el.removeClass('has-error');
  917.             }
  918.             if (invisible === undefined) {
  919.                 invisible = field.get_state_attrs(record).invisible;
  920.                 if (invisible === undefined) {
  921.                     invisible = false;
  922.                 }
  923.             }
  924.             this.set_invisible(invisible);
  925.         },
  926.         record: function() {
  927.             if (this.view && this.view.screen) {
  928.                 return this.view.screen.current_record;
  929.             }
  930.         },
  931.         field: function() {
  932.             var record = this.record();
  933.             if (record) {
  934.                 return record.model.fields[this.field_name];
  935.             }
  936.         },
  937.         focus_out: function() {
  938.             if (!this.field()) {
  939.                 return;
  940.             }
  941.             if (!this.visible) {
  942.                 return;
  943.             }
  944.             this.set_value(this.record(), this.field());
  945.         },
  946.         set_value: function(record, field) {
  947.         },
  948.         set_readonly: function(readonly) {
  949.             this.el.prop('disabled', readonly);
  950.         },
  951.         set_invisible: function(invisible) {
  952.             this.visible = !invisible;
  953.             if (invisible) {
  954.                 this.el.hide();
  955.             } else {
  956.                 this.el.show();
  957.             }
  958.         }
  959.     });
  960.  
  961.     Sao.View.Form.Char = Sao.class_(Sao.View.Form.Widget, {
  962.         class_: 'form-char',
  963.         init: function(field_name, model, attributes) {
  964.             Sao.View.Form.Char._super.init.call(this, field_name, model,
  965.                 attributes);
  966.             this.el = jQuery('<div/>', {
  967.                 'class': this.class_
  968.             });
  969.             this.group = jQuery('<div/>', {
  970.                 'class': 'input-group input-group-sm'
  971.             }).appendTo(this.el);
  972.             this.input = this.labelled = jQuery('<input/>', {
  973.                 'type': 'text',
  974.                 'class': 'form-control input-sm'
  975.             }).appendTo(this.group);
  976.             if (attributes.autocomplete) {
  977.                 this.datalist = jQuery('<datalist/>').appendTo(this.el);
  978.                 this.datalist.uniqueId();
  979.                 this.input.attr('list', this.datalist.attr('id'));
  980.             }
  981.             this.el.change(this.focus_out.bind(this));
  982.  
  983.             if (!attributes.size) {
  984.                 this.group.css('width', '100%');
  985.             }
  986.         },
  987.         display: function(record, field) {
  988.             Sao.View.Form.Char._super.display.call(this, record, field);
  989.             if (this.datalist) {
  990.                 this.datalist.children().remove();
  991.                 var selection = [];
  992.                 if (record) {
  993.                     selection = record.autocompletion[this.field_name] || [];
  994.                 }
  995.                 selection.forEach(function(e) {
  996.                     jQuery('<option/>', {
  997.                         'value': e
  998.                     }).appendTo(this.datalist);
  999.                 }.bind(this));
  1000.             }
  1001.  
  1002.             // Set size
  1003.             var length = '';
  1004.             var width = '100%';
  1005.             if (record) {
  1006.                 length = record.expr_eval(this.attributes.size);
  1007.                 if (length > 0) {
  1008.                     width = null;
  1009.                 }
  1010.             }
  1011.             this.input.attr('maxlength', length);
  1012.             this.input.attr('size', length);
  1013.             this.group.css('width', width);
  1014.  
  1015.             if (record) {
  1016.                 var value = record.field_get_client(this.field_name);
  1017.                 this.input.val(value || '');
  1018.             } else {
  1019.                 this.input.val('');
  1020.             }
  1021.         },
  1022.         set_value: function(record, field) {
  1023.             field.set_client(record, this.input.val());
  1024.         },
  1025.         set_readonly: function(readonly) {
  1026.             this.input.prop('readonly', readonly);
  1027.         },
  1028.         focus: function() {
  1029.             this.input.focus();
  1030.         }
  1031.     });
  1032.  
  1033.     Sao.View.Form.Password = Sao.class_(Sao.View.Form.Char, {
  1034.         class_: 'form-password',
  1035.         init: function(field_name, model, attributes) {
  1036.             Sao.View.Form.Password._super.init.call(this, field_name, model,
  1037.                 attributes);
  1038.             this.input.prop('type', 'password');
  1039.         }
  1040.     });
  1041.  
  1042.     Sao.View.Form.Date = Sao.class_(Sao.View.Form.Widget, {
  1043.         class_: 'form-date',
  1044.         _width: '12em',
  1045.         init: function(field_name, model, attributes) {
  1046.             Sao.View.Form.Date._super.init.call(this, field_name, model,
  1047.                 attributes);
  1048.             this.el = jQuery('<div/>', {
  1049.                 'class': this.class_
  1050.             });
  1051.             this.date = this.labelled = jQuery('<div/>', {
  1052.                 'class': 'input-group input-group-sm'
  1053.             }).appendTo(this.el);
  1054.             this.input = jQuery('<input/>', {
  1055.                 'type': 'text',
  1056.                 'class': 'form-control input-sm'
  1057.             }).appendTo(this.date);
  1058.             jQuery('<span/>', {
  1059.                 'class': 'input-group-btn'
  1060.             }).append(jQuery('<button/>', {
  1061.                 'class': 'btn btn-default',
  1062.                 'type': 'button'
  1063.             }).append(jQuery('<span/>', {
  1064.                 'class': 'glyphicon glyphicon-calendar'
  1065.             }))).appendTo(this.date);
  1066.             this.date.datetimepicker();
  1067.             this.date.css('width', this._width);
  1068.             this.date.on('dp.change', this.focus_out.bind(this));
  1069.         },
  1070.         get_format: function(record, field) {
  1071.             return field.date_format(record);
  1072.         },
  1073.         get_value: function(record, field) {
  1074.             var value = this.date.data('DateTimePicker').date();
  1075.             if (value) {
  1076.                 value.isDate = true;
  1077.             }
  1078.             return value;
  1079.         },
  1080.         display: function(record, field) {
  1081.             if (record && field) {
  1082.                 this.date.data('DateTimePicker').format(
  1083.                     Sao.common.moment_format(this.get_format(record, field)));
  1084.             }
  1085.             Sao.View.Form.Date._super.display.call(this, record, field);
  1086.             var value;
  1087.             if (record) {
  1088.                 value = field.get_client(record);
  1089.             } else {
  1090.                 value = null;
  1091.             }
  1092.             this.date.data('DateTimePicker').date(value);
  1093.         },
  1094.         focus: function() {
  1095.             this.input.focus();
  1096.         },
  1097.         set_value: function(record, field) {
  1098.             field.set_client(record, this.get_value(record, field));
  1099.         },
  1100.         set_readonly: function(readonly) {
  1101.             this.date.find('button').prop('disabled', readonly);
  1102.             this.date.find('input').prop('readonly', readonly);
  1103.         }
  1104.     });
  1105.  
  1106.     Sao.View.Form.DateTime = Sao.class_(Sao.View.Form.Date, {
  1107.         class_: 'form-datetime',
  1108.         _width: '25em',
  1109.         get_format: function(record, field) {
  1110.             return field.date_format(record) + ' ' + field.time_format(record);
  1111.         },
  1112.         get_value: function(record, field) {
  1113.             var value = this.date.data('DateTimePicker').date();
  1114.             if (value) {
  1115.                 value.isDateTime = true;
  1116.             }
  1117.             return value;
  1118.         }
  1119.     });
  1120.  
  1121.     Sao.View.Form.Time = Sao.class_(Sao.View.Form.Date, {
  1122.         class_: 'form-time',
  1123.         _width: '10em',
  1124.         get_format: function(record, field) {
  1125.             return field.time_format(record);
  1126.         },
  1127.         get_value: function(record, field) {
  1128.             var value = this.date.data('DateTimePicker').date();
  1129.             if (value) {
  1130.                 value.isTime = true;
  1131.             }
  1132.             return value;
  1133.         }
  1134.     });
  1135.  
  1136.     Sao.View.Form.TimeDelta = Sao.class_(Sao.View.Form.Widget, {
  1137.         class_: 'form-timedelta',
  1138.         init: function(field_name, model, attributes) {
  1139.             Sao.View.Form.TimeDelta._super.init.call(this, field_name, model,
  1140.                 attributes);
  1141.             this.el = jQuery('<div/>', {
  1142.                 'class': this.class_
  1143.             });
  1144.             this.input = this.labelled = jQuery('<input/>', {
  1145.                 'type': 'text',
  1146.                 'class': 'form-control input-sm'
  1147.             }).appendTo(this.el);
  1148.             this.el.change(this.focus_out.bind(this));
  1149.         },
  1150.         display: function(record, field) {
  1151.             Sao.View.Form.TimeDelta._super.display.call(this, record, field);
  1152.             if (record) {
  1153.                 var value = record.field_get_client(this.field_name);
  1154.                 this.input.val(value || '');
  1155.             } else {
  1156.                 this.input.val('');
  1157.             }
  1158.         },
  1159.         focus: function() {
  1160.             this.input.focus();
  1161.         },
  1162.         set_value: function(record, field) {
  1163.             field.set_client(record, this.input.val());
  1164.         },
  1165.         set_readonly: function(readonly) {
  1166.             this.input.prop('readonly', readonly);
  1167.         }
  1168.     });
  1169.  
  1170.     Sao.View.Form.Integer = Sao.class_(Sao.View.Form.Char, {
  1171.         class_: 'form-integer',
  1172.         init: function(field_name, model, attributes) {
  1173.             Sao.View.Form.Integer._super.init.call(this, field_name, model,
  1174.                 attributes);
  1175.             this.input.attr('type', 'text');
  1176.             this.input.attr('width', 8);
  1177.             this.group.css('width', '');
  1178.             this.factor = Number(attributes.factor || 1);
  1179.         },
  1180.         set_value: function(record, field) {
  1181.             field.set_client(record, this.input.val(), undefined, this.factor);
  1182.         },
  1183.         display: function(record, field) {
  1184.             // Skip Char call
  1185.             Sao.View.Form.Char._super.display.call(this, record, field);
  1186.             if (record) {
  1187.                 var value = record.model.fields[this.field_name]
  1188.                     .get_client(record, this.factor);
  1189.                 this.input.val(value);
  1190.             } else {
  1191.                 this.input.val('');
  1192.             }
  1193.         }
  1194.     });
  1195.  
  1196.     Sao.View.Form.Float = Sao.class_(Sao.View.Form.Integer, {
  1197.         class_: 'form-float'
  1198.     });
  1199.  
  1200.     Sao.View.Form.Selection = Sao.class_(Sao.View.Form.Widget, {
  1201.         class_: 'form-selection',
  1202.         init: function(field_name, model, attributes) {
  1203.             Sao.View.Form.Selection._super.init.call(this, field_name, model,
  1204.                 attributes);
  1205.             this.el = jQuery('<div/>', {
  1206.                 'class': this.class_
  1207.             });
  1208.             this.select = this.labelled = jQuery('<select/>', {
  1209.                 'class': 'form-control input-sm'
  1210.             });
  1211.             this.el.append(this.select);
  1212.             this.select.change(this.focus_out.bind(this));
  1213.             Sao.common.selection_mixin.init.call(this);
  1214.             this.init_selection();
  1215.         },
  1216.         init_selection: function(key) {
  1217.             Sao.common.selection_mixin.init_selection.call(this, key,
  1218.                 this.set_selection.bind(this));
  1219.         },
  1220.         update_selection: function(record, field, callbak) {
  1221.             Sao.common.selection_mixin.update_selection.call(this, record,
  1222.                 field, function(selection) {
  1223.                     this.set_selection(selection);
  1224.                     if (callbak) {
  1225.                         callbak();
  1226.                     }
  1227.                 }.bind(this));
  1228.         },
  1229.         set_selection: function(selection) {
  1230.             var select = this.select;
  1231.             select.empty();
  1232.             selection.forEach(function(e) {
  1233.                 select.append(jQuery('<option/>', {
  1234.                     'value': JSON.stringify(e[0]),
  1235.                     'text': e[1]
  1236.                 }));
  1237.             });
  1238.         },
  1239.         display_update_selection: function(record, field) {
  1240.             this.update_selection(record, field, function() {
  1241.                 if (!field) {
  1242.                     this.select.val('');
  1243.                     return;
  1244.                 }
  1245.                 var value = field.get(record);
  1246.                 var prm, found = false;
  1247.                 for (var i = 0, len = this.selection.length; i < len; i++) {
  1248.                     if (this.selection[i][0] === value) {
  1249.                         found = true;
  1250.                         break;
  1251.                     }
  1252.                 }
  1253.                 if (!found) {
  1254.                     prm = Sao.common.selection_mixin.get_inactive_selection
  1255.                         .call(this, value);
  1256.                     prm.done(function(inactive) {
  1257.                         this.select.append(jQuery('<option/>', {
  1258.                             value: JSON.stringify(inactive[0]),
  1259.                             text: inactive[1],
  1260.                             disabled: true
  1261.                         }));
  1262.                     }.bind(this));
  1263.                 } else {
  1264.                     prm = jQuery.when();
  1265.                 }
  1266.                 prm.done(function() {
  1267.                     this.select.val(JSON.stringify(value));
  1268.                 }.bind(this));
  1269.             }.bind(this));
  1270.         },
  1271.         display: function(record, field) {
  1272.             Sao.View.Form.Selection._super.display.call(this, record, field);
  1273.             this.display_update_selection(record, field);
  1274.         },
  1275.         focus: function() {
  1276.             this.select.focus();
  1277.         },
  1278.         value_get: function() {
  1279.             return JSON.parse(this.select.val());
  1280.         },
  1281.         set_value: function(record, field) {
  1282.             var value = this.value_get();
  1283.             field.set_client(record, value);
  1284.         },
  1285.         set_readonly: function(readonly) {
  1286.             this.select.prop('disabled', readonly);
  1287.         }
  1288.     });
  1289.  
  1290.     Sao.View.Form.Boolean = Sao.class_(Sao.View.Form.Widget, {
  1291.         class_: 'form-boolean',
  1292.         init: function(field_name, model, attributes) {
  1293.             Sao.View.Form.Boolean._super.init.call(this, field_name, model,
  1294.                 attributes);
  1295.             this.el = jQuery('<div/>', {
  1296.                 'class': this.class_
  1297.             });
  1298.             this.input = this.labelled = jQuery('<input/>', {
  1299.                 'type': 'checkbox',
  1300.                 'class': 'form-control input-sm'
  1301.             }).appendTo(this.el);
  1302.             this.input.change(this.focus_out.bind(this));
  1303.         },
  1304.         display: function(record, field) {
  1305.             Sao.View.Form.Boolean._super.display.call(this, record, field);
  1306.             if (record) {
  1307.                 this.input.prop('checked', record.field_get(this.field_name));
  1308.             } else {
  1309.                 this.input.prop('checked', false);
  1310.             }
  1311.         },
  1312.         focus: function() {
  1313.             this.input.focus();
  1314.         },
  1315.         set_value: function(record, field) {
  1316.             var value = this.input.prop('checked');
  1317.             field.set_client(record, value);
  1318.         },
  1319.         set_readonly: function(readonly) {
  1320.             this.input.prop('readonly', readonly);
  1321.         }
  1322.     });
  1323.  
  1324.     Sao.View.Form.Text = Sao.class_(Sao.View.Form.Widget, {
  1325.         class_: 'form-text',
  1326.         init: function(field_name, model, attributes) {
  1327.             Sao.View.Form.Text._super.init.call(this, field_name, model,
  1328.                 attributes);
  1329.             this.el = jQuery('<div/>', {
  1330.                 'class': this.class_
  1331.             });
  1332.             this.input = this.labelled = jQuery('<textarea/>', {
  1333.                 'class': 'form-control input-sm'
  1334.             }).appendTo(this.el);
  1335.             this.input.change(this.focus_out.bind(this));
  1336.         },
  1337.         display: function(record, field) {
  1338.             Sao.View.Form.Text._super.display.call(this, record, field);
  1339.             if (record) {
  1340.                 var value = record.field_get_client(this.field_name);
  1341.                 this.input.val(value);
  1342.             } else {
  1343.                 this.input.val('');
  1344.             }
  1345.         },
  1346.         focus: function() {
  1347.             this.input.focus();
  1348.         },
  1349.         set_value: function(record, field) {
  1350.             var value = this.input.val() || '';
  1351.             field.set_client(record, value);
  1352.         },
  1353.         set_readonly: function(readonly) {
  1354.             this.input.prop('readonly', readonly);
  1355.         }
  1356.     });
  1357.  
  1358.     Sao.View.Form.RichText = Sao.class_(Sao.View.Form.Widget, {
  1359.         class_: 'form-richtext',
  1360.         init: function(field_name, model, attributes) {
  1361.             var i, properties, button;
  1362.             Sao.View.Form.RichText._super.init.call(
  1363.                     this, field_name, model, attributes);
  1364.             this.el = jQuery('<div/>', {
  1365.                 'class': this.class_ + ' panel panel-default'
  1366.             });
  1367.             this.toolbar = jQuery('<div/>', {
  1368.                 'class': 'btn-toolbar',
  1369.                 'role': 'toolbar'
  1370.             }).appendTo(jQuery('<div/>', {
  1371.                 'class': 'panel-heading'
  1372.             }).appendTo(this.el));
  1373.  
  1374.             var button_apply_command = function(evt) {
  1375.                 document.execCommand(evt.data);
  1376.             };
  1377.  
  1378.             var add_buttons = function(buttons) {
  1379.                 var group = jQuery('<div/>', {
  1380.                     'class': 'btn-group',
  1381.                     'role': 'group'
  1382.                 }).appendTo(this.toolbar);
  1383.                 for (i in buttons) {
  1384.                     properties = buttons[i];
  1385.                     button = jQuery('<button/>', {
  1386.                         'class': 'btn btn-default',
  1387.                         'type': 'button'
  1388.                     }).append(jQuery('<span/>', {
  1389.                         'class': 'glyphicon glyphicon-' + properties.icon
  1390.                     })).appendTo(group);
  1391.                     button.click(properties.command, button_apply_command);
  1392.                 }
  1393.             }.bind(this);
  1394.  
  1395.             add_buttons([
  1396.                     {
  1397.                         'icon': 'bold',
  1398.                         'command': 'bold'
  1399.                     }, {
  1400.                         'icon': 'italic',
  1401.                         'command': 'italic'
  1402.                     }, {
  1403.                         'icon': 'text-color',  // XXX
  1404.                         'command': 'underline'
  1405.                     }]);
  1406.  
  1407.             var selections = [
  1408.             {
  1409.                 'heading': Sao.i18n.gettext('Font'),
  1410.                 'options': ['Normal', 'Serif', 'Sans', 'Monospace'],  // XXX
  1411.                 'command': 'fontname'
  1412.             }, {
  1413.                 'heading': Sao.i18n.gettext('Size'),
  1414.                 'options': [1, 2, 3, 4, 5, 6, 7],
  1415.                 'command': 'fontsize'
  1416.             }];
  1417.             var add_option = function(dropdown, properties) {
  1418.                 return function(option) {
  1419.                     dropdown.append(jQuery('<li/>').append(jQuery('<a/>', {
  1420.                         'href': '#'
  1421.                     }).append(option).click(function() {
  1422.                         document.execCommand(properties.command, false, option);
  1423.                     })));
  1424.                 };
  1425.             };
  1426.             for (i in selections) {
  1427.                 properties = selections[i];
  1428.                 var group = jQuery('<div/>', {
  1429.                     'class': 'btn-group',
  1430.                     'role': 'group'
  1431.                 }).appendTo(this.toolbar);
  1432.                 button = jQuery('<button/>', {
  1433.                     'class': 'btn btn-default dropdown-toggle',
  1434.                     'type': 'button',
  1435.                     'data-toggle': 'dropdown',
  1436.                     'aria-expanded': false,
  1437.                     'aria-haspopup': true
  1438.                 }).append(properties.heading)
  1439.                 .append(jQuery('<span/>', {
  1440.                     'class': 'caret'
  1441.                 })).appendTo(group);
  1442.                 var dropdown = jQuery('<ul/>', {
  1443.                     'class': 'dropdown-menu'
  1444.                 }).appendTo(group);
  1445.                 properties.options.forEach(add_option(dropdown, properties));
  1446.             }
  1447.  
  1448.             add_buttons([
  1449.                     {
  1450.                         'icon': 'align-left',
  1451.                         'command': 'justifyLeft'
  1452.                     }, {
  1453.                         'icon': 'align-center',
  1454.                         'command': 'justifyCenter'
  1455.                     }, {
  1456.                         'icon': 'align-right',
  1457.                         'command': 'justifyRight'
  1458.                     }, {
  1459.                         'icon': 'align-justify',
  1460.                         'command': 'justifyFull'
  1461.                     }]);
  1462.  
  1463.             // TODO backColor
  1464.             [['foreColor', '#000000']].forEach(
  1465.                     function(e) {
  1466.                         var command = e[0];
  1467.                         var color = e[1];
  1468.                         jQuery('<input/>', {
  1469.                             'class': 'btn btn-default',
  1470.                             'type': 'color'
  1471.                         }).appendTo(this.toolbar)
  1472.                         .change(function() {
  1473.                             document.execCommand(command, false, jQuery(this).val());
  1474.                         }).focusin(function() {
  1475.                             document.execCommand(command, false, jQuery(this).val());
  1476.                         }).val(color);
  1477.             }.bind(this));
  1478.  
  1479.             this.input = this.labelled = jQuery('<div/>', {
  1480.                 'class': 'richtext',
  1481.                 'contenteditable': true
  1482.             }).appendTo(jQuery('<div/>', {
  1483.                 'class': 'panel-body'
  1484.             }).appendTo(this.el));
  1485.             this.el.focusout(this.focus_out.bind(this));
  1486.         },
  1487.         focus_out: function() {
  1488.             // Let browser set the next focus before testing
  1489.             // if it moved out of the widget
  1490.             window.setTimeout(function() {
  1491.                 if (this.el.find(':focus').length === 0) {
  1492.                     Sao.View.Form.RichText._super.focus_out.call(this);
  1493.                 }
  1494.             }.bind(this), 0);
  1495.         },
  1496.         display: function(record, field) {
  1497.             Sao.View.Form.RichText._super.display.call(this, record, field);
  1498.             var value = '';
  1499.             if (record) {
  1500.                 value = record.field_get_client(this.field_name);
  1501.             }
  1502.             this.input.html(value);
  1503.         },
  1504.         focus: function() {
  1505.             this.input.focus();
  1506.         },
  1507.         set_value: function(record, field) {
  1508.             // TODO order attributes
  1509.             this.input.find('div').each(function(i, el) {
  1510.                 el = jQuery(el);
  1511.                 // Not all browsers respect the styleWithCSS
  1512.                 if (el.css('text-align')) {
  1513.                     // Remove browser specific prefix
  1514.                     var align = el.css('text-align').split('-').pop();
  1515.                     el.attr('align', align);
  1516.                     el.css('text-align', '');
  1517.                 }
  1518.                 // Some browsers set start as default align
  1519.                 if (el.attr('align') == 'start') {
  1520.                     el.attr('align', 'left');
  1521.                 }
  1522.             });
  1523.             var value = this.input.html() || '';
  1524.             field.set_client(record, value);
  1525.         },
  1526.         set_readonly: function(readonly) {
  1527.             this.input.prop('contenteditable', !readonly);
  1528.             this.toolbar.find('button,select').prop('disabled', readonly);
  1529.         }
  1530.     });
  1531.  
  1532.     Sao.View.Form.Many2One = Sao.class_(Sao.View.Form.Widget, {
  1533.         class_: 'form-many2one',
  1534.         init: function(field_name, model, attributes) {
  1535.             Sao.View.Form.Many2One._super.init.call(this, field_name, model,
  1536.                 attributes);
  1537.             this.el = jQuery('<div/>', {
  1538.                 'class': this.class_
  1539.             });
  1540.             var group = jQuery('<div/>', {
  1541.                 'class': 'input-group input-group-sm'
  1542.             }).appendTo(this.el);
  1543.             this.entry = this.labelled = jQuery('<input/>', {
  1544.                 'type': 'input',
  1545.                 'class': 'form-control input-sm'
  1546.             }).appendTo(group);
  1547.             // Use keydown to not receive focus-in TAB
  1548.             this.entry.on('keydown', this.key_press.bind(this));
  1549.  
  1550.             if (!attributes.completion || attributes.completion == "1") {
  1551.                 Sao.common.get_completion(group,
  1552.                     this._update_completion.bind(this),
  1553.                     this._completion_match_selected.bind(this),
  1554.                     this._completion_action_activated.bind(this));
  1555.                 this.wid_completion = true;
  1556.             }
  1557.  
  1558.             // Append buttons after the completion to not break layout
  1559.             var buttons = jQuery('<span/>', {
  1560.                 'class': 'input-group-btn'
  1561.             }).appendTo(group);
  1562.             this.but_open = jQuery('<button/>', {
  1563.                 'class': 'btn btn-default',
  1564.                 'type': 'button'
  1565.             }).append(jQuery('<span/>', {
  1566.                 'class': 'glyphicon glyphicon-search'
  1567.             })).appendTo(buttons);
  1568.             this.but_open.click(this.edit.bind(this));
  1569.  
  1570.             this.el.change(this.focus_out.bind(this));
  1571.             this._readonly = false;
  1572.         },
  1573.         get_screen: function() {
  1574.             var domain = this.field().get_domain(this.record());
  1575.             var context = this.field().get_context(this.record());
  1576.             return new Sao.Screen(this.get_model(), {
  1577.                 'context': context,
  1578.                 'domain': domain,
  1579.                 'mode': ['form'],
  1580.                 'view_ids': (this.attributes.view_ids || '').split(','),
  1581.                 'views_preload': this.attributes.views,
  1582.                 'readonly': this._readonly
  1583.             });
  1584.         },
  1585.         set_text: function(value) {
  1586.             if (jQuery.isEmptyObject(value)) {
  1587.                 value = '';
  1588.             }
  1589.             this.entry.val(value);
  1590.         },
  1591.         get_text: function() {
  1592.             var record = this.record();
  1593.             if (record) {
  1594.                 return record.field_get_client(this.field_name);
  1595.             }
  1596.             return '';
  1597.         },
  1598.         focus_out: function() {
  1599.             if (!this.attributes.completion ||
  1600.                     this.attributes.completion == "1") {
  1601.                 if (this.el.find('.dropdown').hasClass('open')) {
  1602.                     return;
  1603.                 }
  1604.             }
  1605.             Sao.View.Form.Many2One._super.focus_out.call(this);
  1606.         },
  1607.         set_value: function(record, field) {
  1608.             if (field.get_client(record) != this.entry.val()) {
  1609.                 field.set_client(record, this.value_from_id(null, ''));
  1610.                 this.entry.val('');
  1611.             }
  1612.         },
  1613.         display: function(record, field) {
  1614.             var screen_record = this.record();
  1615.             if ((screen_record && record) && (screen_record.id != record.id)) {
  1616.                 return;
  1617.             }
  1618.  
  1619.             var text_value, value;
  1620.             Sao.View.Form.Many2One._super.display.call(this, record, field);
  1621.  
  1622.             this._set_button_sensitive();
  1623.             this._set_completion();
  1624.  
  1625.             if (!record) {
  1626.                 this.entry.val('');
  1627.                 return;
  1628.             }
  1629.             this.set_text(field.get_client(record));
  1630.             value = field.get(record);
  1631.             if (this.has_target(value)) {
  1632.                 this.but_open.button({
  1633.                     'icons': {
  1634.                         'primary': 'glyphicon-folder-open'
  1635.                     }});
  1636.             } else {
  1637.                 this.but_open.button({
  1638.                     'icons': {
  1639.                         'primary': 'glyphicon-search'
  1640.                     }});
  1641.             }
  1642.         },
  1643.         focus: function() {
  1644.             this.entry.focus();
  1645.         },
  1646.         set_readonly: function(readonly) {
  1647.             this._readonly = readonly;
  1648.             this._set_button_sensitive();
  1649.         },
  1650.         _set_button_sensitive: function() {
  1651.             this.entry.prop('readonly', this._readonly);
  1652.             this.but_open.prop('disabled',
  1653.                     this._readonly || !this.read_access());
  1654.         },
  1655.         get_access: function(type) {
  1656.             var model = this.get_model();
  1657.             if (model) {
  1658.                 return Sao.common.MODELACCESS.get(model)[type];
  1659.             }
  1660.             return true;
  1661.         },
  1662.         read_access: function() {
  1663.             return this.get_access('read');
  1664.         },
  1665.         create_access: function() {
  1666.             return this.attributes.create && this.get_access('create');
  1667.         },
  1668.         id_from_value: function(value) {
  1669.             return value;
  1670.         },
  1671.         value_from_id: function(id, str) {
  1672.             if (str === undefined) {
  1673.                 str = '';
  1674.             }
  1675.             return [id, str];
  1676.         },
  1677.         get_model: function() {
  1678.             return this.attributes.relation;
  1679.         },
  1680.         has_target: function(value) {
  1681.             return value !== undefined && value !== null;
  1682.         },
  1683.         edit: function(evt) {
  1684.             var model = this.get_model();
  1685.             if (!model || !Sao.common.MODELACCESS.get(model).read) {
  1686.                 return;
  1687.             }
  1688.             var win, callback;
  1689.             var record = this.record();
  1690.             var value = record.field_get(this.field_name);
  1691.             if (model && this.has_target(value)) {
  1692.                 var screen = this.get_screen();
  1693.                 var m2o_id =
  1694.                     this.id_from_value(record.field_get(this.field_name));
  1695.                 screen.new_group([m2o_id]);
  1696.                 callback = function(result) {
  1697.                     if (result) {
  1698.                         var rec_name_prm = screen.current_record.rec_name();
  1699.                         rec_name_prm.done(function(name) {
  1700.                             var value = this.value_from_id(
  1701.                                 screen.current_record.id, name);
  1702.                             this.record().field_set_client(this.field_name,
  1703.                                 value, true);
  1704.                         }.bind(this));
  1705.                     }
  1706.                 };
  1707.                 win = new Sao.Window.Form(screen, callback.bind(this), {
  1708.                     save_current: true
  1709.                 });
  1710.             } else if (model) {
  1711.                 var dom;
  1712.                 var domain = this.field().get_domain(record);
  1713.                 var context = this.field().get_context(record);
  1714.                 var text = this.entry.val();
  1715.                 callback = function(result) {
  1716.                     if (!jQuery.isEmptyObject(result)) {
  1717.                         var value = this.value_from_id(result[0][0],
  1718.                                 result[0][1]);
  1719.                         this.record().field_set_client(this.field_name,
  1720.                                 value, true);
  1721.                     }
  1722.                 };
  1723.                 var parser = new Sao.common.DomainParser();
  1724.                 win = new Sao.Window.Search(model,
  1725.                         callback.bind(this), {
  1726.                             sel_multi: false,
  1727.                             context: context,
  1728.                             domain: domain,
  1729.                             view_ids: (this.attributes.view_ids ||
  1730.                                 '').split(','),
  1731.                             views_preload: (this.attributes.views || {}),
  1732.                             new_: this.create_access(),
  1733.                             search_filter: parser.quote(text)
  1734.                         });
  1735.             }
  1736.         },
  1737.         new_: function(evt) {
  1738.             var model = this.get_model();
  1739.             if (!model || ! Sao.common.MODELACCESS.get(model).create) {
  1740.                 return;
  1741.             }
  1742.             var screen = this.get_screen();
  1743.             var callback = function(result) {
  1744.                 if (result) {
  1745.                     var rec_name_prm = screen.current_record.rec_name();
  1746.                     rec_name_prm.done(function(name) {
  1747.                         var value = this.value_from_id(
  1748.                             screen.current_record.id, name);
  1749.                         this.record().field_set_client(this.field_name, value);
  1750.                     }.bind(this));
  1751.                 }
  1752.             };
  1753.             var win = new Sao.Window.Form(screen, callback.bind(this), {
  1754.                 new_: true,
  1755.                 save_current: true
  1756.             });
  1757.         },
  1758.         key_press: function(event_) {
  1759.             var editable = !this.entry.prop('readonly');
  1760.             var activate_keys = [Sao.common.TAB_KEYCODE];
  1761.             var delete_keys = [Sao.common.BACKSPACE_KEYCODE,
  1762.                 Sao.common.DELETE_KEYCODE];
  1763.             if (!this.wid_completion) {
  1764.                 activate_keys.push(Sao.common.RETURN_KEYCODE);
  1765.             }
  1766.  
  1767.             if (event_.which == Sao.common.F3_KEYCODE &&
  1768.                     editable &&
  1769.                     this.create_access()) {
  1770.                 this.new_();
  1771.                 event_.preventDefault();
  1772.             } else if (event_.which == Sao.common.F2_KEYCODE &&
  1773.                     this.read_access()) {
  1774.                 this.edit();
  1775.                 event_.preventDefault();
  1776.             } else if (~activate_keys.indexOf(event_.which) && editable) {
  1777.                 if (!this.attributes.completion ||
  1778.                         this.attributes.completion == "1") {
  1779.                     if (this.el.find('.dropdown').hasClass('open')) {
  1780.                         return;
  1781.                     }
  1782.                 }
  1783.                 this.activate();
  1784.             } else if (this.has_target(this.record().field_get(
  1785.                             this.field_name)) && editable) {
  1786.                 var value = this.get_text();
  1787.                 if ((value != this.entry.val()) ||
  1788.                         ~delete_keys.indexOf(event_.which)) {
  1789.                     this.entry.val('');
  1790.                     this.record().field_set_client(this.field_name,
  1791.                         this.value_from_id(null, ''));
  1792.                 }
  1793.             }
  1794.         },
  1795.         activate: function() {
  1796.             var model = this.get_model();
  1797.             if (!model || !Sao.common.MODELACCESS.get(model).read) {
  1798.                 return;
  1799.             }
  1800.             var record = this.record();
  1801.             var value = record.field_get(this.field_name);
  1802.             var sao_model = new Sao.Model(model);
  1803.  
  1804.             if (model && !this.has_target(value)) {
  1805.                 var text = this.entry.val();
  1806.                 if (!this._readonly && (text ||
  1807.                             this.field().get_state_attrs(this.record())
  1808.                             .required)) {
  1809.                     var dom;
  1810.                     var domain = this.field().get_domain(record);
  1811.                     var context = this.field().get_context(record);
  1812.  
  1813.                     var callback = function(result) {
  1814.                         if (!jQuery.isEmptyObject(result)) {
  1815.                             var value = this.value_from_id(result[0][0],
  1816.                                 result[0][1]);
  1817.                             this.record().field_set_client(this.field_name,
  1818.                                 value, true);
  1819.                         } else {
  1820.                             this.entry.val('');
  1821.                         }
  1822.                     };
  1823.                     var parser = new Sao.common.DomainParser();
  1824.                     var win = new Sao.Window.Search(model,
  1825.                             callback.bind(this), {
  1826.                                 sel_multi: false,
  1827.                                 context: context,
  1828.                                 domain: domain,
  1829.                                 view_ids: (this.attributes.view_ids ||
  1830.                                     '').split(','),
  1831.                                 views_preload: (this.attributes.views ||
  1832.                                     {}),
  1833.                                 new_: this.create_access(),
  1834.                                 search_filter: parser.quote(text)
  1835.                             });
  1836.                 }
  1837.             }
  1838.         },
  1839.         _set_completion: function() {
  1840.             var search = this.el.find('.action-search');
  1841.             if (this.read_access()) {
  1842.                 search.removeClass('disabled');
  1843.             } else {
  1844.                 search.addClass('disabled');
  1845.             }
  1846.             var create = this.el.find('.action-create');
  1847.             if (this.create_access()) {
  1848.                 create.removeClass('disabled');
  1849.             } else {
  1850.                 create.addClass('disabled');
  1851.             }
  1852.         },
  1853.         _update_completion: function(text) {
  1854.             var record = this.record();
  1855.             if (!record) {
  1856.                 return;
  1857.             }
  1858.             var field = this.field();
  1859.             var value = field.get(record);
  1860.             if (this.has_target(value)) {
  1861.                 var id = this.id_from_value(value);
  1862.                 if ((id !== undefined) && (id > 0)) {
  1863.                     return jQuery.when();
  1864.                 }
  1865.             }
  1866.             var model = this.get_model();
  1867.  
  1868.             return Sao.common.update_completion(
  1869.                     this.entry, record, field, model);
  1870.         },
  1871.         _completion_match_selected: function(value) {
  1872.             this.record().field_set_client(this.field_name,
  1873.                     this.value_from_id(
  1874.                         value.id, value.rec_name), true);
  1875.         },
  1876.         _completion_action_activated: function(action) {
  1877.             if (action == 'search') {
  1878.                 this.edit();
  1879.             } else if (action == 'create') {
  1880.                 this.new_();
  1881.             }
  1882.         }
  1883.     });
  1884.  
  1885.     Sao.View.Form.One2One = Sao.class_(Sao.View.Form.Many2One, {
  1886.         class_: 'form-one2one'
  1887.     });
  1888.  
  1889.     Sao.View.Form.Reference = Sao.class_(Sao.View.Form.Many2One, {
  1890.         class_: 'form-reference',
  1891.         init: function(field_name, model, attributes) {
  1892.             Sao.View.Form.Reference._super.init.call(this, field_name, model,
  1893.                 attributes);
  1894.             this.el.addClass('form-inline');
  1895.             this.select = jQuery('<select/>', {
  1896.                 'class': 'form-control input-sm',
  1897.                 'aria-label': attributes.string
  1898.             });
  1899.             this.el.prepend(jQuery('<span/>').text('-'));
  1900.             this.el.prepend(this.select);
  1901.             this.select.change(this.select_changed.bind(this));
  1902.             Sao.common.selection_mixin.init.call(this);
  1903.             this.init_selection();
  1904.         },
  1905.         init_selection: function(key) {
  1906.             Sao.common.selection_mixin.init_selection.call(this, key,
  1907.                 this.set_selection.bind(this));
  1908.         },
  1909.         update_selection: function(record, field, callback) {
  1910.             Sao.common.selection_mixin.update_selection.call(this, record,
  1911.                 field, function(selection) {
  1912.                     this.set_selection(selection);
  1913.                     if (callback) {
  1914.                         callback();
  1915.                     }
  1916.                 }.bind(this));
  1917.         },
  1918.         set_selection: function(selection) {
  1919.             var select = this.select;
  1920.             select.empty();
  1921.             selection.forEach(function(e) {
  1922.                 select.append(jQuery('<option/>', {
  1923.                     'value': e[0],
  1924.                     'text': e[1]
  1925.                 }));
  1926.             });
  1927.         },
  1928.         id_from_value: function(value) {
  1929.             return parseInt(value.split(',')[1], 10);
  1930.         },
  1931.         value_from_id: function(id, str) {
  1932.             if (!str) {
  1933.                 str = '';
  1934.             }
  1935.             return [this.get_model(), [id, str]];
  1936.         },
  1937.         get_text: function() {
  1938.             var record = this.record();
  1939.             if (record) {
  1940.                 return record.field_get_client(this.field_name)[1];
  1941.             }
  1942.             return '';
  1943.         },
  1944.         get_model: function() {
  1945.             return this.select.val();
  1946.         },
  1947.         has_target: function(value) {
  1948.             if (value === null) {
  1949.                 return false;
  1950.             }
  1951.             var model = value.split(',')[0];
  1952.             value = value.split(',')[1];
  1953.             if (jQuery.isEmptyObject(value)) {
  1954.                 value = null;
  1955.             } else {
  1956.                 value = parseInt(value, 10);
  1957.                 if (isNaN(value)) {
  1958.                     value = null;
  1959.                 }
  1960.             }
  1961.             return (model == this.get_model()) && (value >= 0);
  1962.         },
  1963.         _set_button_sensitive: function() {
  1964.             Sao.View.Form.Reference._super._set_button_sensitive.call(this);
  1965.             this.select.prop('disabled', this.entry.prop('readonly'));
  1966.         },
  1967.         select_changed: function() {
  1968.             this.entry.val('');
  1969.             var model = this.get_model();
  1970.             var value;
  1971.             if (model) {
  1972.                 value = [model, [-1, '']];
  1973.             } else {
  1974.                 value = ['', ''];
  1975.             }
  1976.             this.record().field_set_client(this.field_name, value);
  1977.         },
  1978.         set_value: function(record, field) {
  1979.             var value;
  1980.             if (!this.get_model()) {
  1981.                 value = this.entry.val();
  1982.                 if (jQuery.isEmptyObject(value)) {
  1983.                     field.set_client(record, null);
  1984.                 } else {
  1985.                     field.set_client(record, ['', value]);
  1986.                 }
  1987.             } else {
  1988.                 value = field.get_client(record, this.field_name);
  1989.                 var model, name;
  1990.                 if (value instanceof Array) {
  1991.                     model = value[0];
  1992.                     name = value[1];
  1993.                 } else {
  1994.                     model = '';
  1995.                     name = '';
  1996.                 }
  1997.                 if ((model != this.get_model()) ||
  1998.                         (name != this.entry.val())) {
  1999.                     field.set_client(record, null);
  2000.                     this.entry.val('');
  2001.                 }
  2002.             }
  2003.         },
  2004.         set_text: function(value) {
  2005.             var model;
  2006.             if (value) {
  2007.                 model = value[0];
  2008.                 value = value[1];
  2009.             } else {
  2010.                 model = null;
  2011.                 value = null;
  2012.             }
  2013.             Sao.View.Form.Reference._super.set_text.call(this, value);
  2014.             if (model) {
  2015.                 this.select.val(model);
  2016.             } else {
  2017.                 this.select.val('');
  2018.             }
  2019.         },
  2020.         display: function(record, field) {
  2021.             this.update_selection(record, field, function() {
  2022.                 Sao.View.Form.Reference._super.display.call(this, record, field);
  2023.             }.bind(this));
  2024.         },
  2025.         set_readonly: function(readonly) {
  2026.             Sao.View.Form.Reference._super.set_readonly.call(this, readonly);
  2027.             this.select.prop('disabled', readonly);
  2028.         }
  2029.     });
  2030.  
  2031.     Sao.View.Form.One2Many = Sao.class_(Sao.View.Form.Widget, {
  2032.         class_: 'form-one2many',
  2033.         init: function(field_name, model, attributes) {
  2034.             Sao.View.Form.One2Many._super.init.call(this, field_name, model,
  2035.                 attributes);
  2036.  
  2037.             this._readonly = true;
  2038.  
  2039.             this.el = jQuery('<div/>', {
  2040.                 'class': this.class_ + ' panel panel-default'
  2041.             });
  2042.             this.menu = jQuery('<div/>', {
  2043.                 'class': this.class_ + '-menu panel-heading'
  2044.             });
  2045.             this.el.append(this.menu);
  2046.  
  2047.             var label = jQuery('<label/>', {
  2048.                 'class': this.class_ + '-string',
  2049.                 text: attributes.string
  2050.             });
  2051.             this.menu.append(label);
  2052.  
  2053.             label.uniqueId();
  2054.             this.el.uniqueId();
  2055.             this.el.attr('aria-labelledby', label.attr('id'));
  2056.             label.attr('for', this.el.attr('id'));
  2057.  
  2058.             var toolbar = jQuery('<div/>', {
  2059.                 'class': this.class_ + '-toolbar'
  2060.             });
  2061.             this.menu.append(toolbar);
  2062.  
  2063.             var group = jQuery('<div/>', {
  2064.                 'class': 'input-group input-group-sm'
  2065.             }).appendTo(toolbar);
  2066.  
  2067.             this.wid_text = jQuery('<input/>', {
  2068.                 type: 'text',
  2069.                 'class': 'form-control input-sm'
  2070.             }).appendTo(group);
  2071.             this.wid_text.hide();
  2072.  
  2073.             var buttons = jQuery('<div/>', {
  2074.                 'class': 'input-group-btn'
  2075.             }).appendTo(group);
  2076.  
  2077.             if (attributes.add_remove) {
  2078.                 this.wid_text.show();
  2079.                 // TODO add completion
  2080.                 //
  2081.                 this.but_add = jQuery('<button/>', {
  2082.                     'class': 'btn btn-default btn-sm',
  2083.                     'type': 'button',
  2084.                     'aria-label': Sao.i18n.gettext('Add')
  2085.                 }).append(jQuery('<span/>', {
  2086.                     'class': 'glyphicon glyphicon-plus'
  2087.                 })).appendTo(buttons);
  2088.                 this.but_add.click(this.add.bind(this));
  2089.  
  2090.                 this.but_remove = jQuery('<button/>', {
  2091.                     'class': 'btn btn-default btn-sm',
  2092.                     'type': 'button',
  2093.                     'aria-label': Sao.i18n.gettext('Remove')
  2094.                 }).append(jQuery('<span/>', {
  2095.                     'class': 'glyphicon glyphicon-minus'
  2096.                 })).appendTo(buttons);
  2097.                 this.but_remove.click(this.remove.bind(this));
  2098.             }
  2099.  
  2100.             this.but_new = jQuery('<button/>', {
  2101.                 'class': 'btn btn-default btn-sm',
  2102.                 'type': 'button',
  2103.                 'aria-label': Sao.i18n.gettext('New')
  2104.             }).append(jQuery('<span/>', {
  2105.                 'class': 'glyphicon glyphicon-edit'
  2106.             })).appendTo(buttons);
  2107.             this.but_new.click(this.new_.bind(this));
  2108.  
  2109.             this.but_open = jQuery('<button/>', {
  2110.                 'class': 'btn btn-default btn-sm',
  2111.                 'type': 'button',
  2112.                 'aria-label': Sao.i18n.gettext('Open')
  2113.             }).append(jQuery('<span/>', {
  2114.                 'class': 'glyphicon glyphicon-folder-open'
  2115.             })).appendTo(buttons);
  2116.             this.but_open.click(this.open.bind(this));
  2117.  
  2118.             this.but_del = jQuery('<button/>', {
  2119.                 'class': 'btn btn-default btn-sm',
  2120.                 'type': 'button',
  2121.                 'aria-label': Sao.i18n.gettext('Delete')
  2122.             }).append(jQuery('<span/>', {
  2123.                 'class': 'glyphicon glyphicon-trash'
  2124.             })).appendTo(buttons);
  2125.             this.but_del.click(this.delete_.bind(this));
  2126.  
  2127.             this.but_undel = jQuery('<button/>', {
  2128.                 'class': 'btn btn-default btn-sm',
  2129.                 'type': 'button',
  2130.                 'aria-label': Sao.i18n.gettext('Undelete')
  2131.             }).append(jQuery('<span/>', {
  2132.                 'class': 'glyphicon glyphicon-repeat'
  2133.             })).appendTo(buttons);
  2134.             this.but_undel.click(this.undelete.bind(this));
  2135.  
  2136.             this.but_previous = jQuery('<button/>', {
  2137.                 'class': 'btn btn-default btn-sm',
  2138.                 'type': 'button',
  2139.                 'aria-label': Sao.i18n.gettext('Previous')
  2140.             }).append(jQuery('<span/>', {
  2141.                 'class': 'glyphicon glyphicon-chevron-left'
  2142.             })).appendTo(buttons);
  2143.             this.but_previous.click(this.previous.bind(this));
  2144.  
  2145.             this.label = jQuery('<span/>', {
  2146.                 'class': 'btn'
  2147.             }).appendTo(buttons);
  2148.             this.label.text('(0, 0)');
  2149.  
  2150.             this.but_next = jQuery('<button/>', {
  2151.                 'class': 'btn btn-default btn-sm',
  2152.                 'type': 'button',
  2153.                 'aria-label': Sao.i18n.gettext('Next')
  2154.             }).append(jQuery('<span/>', {
  2155.                 'class': 'glyphicon glyphicon-chevron-right'
  2156.             })).appendTo(buttons);
  2157.             this.but_next.click(this.next.bind(this));
  2158.  
  2159.             this.but_switch = jQuery('<button/>', {
  2160.                 'class': 'btn btn-default btn-sm',
  2161.                 'type': 'button',
  2162.                 'aria-label': Sao.i18n.gettext('Switch')
  2163.             }).append(jQuery('<span/>', {
  2164.                 'class': 'glyphicon glyphicon-list-alt'
  2165.             })).appendTo(buttons);
  2166.             this.but_switch.click(this.switch_.bind(this));
  2167.  
  2168.             this.content = jQuery('<div/>', {
  2169.                 'class': this.class_ + '-content panel-body'
  2170.             });
  2171.             this.el.append(this.content);
  2172.  
  2173.             var modes = (attributes.mode || 'tree,form').split(',');
  2174.             this.screen = new Sao.Screen(attributes.relation, {
  2175.                 mode: modes,
  2176.                 view_ids: (attributes.view_ids || '').split(','),
  2177.                 views_preload: attributes.views || {},
  2178.                 row_activate: this.activate.bind(this),
  2179.                 readonly: attributes.readonly || false,
  2180.                 exclude_field: attributes.relation_field || null,
  2181.                 pre_validate: attributes.pre_validate
  2182.             });
  2183.             this.screen.pre_validate = attributes.pre_validate == 1;
  2184.             this.prm = this.screen.switch_view(modes[0]).done(function() {
  2185.                 this.content.append(this.screen.screen_container.el);
  2186.             }.bind(this));
  2187.  
  2188.             // TODO key_press
  2189.  
  2190.             this.but_switch.prop('disabled', this.screen.number_of_views() <= 0);
  2191.         },
  2192.         set_readonly: function(readonly) {
  2193.             this._readonly = readonly;
  2194.             this._set_button_sensitive();
  2195.         },
  2196.         _set_button_sensitive: function() {
  2197.             var access = Sao.common.MODELACCESS.get(this.screen.model_name);
  2198.             var size_limit, o2m_size;
  2199.             var record = this.record();
  2200.             var field = this.field();
  2201.             if (record && field) {
  2202.                 var field_size = record.expr_eval(this.attributes.size);
  2203.                 o2m_size = field.get_eval(record);
  2204.                 size_limit = (((field_size !== undefined) &&
  2205.                             (field_size !== null)) &&
  2206.                         (o2m_size >= field_size >= 0));
  2207.             } else {
  2208.                 o2m_size = null;
  2209.                 size_limit = false;
  2210.             }
  2211.             var create = this.attributes.create;
  2212.             if (create === undefined) {
  2213.                 create = true;
  2214.             }
  2215.             this.but_new.prop('disabled', this._readonly || !create ||
  2216.                     size_limit || !access.create);
  2217.  
  2218.             var delete_ = this.attributes['delete'];
  2219.             if (delete_ === undefined) {
  2220.                 delete_ = true;
  2221.             }
  2222.             // TODO position
  2223.             this.but_del.prop('disabled', this._readonly || !delete_ ||
  2224.                     !access['delete']);
  2225.             this.but_undel.prop('disabled', this._readonly || size_limit);
  2226.             this.but_open.prop('disabled', !access.read);
  2227.             // TODO but_next, but_previous
  2228.             if (this.attributes.add_remove) {
  2229.                 this.wid_text.prop('disabled', this._readonly);
  2230.                 this.but_add.prop('disabled', this._readonly || size_limit ||
  2231.                         !access.write || !access.read);
  2232.                 this.but_remove.prop('disabled', this._readonly ||
  2233.                         !access.write || !access.read);
  2234.             }
  2235.         },
  2236.         display: function(record, field) {
  2237.             Sao.View.Form.One2Many._super.display.call(this, record, field);
  2238.  
  2239.             this._set_button_sensitive();
  2240.  
  2241.             this.prm.done(function() {
  2242.                 if (!record) {
  2243.                     return;
  2244.                 }
  2245.                 if (field === undefined) {
  2246.                     this.screen.new_group();
  2247.                     this.screen.set_current_record(null);
  2248.                     this.screen.group.parent = null;
  2249.                     this.screen.display();
  2250.                     return;
  2251.                 }
  2252.  
  2253.                 var new_group = record.field_get_client(this.field_name);
  2254.                 if (new_group != this.screen.group) {
  2255.                     this.screen.set_group(new_group);
  2256.                     if ((this.screen.current_view.view_type == 'tree') &&
  2257.                             this.screen.current_view.editable) {
  2258.                         this.screen.set_current_record(null);
  2259.                     }
  2260.                 }
  2261.                 var readonly = false;
  2262.                 var domain = [];
  2263.                 var size_limit = null;
  2264.                 if (record) {
  2265.                     readonly = field.get_state_attrs(record).readonly;
  2266.                     domain = field.get_domain(record);
  2267.                     size_limit = record.expr_eval(this.attributes.size);
  2268.                 }
  2269.                 if (!Sao.common.compare(this.screen.domain, domain)) {
  2270.                     this.screen.domain = domain;
  2271.                 }
  2272.                 this.screen.group.set_readonly(readonly);
  2273.                 this.screen.size_limit = size_limit;
  2274.                 this.screen.display();
  2275.             }.bind(this));
  2276.         },
  2277.         focus: function() {
  2278.             if (this.wid_text.is(':visible')) {
  2279.                 this.wid_text.focus();
  2280.             }
  2281.         },
  2282.         activate: function(event_) {
  2283.             this.edit();
  2284.         },
  2285.         add: function(event_) {
  2286.             var access = Sao.common.MODELACCESS.get(this.screen.model_name);
  2287.             if (!access.write || !access.read) {
  2288.                 return;
  2289.             }
  2290.             this.view.set_value();
  2291.             var domain = this.field().get_domain(this.record());
  2292.             var context = this.field().get_context(this.record());
  2293.             domain = [domain,
  2294.                 this.record().expr_eval(this.attributes.add_remove)];
  2295.             var removed_ids = this.field().get_removed_ids(this.record());
  2296.             domain = ['OR', domain, ['id', 'in', removed_ids]];
  2297.             var text = this.wid_text.val();
  2298.  
  2299.             // TODO sequence
  2300.  
  2301.             var callback = function(result) {
  2302.                 var prm = jQuery.when();
  2303.                 if (!jQuery.isEmptyObject(result)) {
  2304.                     var ids = [];
  2305.                     var i, len;
  2306.                     for (i = 0, len = result.length; i < len; i++) {
  2307.                         ids.push(result[i][0]);
  2308.                     }
  2309.                     this.screen.group.load(ids, true);
  2310.                     prm = this.screen.display();
  2311.                 }
  2312.                 prm.done(function() {
  2313.                     this.screen.set_cursor();
  2314.                 }.bind(this));
  2315.                 this.wid_text.val('');
  2316.             }.bind(this);
  2317.             var parser = new Sao.common.DomainParser();
  2318.             var win = new Sao.Window.Search(this.attributes.relation,
  2319.                     callback, {
  2320.                         sel_multi: true,
  2321.                         context: context,
  2322.                         domain: domain,
  2323.                         view_ids: (this.attributes.view_ids ||
  2324.                                 '').split(','),
  2325.                         views_preload: this.attributes.views || {},
  2326.                         new_: !this.but_new.prop('disabled'),
  2327.                         search_filter: parser.quote(text)
  2328.                     });
  2329.         },
  2330.         remove: function(event_) {
  2331.             var access = Sao.common.MODELACCESS.get(this.screen.model_name);
  2332.             if (!access.write || !access.read) {
  2333.                 return;
  2334.             }
  2335.             this.screen.remove(false, true, false);
  2336.         },
  2337.         new_: function(event_) {
  2338.             if (!Sao.common.MODELACCESS.get(this.screen.model_name).create) {
  2339.                 return;
  2340.             }
  2341.             this.validate().done(function() {
  2342.                 if (this.attributes.product) {
  2343.                     this.new_product();
  2344.                 } else {
  2345.                     this.new_single();
  2346.                 }
  2347.             }.bind(this));
  2348.         },
  2349.         new_single: function() {
  2350.             var context = jQuery.extend({},
  2351.                     this.field().get_context(this.record()));
  2352.             // TODO sequence
  2353.             if (this.screen.current_view.type == 'form' ||
  2354.                     this.screen.current_view.editable) {
  2355.                 this.screen.new_();
  2356.                 this.screen.current_view.el.prop('disabled', false);
  2357.             } else {
  2358.                 var record = this.record();
  2359.                 var field_size = record.expr_eval(
  2360.                     this.attributes.size) || -1;
  2361.                 field_size -= this.field().get_eval(record);
  2362.                 var win = new Sao.Window.Form(this.screen, function() {}, {
  2363.                     new_: true,
  2364.                     many: field_size,
  2365.                     context: context
  2366.                 });
  2367.             }
  2368.         },
  2369.         new_product: function() {
  2370.             var fields = this.attributes.product.split(',');
  2371.             var product = {};
  2372.             var screen = this.screen;
  2373.  
  2374.             screen.new_(false).then(function(first) {
  2375.                 first.default_get().then(function(default_) {
  2376.                     first.set_default(default_);
  2377.  
  2378.                     var search_set = function() {
  2379.                         if (jQuery.isEmptyObject(fields)) {
  2380.                             return make_product();
  2381.                         }
  2382.                         var field = screen.model.fields[fields.pop()];
  2383.                         var relation = field.description.relation;
  2384.                         if (!relation) {
  2385.                             search_set();
  2386.                         }
  2387.  
  2388.                         var domain = field.get_domain(first);
  2389.                         var context = field.get_context(first);
  2390.  
  2391.                         var callback = function(result) {
  2392.                             if (!jQuery.isEmptyObject(result)) {
  2393.                                 product[field.name] = result;
  2394.                             }
  2395.                             search_set();
  2396.                         };
  2397.                         var win_search = new Sao.Window.Search(relation,
  2398.                                 callback, {
  2399.                                     sel_multi: true,
  2400.                                     context: context,
  2401.                                     domain: domain,
  2402.                                     search_filter: ''
  2403.                         });
  2404.                     };
  2405.  
  2406.                     var make_product = function() {
  2407.                         if (jQuery.isEmptyObject(product)) {
  2408.                             screen.group.remove(first, true);
  2409.                             return;
  2410.                         }
  2411.  
  2412.                         var fields = Object.keys(product);
  2413.                         var values = fields.map(function(field) {
  2414.                             return product[field];
  2415.                         });
  2416.                         Sao.common.product(values).forEach(function(values) {
  2417.                             var set_default = function(record) {
  2418.                                 var default_value = jQuery.extend({}, default_);
  2419.                                 fields.forEach(function(field, i) {
  2420.                                     default_value[field] = values[i][0];
  2421.                                     default_value[field + '.rec_name'] = values[i][1];
  2422.                                 });
  2423.                                 record.set_default(default_value);
  2424.                             };
  2425.  
  2426.                             var record;
  2427.                             if (first) {
  2428.                                 set_default(first);
  2429.                                 first = null;
  2430.                             } else {
  2431.                                 screen.new_(false).then(set_default);
  2432.                             }
  2433.                         });
  2434.                     };
  2435.  
  2436.                     search_set();
  2437.                 });
  2438.             });
  2439.         },
  2440.         open: function(event_) {
  2441.             this.edit();
  2442.         },
  2443.         delete_: function(event_) {
  2444.             if (!Sao.common.MODELACCESS.get(this.screen.model_name)['delete']) {
  2445.                 return;
  2446.             }
  2447.             this.screen.remove(false, false, false);
  2448.         },
  2449.         undelete: function(event_) {
  2450.             this.screen.unremove();
  2451.         },
  2452.         previous: function(event_) {
  2453.             this.validate().done(function() {
  2454.                 this.screen.display_previous();
  2455.             }.bind(this));
  2456.         },
  2457.         next: function(event_) {
  2458.             this.validate().done(function() {
  2459.                 this.screen.display_next();
  2460.             }.bind(this));
  2461.         },
  2462.         switch_: function(event_) {
  2463.             this.screen.switch_view();
  2464.         },
  2465.         edit: function() {
  2466.             if (!Sao.common.MODELACCESS.get(this.screen.model_name).read) {
  2467.                 return;
  2468.             }
  2469.             this.validate().done(function() {
  2470.                 var record = this.screen.current_record;
  2471.                 if (record) {
  2472.                     var win = new Sao.Window.Form(this.screen, function() {});
  2473.                 }
  2474.             }.bind(this));
  2475.         },
  2476.         validate: function() {
  2477.             var prm = jQuery.Deferred();
  2478.             this.view.set_value();
  2479.             var record = this.screen.current_record;
  2480.             if (record) {
  2481.                 var fields = this.screen.current_view.get_fields();
  2482.                 record.validate(fields).then(function(validate) {
  2483.                     if (!validate) {
  2484.                         this.screen.display(true);
  2485.                         prm.reject();
  2486.                         return;
  2487.                     }
  2488.                     if (this.screen.pre_validate) {
  2489.                         return record.pre_validate().then(function(validate) {
  2490.                             if (!validate) {
  2491.                                 prm.reject();
  2492.                                 return;
  2493.                             }
  2494.                             prm.resolve();
  2495.                         });
  2496.                     }
  2497.                     prm.resolve();
  2498.                 }.bind(this));
  2499.             } else {
  2500.                 prm.resolve();
  2501.             }
  2502.             return prm;
  2503.         },
  2504.         set_value: function(record, field) {
  2505.             this.screen.save_tree_state();
  2506.         }
  2507.     });
  2508.  
  2509.     Sao.View.Form.Many2Many = Sao.class_(Sao.View.Form.Widget, {
  2510.         class_: 'form-many2many',
  2511.         init: function(field_name, model, attributes) {
  2512.             Sao.View.Form.Many2Many._super.init.call(this, field_name, model,
  2513.                 attributes);
  2514.  
  2515.             this._readonly = true;
  2516.  
  2517.             this.el = jQuery('<div/>', {
  2518.                 'class': this.class_ + ' panel panel-default'
  2519.             });
  2520.             this.menu = jQuery('<div/>', {
  2521.                 'class': this.class_ + '-menu panel-heading'
  2522.             });
  2523.             this.el.append(this.menu);
  2524.  
  2525.             var label = jQuery('<label/>', {
  2526.                 'class': this.class_ + '-string',
  2527.                 text: attributes.string
  2528.             });
  2529.             this.menu.append(label);
  2530.  
  2531.             label.uniqueId();
  2532.             this.el.uniqueId();
  2533.             this.el.attr('aria-labelledby', label.attr('id'));
  2534.             label.attr('for', this.el.attr('id'));
  2535.  
  2536.             var toolbar = jQuery('<div/>', {
  2537.                 'class': this.class_ + '-toolbar'
  2538.             });
  2539.             this.menu.append(toolbar);
  2540.  
  2541.             var group = jQuery('<div/>', {
  2542.                 'class': 'input-group input-group-sm'
  2543.             }).appendTo(toolbar);
  2544.             this.entry = jQuery('<input/>', {
  2545.                 type: 'text',
  2546.                 'class': 'form-control input-sm'
  2547.             }).appendTo(group);
  2548.             // Use keydown to not receive focus-in TAB
  2549.             this.entry.on('keydown', this.key_press.bind(this));
  2550.  
  2551.             // TODO completion
  2552.  
  2553.             var buttons = jQuery('<div/>', {
  2554.                 'class': 'input-group-btn'
  2555.             }).appendTo(group);
  2556.             this.but_add = jQuery('<button/>', {
  2557.                 'class': 'btn btn-default btn-sm',
  2558.                 'type': 'button',
  2559.                 'aria-label': Sao.i18n.gettext('Add')
  2560.             }).append(jQuery('<span/>', {
  2561.                 'class': 'glyphicon glyphicon-plus'
  2562.             })).appendTo(buttons);
  2563.             this.but_add.click(this.add.bind(this));
  2564.  
  2565.             this.but_remove = jQuery('<button/>', {
  2566.                 'class': 'btn btn-default btn-sm',
  2567.                 'type': 'button',
  2568.                 'aria-label': Sao.i18n.gettext('Remove')
  2569.             }).append(jQuery('<span/>', {
  2570.                 'class': 'glyphicon glyphicon-minus'
  2571.             })).appendTo(buttons);
  2572.             this.but_remove.click(this.remove.bind(this));
  2573.  
  2574.             this.content = jQuery('<div/>', {
  2575.                 'class': this.class_ + '-content panel-body'
  2576.             });
  2577.             this.el.append(this.content);
  2578.  
  2579.             this.screen = new Sao.Screen(attributes.relation, {
  2580.                 mode: ['tree'],
  2581.                 view_ids: (attributes.view_ids || '').split(','),
  2582.                 views_preload: attributes.views || {},
  2583.                 row_activate: this.activate.bind(this)
  2584.             });
  2585.             this.prm = this.screen.switch_view('tree').done(function() {
  2586.                 this.content.append(this.screen.screen_container.el);
  2587.             }.bind(this));
  2588.         },
  2589.         set_readonly: function(readonly) {
  2590.             this._readonly = readonly;
  2591.             this._set_button_sensitive();
  2592.         },
  2593.         _set_button_sensitive: function() {
  2594.             var size_limit = false;
  2595.             if (this.record() && this.field()) {
  2596.                 // TODO
  2597.             }
  2598.  
  2599.             this.entry.prop('disabled', this._readonly);
  2600.             this.but_add.prop('disabled', this._readonly || size_limit);
  2601.             // TODO position
  2602.             this.but_remove.prop('disabled', this._readonly);
  2603.         },
  2604.         display: function(record, field) {
  2605.             Sao.View.Form.Many2Many._super.display.call(this, record, field);
  2606.  
  2607.             this.prm.done(function() {
  2608.                 if (!record) {
  2609.                     return;
  2610.                 }
  2611.                 if (field === undefined) {
  2612.                     this.screen.new_group();
  2613.                     this.screen.set_current_record(null);
  2614.                     this.screen.group.parent = null;
  2615.                     this.screen.display();
  2616.                     return;
  2617.                 }
  2618.                 var new_group = record.field_get_client(this.field_name);
  2619.                 if (new_group != this.screen.group) {
  2620.                     this.screen.set_group(new_group);
  2621.                 }
  2622.                 this.screen.display();
  2623.             }.bind(this));
  2624.         },
  2625.         focus: function() {
  2626.             this.entry.focus();
  2627.         },
  2628.         activate: function() {
  2629.             this.edit();
  2630.         },
  2631.         add: function() {
  2632.             var dom;
  2633.             var domain = this.field().get_domain(this.record());
  2634.             var context = this.field().get_context(this.record());
  2635.             var value = this.entry.val();
  2636.  
  2637.             var callback = function(result) {
  2638.                 if (!jQuery.isEmptyObject(result)) {
  2639.                     var ids = [];
  2640.                     var i, len;
  2641.                     for (i = 0, len = result.length; i < len; i++) {
  2642.                         ids.push(result[i][0]);
  2643.                     }
  2644.                     this.screen.group.load(ids, true);
  2645.                     this.screen.display();
  2646.                 }
  2647.                 this.entry.val('');
  2648.             }.bind(this);
  2649.             var parser = new Sao.common.DomainParser();
  2650.             var win = new Sao.Window.Search(this.attributes.relation,
  2651.                     callback, {
  2652.                         sel_multi: true,
  2653.                         context: context,
  2654.                         domain: domain,
  2655.                         view_ids: (this.attributes.view_ids ||
  2656.                             '').split(','),
  2657.                         views_preload: this.attributes.views || {},
  2658.                         new_: this.attributes.create,
  2659.                         search_filter: parser.quote(value)
  2660.                     });
  2661.         },
  2662.         remove: function() {
  2663.             this.screen.remove(false, true, false);
  2664.         },
  2665.         key_press: function(event_) {
  2666.             var activate_keys = [Sao.common.TAB_KEYCODE];
  2667.             if (!this.wid_completion) {
  2668.                 activate_keys.push(Sao.common.RETURN_KEYCODE);
  2669.             }
  2670.  
  2671.             if (event_.which == Sao.common.F3_KEYCODE) {
  2672.                 this.new_();
  2673.                 event_.preventDefault();
  2674.             } else if (event_.which == Sao.common.F2_KEYCODE) {
  2675.                 this.add();
  2676.                 event_.preventDefault();
  2677.             } else if (~activate_keys.indexOf(event_.which) && this.entry.val()) {
  2678.                 this.add();
  2679.             }
  2680.         },
  2681.         edit: function() {
  2682.             if (jQuery.isEmptyObject(this.screen.current_record)) {
  2683.                 return;
  2684.             }
  2685.             // Create a new screen that is not linked to the parent otherwise
  2686.             // on the save of the record will trigger the save of the parent
  2687.             var domain = this.field().get_domain(this.record());
  2688.             var add_remove = this.record().expr_eval(
  2689.                     this.attributes.add_remove);
  2690.             if (!jQuery.isEmptyObject(add_remove)) {
  2691.                 domain = [domain, add_remove];
  2692.             }
  2693.             var context = this.field().get_context(this.record());
  2694.             var screen = new Sao.Screen(this.attributes.relation, {
  2695.                 'domain': domain,
  2696.                 'view_ids': (this.attributes.view_ids || '').split(','),
  2697.                 'mode': ['form'],
  2698.                 'views_preload': this.attributes.views,
  2699.                 'readonly': this.attributes.readonly || false,
  2700.                 'context': context
  2701.             });
  2702.             screen.new_group([this.screen.current_record.id]);
  2703.             var callback = function(result) {
  2704.                 if (result) {
  2705.                     screen.current_record.save().done(function() {
  2706.                         // Force a reload on next display
  2707.                         this.screen.current_record.cancel();
  2708.                     }.bind(this));
  2709.                 }
  2710.             }.bind(this);
  2711.             screen.switch_view().done(function() {
  2712.                 new Sao.Window.Form(screen, callback);
  2713.             });
  2714.         },
  2715.         new_: function() {
  2716.             var domain = this.field().get_domain(this.record());
  2717.             var add_remove = this.record().expr_eval(
  2718.                     this.attributes.add_remove);
  2719.             if (!jQuery.isEmptyObject(add_remove)) {
  2720.                 domain = [domain, add_remove];
  2721.             }
  2722.             var context = this.field().get_context(this.record());
  2723.  
  2724.             var screen = new Sao.Screen(this.attributes.relation, {
  2725.                 'domain': domain,
  2726.                 'view_ids': (this.attributes.view_ids || '').split(','),
  2727.                 'mode': ['form'],
  2728.                 'views_preload': this.attributes.views,
  2729.                 'context': context
  2730.             });
  2731.             var callback = function(result) {
  2732.                 if (result) {
  2733.                     var record = screen.current_record;
  2734.                     this.screen.group.load([record.id], true);
  2735.                 }
  2736.                 this.entry.val('');
  2737.             }.bind(this);
  2738.             screen.switch_view().done(function() {
  2739.                 new Sao.Window.Form(screen, callback, {
  2740.                     'new_': true,
  2741.                     'save_current': true
  2742.                 });
  2743.             });
  2744.         }
  2745.     });
  2746.  
  2747.     Sao.View.Form.BinaryMixin = Sao.class_(Sao.View.Form.Widget, {
  2748.         init: function(field_name, model, attributes) {
  2749.             Sao.View.Form.BinaryMixin._super.init.call(
  2750.                     this, field_name, model, attributes);
  2751.             this.filename = attributes.filename || null;
  2752.         },
  2753.         toolbar: function(class_) {
  2754.             var group = jQuery('<div/>', {
  2755.                 'class': class_,
  2756.                 'role': 'group'
  2757.             });
  2758.  
  2759.             this.but_select = jQuery('<button/>', {
  2760.                 'class': 'btn btn-default',
  2761.                 'type': 'button'
  2762.             }).append(jQuery('<span/>', {
  2763.                 'class': 'glyphicon glyphicon-search'
  2764.             })).appendTo(group);
  2765.             this.but_select.click(this.select.bind(this));
  2766.  
  2767.             if (this.filename) {
  2768.                 this.but_open = jQuery('<button/>', {
  2769.                     'class': 'btn btn-default',
  2770.                     'type': 'button'
  2771.                 }).append(jQuery('<span/>', {
  2772.                     'class': 'glyphicon glyphicon-folder-open'
  2773.                 })).appendTo(group);
  2774.                 this.but_open.click(this.open.bind(this));
  2775.             }
  2776.  
  2777.             this.but_save_as = jQuery('<button/>', {
  2778.                 'class': 'btn btn-default',
  2779.                 'type': 'button'
  2780.             }).append(jQuery('<span/>', {
  2781.                 'class': 'glyphicon glyphicon-save'
  2782.             })).appendTo(group);
  2783.             this.but_save_as.click(this.save_as.bind(this));
  2784.  
  2785.             this.but_clear = jQuery('<button/>', {
  2786.                 'class': 'btn btn-default',
  2787.                 'type': 'button'
  2788.             }).append(jQuery('<span/>', {
  2789.                 'class': 'glyphicon glyphicon-erase'
  2790.             })).appendTo(group);
  2791.             this.but_clear.click(this.clear.bind(this));
  2792.  
  2793.             return group;
  2794.         },
  2795.         filename_field: function() {
  2796.             var record = this.record();
  2797.             if (record) {
  2798.                 return record.model.fields[this.filename];
  2799.             }
  2800.         },
  2801.         select: function() {
  2802.             var record = this.record();
  2803.  
  2804.             var close = function() {
  2805.                 file_dialog.modal.on('hidden.bs.modal', function(event) {
  2806.                     jQuery(this).remove();
  2807.                 });
  2808.                 file_dialog.modal.modal('hide');
  2809.             };
  2810.  
  2811.             var save_file = function() {
  2812.                 var reader = new FileReader();
  2813.                 reader.onload = function(evt) {
  2814.                     var uint_array = new Uint8Array(reader.result);
  2815.                     this.field().set_client(record, uint_array);
  2816.                 }.bind(this);
  2817.                 reader.onloadend = function(evt) {
  2818.                     close();
  2819.                 };
  2820.                 var file = file_selector[0].files[0];
  2821.                 reader.readAsArrayBuffer(file);
  2822.                 if (this.filename) {
  2823.                     this.filename_field().set_client(record, file.name);
  2824.                 }
  2825.             }.bind(this);
  2826.  
  2827.             var file_dialog = new Sao.Dialog(
  2828.                     Sao.i18n.gettext('Select'), 'file-dialog');
  2829.             file_dialog.footer.append(jQuery('<button/>', {
  2830.                 'class': 'btn btn-link',
  2831.                 'type': 'button'
  2832.             }).append(Sao.i18n.gettext('Cancel')).click(close))
  2833.             .append(jQuery('<button/>', {
  2834.                 'class': 'btn btn-primary',
  2835.                 'type': 'submit'
  2836.             }).append(Sao.i18n.gettext('OK')).click(save_file));
  2837.             file_dialog.content.submit(function(e) {
  2838.                 save_file();
  2839.                 e.preventDefault();
  2840.             });
  2841.  
  2842.             var file_selector = jQuery('<input/>', {
  2843.                 type: 'file'
  2844.             }).appendTo(file_dialog.body);
  2845.  
  2846.             file_dialog.modal.modal('show');
  2847.         },
  2848.         open: function() {
  2849.             // TODO find a way to make the difference
  2850.             // between downloading and opening
  2851.             this.save_as();
  2852.         },
  2853.         save_as: function() {
  2854.             var field = this.field();
  2855.             var record = this.record();
  2856.             field.get_data(record).done(function(data) {
  2857.                 var blob = new Blob([data],
  2858.                         {type: 'application/octet-binary'});
  2859.                 var blob_url = window.URL.createObjectURL(blob);
  2860.                 if (this.blob_url) {
  2861.                     window.URL.revokeObjectURL(this.blob_url);
  2862.                 }
  2863.                 this.blob_url = blob_url;
  2864.                 window.open(blob_url);
  2865.             }.bind(this));
  2866.         },
  2867.         clear: function() {
  2868.             this.field().set_client(this.record(), null);
  2869.         }
  2870.     });
  2871.  
  2872.     Sao.View.Form.Binary = Sao.class_(Sao.View.Form.BinaryMixin, {
  2873.         class_: 'form-binary',
  2874.         blob_url: '',
  2875.         init: function(field_name, model, attributes) {
  2876.             Sao.View.Form.Binary._super.init.call(this, field_name, model,
  2877.                 attributes);
  2878.  
  2879.             this.el = jQuery('<div/>', {
  2880.                 'class': this.class_
  2881.             });
  2882.  
  2883.             if (this.filename && attributes.filename_visible) {
  2884.                 this.text = jQuery('<input/>', {
  2885.                     type: 'input',
  2886.                     'class': 'form-control input-sm'
  2887.                 }).appendTo(this.el);
  2888.                 this.text.change(this.focus_out.bind(this));
  2889.                 // Use keydown to not receive focus-in TAB
  2890.                 this.text.on('keydown', this.key_press.bind(this));
  2891.             }
  2892.  
  2893.             var group = jQuery('<div/>', {
  2894.                 'class': 'input-group input-group-sm'
  2895.             }).appendTo(this.el);
  2896.             this.size = jQuery('<input/>', {
  2897.                 type: 'input',
  2898.                 'class': 'form-control input-sm'
  2899.             }).appendTo(group);
  2900.  
  2901.             this.toolbar('input-group-btn').appendTo(group);
  2902.         },
  2903.         display: function(record, field) {
  2904.             Sao.View.Form.Binary._super.display.call(this, record, field);
  2905.             if (!field) {
  2906.                 this.size.val('');
  2907.                 if (this.filename) {
  2908.                     this.but_open.button('disable');
  2909.                 }
  2910.                 if (this.text) {
  2911.                     this.text.val('');
  2912.                 }
  2913.                 this.but_save_as.button('disable');
  2914.                 return;
  2915.             }
  2916.             var size = field.get_size(record);
  2917.             var button_sensitive;
  2918.             if (size) {
  2919.                 button_sensitive = 'enable';
  2920.             } else {
  2921.                 button_sensitive = 'disable';
  2922.             }
  2923.  
  2924.             if (this.filename) {
  2925.                 if (this.text) {
  2926.                     this.text.val(this.filename_field().get(record) || '');
  2927.                 }
  2928.                 this.but_open.button(button_sensitive);
  2929.             }
  2930.             this.size.val(Sao.common.humanize(size));
  2931.             this.but_save_as.button(button_sensitive);
  2932.         },
  2933.         key_press: function(evt) {
  2934.             var editable = !this.wid_text.prop('readonly');
  2935.             if (evt.which == Sao.common.F3_KEYCODE && editable) {
  2936.                 this.new_();
  2937.                 evt.preventDefault();
  2938.             } else if (evt.which == Sao.common.F2_KEYCODE) {
  2939.                 this.open();
  2940.                 evt.preventDefault();
  2941.             }
  2942.         },
  2943.         set_value: function(record, field) {
  2944.             if (this.text) {
  2945.                 this.filename_field().set_client(record,
  2946.                         this.text.val() || '');
  2947.             }
  2948.         },
  2949.         set_readonly: function(readonly) {
  2950.             if (readonly) {
  2951.                 this.but_select.hide();
  2952.                 this.but_clear.hide();
  2953.  
  2954.             } else {
  2955.                 this.but_select.show();
  2956.                 this.but_clear.show();
  2957.             }
  2958.             if (this.wid_text) {
  2959.                 this.wid_text.prop('readonly', readonly);
  2960.             }
  2961.         }
  2962.     });
  2963.  
  2964.     Sao.View.Form.MultiSelection = Sao.class_(Sao.View.Form.Selection, {
  2965.         class_: 'form-multiselection',
  2966.         init: function(field_name, model, attributes) {
  2967.             this.nullable_widget = false;
  2968.             Sao.View.Form.MultiSelection._super.init.call(this, field_name,
  2969.                 model, attributes);
  2970.             this.select.prop('multiple', true);
  2971.         },
  2972.         display_update_selection: function(record, field) {
  2973.             var i, len, element;
  2974.             this.update_selection(record, field, function() {
  2975.                 if (!field) {
  2976.                     return;
  2977.                 }
  2978.                 var value = [];
  2979.                 var group = record.field_get_client(this.field_name);
  2980.                 for (i = 0, len = group.length; i < len; i++) {
  2981.                     element = group[i];
  2982.                     if (!~group.record_removed.indexOf(element) &&
  2983.                         !~group.record_deleted.indexOf(element)) {
  2984.                             value.push(element.id);
  2985.                     }
  2986.                 }
  2987.                 this.el.val(value);
  2988.             }.bind(this));
  2989.         },
  2990.         set_value: function(record, field) {
  2991.             var value = this.el.val();
  2992.             if (value) {
  2993.                 value = value.map(function(e) { return parseInt(e, 10); });
  2994.             } else {
  2995.                 value = [];
  2996.             field.set_client(record, value);
  2997.             }
  2998.         }
  2999.     });
  3000.  
  3001.     Sao.View.Form.Image = Sao.class_(Sao.View.Form.BinaryMixin, {
  3002.         class_: 'form-image',
  3003.         init: function(field_name, model, attributes) {
  3004.             Sao.View.Form.Image._super.init.call(
  3005.                     this, field_name, model, attributes);
  3006.             this.height = parseInt(attributes.height || 100, 10);
  3007.             this.width = parseInt(attributes.width || 300, 10);
  3008.  
  3009.             this.el = jQuery('<div/>');
  3010.             this.image = jQuery('<img/>', {
  3011.                 'class': 'center-block'
  3012.             }).appendTo(this.el);
  3013.             this.image.css('max-height', this.height);
  3014.             this.image.css('max-width', this.width);
  3015.             this.image.css('height', 'auto');
  3016.             this.image.css('width', 'auto');
  3017.  
  3018.             var group = this.toolbar('btn-group');
  3019.             if (!attributes.readonly) {
  3020.                 jQuery('<div/>', {
  3021.                     'class': 'text-center'
  3022.                 }).append(group).appendTo(this.el);
  3023.             }
  3024.             this.update_img();
  3025.         },
  3026.         set_readonly: function(readonly) {
  3027.             [this.but_select, this.but_open, this.but_save_as, this.but_clear]
  3028.                 .forEach(function(button) {
  3029.                     if (button) {
  3030.                         button.prop('disable', readonly);
  3031.                     }
  3032.                 });
  3033.         },
  3034.         clear: function() {
  3035.             Sao.View.Form.Image._super.clear.call(this);
  3036.             this.update_img();
  3037.         },
  3038.         update_img: function() {
  3039.             var value;
  3040.             var record = this.record();
  3041.             if (record) {
  3042.                 value = record.field_get_client(this.field_name);
  3043.             }
  3044.             if (value) {
  3045.                 if (value > Sao.common.BIG_IMAGE_SIZE) {
  3046.                     value = jQuery.when(null);
  3047.                 } else {
  3048.                     value = record.model.fields[this.field_name]
  3049.                         .get_data(record);
  3050.                 }
  3051.             } else {
  3052.                 value = jQuery.when(null);
  3053.             }
  3054.             value.done(function(data) {
  3055.                 var url, blob;
  3056.                 if (!data) {
  3057.                     url = null;
  3058.                 } else {
  3059.                     blob = new Blob([data[0][this.field_name]]);
  3060.                     url = window.URL.createObjectURL(blob);
  3061.                 }
  3062.                 this.image.attr('src', url);
  3063.             }.bind(this));
  3064.         },
  3065.         display: function(record, field) {
  3066.             Sao.View.Form.Image._super.display.call(this, record, field);
  3067.             this.update_img();
  3068.         }
  3069.     });
  3070.  
  3071.     Sao.View.Form.URL = Sao.class_(Sao.View.Form.Char, {
  3072.         class_: 'form-url',
  3073.         init: function(field_name, model, attributes) {
  3074.             Sao.View.Form.URL._super.init.call(
  3075.                     this, field_name, model, attributes);
  3076.             this.button = jQuery('<a/>', {
  3077.                 'class': 'btn btn-default',
  3078.                 'target': '_new'
  3079.             }).appendTo(jQuery('<span/>', {
  3080.                 'class': 'input-group-btn'
  3081.             }).appendTo(this.group));
  3082.             this.icon = jQuery('<img/>').appendTo(this.button);
  3083.             this.set_icon();
  3084.         },
  3085.         display: function(record, field) {
  3086.             Sao.View.Form.URL._super.display.call(this, record, field);
  3087.             var url = '';
  3088.             if (record) {
  3089.                 url = record.field_get_client(this.field_name);
  3090.             }
  3091.             this.set_url(url);
  3092.             if (record & this.attributes.icon) {
  3093.                 var icon = this.attributes.icon;
  3094.                 var value;
  3095.                 if (icon in record.model.fields) {
  3096.                     value = record.field_get_client(icon);
  3097.                 } else {
  3098.                     value = icon;
  3099.                 }
  3100.                 this.set_icon(value);
  3101.             }
  3102.         },
  3103.         set_icon: function(value) {
  3104.             value = value || 'tryton-web-browser';
  3105.             Sao.common.ICONFACTORY.register_icon(value).done(function(url) {
  3106.                 this.icon.attr('src', url);
  3107.             }.bind(this));
  3108.         },
  3109.         set_url: function(value) {
  3110.             this.button.attr('href', value);
  3111.         },
  3112.         set_readonly: function(readonly) {
  3113.             Sao.View.Form.URL._super.set_readonly.call(this, readonly);
  3114.             if (readonly) {
  3115.                 this.input.hide();
  3116.                 this.button.removeClass('btn-default');
  3117.                 this.button.addClass('btn-link');
  3118.             } else {
  3119.                 this.input.show();
  3120.                 this.button.removeClass('btn-link');
  3121.                 this.button.addClass('btn-default');
  3122.             }
  3123.         }
  3124.     });
  3125.  
  3126.     Sao.View.Form.Email = Sao.class_(Sao.View.Form.URL, {
  3127.         class_: 'form-email',
  3128.         set_url: function(value) {
  3129.             Sao.View.Form.Email._super.set_url.call(this, 'mailto:' + value);
  3130.         }
  3131.     });
  3132.  
  3133.     Sao.View.Form.CallTo = Sao.class_(Sao.View.Form.URL, {
  3134.         class_: 'form-callto',
  3135.         set_url: function(value) {
  3136.             Sao.View.Form.CallTo._super.set_url.call(this, 'callto:' + value);
  3137.         }
  3138.     });
  3139.  
  3140.     Sao.View.Form.SIP = Sao.class_(Sao.View.Form.URL, {
  3141.         class_: 'form-sip',
  3142.         set_url: function(value) {
  3143.             Sao.View.Form.SIP._super.set_url.call(this, 'sip:' + value);
  3144.         }
  3145.     });
  3146.  
  3147.     Sao.View.Form.ProgressBar = Sao.class_(Sao.View.Form.Widget, {
  3148.         class_: 'form-char',
  3149.         init: function(field_name, model, attributes) {
  3150.             Sao.View.Form.ProgressBar._super.init.call(
  3151.                     this, field_name, model, attributes);
  3152.             this.el = jQuery('<div/>', {
  3153.                 'class': this.class_ + ' progress'
  3154.             });
  3155.             this.progressbar = jQuery('<div/>', {
  3156.                 'class': 'progress-bar',
  3157.                 'role': 'progressbar',
  3158.                 'aria-valuemin': 0,
  3159.                 'aria-valuemax': 100
  3160.             }).appendTo(this.el);
  3161.             this.progressbar.css('min-width: 2em');
  3162.         },
  3163.         display: function(record, field) {
  3164.             Sao.View.Form.ProgressBar._super.display.call(
  3165.                     this, record, field);
  3166.             var value, text;
  3167.             if (!field) {
  3168.                 value = 0;
  3169.                 text = '';
  3170.             } else {
  3171.                 value = field.get(record);
  3172.                 text = field.get_client(record, 100);
  3173.                 if (text) {
  3174.                     text = Sao.i18n.gettext('%1%', text);
  3175.                 }
  3176.             }
  3177.             this.progressbar.attr('aria-valuenow', value * 100);
  3178.             this.progressbar.css('width', value * 100 + '%');
  3179.             this.progressbar.text(text);
  3180.         }
  3181.     });
  3182.  
  3183.     Sao.View.Form.Dict = Sao.class_(Sao.View.Form.Widget, {
  3184.         class_: 'form-dict',
  3185.         init: function(field_name, model, attributes) {
  3186.             Sao.View.Form.Dict._super.init.call(
  3187.                     this, field_name, model, attributes);
  3188.  
  3189.             this.schema_model = new Sao.Model(attributes.schema_model);
  3190.             this.keys = {};
  3191.             this.fields = {};
  3192.             this.rows = {};
  3193.  
  3194.             this.el = jQuery('<div/>', {
  3195.                 'class': this.class_ + ' panel panel-default'
  3196.             });
  3197.             var heading = jQuery('<div/>', {
  3198.                 'class': this.class_ + '-heading panel-heading'
  3199.             }).appendTo(this.el);
  3200.             var label = jQuery('<label/>', {
  3201.                 'class': this.class_ + '-string',
  3202.                 'text': attributes.string
  3203.             }).appendTo(heading);
  3204.  
  3205.             label.uniqueId();
  3206.             this.el.uniqueId();
  3207.             this.el.attr('aria-labelledby', label.attr('id'));
  3208.             label.attr('for', this.el.attr('id'));
  3209.  
  3210.             var body = jQuery('<div/>', {
  3211.                 'class': this.class_ + '-body panel-body'
  3212.             }).appendTo(this.el);
  3213.             this.container = jQuery('<div/>', {
  3214.                 'class': this.class_ + '-container'
  3215.             }).appendTo(body);
  3216.  
  3217.             var group = jQuery('<div/>', {
  3218.                 'class': 'input-group input-group-sm'
  3219.             }).appendTo(jQuery('<div>', {
  3220.                 'class': 'col-md-12'
  3221.             }).appendTo(jQuery('<div/>', {
  3222.                 'class': 'row'
  3223.             }).appendTo(jQuery('<div/>', {
  3224.                 'class': 'container-fluid'
  3225.             }).appendTo(body))));
  3226.             this.wid_text = jQuery('<input/>', {
  3227.                 'type': 'text',
  3228.                 'class': 'form-control input-sm'
  3229.             }).appendTo(group);
  3230.  
  3231.             // TODO completion
  3232.  
  3233.             this.but_add = jQuery('<button/>', {
  3234.                 'class': 'btn btn-default btn-sm',
  3235.                 'type': 'button',
  3236.                 'aria-label': Sao.i18n.gettext('Add')
  3237.             }).append(jQuery('<span/>', {
  3238.                 'class': 'glyphicon glyphicon-plus'
  3239.             })).appendTo(jQuery('<div/>', {
  3240.                 'class': 'input-group-btn'
  3241.             }).appendTo(group));
  3242.             this.but_add.click(this.add.bind(this));
  3243.  
  3244.             this._readonly = false;
  3245.             this._record_id = null;
  3246.         },
  3247.         add: function() {
  3248.             var context = this.field().get_context(this.record());
  3249.             var value = this.wid_text.val();
  3250.             var domain = this.field().get_domain(this.record());
  3251.  
  3252.             var callback = function(result) {
  3253.                 if (!jQuery.isEmptyObject(result)) {
  3254.                     var ids = result.map(function(e) {
  3255.                         return e[0];
  3256.                     });
  3257.                     this.add_new_keys(ids);
  3258.                 }
  3259.                 this.wid_text.val('');
  3260.             }.bind(this);
  3261.  
  3262.             var parser = new Sao.common.DomainParser();
  3263.             var win = new Sao.Window.Search(this.schema_model.name,
  3264.                     callback, {
  3265.                         sel_multi: true,
  3266.                         context: context,
  3267.                         domain: domain,
  3268.                         new_: false,
  3269.                         search_filter: parser.quote(value)
  3270.                     });
  3271.         },
  3272.         add_new_keys: function(ids) {
  3273.             var context = this.field().get_context(this.record());
  3274.             this.schema_model.execute('get_keys', [ids], context)
  3275.                 .then(function(new_fields) {
  3276.                     var focus = false;
  3277.                     new_fields.forEach(function(new_field) {
  3278.                         if (this.fields[new_field.name]) {
  3279.                             return;
  3280.                         }
  3281.                         this.keys[new_field.name] = new_field;
  3282.                         this.add_line(new_field.name);
  3283.                         if (!focus) {
  3284.                             this.fields[new_field.name].input.focus();
  3285.                             focus = true;
  3286.                         }
  3287.                     }.bind(this));
  3288.                 }.bind(this));
  3289.         },
  3290.         remove: function(key, modified) {
  3291.             if (modified === undefined) {
  3292.                 modified = true;
  3293.             }
  3294.             delete this.fields[key];
  3295.             this.rows[key].remove();
  3296.             delete this.rows[key];
  3297.             if (modified) {
  3298.                 this.set_value(this.record(), this.field());
  3299.             }
  3300.         },
  3301.         set_value: function(record, field) {
  3302.             field.set_client(record, this.get_value());
  3303.         },
  3304.         get_value: function() {
  3305.             var value = {};
  3306.             for (var key in this.fields) {
  3307.                 var widget = this.fields[key];
  3308.                 value[key] = widget.get_value();
  3309.             }
  3310.             return value;
  3311.         },
  3312.         set_readonly: function(readonly) {
  3313.             this._readonly = readonly;
  3314.             this._set_button_sensitive();
  3315.             for (var key in this.fields) {
  3316.                 var widget = this.fields[key];
  3317.                 widget.set_readonly(readonly);
  3318.             }
  3319.             this.wid_text.prop('disabled', readonly);
  3320.         },
  3321.         _set_button_sensitive: function() {
  3322.             var create = this.attributes.create;
  3323.             if (create === undefined) {
  3324.                 create = true;
  3325.             }
  3326.             var delete_ = this.attributes['delete'];
  3327.             if (delete_ === undefined) {
  3328.                 delete_ = true;
  3329.             }
  3330.             this.but_add.prop('disabled', this._readonly || !create);
  3331.             for (var key in this.fields) {
  3332.                 var button = this.fields[key].button;
  3333.                 button.prop('disabled', this._readonly || !delete_);
  3334.             }
  3335.         },
  3336.         add_line: function(key) {
  3337.             var field, row;
  3338.             this.fields[key] = field = new (this.get_entries(
  3339.                         this.keys[key].type_))(key, this);
  3340.             this.rows[key] = row = jQuery('<div/>', {
  3341.                 'class': 'row'
  3342.             });
  3343.             // TODO RTL
  3344.             var text = this.keys[key].string + Sao.i18n.gettext(':');
  3345.             var label = jQuery('<label/>', {
  3346.                 'text': text
  3347.             }).appendTo(jQuery('<div/>', {
  3348.                 'class': 'dict-label col-md-4'
  3349.             }).appendTo(row));
  3350.  
  3351.             field.el.addClass('col-md-8').appendTo(row);
  3352.  
  3353.             label.uniqueId();
  3354.             field.labelled.uniqueId();
  3355.             field.labelled.attr('aria-labelledby', label.attr('id'));
  3356.             label.attr('for', field.labelled.attr('id'));
  3357.  
  3358.             field.button.click(function() {
  3359.                 this.remove(key, true);
  3360.             }.bind(this));
  3361.  
  3362.             row.appendTo(this.container);
  3363.         },
  3364.         add_keys: function(keys) {
  3365.             var context = this.field().get_context(this.record());
  3366.             var domain = this.field().get_domain(this.record());
  3367.             var batchlen = Math.min(10, Sao.config.limit);
  3368.             keys = jQuery.extend([], keys);
  3369.  
  3370.             var get_keys = function(key_ids) {
  3371.                 return this.schema_model.execute('get_keys',
  3372.                         [key_ids], context).then(update_keys);
  3373.             }.bind(this);
  3374.             var update_keys = function(values) {
  3375.                 for (var i = 0, len = values.length; i < len; i++) {
  3376.                     var k = values[i];
  3377.                     this.keys[k.name] = k;
  3378.                 }
  3379.             }.bind(this);
  3380.  
  3381.             var prms = [];
  3382.             while (keys.length > 0) {
  3383.                 var sub_keys = keys.splice(0, batchlen);
  3384.                 prms.push(this.schema_model.execute('search',
  3385.                             [[['name', 'in', sub_keys], domain],
  3386.                             0, Sao.config.limit, null], context)
  3387.                         .then(get_keys));
  3388.             }
  3389.             return jQuery.when.apply(jQuery, prms);
  3390.         },
  3391.         display: function(record, field) {
  3392.             Sao.View.Form.Dict._super.display.call(this, record, field);
  3393.  
  3394.             if (!field) {
  3395.                 return;
  3396.             }
  3397.  
  3398.             var record_id = record ? record.id : null;
  3399.             var key;
  3400.  
  3401.             if (record_id != this._record_id) {
  3402.                 for (key in this.fields) {
  3403.                     this.remove(key, false);
  3404.                 }
  3405.                 this._record_id = record_id;
  3406.             }
  3407.  
  3408.             var value = field.get_client(record);
  3409.             var new_key_names = Object.keys(value).filter(function(e) {
  3410.                 return !this.keys[e];
  3411.             }.bind(this));
  3412.  
  3413.             var prm;
  3414.             if (!jQuery.isEmptyObject(new_key_names)) {
  3415.                 prm = this.add_keys(new_key_names);
  3416.             } else {
  3417.                 prm = jQuery.when();
  3418.             }
  3419.             prm.then(function() {
  3420.                 var i, len, key;
  3421.                 var keys = Object.keys(value).sort();
  3422.                 for (i = 0, len = keys.length; i < len; i++) {
  3423.                     key = keys[i];
  3424.                     var val = value[key];
  3425.                     if (!this.keys[key]) {
  3426.                         continue;
  3427.                     }
  3428.                     if (!this.fields[key]) {
  3429.                         this.add_line(key);
  3430.                     }
  3431.                     var widget = this.fields[key];
  3432.                     widget.set_value(val);
  3433.                     widget.set_readonly(this._readonly);
  3434.                 }
  3435.                 var removed_key_names = Object.keys(this.fields).filter(
  3436.                         function(e) {
  3437.                             return !value[e];
  3438.                         });
  3439.                 for (i = 0, len = removed_key_names.length; i < len; i++) {
  3440.                     key = removed_key_names[i];
  3441.                     this.remove(key, false);
  3442.                 }
  3443.             }.bind(this));
  3444.             this._set_button_sensitive();
  3445.         },
  3446.         get_entries: function(type) {
  3447.             switch (type) {
  3448.                 case 'char':
  3449.                     return Sao.View.Form.Dict.Entry;
  3450.                 case 'boolean':
  3451.                     return Sao.View.Form.Dict.Boolean;
  3452.                 case 'selection':
  3453.                     return Sao.View.Form.Dict.Selection;
  3454.                 case 'integer':
  3455.                     return Sao.View.Form.Dict.Integer;
  3456.                 case 'float':
  3457.                     return Sao.View.Form.Dict.Float;
  3458.                 case 'numeric':
  3459.                     return Sao.View.Form.Dict.Numeric;
  3460.                 case 'date':
  3461.                     return Sao.View.Form.Dict.Date;
  3462.                 case 'datetime':
  3463.                     return Sao.View.Form.Dict.DateTime;
  3464.             }
  3465.         }
  3466.     });
  3467.  
  3468.     Sao.View.Form.Dict.Entry = Sao.class_(Object, {
  3469.         class_: 'dict-char',
  3470.         init: function(name, parent_widget) {
  3471.             this.name = name;
  3472.             this.definition = parent_widget.keys[name];
  3473.             this.parent_widget = parent_widget;
  3474.             this.create_widget();
  3475.         },
  3476.         create_widget: function() {
  3477.             this.el = jQuery('<div/>', {
  3478.                 'class': this.class_
  3479.             });
  3480.             var group = jQuery('<div/>', {
  3481.                 'class': 'input-group input-group-sm'
  3482.             }).appendTo(this.el);
  3483.             this.input = this.labelled = jQuery('<input/>', {
  3484.                 'type': 'text',
  3485.                 'class': 'form-control input-sm'
  3486.             }).appendTo(group);
  3487.             this.button = jQuery('<button/>', {
  3488.                 'class': 'btn btn-default',
  3489.                 'type': 'button',
  3490.                 'arial-label': Sao.i18n.gettext('Remove')
  3491.             }).append(jQuery('<span/>', {
  3492.                 'class': 'glyphicon glyphicon-minus'
  3493.             })).appendTo(jQuery('<div/>', {
  3494.                 'class': 'input-group-btn'
  3495.             }).appendTo(group));
  3496.  
  3497.             this.el.change(
  3498.                     this.parent_widget.focus_out.bind(this.parent_widget));
  3499.         },
  3500.         get_value: function() {
  3501.             return this.input.val();
  3502.         },
  3503.         set_value: function(value) {
  3504.             this.input.val(value || '');
  3505.         },
  3506.         set_readonly: function(readonly) {
  3507.             this.input.prop('readonly', readonly);
  3508.         }
  3509.     });
  3510.  
  3511.     Sao.View.Form.Dict.Boolean = Sao.class_(Sao.View.Form.Dict.Entry, {
  3512.         class_: 'dict-boolean',
  3513.         create_widget: function() {
  3514.             Sao.View.Form.Dict.Boolean._super.create_widget.call(this);
  3515.             this.input.attr('type', 'checkbox');
  3516.             this.input.change(
  3517.                     this.parent_widget.focus_out.bind(this.parent_widget));
  3518.         },
  3519.         get_value: function() {
  3520.             return this.input.prop('checked');
  3521.         },
  3522.         set_value: function(value) {
  3523.             this.input.prop('checked', value);
  3524.         }
  3525.     });
  3526.  
  3527.     Sao.View.Form.Dict.Selection = Sao.class_(Sao.View.Form.Dict.Entry, {
  3528.         class_: 'dict-selection',
  3529.         create_widget: function() {
  3530.             Sao.View.Form.Dict.Selection._super.create_widget.call(this);
  3531.             var select = jQuery('<select/>', {
  3532.                 'class': 'form-control input-sm'
  3533.             });
  3534.             select.change(
  3535.                     this.parent_widget.focus_out.bind(this.parent_widget));
  3536.             this.input.replaceWith(select);
  3537.             this.input = this.labelled = select;
  3538.             var selection = jQuery.extend([], this.definition.selection);
  3539.             selection.splice(0, 0, [null, '']);
  3540.             selection.forEach(function(e) {
  3541.                 select.append(jQuery('<option/>', {
  3542.                     'value': JSON.stringify(e[0]),
  3543.                     'text': e[1],
  3544.                 }));
  3545.             });
  3546.         },
  3547.         get_value: function() {
  3548.             return JSON.parse(this.input.val());
  3549.         },
  3550.         set_value: function(value) {
  3551.             this.input.val(JSON.stringify(value));
  3552.         }
  3553.     });
  3554.  
  3555.     Sao.View.Form.Dict.Integer = Sao.class_(Sao.View.Form.Dict.Entry, {
  3556.         class_: 'dict-integer',
  3557.         get_value: function() {
  3558.             var value = parseInt(this.input.val(), 10);
  3559.             if (isNaN(value)) {
  3560.                 return null;
  3561.             }
  3562.             return value;
  3563.         }
  3564.     });
  3565.  
  3566.     Sao.View.Form.Dict.Float = Sao.class_(Sao.View.Form.Dict.Integer, {
  3567.         class_: 'dict-float',
  3568.         get_value: function() {
  3569.             var value = Number(this.input.val());
  3570.             if (isNaN(value)) {
  3571.                 return null;
  3572.             }
  3573.             return value;
  3574.         }
  3575.     });
  3576.  
  3577.     Sao.View.Form.Dict.Numeric = Sao.class_(Sao.View.Form.Dict.Float, {
  3578.         class_: 'dict-numeric',
  3579.         get_value: function() {
  3580.             var value = new Sao.Decimal(this.input.val());
  3581.             if (isNaN(value.valueOf())) {
  3582.                 return null;
  3583.             }
  3584.             return value;
  3585.         }
  3586.     });
  3587.  
  3588.     Sao.View.Form.Dict.Date = Sao.class_(Sao.View.Form.Dict.Entry, {
  3589.         class_: 'dict-date',
  3590.         format: '%x',
  3591.         create_widget: function() {
  3592.             Sao.View.Form.Dict.Date._super.create_widget.call(this);
  3593.             var group = this.button.parent();
  3594.             jQuery('<button/>', {
  3595.                 'class': 'datepickerbutton btn btn-default',
  3596.                 'type': 'button'
  3597.             }).append(jQuery('<span/>', {
  3598.                 'class': 'glyphicon glyphicon-calendar'
  3599.             })).prependTo(group);
  3600.             this.input.datetimepicker({
  3601.                 'format': Sao.common.moment_format(this.format)
  3602.             });
  3603.             this.input.on('dp.change',
  3604.                     this.parent_widget.focus_out.bind(this.parent_widget));
  3605.         },
  3606.         get_value: function() {
  3607.             var value = this.input.data('DateTimePicker').date();
  3608.             if (value) {
  3609.                 value.isDate = true;
  3610.             }
  3611.             return value;
  3612.         },
  3613.         set_value: function(value) {
  3614.             this.date.data('DateTimePicker').date(value);
  3615.         }
  3616.     });
  3617.  
  3618.     Sao.View.Form.Dict.DateTime = Sao.class_(Sao.View.Form.Dict.Date, {
  3619.         class_: 'dict-datetime',
  3620.         format: '%x %X',
  3621.         get_value: function() {
  3622.             var value = this.input.data('DateTimePicker').date();
  3623.             if (value) {
  3624.                 value.isDateTime = true;
  3625.             }
  3626.             return value;
  3627.         }
  3628.     });
  3629.  
  3630. }());
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement