/* * Axelor Business Solutions * * Copyright (C) 2005-2019 Axelor (). * * 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 . */ (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 = $('
'); } 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 = '' + template_readonly + ''; } 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: '', template_readonly: '{{text}}', template: '' }; 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; }; })();