Files
ERP/sophal/js/form/form.layout.js

603 lines
17 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 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 = $('<label ui-label></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 = $('<table class="form-layout"></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 = $('<tr></tr>'),
numCells = 0,
widths = colWidths || computeWidths(row);
_.each(row, function(cell, i) {
var el = $('<td></td>')
.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++) {
$('<td></td>').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 <br> 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 = [$('<div>').addClass(rowClass)];
function add(item, label) {
var row = _.last(layout),
cell = $('<div>'),
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 = $('<div>').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 = $('<label ui-label></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 = $('<div class="panel-layout"></div>').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 = $('<div class="bar-main">');
var side = $('<div class="bar-side">');
var wrap = $('<div class="bar-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 = $('<div class="bar-container">').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);
}
});
}
};
}]);
})();