/* * 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 newcap: false */ "use strict"; var ui = angular.module('axelor.ui'); function TableLayout(items, attrs, $scope, $compile) { var colWidths = attrs.widths, numCols = +attrs.cols || 4, curCol = 0, layout = [[]]; function add(item, label) { if (item.is('br')) { curCol = 0; item.hide(); return layout.push([]); } var row = _.last(layout), cell = null, colspan = +item.attr('x-colspan') || 1, rowspan = +item.attr('x-rowspan') || 1; if (curCol + colspan >= numCols + 1) { curCol = 0, row = []; layout.push(row); } if (label) { cell = {}; cell.elem = label; cell.css = label.attr('x-cell-css'); row.push(cell); if (rowspan > 1) cell.rowspan = rowspan; if (colspan > 1) colspan -= 1; curCol += 1; } cell = {}; cell.elem = item; cell.css = item.attr('x-cell-css'); if (colspan > 1) cell.colspan = colspan; if (rowspan > 1) cell.rowspan = rowspan; row.push(cell); curCol += colspan; } if (colWidths && angular.isString(colWidths)) { colWidths = colWidths.trim().split(/\s*,\s*/); for(var i = 0 ; i < colWidths.length; i++) { var width = colWidths[i]; if (/^(\d+)$/.test(width)) width = width + 'px'; if (width == '*') width = 'auto'; colWidths[i] = width; } } items.each(function(){ var el = $(this), title = el.attr('x-title'), noTitle = el.attr('x-show-title') == 'false'; var labelScope = el.data('$scope'); if (labelScope) { labelScope = labelScope.$new(); } if (numCols > 1 && !noTitle && title) { var label = $('').html(title).attr('x-for-widget', el.attr('id')), labelElem = $compile(label)(labelScope || $scope); el.data('label', labelElem); return add(el, labelElem); } add(el); }); var table = $('
'); function isLabel(cell) { return cell.css === "form-label" || (cell.elem && cell.elem.is('label,.spacer-item')); } function computeWidths(row) { if (row.length === 1) return null; var widths = [], labelCols = 0, itemCols = 0, emptyCols = 0; _.each(row, function(cell) { if (isLabel(cell)) { labelCols += (cell.colspan || 1); } else { itemCols += (cell.colspan || 1); } }); emptyCols = numCols - (labelCols + itemCols); labelCols += (emptyCols / 2); itemCols += (emptyCols / 2) + (emptyCols % 2); var labelWidth = labelCols ? Math.min(50, (12 * labelCols)) / labelCols : 0; var itemWidth = (100 - (labelWidth * labelCols)) / itemCols; _.each(row, function(cell, i) { var width = ((isLabel(cell) ? labelWidth : itemWidth) * (cell.colspan || 1)); widths[i] = width + "%"; }); return widths; } _.each(layout, function(row){ var tr = $(''), numCells = 0, widths = colWidths || computeWidths(row); _.each(row, function(cell, i) { var el = $('') .addClass(cell.css) .attr('colspan', cell.colspan) .attr('rowspan', cell.rowspan) .append(cell.elem) .appendTo(tr); if (_.isArray(widths) && widths[i]) { el.width(widths[i]); } numCells += cell.colspan || 1; }); // append remaining cells for (var i = numCells ; i < numCols ; i++) { $('').appendTo(tr).width((widths||[])[i]); } tr.appendTo(table); }); return table; } //- TableLayout ui.directive('uiTableLayout', ['$compile', function($compile) { return function(scope, element, attrs) { var elem = attrs.layoutSelector ? element.find(attrs.layoutSelector) : element; var items = elem.children(); var layout = TableLayout(items, attrs, scope, $compile); var brTags = element.children('br:hidden'); // detach all the
tags scope.$on('$destroy', function(){ brTags.remove(); }); elem.append(layout); }; }]); function PanelLayout(items, attrs, $scope, $compile) { var stacked = attrs.stacked || false, flexbox = attrs.flexbox || false, numCols = 12, numSpan = +(attrs.itemSpan) || 6, curCol = 0, canAddRow = !stacked && !flexbox, rowClass = flexbox ? 'panel-flex' : 'row-fluid', cellClass = flexbox ? 'flex' : 'span', layout = [$('
').addClass(rowClass)]; function add(item, label) { var row = _.last(layout), cell = $('
'), span = +item.attr('x-colspan') || numSpan, offset = +item.attr('x-coloffset') || 0; span = Math.min(span, numCols); if (stacked) { span = 0; } if (curCol + (span + offset) >= numCols + 1 && canAddRow) { curCol = 0, row = $('
').addClass(rowClass); layout.push(row); } if (label) { label.appendTo(cell); row.addClass('has-labels'); } cell.addClass(item.attr('x-cell-css')); if (span) { cell.addClass(cellClass + span); } if (offset) { cell.addClass('offset' + offset); } cell.append(item); cell.appendTo(row); curCol += (span + offset); } items.each(function (item, i) { var el = $(this), title = el.attr('x-title'), noTitle = el.attr('x-show-title') == 'false'; var labelScope = el.data('$scope'); if (labelScope) { labelScope = labelScope.$new(); } if (!noTitle && title) { var label = $('').html(title).attr('x-for-widget', el.attr('id')), labelElem = $compile(label)(labelScope || $scope); el.data('label', labelElem); return add(el, labelElem); } add(el); }); var container = $('
').append(layout); return container; } ui.directive('uiPanelLayout', ['$compile', function($compile) { return { priority: 1000, link: function(scope, element, attrs) { var elem = element.children('[ui-transclude]:first'); var items = elem.children(); var layout = PanelLayout(items, attrs, scope, $compile); elem.append(layout); } }; }]); function BarLayout(items, attrs, $scope, $compile) { var main = $('
'); var side = $('
'); var wrap = $('
').appendTo(main); items.each(function(item, i) { var elem = $(this); var prop = elem.scope().field || {}; if (elem.attr('x-sidebar')) { elem.appendTo(side); } else { elem.appendTo(wrap); } if (prop.attached) { elem.addClass("attached"); } }); var row = $('
').append(main); if (side && axelor.device.small) { side.children().first().prependTo(wrap); side.children().appendTo(wrap); } wrap.children('[ui-panel-mail]').appendTo(main); if (side.children().length > 0) { side.appendTo(row.addClass('has-side')); } return row; } ui.directive('uiBarLayout', ['$compile', function($compile) { return function(scope, element, attrs) { var items = element.children(); var layout = BarLayout(items, attrs, scope, $compile); var schema = scope.schema || {}; var css = null; scope._isPanelForm = true; element.append(layout); element.addClass('bar-layout'); if (element.has('[x-sidebar]').length === 0) { css = "mid"; } if (element.is('form') && ["mini", "mid", "large"].indexOf(schema.width) > -1) { css = scope.schema.width; } if (css) { element.addClass(css + '-form'); } }; }]); ui.directive('uiPanelViewer', function () { return { scope: true, link: function (scope, element, attrs) { var field = scope.field; var isRelational = /-to-one$/.test(field.type); if (isRelational) { Object.defineProperty(scope, 'record', { enumerable: true, get: function () { return (scope.$parent.record||{})[field.name]; } }); } } }; }); ui.directive('uiPanelEditor', ['$compile', 'ActionService', function($compile, ActionService) { return { scope: true, link: function(scope, element, attrs) { var field = scope.field; var editor = field.editor; if (!editor) { return; } function applyAttrs(item, level) { if (item.showTitle === undefined && !item.items) { item.showTitle = (editor.widgetAttrs||{}).showTitles !== "false"; } if (!item.showTitle && !item.items) { var itemField = (editor.fields||scope.fields||{})[item.name] || {}; item.placeholder = item.placeholder || itemField.placeholder || item.title || itemField.title || item.autoTitle; } if (editor.itemSpan && !item.colSpan && !level) { item.colSpan = editor.itemSpan; } if (item.items) { _.map(item.items, function (x) { applyAttrs(x, (level||0) + 1); }); } } var items = editor.items || []; var hasColSpan = false; var widths = _.map(items, function (item) { applyAttrs(item); if (item.colSpan) { hasColSpan = true; } var width = item.width || (item.widgetAttrs||{}).width; return width ? width : (item.widget === 'toggle' ? 24 : '*'); }); var schema = hasColSpan ? { cols: 12, items: items } : { cols: items.length, colWidths: widths, items: items }; if (editor.layout !== 'table') { schema = { items: [{ type: 'panel', items: items, flexbox: editor.flexbox }] }; } scope.fields = editor.fields || scope.fields; var form = ui.formBuild(scope, schema, scope.fields); var isRelational = /-to-one$/.test(field.type); if (isRelational) { Object.defineProperty(scope, 'record', { enumerable: true, get: function () { return (scope.$parent.record||{})[field.name]; }, set: function (value) { scope.setValue(value, true); } }); Object.defineProperty(scope, '$$original', { enumerable: true, get: function () { return (scope.$parent.$$original||{})[field.name]; }, set: function (value) {} }); scope.$$setEditorValue = function (value, fireOnChange) { scope.setValue(value, fireOnChange === undefined ? true: fireOnChange); }; } if (field.target) { scope.getDummyValues = function() { if (!scope.record) return {}; var fields = _.keys(scope.fields); var extra = _.chain(scope.fields_view) .filter(function(f) { return f.name && f.name[0] === '$' && !_.contains(fields, f.name); }) .filter(function(f) { return ['$changed', '$editorModel', '$version', '$fetched', '$fetchedRelated'].indexOf(f) === -1; }) .pluck('name') .compact() .value(); if (scope._model === 'com.axelor.auth.db.User') { extra = extra.filter(function (n) { return ['change', 'oldPassword', 'newPassword', 'chkPassword'].indexOf(n) === -1; }); } return _.pick(scope.record, extra); }; scope.getContext = function () { var context = _.extend({}, scope.record); var dummy = scope.getDummyValues(); context._model = scope._model; context._parent = scope.$parent.getContext(); return ui.prepareContext(scope._model, context, dummy); }; scope.$broadcastRecordChange = function () { scope.$broadcast("on:record-change", scope.record); }; scope.$on('on:before-save', function watchParentRecord() { var dummyValues = scope.getDummyValues(); var watcher = scope.$watch('$parent.record', function (record, old) { if (record === old) return; var value = (record||{})[field.name]; if (value && dummyValues) { value = _.extend(value, dummyValues); } dummyValues = null; watcher(); }); }); scope.$watch('record', function (record, old) { if (record && !record.$editorModel) { record.$editorModel = scope._model; } }); // make sure to fetch missing values var fetchMissing = function (value) { var ds = scope._dataSource; var record = scope.record; if (value <= 0 || !value || record.$fetched || record.$fetchedRelated) { return; } var missing = _.filter(_.keys(editor.fields), function (name) { if (!record) return false; if (name.indexOf('.') === -1) { return !record.hasOwnProperty(name); } var path = name.split('.'); var nested = record; for (var i = 0; i < path.length - 1; i++) { nested = nested[path[i]]; if (!nested) { return false; } } return !nested.hasOwnProperty(path[path.length - 1]); }); if (missing.length === 0) { return; } record.$fetchedRelated = true; return ds.read(value, {fields: missing}).success(function(rec) { var values = _.pick(rec, missing); record = _.extend(record, values); }); }; // make sure to trigger record-change with proper record data var watchRun = function (value, old) { if (value && value !== old) { value.$changed = true; value.version = _.isNumber(value.version) ? value.version : value.$version; } if (value) { // parent form's getContext will check this to prepare context for editor // to have proper selection flags in nest o2m/m2m value.$editorModel = scope._model; fetchMissing(value.id); } scope.$applyAsync(function () { scope.$broadcast("on:record-change", value || {}, true); }); // if it's an o2m editor, make sure to update values if (scope.$itemsChanged) { scope.$itemsChanged(); } }; scope.$watch('record', _.debounce(watchRun, 100), true); scope.$timeout(function () { scope.$broadcast("on:record-change", scope.record || {}, true); }); } form = $compile(form)(scope); form.removeClass('mid-form mini-form').children('div.row').removeClass('row').addClass('row-fluid'); element.append(form); if (field.target) { var handler = null; if (editor.onNew) { schema.onNew = editor.onNew; form.data('$editorForm', form); handler = ActionService.handler(scope, form, { action: editor.onNew }); } scope.$watch('record.id', function editorRecordIdWatch(value, old) { if (!value && handler) { handler.onNew(); } }); } scope.isValid = function () { return scope.form && scope.form.$valid; }; function isEmpty(record) { if (!record || _.isEmpty(record)) return true; var values = _.filter(record, function (value, name) { return !(/[\$_]/.test(name) || value === null || value === undefined); }); return values.length === 0; } scope.$watch(function editorValidWatch() { if (isRelational && editor.showOnNew === false && !scope.canShowEditor()) { return; } var valid = scope.isValid(); if (!valid && !field.jsonFields && !scope.$parent.isRequired() && isEmpty(scope.record)) { var errors = (scope.form || {}).$error || {}; valid = !errors.valid; } if (scope.setValidity) { scope.setValidity('valid', valid, scope.record); element.toggleClass('nested-not-required', valid); } else { scope.$parent.form.$setValidity('valid', valid, scope.form); } }); scope.$on('$destroy', function () { if (scope.setValidity) { scope.setValidity('valid', true); } }); } }; }]); })();