701 lines
19 KiB
JavaScript
701 lines
19 KiB
JavaScript
/*
|
|
* Axelor Business Solutions
|
|
*
|
|
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
(function() {
|
|
|
|
/* jshint validthis: true */
|
|
|
|
"use strict";
|
|
|
|
var ui = angular.module('axelor.ui');
|
|
var widgets = {};
|
|
var registry = {};
|
|
var metaWidgets = [];
|
|
|
|
/**
|
|
* Perform common compile operations.
|
|
*
|
|
* example:
|
|
* ui.formCompile.call(this, element, attrs)
|
|
*/
|
|
ui.formCompile = function(element, attrs, linkerFn) {
|
|
|
|
var showTitle = attrs.showTitle || this.showTitle,
|
|
title = attrs.title || attrs.field;
|
|
|
|
attrs.$set('show-title', showTitle, true, 'x-show-title');
|
|
if (title) {
|
|
attrs.$set('title', title, true, 'x-title');
|
|
}
|
|
if (this.cellCss) {
|
|
attrs.$set('x-cell-css', this.cellCss);
|
|
}
|
|
|
|
function link(scope, element, attrs, controller) {
|
|
|
|
element.addClass(this.css).parent().addClass(this.cellCss);
|
|
element.data('$attrs', attrs); // store the attrs object for event handlers
|
|
|
|
var getViewDef = this.getViewDef || scope.getViewDef || function() { return {}; };
|
|
|
|
var field = getViewDef.call(scope, element);
|
|
var props = _.extend(_.pick(field, 'readonly,required,hidden,collapse,precision,scale,prompt,title,domain,css,icon,selection-in'.split(',')),
|
|
_.pick(field.widgetAttrs || {}, 'precision,scale,domain'.split(',')));
|
|
|
|
var state = _.clone(props);
|
|
|
|
function resetAttrs() {
|
|
var label = element.data('label');
|
|
state = _.clone(props);
|
|
state["force-edit"] = false;
|
|
if (label && state.title) {
|
|
var span = label.children('span[ui-help-popover]:first');
|
|
if (span.length === 0) {
|
|
span = label;
|
|
}
|
|
span.html(state.title);
|
|
}
|
|
}
|
|
|
|
if (field.css) {
|
|
element.addClass(field.css);
|
|
}
|
|
if (field.width && field.width !== '*' && !element.is('label')) {
|
|
element.width(field.width);
|
|
}
|
|
if (field.translatable) {
|
|
element.addClass("translatable");
|
|
}
|
|
|
|
scope.$events = {};
|
|
scope.field = field || {};
|
|
|
|
scope.$$readonly = undefined;
|
|
|
|
scope.attr = function(name) {
|
|
if (arguments.length > 1) {
|
|
var old = state[name];
|
|
state[name] = arguments[1];
|
|
if (name === "highlight") {
|
|
setHighlight(state.highlight);
|
|
}
|
|
if (old !== state[name]) {
|
|
scope.$broadcast("on:attrs-changed", {
|
|
name: name,
|
|
value: state[name]
|
|
});
|
|
}
|
|
}
|
|
var res = state[name];
|
|
if (res === undefined) {
|
|
res = field[name];
|
|
}
|
|
return res;
|
|
};
|
|
|
|
scope.$on("on:edit", function(e, rec) {
|
|
if (angular.equals(rec, {})) {
|
|
resetAttrs();
|
|
}
|
|
scope.$$readonly = scope.$$isReadonly();
|
|
});
|
|
|
|
scope.$on("on:attrs-changed", function(event, attr) {
|
|
if (attr.name === "readonly" || attr.name === "force-edit") {
|
|
scope.$$readonly = scope.$$isReadonly();
|
|
}
|
|
if (attr.name === "readonly") {
|
|
element.attr("x-readonly", scope.$$readonly);
|
|
}
|
|
});
|
|
|
|
scope.$watch("isEditable()", function isEditableWatch(editable, old) {
|
|
if (editable === undefined) return;
|
|
if (editable === old) return;
|
|
scope.$$readonly = scope.$$isReadonly();
|
|
});
|
|
|
|
// js expressions should be evaluated on dummy value changes
|
|
if (field.name && field.name[0] === '$') {
|
|
scope.$watch('record.' + field.name, function fieldValueWatch(a, b) {
|
|
if (a !== b) {
|
|
scope.$broadcastRecordChange();
|
|
}
|
|
});
|
|
}
|
|
|
|
scope.isRequired = function() {
|
|
return this.attr("required") && this.text !== 0 && !this.text;
|
|
};
|
|
|
|
scope.isReadonlyExclusive = function() {
|
|
var parent = this.$parent || {};
|
|
var readonly = this.attr("readonly");
|
|
|
|
if (scope._isPopup && !parent._isPopup) {
|
|
return readonly || false;
|
|
}
|
|
if (parent.isReadonlyExclusive && parent.isReadonlyExclusive()) {
|
|
return true;
|
|
}
|
|
if (readonly !== undefined) {
|
|
return readonly || false;
|
|
}
|
|
|
|
return readonly || false;
|
|
};
|
|
|
|
scope.isReadonly = function() {
|
|
if (scope.$$readonly === undefined) {
|
|
scope.$$readonly = scope.$$isReadonly();
|
|
}
|
|
return scope.$$readonly;
|
|
};
|
|
|
|
scope.$$isReadonly = function() {
|
|
if ((this.hasPermission && !this.hasPermission('read')) || this.isReadonlyExclusive()) {
|
|
return true;
|
|
}
|
|
if (!this.attr("readonly") && this.attr("force-edit")) {
|
|
return false;
|
|
}
|
|
if (scope.isEditable && !scope.isEditable()) {
|
|
return true;
|
|
}
|
|
return this.attr("readonly") || false;
|
|
};
|
|
|
|
scope.isHidden = function() {
|
|
return this.attr("hidden") || (this.$parent && this.$parent.isHidden && this.$parent.isHidden()) || false;
|
|
};
|
|
|
|
scope.fireAction = function(name, success, error) {
|
|
var handler = this.$events[name];
|
|
if (handler) {
|
|
return handler().then(success, error);
|
|
}
|
|
};
|
|
|
|
if (angular.isFunction(this._link_internal)) {
|
|
this._link_internal.call(this, scope, element, attrs, controller);
|
|
}
|
|
if (angular.isFunction(this.init)) {
|
|
this.init.call(this, scope);
|
|
}
|
|
if (angular.isFunction(this.link)) {
|
|
this.link.call(this, scope, element, attrs, controller);
|
|
}
|
|
|
|
function hideWidget(hidden) {
|
|
var elem = element,
|
|
parent = elem.parent('td,.form-item'),
|
|
label = elem.data('label') || $(),
|
|
label_parent = label.parent('td,.form-item'),
|
|
isTable = parent.is('td');
|
|
|
|
// label scope should use same isHidden method (#1514)
|
|
var lScope = label.data('$scope');
|
|
if (lScope && lScope.isHidden !== scope.isHidden) {
|
|
lScope.isHidden = scope.isHidden;
|
|
}
|
|
|
|
elem = isTable && parent.length ? parent : elem;
|
|
label = isTable && label_parent.length ? label_parent : label;
|
|
|
|
if (!isTable) {
|
|
parent.toggleClass("form-item-hidden", hidden);
|
|
}
|
|
|
|
if (hidden) {
|
|
elem.add(label).hide();
|
|
} else {
|
|
elem.add(label).show().css('display', ''); //XXX: jquery may add display style
|
|
}
|
|
|
|
return axelor.$adjustSize();
|
|
}
|
|
|
|
var hideFn = _.contains(this.handles, 'isHidden') ? angular.noop : hideWidget;
|
|
|
|
var hiddenSet = false;
|
|
scope.$watch("isHidden()", function isHiddenWatch(hidden, old) {
|
|
if (hiddenSet && hidden === old) return;
|
|
hiddenSet = true;
|
|
return hideFn(hidden);
|
|
});
|
|
|
|
var readonlySet = false;
|
|
scope.$watch("isReadonly()", function isReadonlyWatch(readonly, old) {
|
|
if (readonlySet && readonly === old) return;
|
|
readonlySet = true;
|
|
element.toggleClass("readonly", readonly);
|
|
element.toggleClass("editable", !readonly);
|
|
if (scope.canEdit) {
|
|
element.toggleClass("no-edit", scope.canEdit() === false);
|
|
}
|
|
});
|
|
|
|
function setHighlight(args) {
|
|
|
|
function doHilite(params, passed) {
|
|
var label = element.data('label') || $();
|
|
element.toggleClass(params.css, passed);
|
|
label.toggleClass(params.css.replace(/(hilite-[^-]+\b(?!-))/g, ''), passed);
|
|
}
|
|
|
|
_.each(field.hilites, function(p) {
|
|
if (p.css) doHilite(p, false);
|
|
});
|
|
|
|
if (args && args.hilite && args.hilite.css) {
|
|
doHilite(args.hilite, args.passed);
|
|
}
|
|
}
|
|
|
|
this.prepare(scope, element, attrs, controller);
|
|
|
|
scope.$evalAsync(function() {
|
|
if (scope.isHidden()) {
|
|
hideFn(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
return angular.bind(this, link);
|
|
};
|
|
|
|
ui.formDirective = function(name, object) {
|
|
|
|
if (object.compile === undefined) {
|
|
object.compile = angular.bind(object, function(element, attrs){
|
|
return ui.formCompile.apply(this, arguments);
|
|
});
|
|
}
|
|
|
|
if (object.restrict === undefined) {
|
|
object.restrict = 'EA';
|
|
}
|
|
|
|
if (object.template && !object.replace) {
|
|
object.replace = true;
|
|
}
|
|
|
|
if (object.cellCss === undefined) {
|
|
object.cellCss = 'form-item';
|
|
}
|
|
|
|
if (object.scope === undefined) {
|
|
object.scope = true;
|
|
}
|
|
|
|
if (object.require === undefined) {
|
|
object.require = '?ngModel';
|
|
}
|
|
|
|
function prepare_templates($compile) {
|
|
|
|
object.prepare = angular.bind(object, function(scope, element, attrs, model) {
|
|
|
|
var self = this;
|
|
|
|
if (!this.template_editable && !this.template_readonly) {
|
|
return;
|
|
}
|
|
|
|
scope.$elem_editable = null;
|
|
scope.$elem_readonly = null;
|
|
|
|
function showEditable() {
|
|
var template_editable = self.template_editable;
|
|
if (scope.field && scope.field.editor) {
|
|
template_editable = $('<div ui-panel-editor>');
|
|
}
|
|
if (_.isFunction(self.template_editable)) {
|
|
template_editable = self.template_editable(scope);
|
|
}
|
|
if (!template_editable) {
|
|
return false;
|
|
}
|
|
if (!scope.$elem_editable) {
|
|
scope.$elem_editable = $compile(template_editable)(scope);
|
|
if (self.link_editable) {
|
|
self.link_editable.call(self, scope, scope.$elem_editable, attrs, model);
|
|
}
|
|
if (scope.validate) {
|
|
model.$validators.valid = function(modelValue, viewValue) {
|
|
return !!scope.validate(viewValue);
|
|
};
|
|
}
|
|
// focus the first input field
|
|
if (scope.$elem_editable.is('.input-append,.picker-input')) {
|
|
scope.$elem_editable.on('click', '.btn, i', function(){
|
|
if (!axelor.device.mobile) {
|
|
scope.$elem_editable.find('input:first').focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (scope.$elem_editable.is(':input')) {
|
|
scope.$elem_editable.attr('placeholder', scope.field.placeholder);
|
|
}
|
|
|
|
if (scope.$elem_editable.is('.picker-input:not(.tag-select)')) {
|
|
scope.$elem_editable.find(':input:first').attr('placeholder', scope.field.placeholder);
|
|
}
|
|
}
|
|
if (scope.$elem_readonly) {
|
|
scope.$elem_readonly.detach();
|
|
}
|
|
element.append(scope.$elem_editable);
|
|
if (scope.$render_editable) scope.$render_editable();
|
|
return true;
|
|
}
|
|
|
|
function showReadonly() {
|
|
var field = scope.field || {};
|
|
var template_readonly = self.template_readonly;
|
|
if (field.viewer) {
|
|
template_readonly = field.viewer.template;
|
|
scope.$moment = function(d) { return moment(d); };
|
|
scope.$number = function(d) { return +d; };
|
|
scope.$image = function (fieldName, imageName) { return ui.formatters.$image(this, fieldName, imageName); };
|
|
scope.$fmt = function (fieldName, fieldValue) {
|
|
var args = [this, fieldName];
|
|
if (arguments.length > 1) {
|
|
args.push(fieldValue);
|
|
}
|
|
return ui.formatters.$fmt.apply(null, args);
|
|
};
|
|
} else if (field.editor && field.editor.viewer) {
|
|
return showEditable();
|
|
}
|
|
if (_.isFunction(self.template_readonly)) {
|
|
template_readonly = self.template_readonly(scope);
|
|
}
|
|
if (!template_readonly) {
|
|
return false;
|
|
}
|
|
if (_.isString(template_readonly)) {
|
|
template_readonly = axelor.sanitize(template_readonly.trim());
|
|
if (template_readonly[0] !== '<' || $(template_readonly).length > 1) {
|
|
template_readonly = '<span>' + template_readonly + '</span>';
|
|
}
|
|
if (field.viewer) {
|
|
template_readonly = template_readonly.replace(/^(\s*<\w+)/, '$1 ui-panel-viewer');
|
|
}
|
|
}
|
|
if (!scope.$elem_readonly) {
|
|
scope.$elem_readonly = $compile(template_readonly)(scope);
|
|
if (self.link_readonly) {
|
|
self.link_readonly.call(self, scope, scope.$elem_readonly, attrs, model);
|
|
}
|
|
}
|
|
if (scope.$elem_editable) {
|
|
scope.$elem_editable.detach();
|
|
}
|
|
element.append(scope.$elem_readonly);
|
|
return true;
|
|
}
|
|
|
|
scope.$watch("isReadonly()", function isReadonlyWatch(readonly) {
|
|
if (readonly && showReadonly()) {
|
|
return;
|
|
}
|
|
return showEditable();
|
|
});
|
|
scope.$watch("isRequired()", function isRequiredWatch(required, old) {
|
|
if (required === old) return;
|
|
var elem = element,
|
|
label = elem.data('label') || $();
|
|
if (label) {
|
|
label.toggleClass('required', required);
|
|
}
|
|
attrs.$set('required', required);
|
|
});
|
|
|
|
if (scope.field && scope.field.validIf) {
|
|
scope.$watch("attr('valid')", function attrValidWatch(valid) {
|
|
if (valid === undefined) return;
|
|
model.$setValidity('invalid', valid);
|
|
});
|
|
}
|
|
|
|
scope.$on('$destroy', function() {
|
|
if (scope.$elem_editable) {
|
|
scope.$elem_editable.remove();
|
|
scope.$elem_editable = null;
|
|
}
|
|
if (scope.$elem_readonly) {
|
|
scope.$elem_readonly.remove();
|
|
scope.$elem_readonly = null;
|
|
}
|
|
});
|
|
});
|
|
|
|
return object;
|
|
}
|
|
|
|
return ui.directive(name, ['$compile', function($compile) {
|
|
return prepare_templates($compile);
|
|
}]);
|
|
};
|
|
|
|
var FormItem = {
|
|
|
|
css: 'form-item',
|
|
|
|
cellCss: 'form-item'
|
|
};
|
|
|
|
var FormInput = {
|
|
|
|
_link_internal: function(scope, element, attrs, model) {
|
|
|
|
scope.format = function(value) {
|
|
return value;
|
|
};
|
|
|
|
scope.parse = function(value) {
|
|
return value;
|
|
};
|
|
|
|
scope.validate = function(value) {
|
|
return true;
|
|
};
|
|
|
|
scope.setValue = function(value, fireOnChange) {
|
|
|
|
var val = this.parse(value);
|
|
var txt = this.format(value);
|
|
var onChange = this.$events.onChange;
|
|
|
|
model.$setViewValue(val);
|
|
this.text = txt;
|
|
|
|
model.$render();
|
|
if (onChange && fireOnChange) {
|
|
onChange();
|
|
}
|
|
};
|
|
|
|
scope.getValue = function() {
|
|
if (model) {
|
|
return model.$viewValue;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
scope.getText = function() {
|
|
return this.text;
|
|
};
|
|
|
|
scope.initValue = function(value) {
|
|
this.text = this.format(value);
|
|
};
|
|
|
|
model.$render = function() {
|
|
scope.initValue(scope.getValue());
|
|
if (scope.$render_editable) {
|
|
scope.$render_editable();
|
|
}
|
|
if (scope.$render_readonly) {
|
|
scope.$render_readonly();
|
|
}
|
|
};
|
|
|
|
// Clear invalid fields (use $setPrestine of angular.js 1.1)
|
|
scope.$on('on:new', function(e, rec) {
|
|
if (!model.$valid && model.$viewValue) {
|
|
model.$viewValue = undefined;
|
|
model.$render();
|
|
}
|
|
});
|
|
},
|
|
|
|
link: function(scope, element, attrs, model) {
|
|
|
|
},
|
|
|
|
link_editable: function(scope, element, attrs, model) {
|
|
|
|
scope.$render_editable = function() {
|
|
var value = this.format(this.getValue());
|
|
element.val(value);
|
|
};
|
|
|
|
function bindListeners() {
|
|
var onChange = scope.$events.onChange || angular.noop,
|
|
onChangePending = false;
|
|
|
|
function listener() {
|
|
var value = _.str.trim(element.val()) || null;
|
|
if (value !== model.$viewValue) {
|
|
scope.$applyAsync(function() {
|
|
var val = scope.parse(value);
|
|
var txt = scope.format(value);
|
|
if (scope.$$setEditorValue && !scope.record) { // m2o editor with null value?
|
|
scope.$$setEditorValue({}, false);
|
|
}
|
|
model.$setViewValue(val);
|
|
scope.text = txt;
|
|
});
|
|
onChangePending = true;
|
|
}
|
|
}
|
|
|
|
var field = scope.field || {};
|
|
if (!field.bind) {
|
|
element.bind('input', listener);
|
|
}
|
|
|
|
element.change(listener);
|
|
|
|
element.blur(function(e){
|
|
if (onChangePending) {
|
|
onChangePending = false;
|
|
setTimeout(onChange);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (element.is(':input')) {
|
|
setTimeout(bindListeners);
|
|
// clear input value
|
|
if (scope.$$setEditorValue) {
|
|
scope.$on('on:edit', function () {
|
|
if (model.$viewValue && !scope.record) {
|
|
model.$setViewValue(undefined);
|
|
scope.$render_editable();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
scope.$render_editable();
|
|
},
|
|
|
|
link_readonly: function(scope, element, attrs, model) {
|
|
|
|
},
|
|
|
|
template_editable: '<input type="text">',
|
|
|
|
template_readonly: '<span class="display-text">{{text}}</span>',
|
|
|
|
template: '<span class="form-item-container"></span>'
|
|
};
|
|
|
|
function inherit(array) {
|
|
|
|
var args = _.chain(array).rest(1).flatten(true).value();
|
|
var last = _.last(args);
|
|
var base = null;
|
|
var obj = {};
|
|
|
|
_.chain(args).each(function(source, index) {
|
|
if (_.isString(source)) {
|
|
source = widgets[source];
|
|
}
|
|
if (index === args.length - 2) {
|
|
base = source;
|
|
}
|
|
_.extend(obj, source);
|
|
});
|
|
|
|
if (!base) {
|
|
return obj;
|
|
}
|
|
|
|
function overridden(name) {
|
|
return name !== "controller" &&
|
|
_.isFunction(last[name]) && !last[name].$inject &&
|
|
_.isFunction(base[name]);
|
|
}
|
|
|
|
function override(name, fn){
|
|
return function() {
|
|
var tmp = this._super;
|
|
this._super = base[name];
|
|
var ret = fn.apply(this, arguments);
|
|
this._super = tmp;
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
for(var name in last) {
|
|
if (overridden(name)) {
|
|
obj[name] = override(name, obj[name]);
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
ui.formWidget = function(name, object) {
|
|
var obj = inherit(arguments);
|
|
var widget = _.str.capitalize(name.replace(/^ui/, ''));
|
|
var directive = "ui" + widget;
|
|
|
|
if (obj.metaWidget) {
|
|
metaWidgets.push(widget);
|
|
}
|
|
|
|
registry[directive] = directive;
|
|
_.each(obj.widgets, function(alias){
|
|
registry[alias] = directive;
|
|
});
|
|
delete obj.widgets;
|
|
|
|
widgets[widget] = _.clone(obj);
|
|
|
|
ui.formDirective(directive, obj);
|
|
|
|
return obj;
|
|
};
|
|
|
|
ui.formItem = function(name, object) {
|
|
return ui.formWidget(name, FormItem, _.rest(arguments, 1));
|
|
};
|
|
|
|
ui.formInput = function(name, object) {
|
|
return ui.formWidget(name, FormItem, FormInput, _.rest(arguments, 1));
|
|
};
|
|
|
|
ui.getWidget = function(type) {
|
|
var name = type,
|
|
widget = registry["ui" + name] || registry[name];
|
|
if (!widget) {
|
|
name = _.str.classify(name);
|
|
widget = registry["ui" + name] || registry[name];
|
|
}
|
|
if (widget) {
|
|
widget = widget.replace(/^ui/, '');
|
|
return _.chain(widget).underscored().dasherize().value();
|
|
}
|
|
return null;
|
|
};
|
|
|
|
ui.getWidgetDef = function (name) {
|
|
return widgets[name];
|
|
};
|
|
|
|
ui.getMetaWidgets = function () {
|
|
return metaWidgets;
|
|
};
|
|
|
|
})();
|