bzr revid: nicolas.vanhoren@openerp.com-20110509144338-n4ilx5gjo7e6sesn
This commit is contained in:
niv-openerp
2011-05-09 16:43:38 +02:00
7 changed files with 362 additions and 123 deletions
@@ -0,0 +1,32 @@
<templates>
<t t-name="jquery-extend">
<ul><li>one</li></ul>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>3</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul li:first-child" t-operation="replace"><li>2</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="prepend"><li>1</li></t>
<t t-jquery="ul" t-operation="before"><hr/></t>
<t t-jquery="ul" t-operation="after"><hr/></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul">
this.attr('class', 'main');
</t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="hr:eq(1)" t-operation="replace"><footer></footer></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="footer" t-operation="inner"><b>END</b></t>
</t>
<t t-name="jquery-extend-clone" t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>CLONED TEMPLATE</li></t>
</t>
</templates>
@@ -5,6 +5,7 @@
<link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen"/>
<script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
<script type="text/javascript" src="/base/static/lib/jquery/jquery-1.5.2.js"></script>
<script type="text/javascript" src="qweb2.js"></script>
<script>
@@ -235,6 +236,24 @@
equals(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ',
"inner loop variables should be rebound as well");
});
module("Template inheritance tests", {
setup: function () {
QWeb.add_template('qweb-test-extend.xml');
},
teardown: function () {
QWeb.templates = [];
QWeb.tag = {};
QWeb.att = {};
}
});
test("jQuery extend", function () {
equals(render('jquery-extend', {}), '<hr/><ul class="main"><li>1</li><li>2</li><li>3</li></ul><footer><b>END</b></footer>',
"Extend template with jQuery");
equals(render('jquery-extend-clone', {}), '<ul><li>one</li><li>CLONED TEMPLATE</li></ul>',
"Clone template");
});
});
</script>
+114 -2
View File
@@ -77,6 +77,41 @@ var QWeb2 = {
}
return -1;
},
xml_node_to_string : function(node, childs_only) {
if (childs_only) {
var childs = node.childNodes, r = [];
for (var i = 0, ilen = childs.length; i < ilen; i++) {
r.push(this.xml_node_to_string(childs[i]));
}
return r.join('');
} else {
if (node.xml !== undefined) {
return node.xml;
} else {
return (new XMLSerializer()).serializeToString(node);
}
}
},
xpath_selectNodes : function(node, expr) {
if (node.selectNodes !== undefined) {
return node.selectNodes(expr);
} else {
var xpath = new XPathEvaluator();
try {
var result = xpath.evaluate(expr, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
} catch(error) {
this.exception("Error with XPath expression '" + expr + "' : " + error);
}
var r = [];
if (result != null) {
var element;
while (element = result.iterateNext()) {
r.push(element);
}
}
return r
}
},
call: function(context, template, old_dict, _import, callback) {
var new_dict = this.extend({}, old_dict);
new_dict['__caller__'] = old_dict['__template__'];
@@ -157,8 +192,10 @@ QWeb2.Engine = (function() {
this.templates_resources = []; // TODO: implement this.reload()
this.templates = {};
this.compiled_templates = {};
this.extend_templates = {};
this.dict = {};
this.tools = QWeb2.tools;
this.jQuery = window.jQuery;
for (var i = 0; i < arguments.length; i++) {
this.add_template(arguments[i]);
}
@@ -178,8 +215,25 @@ QWeb2.Engine = (function() {
return this.tools.exception(node.innerText);
}
var name = node.getAttribute(this.prefix + '-name');
var extend = node.getAttribute(this.prefix + '-extend');
if (name && extend) {
// Clone template and extend it
if (!this.templates[extend]) {
return this.tools.exception("Can't clone undefined template " + extend);
}
this.templates[name] = this.templates[extend].cloneNode(true);
extend = name;
name = undefined;
}
if (name) {
this.templates[name] = node;
} else if (extend) {
delete(this.compiled_templates[extend]);
if (this.extend_templates[extend]) {
this.extend_templates[extend].push(node);
} else {
this.extend_templates[extend] = [node];
}
}
}
}
@@ -256,15 +310,26 @@ QWeb2.Engine = (function() {
"});";
},
render : function(template, dict) {
//console.time("QWeb render template " + template);
if (this.debug && window['console'] !== undefined) {
console.time("QWeb render template " + template);
}
var r = this._render(template, dict);
//console.timeEnd("QWeb render template " + template);
if (this.debug && window['console'] !== undefined) {
console.timeEnd("QWeb render template " + template);
}
return r;
},
_render : function(template, dict) {
if (this.compiled_templates[template]) {
return this.compiled_templates[template].apply(this, [dict || {}]);
} else if (this.templates[template]) {
var ext;
if (ext = this.extend_templates[template]) {
var extend_node;
while (extend_node = ext.shift()) {
this.extend(template, extend_node);
}
}
var code = this.compile(this.templates[template]), tcompiled;
try {
// tcompiled = new Function(code); TODO: Check if could use this in order to avoid eval
@@ -278,6 +343,53 @@ QWeb2.Engine = (function() {
} else {
return this.tools.exception("Template '" + template + "' not found");
}
},
extend : function(template, extend_node) {
if (!this.jQuery) {
return this.tools.exception("Can't extend template " + template + " without jQuery");
}
for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) {
var child = extend_node.childNodes[i];
if (child.nodeType === 1) {
var xpath = child.getAttribute(this.prefix + '-xpath'),
jquery = child.getAttribute(this.prefix + '-jquery'),
operation = child.getAttribute(this.prefix + '-operation'),
target,
error_msg = "Error while extending template '" + template;
if (jquery) {
target = this.jQuery(jquery, this.templates[template]);
} else if (xpath) {
// NOTE: due to the XPath implementation, extending a template will only work once
// when using XPath because XPathResult won't match objects with other constructor than 'Element'
// As jQuery will create HTMLElements, xpath will ignore them then causing problems when a
// template has already been extended. XPath selectors will probably be removed in QWeb.
target = this.jQuery(this.tools.xpath_selectNodes(this.templates[template], xpath));
} else {
this.tools.exception(error_msg + "No expression given");
}
error_msg += "' (expression='" + (jquery || xpath) + "') : ";
var inner = this.tools.xml_node_to_string(child, true);
if (operation) {
var allowed_operations = "append,prepend,before,after,replace,inner".split(',');
if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
}
operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
target[operation]($(inner));
} else {
try {
var f = new Function(['$'], inner);
} catch(error) {
return this.tools.exception("Parse " + error_msg + error);
}
try {
f.apply(target, [this.jQuery]);
} catch(error) {
return this.tools.exception("Runtime " + error_msg + error);
}
}
}
}
}
});
return Engine;
+9 -1
View File
@@ -568,7 +568,6 @@ body.openerp {
/* Form */
.openerp table.oe_frame td {
color: #4c4c4c;
font-weight: bold;
}
.openerp .required.error {
border: 1px solid #900;
@@ -611,6 +610,9 @@ body.openerp {
min-width: 90px;
color: #1f1f1f;
}
.openerp textarea {
resize:vertical;
}
.openerp input[type="text"], .openerp input[type="password"], .openerp select, .openerp .button {
height: 22px;
}
@@ -646,6 +648,12 @@ body.openerp {
background-color: #F66;
border: 1px solid #D00;
}
.openerp div.oe-progressbar span {
position: absolute;
margin-left: 10px;
margin-top: 5px;
font-weight: bold;
}
/* jQuery UI override */
.openerp .ui-widget {
+8
View File
@@ -773,6 +773,14 @@ openerp.base.form.FieldProgressBar = openerp.base.form.Field.extend({
value: this.value,
disabled: this.readonly
});
},
set_value: function(value) {
this._super.apply(this, arguments);
var show_value = Number(value);
if (show_value === NaN) {
show_value = 0;
}
this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
}
});
+176 -118
View File
@@ -44,6 +44,7 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @borrows openerp.base.ActionExecutor#execute_action as #execute_action
*/
init: function(view_manager, session, element_id, dataset, view_id, options) {
var self = this;
this._super(session, element_id);
this.view_manager = view_manager;
this.dataset = dataset;
@@ -54,6 +55,35 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.rows = [];
this.options = _.extend({}, this.defaults, options || {});
this.list = new openerp.base.ListView.List({
options: this.options,
columns: this.columns,
rows: this.rows
});
$(this.list).bind({
'selected': function (e, selection) {
self.$element.find('#oe-list-delete')
.toggle(!!selection.length);
},
'deleted': function (e, ids) {
self.do_delete(ids);
},
'action': function (e, action_name, id) {
var action = _.detect(self.columns, function (field) {
return field.name === action_name;
});
if (!action) { return; }
// TODO: not supposed to reload everything, I think
self.execute_action(
action, self.dataset, self.session.action_manager,
id, self.do_reload);
},
'row_link': function (e, index) {
self.switch_to_record(index);
}
});
},
/**
* View startup method, the default behavior is to set the ``oe-listview``
@@ -99,7 +129,9 @@ openerp.base.ListView = openerp.base.Controller.extend(
var fields = this.fields_view.fields;
var domain_computer = openerp.base.form.compute_domain;
this.columns = _(this.fields_view.arch.children).chain()
this.columns.splice(0, this.columns.length);
this.columns.push.apply(this.columns, _(this.fields_view.arch.children).chain()
.map(function (field) {
var name = field.attrs.name;
var column = _.extend({id: name, tag: field.tag},
@@ -118,13 +150,14 @@ openerp.base.ListView = openerp.base.Controller.extend(
column.attrs_for = function () { return {}; };
}
return column;
}).value();
}).value());
this.visible_columns = _.filter(this.columns, function (column) {
return column.invisible !== '1';
});
if (!this.fields_view.sorted) { this.fields_view.sorted = {}; }
this.$element.html(QWeb.render("ListView", this));
// Head hook
@@ -142,42 +175,7 @@ openerp.base.ListView = openerp.base.Controller.extend(
});
var $table = this.$element.find('table');
// Cell events
$table.delegate(
'th.oe-record-selector', 'click', function (e) {
// TODO: ~linear performances, would a simple counter work?
if ($table.find('th.oe-record-selector input:checked').length) {
$table.find('#oe-list-delete').show();
} else {
$table.find('#oe-list-delete').hide();
}
// A click in the selection cell should not activate the
// linking feature
e.stopImmediatePropagation();
});
$table.delegate(
'td.oe-field-cell button', 'click', function (e) {
e.stopImmediatePropagation();
var $cell = $(e.currentTarget).closest('td');
var col_index = $cell.prevAll('td').length;
var field = self.visible_columns[col_index];
var $row = $cell.parent('tr');
var row = self.rows[$row.prevAll().length];
// TODO: we should probably only reload content, also maybe diff records or something, instead of replacing every single row
self.execute_action(
field, self.dataset, self.session.action_manager,
row.data.id.value, self.do_reload);
});
$table.delegate(
'td.oe-record-delete button', 'click', this.do_delete);
// Global rows handlers
$table.delegate(
'tr', 'click', this.on_select_row);
this.list.move_to($table);
// sidebar stuff
if (this.view_manager && this.view_manager.sidebar) {
@@ -192,15 +190,15 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @param {Object} result filling result
* @param {Array} [result.view] the new view (wrapped fields_view_get result)
* @param {Array} result.records records the records to fill the list view with
* @returns {Promise} promise to the end of view rendering (list views are asynchronously filled for improved responsiveness)
*/
do_fill_table: function(result) {
if (result.view) {
this.on_loaded({fields_view: result.view});
}
var records = result.records;
var $table = this.$element.find('table');
this.rows = records;
this.rows.splice(0, this.rows.length);
this.rows.push.apply(this.rows, records);
// Keep current selected record, if it's still in our new search
var current_record_id = this.dataset.ids[this.dataset.index];
@@ -214,46 +212,11 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.dataset.count = this.dataset.ids.length;
var results = this.rows.length;
$table.find('.oe-pager-last').text(results);
$table.find('.oe-pager-total').text(results);
this.$element.find('table')
.find('.oe-pager-last').text(results).end()
.find('.oe-pager-total').text(results);
// remove all data lines
var $old_body = $table.find('tbody');
// add new content
var columns = this.columns,
rows = this.rows,
options = this.options;
// Paginate by groups of 50 for rendering
var PAGE_SIZE = 50,
bodies_count = Math.ceil(this.rows.length / PAGE_SIZE),
body = 0,
$body = $('<tbody class="ui-widget-content">').appendTo($table);
var rendered = $.Deferred();
var render_body = function () {
setTimeout(function () {
$body.append(
QWeb.render("ListView.rows", {
columns: columns,
rows: rows.slice(body*PAGE_SIZE, (body+1)*PAGE_SIZE),
options: options
}));
++body;
if (body < bodies_count) {
render_body();
} else {
rendered.resolve();
}
}, 0);
};
render_body();
return rendered.promise().then(function () {
$old_body.remove();
});
this.list.refresh();
},
/**
* Used to handle a click on a table row, if no other handler caught the
@@ -279,30 +242,6 @@ openerp.base.ListView = openerp.base.Controller.extend(
}
}, this));
},
/**
* Base handler for clicking on a row, discovers the index of the record
* corresponding to the clicked row in the list view's dataset.
*
* Should not be overridden, use
* :js:func:`~openerp.base.ListView.switch_to_record` to customize the
* behavior of the list view when clicking on a row instead.
*
* @param {Object} event jQuery DOM event object
*/
on_select_row: function (event) {
var $target = $(event.currentTarget);
if (!$target.parent().is('tbody')) {
return;
}
// count number of preceding siblings to line clicked
var row = this.rows[$target.prevAll().length];
var index = _.indexOf(this.dataset.ids, row.data.id.value);
if (index == undefined || index === -1) {
return;
}
this.switch_to_record(index);
},
do_show: function () {
this.$element.show();
if (this.hidden) {
@@ -361,13 +300,35 @@ openerp.base.ListView = openerp.base.Controller.extend(
/**
* Handles the signal to delete a line from the DOM
*
* @param e jQuery event object
* @param {Array} ids the id of the object to delete
*/
do_delete: function (e) {
// don't link to forms
e.stopImmediatePropagation();
this.dataset.unlink(
[this.rows[$(e.currentTarget).closest('tr').prevAll().length].data.id.value]);
do_delete: function (ids) {
if (!ids.length) {
return;
}
var self = this;
return $.when(this.dataset.unlink(ids)).then(function () {
_(self.rows).chain()
.map(function (row, index) {
return {
index: index,
id: row.data.id.value
};})
.filter(function (record) {
return _.contains(ids, record.id);
})
.sort(function (a, b) {
// sort in reverse index order, so we delete from the end
// and don't blow up the following indexes (leading to
// removing the wrong records from the visible list)
return b.index - a.index;
})
.each(function (record) {
self.rows.splice(record.index, 1);
});
// TODO only refresh modified rows
self.list.refresh();
});
},
/**
* Handles signal for the addition of a new record (can be a creation,
@@ -383,10 +344,89 @@ openerp.base.ListView = openerp.base.Controller.extend(
* Handles deletion of all selected lines
*/
do_delete_selected: function () {
var selection = this.get_selection();
if (selection.length) {
this.dataset.unlink(selection);
}
this.do_delete(
this.list.get_selection());
}
// TODO: implement reorder (drag and drop rows)
});
_.extend(openerp.base.ListView.prototype, openerp.base.ActionExecutor);
openerp.base.ListView.List = Class.extend(
/** @lends openerp.base.ListView.List# */{
/**
* List display for the ListView, handles basic DOM events and transforms
* them in the relevant higher-level events, to which the list view (or
* other consumers) can subscribe.
*
* Events on this object are registered via jQuery.
*
* Available events:
*
* `selected`
* Triggered when a row is selected (using check boxes), provides an
* array of ids of all the selected records.
* `deleted`
* Triggered when deletion buttons are hit, provide an array of ids of
* all the records being marked for suppression.
* `action`
* Triggered when an action button is clicked, provides two parameters:
*
* * The name of the action to execute (as a string)
* * The id of the record to execute the action on
* `row_link`
* Triggered when a row of the table is clicked, provides the index (in
* the rows array) and id of the selected record to the handle function.
*
* @constructs
* @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView`
*/
init: function (opts) {
var self = this;
// columns, rows, options
this.options = opts.options;
this.columns = opts.columns;
this.rows = opts.rows;
this.$_element = $('<tbody class="ui-widget-content">')
.appendTo(document.body)
.delegate('th.oe-record-selector', 'click', function (e) {
e.stopPropagation();
$(self).trigger('selected', [self.get_selection()]);
})
.delegate('td.oe-record-delete button', 'click', function (e) {
e.stopPropagation();
var $row = $(e.target).closest('tr');
$(self).trigger('deleted', [[self.row_id($row)]]);
})
.delegate('td.oe-field-cell button', 'click', function (e) {
e.stopPropagation();
var $target = $(e.currentTarget),
field = $target.closest('td').data('field'),
record_id = self.row_id($target.closest('tr'));
$(self).trigger('action', [field, record_id]);
})
.delegate('tr', 'click', function (e) {
e.stopPropagation();
$(self).trigger(
'row_link',
[self.row_position(e.currentTarget),
self.row_id(e.currentTarget)]);
});
},
move_to: function (element) {
this.$current = this.$_element.clone(true).appendTo(element);
this.render();
return this;
},
render: function () {
this.$current.empty().append($(QWeb.render('ListView.rows', this)));
return this;
},
refresh: function () {
this.render();
return this;
},
/**
* Gets the ids of all currently selected records, if any
@@ -397,15 +437,33 @@ openerp.base.ListView = openerp.base.Controller.extend(
return [];
}
var rows = this.rows;
return this.$element.find('th.oe-record-selector input:checked')
return this.$current.find('th.oe-record-selector input:checked')
.closest('tr').map(function () {
return rows[$(this).prevAll().length].data.id.value;
}).get();
},
/**
* Returns the index of the row in the list of rows.
*
* @param {Object} row the selected row
* @returns {Number} the position of the row in this.rows
*/
row_position: function (row) {
return $(row).prevAll().length;
},
/**
* Returns the identifier of the object displayed in the provided table
* row
*
* @param {Object} row the selected table row
* @returns {Number|String} the identifier of the row's object
*/
row_id: function (row) {
return this.rows[this.row_position(row)].data.id.value;
}
// TODO: implement sort (click on column headers), if sorted, the list can not be reordered anymore
// TODO: implement reorder (drag and drop rows)
// drag and drop
// editable?
});
_.extend(openerp.base.ListView.prototype, openerp.base.ActionExecutor);
openerp.base.TreeView = openerp.base.Controller.extend({
});
+4 -2
View File
@@ -208,7 +208,7 @@
<t t-foreach="columns" t-as="column">
<t t-set="attrs" t-value="column.attrs_for(row.data)"/>
<td t-if="column.invisible !== '1'" t-att-title="column.help"
class="oe-field-cell">
class="oe-field-cell" t-att-data-field="column.id">
<t t-if="!attrs.invisible">
<t t-set="is_button" t-value="column.tag === 'button'"/>
<!-- TODO: get correct widget from form -->
@@ -365,7 +365,9 @@
t-att-class="'field_' + widget.type"/>
</t>
<t t-name="FieldProgressBar">
<div t-opentag="true"></div>
<div t-opentag="true" class="oe-progressbar">
<span></span>
</div>
</t>
<t t-name="WidgetButton">
<button type="button"