1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
|
odoo.define('web.public.widget', function (require) {
'use strict';
/**
* Provides a way to start JS code for public contents.
*/
var Class = require('web.Class');
var dom = require('web.dom');
var mixins = require('web.mixins');
var session = require('web.session');
var Widget = require('web.Widget');
/**
* Specialized Widget which automatically instantiates child widgets to attach
* to internal DOM elements once it is started. The widgets to instantiate are
* known thanks to a linked registry which contains info about the widget
* classes and jQuery selectors to use to find the elements to attach them to.
*
* @todo Merge with 'PublicWidget' ?
*/
var RootWidget = Widget.extend({
custom_events: _.extend({}, Widget.prototype.custom_events || {}, {
'registry_update': '_onRegistryUpdate',
'get_session': '_onGetSession',
}),
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this._widgets = [];
this._listenToUpdates = false;
this._getRegistry().setParent(this);
},
/**
* @override
* @see _attachComponents
*/
start: function () {
var defs = [this._super.apply(this, arguments)];
defs.push(this._attachComponents());
this._listenToUpdates = true;
return Promise.all(defs);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Instantiates a child widget according to the given registry data.
*
* @private
* @param {Object} childInfo
* @param {function} childInfo.Widget - the widget class to instantiate
* @param {string} childInfo.selector
* the jQuery selector to use to find the internal DOM element which
* needs to be attached to the instantiated widget
* @param {jQuery} [$from] - only check DOM elements which are descendant of
* the given one. If not given, use this.$el.
* @returns {Deferred}
*/
_attachComponent: function (childInfo, $from) {
var self = this;
var $elements = dom.cssFind($from || this.$el, childInfo.selector);
var defs = _.map($elements, function (element) {
var w = new childInfo.Widget(self);
self._widgets.push(w);
return w.attachTo(element);
});
return Promise.all(defs);
},
/**
* Instantiates the child widgets that need to be according to the linked
* registry.
*
* @private
* @param {jQuery} [$from] - only check DOM elements which are descendant of
* the given one. If not given, use this.$el.
* @returns {Deferred}
*/
_attachComponents: function ($from) {
var self = this;
var childInfos = this._getRegistry().get();
var defs = _.map(childInfos, function (childInfo) {
return self._attachComponent(childInfo, $from);
});
return Promise.all(defs);
},
/**
* Returns the `RootWidgetRegistry` instance that is linked to this
* `RootWidget` instance.
*
* @abstract
* @private
* @returns {RootWidgetRegistry}
*/
_getRegistry: function () {},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Get the curuent session module.
*
* @private
* @param {OdooEvent} ev
*/
_onGetSession: function (event) {
if (event.data.callback) {
event.data.callback(session);
}
},
/**
* Called when the linked registry is updated after this `RootWidget`
*
* @private
* @param {OdooEvent} ev
*/
_onRegistryUpdate: function (ev) {
ev.stopPropagation();
if (this._listenToUpdates) {
this._attachComponent(ev.data);
}
},
});
var RootWidgetRegistry = Class.extend(mixins.EventDispatcherMixin, {
/**
* @constructor
*/
init: function () {
mixins.EventDispatcherMixin.init.call(this);
this._registry = [];
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Adds an element to the registry (info of what and how to instantiate).
*
* @param {function} Widget - the widget class to instantiate
* @param {string} selector
* the jQuery selector to use to find the internal DOM element which
* needs to be attached to the instantiated widget
*/
add: function (Widget, selector) {
var registryInfo = {
Widget: Widget,
selector: selector,
};
this._registry.push(registryInfo);
this.trigger_up('registry_update', registryInfo);
},
/**
* Retrieves all the registry elements.
*/
get: function () {
return this._registry;
},
});
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
/**
* Provides a way for executing code once a website DOM element is loaded in the
* dom.
*/
var PublicWidget = Widget.extend({
/**
* The selector attribute, if defined, allows to automatically create an
* instance of this widget on page load for each DOM element which
* matches this selector. The `PublicWidget.$target` element will then be
* that particular DOM element. This should be the main way of instantiating
* `PublicWidget` elements.
*
* @todo do not make this part of the Widget but rather an info to give when
* registering the widget.
*/
selector: false,
/**
* Extension of @see Widget.events
*
* A description of the event handlers to bind/delegate once the widget
* has been rendered::
*
* 'click .hello .world': 'async _onHelloWorldClick',
* _^_ _^_ _^_ _^_
* | | | |
* | (Optional) jQuery | Handler method name
* | delegate selector |
* | |_ (Optional) space separated options
* | * async: use the automatic system
* |_ Event name with making handlers promise-ready (see
* potential jQuery makeButtonHandler, makeAsyncHandler)
* namespaces
*
* Note: the values may be replaced by a function declaration. This is
* however a deprecated behavior.
*
* @type {Object}
*/
events: {},
/**
* @constructor
* @param {Object} parent
* @param {Object} [options]
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.options = options || {};
},
/**
* Destroys the widget and basically restores the target to the state it
* was before the start method was called (unlike standard widget, the
* associated $el DOM is not removed, if this was instantiated thanks to the
* selector property).
*/
destroy: function () {
if (this.selector) {
var $oldel = this.$el;
// The difference with the default behavior is that we unset the
// associated element first so that:
// 1) its events are unbinded
// 2) it is not removed from the DOM
this.setElement(null);
}
this._super.apply(this, arguments);
if (this.selector) {
// Reassign the variables afterwards to allow extensions to use them
// after calling the _super method
this.$el = $oldel;
this.el = $oldel[0];
this.$target = this.$el;
this.target = this.el;
}
},
/**
* @override
*/
setElement: function () {
this._super.apply(this, arguments);
if (this.selector) {
this.$target = this.$el;
this.target = this.el;
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @see this.events
* @override
*/
_delegateEvents: function () {
var self = this;
var originalEvents = this.events;
var events = {};
_.each(this.events, function (method, event) {
// If the method is a function, use the default Widget system
if (typeof method !== 'string') {
events[event] = method;
return;
}
// If the method is only a function name without options, use the
// default Widget system
var methodOptions = method.split(' ');
if (methodOptions.length <= 1) {
events[event] = method;
return;
}
// If the method has no meaningful options, use the default Widget
// system
var isAsync = _.contains(methodOptions, 'async');
if (!isAsync) {
events[event] = method;
return;
}
method = self.proxy(methodOptions[methodOptions.length - 1]);
if (_.str.startsWith(event, 'click')) {
// Protect click handler to be called multiple times by
// mistake by the user and add a visual disabling effect
// for buttons.
method = dom.makeButtonHandler(method);
} else {
// Protect all handlers to be recalled while the previous
// async handler call is not finished.
method = dom.makeAsyncHandler(method);
}
events[event] = method;
});
this.events = events;
this._super.apply(this, arguments);
this.events = originalEvents;
},
/**
* @private
* @param {boolean} [extra=false]
* @param {Object} [extraContext]
* @returns {Object}
*/
_getContext: function (extra, extraContext) {
var context;
this.trigger_up('context_get', {
extra: extra || false,
context: extraContext,
callback: function (ctx) {
context = ctx;
},
});
return context;
},
});
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
/**
* The registry object contains the list of widgets that should be instantiated
* thanks to their selector property if any.
*/
var registry = {};
/**
* This is a fix for apple device (<= IPhone 4, IPad 2)
* Standard bootstrap requires data-toggle='collapse' element to be <a/> tags.
* Unfortunatly some layouts use a <div/> tag instead. The fix forces an empty
* click handler on these div, which allows standard bootstrap to work.
*/
registry._fixAppleCollapse = PublicWidget.extend({
selector: 'div[data-toggle="collapse"]',
events: {
'click': function () {},
},
});
return {
RootWidget: RootWidget,
RootWidgetRegistry: RootWidgetRegistry,
Widget: PublicWidget,
registry: registry,
};
});
|