/* * 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() { "use strict"; var ui = angular.module('axelor.ui'); /** * The Form widget. * */ ui.formWidget('Form', { priority: 100, css: "dynamic-form", scope: false, compile: function(element, attrs) { element.hide(); element.find('[x-field],[data-field]').each(function(){ var elem = $(this), name = elem.attr('x-field') || elem.attr('data-field'); if (name && elem.attr('ui-button') === undefined) { if (!elem.attr('ng-model')) { elem.attr('ng-model', 'record.' + name); } if (!elem.attr('ng-required')) { // always attache a required validator to make // dynamic `required` attribute change effective elem.attr('ng-required', false); } } }); return ui.formCompile.apply(this, arguments); }, link: function(scope, element, attrs, controller) { element.on('submit', function(e) { e.preventDefault(); }); scope.$watch('record', function formRecordWatch(rec, old) { if (element.is(':visible')) { return; } scope.ajaxStop(function() { element.show(); axelor.$adjustSize(); }); }); } }); /** * This directive is used filter $watch on scopes of inactive tabs. * */ ui.directive('uiTabGate', function() { return { compile: function compile(tElement, tAttrs) { return { pre: function preLink(scope, element, attrs) { scope.$watchChecker(function(current) { if (current.$$popupStack.length) return true; if (current.tabSelected === undefined) { return !scope.tab || scope.tab.selected === undefined || scope.tab.selected; } return current.tabSelected; }); } }; } }; }); /** * This directive is used to filter $watch on scopes of hidden forms. * */ ui.directive('uiFormGate', function() { return { compile: function compile(tElement, tAttrs) { return { pre: function preLink(scope, element, attrs) { var parent = null; scope.$watchChecker(function(current) { if (scope.tabSelected === false) { return false; } if (parent === null) { parent = element.parents('[ui-show]:first'); } // hack for hidden nested editors (#2173) if (scope.$$forceWatch) { return true; } return !(parent.hasClass('ui-hide') || parent.hasClass('ui-hide')); }); } }; } }; }); /** * This directive is used to filter $watch on scopes based on some condition. * */ ui.directive('uiWatchIf', ['$parse', function($parse) { return { compile: function compile(tElement, tAttrs) { return { pre: function preLink(scope, element, attrs) { var value = false, expression = $parse(attrs.uiWatchIf); scope.$watchChecker(function (current) { if (current === scope) { return value = expression(scope); } return value; }); } }; } }; }]); function toBoolean(value) { if (value && value.length !== 0) { var v = angular.lowercase("" + value); value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); } else { value = false; } return value; } /** * This directive is used to speedup uiFormGate. */ ui.directive('uiShow', function() { return { scope: true, // create new scope to always watch the expression link: function link(scope, element, attrs) { scope.$$shouldWatch = true; scope.$watch(attrs.uiShow, function uiShowWatchAction(value){ var val = toBoolean(value); element.css({ display: val ? '' : 'none', opacity: 0 }).toggleClass('ui-hide', !val); if (val) { element.animate({ opacity: 1 }, 300); } }); } }; }); /** * This directive is used by view-pane to attach/detach element from DOM tree */ ui.directive('uiAttach', function () { return function (scope, element, attrs) { var parent = null; var detachTimer = null; var uiAttachWatch = function uiAttachWatch(attach) { var result = toBoolean(attach); if (result) { if (parent) { if (detachTimer) { clearTimeout(detachTimer); detachTimer = null; } else { element.appendTo(parent); } parent = null; scope.$broadcast('dom:attach'); } } else { parent = element.parent(); scope.$broadcast('dom:detach'); detachTimer = setTimeout(function () { detachTimer = null; element.detach(); }, 200); } }; uiAttachWatch.uiAttachWatch = true; scope.$watch(attrs.uiAttach, uiAttachWatch, true); scope.$on('$destroy', function () { if (detachTimer) { clearTimeout(detachTimer); detachTimer = null; } if (parent) { parent = null; element.remove(); } }); }; }); /** * This directive can be used by widget to restore scroll when element is re-attached to DOM tree. */ ui.directive('uiAttachScroll', function () { return function (scope, element, attrs) { setTimeout(function () { var elem = element; var scrollTop = 0; if (attrs.uiAttachScroll) { elem = element.find(attrs.uiAttachScroll); } elem.on('scroll', function () { scrollTop = this.scrollTop; }); function resetScroll() { elem.scrollTop(scrollTop); } scope.$on('dom:attach', resetScroll); scope.$on('tab:select', resetScroll); }, 300); }; }); ui.directive('uiWidgetStates', ['$parse', '$interpolate', function($parse, $interpolate) { function isValid(scope, name) { if (!name) return scope.isValid(); var ctrl = scope.form; if (ctrl) { ctrl = ctrl[name]; } if (ctrl) { return ctrl.$valid; } } function withContext(scope, record) { var values = _.extend({}, scope._context, scope._jsonContext, record); return _.extend(values, { $user: axelor.config['user.login'], $group: axelor.config['user.group'], $userId: axelor.config['user.id'], }); } function handleCondition(scope, field, attr, condition, negative) { if (!condition || _.isBoolean(condition)) { return; } scope.$on("on:record-change", function(e, rec, force) { if (field && field.jsonField) { handle(scope.record); } else if (rec === scope.record || force) { handle(rec); } }); scope.$on("on:grid-selection-change", function(e, context) { if (field && field.jsonField) return; if (!scope._isDetailsForm) { handle(context); } }); scope.$watch("isReadonly()", watcher); scope.$watch("isRequired()", watcher); scope.$watch("isValid()", watcher); var expr = $parse(condition); function watcher(current, old) { var rec = scope.record; if (rec === undefined && current === old) return; if (rec === undefined && scope.getContext) { rec = scope.getContext(); } handle(rec); } function handle(rec) { var value; try { value = !!axelor.$eval(scope, expr, withContext(scope, rec)); } catch (e) { console.error('FAILED:', condition, e); } // defer attr change to allow field init, see RM-14998 scope.$applyAsync(function () { scope.attr(attr, negative ? !value : value); }); } } function handleHilites(scope, field) { if (!field || _.isEmpty(field.hilites)) { return; } var hilites = field.hilites || []; var exprs = _.map(_.pluck(hilites, 'condition'), function (s) { return $parse(s); }); function handle(rec) { for (var i = 0; i < hilites.length; i++) { var hilite = hilites[i]; var expr = exprs[i]; var value = false; try { value = axelor.$eval(scope, expr, withContext(scope, rec)); } catch (e) { console.error('FAILED:', hilite, e); } if (value) { return scope.attr('highlight', { hilite: hilite, passed: value }); } } return scope.attr('highlight', {}); } scope.$on("on:record-change", function(e, rec) { if (rec === scope.record) { handle(rec); } }); } function handleBind(scope, field) { if (!field.bind || !field.name) { return; } var expr = $interpolate(field.bind); var last = null; function handle(rec) { var value; try { value = expr(withContext(scope, rec)); if (value.length === 0) { value = null; } } catch (e) { console.error('FAILED:', field.bind, e); } if (scope.setValue && scope.record && last !== value) { scope.setValue(last = value); } } scope.$on("on:record-change", function(e, rec) { if (field && field.jsonField) { handle(scope.record); } else if (rec && rec === scope.record) { handle(rec); } }); } function handleValueExpr(scope, field) { if (!field.valueExpr || !field.name) { return; } var expr = $parse(field.valueExpr); function handle(rec) { var value; try { value = axelor.$eval(scope, expr, withContext(scope, rec)); if (value && value.length === 0) { value = null; } } catch (e) { console.error('FAILED:', field.valueExpr, e); } if (scope.setValue && scope.record) { scope.setValue(value, false); } } scope.$on("on:record-change", function(e, rec) { scope.$timeout(function () { if (field && field.jsonField) { handle(scope.record); } else if (rec && rec === scope.record) { handle(rec); } }); }); } function handleFor(scope, field, attr, conditional, negative) { if (field[conditional]) { handleCondition(scope, field, attr, field[conditional], negative); } } function handleForField(scope) { var field = scope.field; if (!field) return; handleFor(scope, field, "valid", "validIf"); handleFor(scope, field, "hidden", "hideIf"); handleFor(scope, field, "hidden", "showIf", true); handleFor(scope, field, "readonly", "readonlyIf"); handleFor(scope, field, "required", "requiredIf"); handleFor(scope, field, "collapse", "collapseIf"); handleFor(scope, field, "canNew", "canNew"); handleFor(scope, field, "canView", "canView"); handleFor(scope, field, "canEdit", "canEdit"); handleFor(scope, field, "canRemove", "canRemove"); handleFor(scope, field, "canSelect", "canSelect"); handleHilites(scope, field); handleBind(scope, field); handleValueExpr(scope, field); } function handleForView(scope) { var field = scope.schema; if (!field) return; handleFor(scope, field, "canNew", "canNew"); handleFor(scope, field, "canEdit", "canEdit"); handleFor(scope, field, "canSave", "canSave"); handleFor(scope, field, "canCopy", "canCopy"); handleFor(scope, field, "canDelete", "canDelete"); handleFor(scope, field, "canArchive", "canArchive"); handleFor(scope, field, "canAttach", "canAttach"); } return function(scope, element, attrs) { scope.$evalAsync(function() { if (element.is('[ui-form]')) { return handleForView(scope); } handleForField(scope); }); }; }]); })();