First commit waiting for Budget Alert

This commit is contained in:
2025-09-04 13:37:35 +01:00
commit 2d681f27f5
4563 changed files with 1061534 additions and 0 deletions

File diff suppressed because it is too large Load Diff

700
sophal/js/form/form.base.js Normal file
View File

@ -0,0 +1,700 @@
/*
* 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;
};
})();

134
sophal/js/form/form.code.js Normal file
View File

@ -0,0 +1,134 @@
/*
* 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 */
/* global CodeMirror: true */
"use strict";
var ui = angular.module('axelor.ui');
ui.formInput('CodeEditor', {
css: "code-editor",
metaWidget: true,
link: function(scope, element, attrs, model) {
var editor = null;
var loading = false;
var field = scope.field;
var props = {
autofocus: true,
lineNumbers: true,
tabSize : 2,
indentUnit : 2,
indentWithTabs: false,
theme: field.codeTheme || "default",
extraKeys: {
'Ctrl-F': function () {}
}
};
if (field.mode || field.codeSyntax) {
props.mode = field.mode || field.codeSyntax;
}
if (props.mode === "xml") {
props = _.extend(props, {
foldGutter : true,
gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
autoCloseBrackets : true,
autoCloseTags : true
});
}
if (field.height) {
element.height(field.height);
}
setTimeout(function () {
props.readOnly = scope.$$readonly;
editor = CodeMirror(element.get(0), props);
model.$render();
readonlySet(props.readOnly);
editor.on("change", changed);
});
scope.$watch('$$readonly', readonlySet);
model.$render = function() {
loading = true;
var val = model.$modelValue;
if (editor) {
editor.setValue(val || "");
editor.clearHistory();
}
loading = false;
};
model.$formatters.push(function (value) {
return value || '';
});
function readonlySet(readonly) {
if (editor) {
editor.setOption('readOnly', _.toBoolean(readonly));
}
}
function changed(instance, changedObj) {
if (loading || !editor) return;
var value = editor.getValue();
if (value !== model.$viewValue) {
model.$setViewValue(value);
}
scope.$applyAsync();
}
function resize() {
if (element[0].offsetHeight === 0) return; // is hidden?
if (editor) {
editor.refresh();
}
element.width('');
}
element.resizable({
handles: 's',
resize: resize
});
scope.$onAdjust(resize);
},
replace: true,
transclude: true,
template_editable: null,
template_readonly: null,
template: '<div ng-transclude></div>'
});
})();

View File

@ -0,0 +1,841 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
// this directive is used as a replacement for ng-transclude directive
// which fails to keep scope hierarchy (see: https://github.com/angular/angular.js/issues/1809)
ui.directive('uiTransclude', function() {
return {
compile: function(tElement, tAttrs, transclude) {
return function(scope, element, attrs, ctrl) {
transclude(scope.$new(), function(clone) {
element.append(clone);
});
};
}
};
});
/**
* The Group widget.
*
*/
ui.formWidget('Group', {
css: 'form-item-group',
cellCss: 'form-item v-align-top',
link: function(scope, element, attrs) {
var props = scope.field;
scope.collapsed = false;
scope.canCollapse = function() {
return props.canCollapse || props.collapseIf;
};
scope.setCollapsed = function(collapsed) {
scope.collapsed = collapsed;
element.children('legend').nextAll(':not(br)')[collapsed ? 'hide' : 'show']();
axelor.$adjustSize();
};
scope.toggle = function() {
scope.collapsed = !scope.collapsed;
scope.setCollapsed(scope.collapsed);
};
scope.$watch("attr('collapse')", function groupCollapseWatch(collapsed) {
scope.setCollapsed(collapsed);
});
// if auto title, then don't show it
if (attrs.title === attrs.field) {
attrs.$set('title', '');
}
if (props.showTitle !== false) {
scope.$watch('attr("title")', function groupTitleWatch(value){
scope.title = value;
});
}
},
transclude: true,
template:
'<fieldset ng-class="{\'bordered-box\': title, \'has-title\': title}" x-layout-selector="&gt; div:first">'+
'<legend ng-show="title">'+
'<i ng-show="canCollapse()" ng-click="toggle()" ng-class="{\'fa fa-plus\': collapsed, \'fa fa-minus\': !collapsed}"></i>'+
'<span ng-bind-html="title"></span></legend>'+
'<div ui-transclude></div>'+
'</fieldset>'
});
ui.formWidget('Portlet', {
css: 'form-item-portlet',
cellCss: 'form-item v-align-top',
showTitle: false,
link: function(scope, element, attrs) {
var field = scope.field;
scope.canSearch = field.canSearch !== "false";
scope.actionName = field.action;
if (field.name) {
scope.formPath = field.name;
}
if (field.height) {
element.height(field.height);
}
element.resizable({
handles: 's',
resize: _.debounce(function() {
axelor.$adjustSize();
element.width('auto');
}, 100)
});
},
template:
'<div>'+
'<div ui-view-portlet x-action="{{actionName}}" x-can-search="{{canSearch}}"></div>'+
'</div>'
});
ui.formWidget('Dashlet', {
css: 'dashboard',
showTitle: false,
link: function(scope, element, attrs) {
var field = scope.field;
var dashlet = _.extend({}, scope.field);
scope.dashlet = dashlet;
scope.formPath = field.name || field.action;
scope.$watch('attr("title")', function dashletTitleWatch(title, old) {
if (title === old) {
return;
}
var dashletScope = element.children('[ui-view-dashlet]').scope();
if (dashletScope) {
dashletScope.title = title;
}
});
},
template:
'<div>'+
'<div ui-view-dashlet></div>'+
'</div>'
});
/**
* The Tabs widget (notebook).
*/
ui.formWidget('Tabs', {
cellCss: 'form-item v-align-top',
widgets: ['Notebook'],
controller: ['$scope', '$element', function($scope, $element) {
var tabs = $scope.tabs = [],
selected = -1;
var doOnSelectPending = false;
var doOnSelect = _.debounce(function () {
var select = tabs[selected];
if (doOnSelectPending || !select) {
return;
}
doOnSelectPending = true;
$scope.waitForActions(function () {
if (select.handleSelect) {
select.handleSelect();
}
$scope.waitForActions(function () {
doOnSelectPending = false;
});
});
}, 100);
$scope.select = function(tab) {
var current = selected;
angular.forEach(tabs, function(tab, i){
tab.tabSelected = false;
});
tab.tabSelected = true;
selected = _.indexOf(tabs, tab);
if (current === selected) {
return;
}
setTimeout(function() {
if ($scope.$tabs) {
$scope.$tabs.trigger('adjust:tabs');
}
axelor.$adjustSize();
if(current != selected){
doOnSelect();
}
});
};
$scope.$on('on:edit', function (e, record) {
if ($scope.record === record) {
doOnSelect();
}
});
this.addTab = function(tab) {
if (tabs.length === 0) $scope.select(tab);
tab.index = tabs.length;
tabs.push(tab);
};
function inRange(index) {
return index > -1 && index < tabs.length;
}
function findItem(index) {
return $element.find('ul.nav-tabs:first > li:nth-child(' + (index+1) + ')');
}
this.showTab = function(index) {
if (!inRange(index)) {
return;
}
var tab = tabs[index];
var item = findItem(index);
tab.hidden = false;
item.show();
if (selected == -1 || selected === index) {
return $scope.select(tabs[index]);
}
axelor.$adjustSize();
};
this.hideTab = function(index) {
if (!inRange(index))
return;
var item = findItem(index),
tab = tabs[index];
var wasHidden = item.is(":hidden");
item.hide();
item.removeClass('active');
tab.hidden = true;
tab.tabSelected = false;
if (!wasHidden && selected > -1 && selected !== index)
return axelor.$adjustSize();
for(var i = 0 ; i < tabs.length ; i++) {
tab = tabs[i];
if (!tab.hidden) {
return $scope.select(tabs[i]);
}
}
selected = -1;
};
$scope.setTitle = function(value,index){
var item = findItem(index),
pageScope = item.first().data('$scope');
pageScope.tab.title = value;
};
}],
link: function(scope, elem, attrs) {
var props = scope.field;
scope.$tabs = $(elem).bsTabs({
closable: false
});
elem.on('click', '.dropdown-toggle', function(e){
axelor.$adjustSize();
});
// set height (#1011)
if (props.height) {
elem.children('.tab-content:first').height(props.height);
}
},
transclude: true,
template:
'<div class="tabbable-tabs">' +
'<div class="nav-tabs-wrap">' +
'<div class="nav-tabs-scroll-l"><a tabindex="-1" href="#"><i class="fa fa-chevron-left"></i></a></div>' +
'<div class="nav-tabs-scroll-r"><a tabindex="-1" href="#"><i class="fa fa-chevron-right"></i></a></div>' +
'<div class="nav-tabs-strip">' +
'<ul class="nav nav-tabs">' +
'<li tabindex="-1" ng-repeat="tab in tabs" ng-class="{active:tab.tabSelected}">'+
'<a tabindex="-1" href="" ng-click="select(tab)">'+
'<img class="prefix-icon" ng-show="tab.icon" ng-src="{{tab.icon}}">'+
'<span ng-bind-html="tab.title"></span>'+
'</a>' +
'</li>' +
'</ul>' +
'</div>' +
'<div class="nav-tabs-menu">'+
'<div class="dropdown pull-right">'+
'<a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="caret"></i></a>'+
'<ul class="dropdown-menu" role="menu">'+
'<li ng-repeat="tab in tabs">'+
'<a tabindex="-1" href="javascript: void(0)" ng-click="select(tab)" ng-bind-html="tab.title"></a>'+
'</li>' +
'</ul>' +
'</a>'+
'</div>'+
'</div>'+
'</div>' +
'<div class="tab-content" ui-transclude></div>' +
'</div>'
});
/**
* The Tab widget (notebook page).
*/
ui.formWidget('Tab', {
require: '^uiTabs',
widgets: ['Page'],
handles: ['isHidden'],
link: function(scope, elem, attrs, tabs) {
scope.tabSelected = false;
scope.icon = scope.field && scope.field.icon;
tabs.addTab(scope);
scope.$watch('attr("title")', function tabTitleWatch(value){
scope.title = value;
});
scope.$watch("isHidden()", function tabHiddenWatch(hidden, old) {
if (hidden) {
return tabs.hideTab(scope.index);
}
return tabs.showTab(scope.index);
});
scope.handleSelect = function () {
var onSelect = scope.$events.onSelect;
if (onSelect && !elem.is(":hidden")) {
onSelect();
}
};
},
cellCss: 'form-item v-align-top',
transclude: true,
template: '<div ui-actions class="tab-pane" ng-class="{active: tabSelected}" x-layout-selector="&gt; div:first">'+
'<div ui-transclude></div>'+
'</div>'
});
ui.formWidget('ButtonGroup', {
link: function (scope, element, attrs) {
function adjustButtons() {
var visible = element.children('.btn:visible').length;
if (visible) {
element.children('.btn:visible')
.css('max-width', (100.00/visible) + '%')
.css('width', (100.00/visible) + '%');
}
}
scope.$watch(adjustButtons);
scope.$callWhen(function () {
return element.is(':visible');
}, adjustButtons);
},
transclude: true,
template_editable: null,
template_readonly: null,
template:
"<div class='btn-group' ui-transclude></div>"
});
ui.formWidget('Panel', {
showTitle: false,
link: function (scope, element, attrs) {
var field = scope.field || {};
var body = element.children(".panel-body");
element.addClass(field.serverType);
if (field.sidebar && !attrs.itemSpan) {
attrs.$set('itemSpan', 12, true, 'x-item-span');
}
scope.menus = null;
if (field.menu) {
scope.menus = [field.menu];
}
scope.canCollapse = function() {
return field.canCollapse || field.collapseIf;
};
scope.setCollapsed = function(collapsed) {
var old = scope.collapsed;
var action = collapsed ? "hide" : "show";
scope.collapsed = collapsed;
scope.collapsedIcon = collapsed ? 'fa-chevron-down' : 'fa-chevron-up';
if (collapsed === old) {
return;
}
element.removeClass("collapsed");
body[action]("blind", 200, function () {
element.toggleClass("collapsed", !!collapsed);
if (body.css('display') !== 'none' && action === 'hide') {
body.hide();
}
axelor.$adjustSize();
});
};
scope.toggle = function() {
if (scope.canCollapse()) {
scope.setCollapsed(!scope.collapsed);
}
};
scope.$watch("attr('collapse')", function panelCollapseWatch(collapsed) {
scope.setCollapsed(collapsed);
});
var nested = element.parents('.panel:first').length > 0;
if (nested) {
element.addClass("panel-nested");
}
if (field.showFrame === false) {
element.addClass('noframe');
}
scope.notitle = field.showFrame === false || field.showTitle === false;
scope.title = field.title;
scope.$watch('attr("title")', function panelTitleWatch(title, old) {
if (title === undefined || title === old) return;
scope.title = title;
});
var icon = field.icon;
var iconBg = field.iconBackground;
if (icon && icon.indexOf('fa-') === 0) {
scope.icon = icon;
} else if (icon) {
scope.image = icon;
}
if (scope.icon && iconBg) {
setTimeout(function() {
var iconElem = element.children('.panel-header').children('.panel-icon');
if (iconBg.indexOf("#") === 0) {
iconElem.css('background-color', iconBg);
} else {
iconElem.addClass('bg-' + iconBg);
}
iconElem.addClass('has-bg');
iconElem.find('i').addClass('fg-white');
});
}
setTimeout(function () {
var nestedJson = element.parents('.panel-json:first').length > 0;
if (nestedJson) {
element.removeClass("panel-nested");
}
});
},
transclude: true,
template:
"<div class='panel panel-default'>" +
"<div class='panel-header' ng-click='toggle()' ng-if='!notitle &amp;&amp; field.title' ng-class=\"{'clickable-header' : canCollapse()}\" tabindex='-1'>" +
"<div class='panel-icon' ng-if='icon'><i class='fa' ng-class='icon'></i></div>" +
"<img class='panel-image' ng-if='image' ng-src='{{image}}'>" +
"<div class='panel-title'>{{title}}</div>" +
"<div ng-if='menus' ui-menu-bar menus='menus' handler='this'></div>" +
"<div ng-show='canCollapse()' class='panel-icons'>" +
"<a href=''><i class='fa' ng-class='collapsedIcon'></i></a>" +
"</div>" +
"</div>" +
"<div class='panel-body' ui-transclude></div>" +
"</div>"
});
ui.formWidget('PanelStack', {
transclude: true,
template: "<div class='panel-stack'>" +
"<span ui-transclude></span>" +
"</div>"
});
ui.formWidget('PanelTabs', {
link: function (scope, element, attrs) {
scope.tabs = [];
scope.more = null;
element.find('> .tab-content > div').each(function (index) {
var elem = $(this);
var tab = {
title: elem.attr('x-title'),
selected: false,
hidden: false,
elem: elem,
tabItem: $(),
menuItem: $()
};
scope.tabs.push(tab);
});
var selected = null;
var adjustPending = false;
function findTab(tab) {
var found = scope.tabs[tab] || tab;
if (!found || _.isNumber(found)) {
return null;
}
return found;
}
var doOnSelectPending = false;
var doOnSelect = _.debounce(function () {
if (doOnSelectPending || !selected || !selected.elem) {
return;
}
doOnSelectPending = true;
scope.waitForActions(function () {
var elemScope = selected.elem.scope();
if (elemScope.handleSelect) {
elemScope.handleSelect();
}
doOnSelectPending = false;
});
}, 100);
scope.selectTab = function(tab) {
var current = selected;
var found = findTab(tab);
if (!found) {
return;
}
scope.tabs.forEach(function (current) {
current.selected = false;
current.elem.hide();
});
selected = found;
found.selected = true;
found.elem.show();
found.elem
.add(found.tabItem)
.add(found.menuItem)
.addClass('active');
setTimeout(function () {
scope.$broadcast('tab:select');
elemTabs.removeClass('open');
elemMenu.removeClass('open');
axelor.$adjustSize();
if (current != selected) {
doOnSelect();
}
});
};
scope.showTab = function (tab) {
var found = findTab(tab);
if (!found) {
return;
}
adjustPending = true;
found.hidden = false;
found.tabItem.show();
if (!selected || selected === found) {
return scope.selectTab(found);
}
found.elem.hide();
axelor.$adjustSize();
};
scope.hideTab = function (tab) {
var found = findTab(tab);
if (!found) {
return;
}
adjustPending = true;
var wasHidden = found.hidden;
found.hidden = true;
found.selected = false;
found.elem.hide();
found.tabItem.add(found.menuItem).hide().removeClass('active');
if (!wasHidden && selected && selected !== found) {
return axelor.$adjustSize();
}
var tabs = scope.tabs;
for(var i = 0 ; i < tabs.length ; i++) {
tab = tabs[i];
if (!tab.hidden) {
return scope.selectTab(tabs[i]);
}
}
selected = null;
};
var lastWidth = 0;
var lastTab = null;
var elemTabs = $();
var elemMenu = $();
var elemMenuTitle = $();
var elemMenuItems = $();
function setup() {
elemTabs = element.children('.nav-tabs').children('li:not(.dropdown)');
elemMenu = element.children('.nav-tabs').children('li.dropdown');
elemMenuTitle = elemMenu.children('a:first').children('span');
elemMenuItems = elemMenu.find('li');
_.each(scope.tabs, function (tab, index) {
tab.tabItem = $(elemTabs[index]);
tab.menuItem = $(elemMenuItems[index]);
});
}
var setMenuTitle = (function() {
var setActive = _.debounce(function(selected) {
elemMenu.toggleClass('active', !!selected);
});
return function setMenuTitle(selected) {
elemMenu.show();
elemMenuTitle.html(selected && selected.title);
setActive(selected);
};
}());
function adjust() {
if (elemTabs === null || !scope.tabs || element.is(":hidden")) {
return;
}
var parentWidth = element.width() - 2;
if (parentWidth === lastWidth && lastTab === selected && !adjustPending) {
return;
}
lastWidth = parentWidth;
lastTab = selected;
elemTabs.parent().css('visibility', 'hidden');
elemMenu.hide();
// show visible tabs
scope.tabs.forEach(function (tab, i) {
if (tab.hidden) {
$(elemTabs[i]).hide();
} else {
$(elemTabs[i]).show();
}
});
if (elemTabs.parent().width() <= parentWidth) {
elemTabs.parent().css('visibility', '');
return;
}
setMenuTitle(null);
var elem = null;
var index = elemTabs.length;
var selectedIndex = scope.tabs.indexOf(selected);
while (elemTabs.parent().width() > parentWidth) {
elem = $(elemTabs[--index]);
elem.hide();
if (index === selectedIndex) {
setMenuTitle(selected);
}
}
elemMenuItems.hide();
var tab = null;
while(index < scope.tabs.length) {
tab = scope.tabs[index++];
if (!tab.hidden) {
tab.menuItem.show();
}
}
elemTabs.parent().css('visibility', '');
}
var adjusting = false;
scope.$onAdjust(function() {
if (adjusting) { return; }
try {
adjusting = true;
adjust();
} finally {
adjusting = false;
adjustPending = false;
}
}, 10);
scope.$timeout(function() {
setup();
var first = _.find(scope.tabs, function (tab) {
return !tab.hidden;
});
if (first) {
scope.selectTab(first);
}
});
scope.$on('on:edit', function (e, record) {
if (scope.record === record && !doOnSelectPending) {
scope.ajaxStop(doOnSelect, 100);
}
});
scope.$watch(function tabsWatch() {
var hidden = scope.attr('hidden');
if (hidden) return;
// show selected tab only
scope.tabs.forEach(function (tab) {
if (!tab.selected) tab.elem.hide();
});
});
},
transclude: true,
template:
"<div class='panel-tabs tabbable-tabs'>" +
"<ul class='nav nav-tabs nav-tabs-responsive'>" +
"<li ng-repeat='tab in tabs' ng-class='{active: tab.selected}'>" +
"<a tabindex='-1' href='' ng-click='selectTab(tab)' ng-bind-html='tab.title'></a>" +
"</li>" +
"<li class='dropdown' style='display: none'>" +
"<a tabindex='-1' href='' class='dropdown-toggle' data-toggle='dropdown'><span></span><b class='caret'></b></a>" +
"<ul class='dropdown-menu pull-right'>" +
"<li ng-repeat='tab in tabs' ng-class='{active: tab.selected}'>" +
"<a tabindex='-1' href='' ng-click='selectTab(tab)' ng-bind-html='tab.title'></a>" +
"</li>" +
"</ul>" +
"</li>" +
"</ul>" +
"<div class='tab-content' ui-transclude></div>" +
"</div>"
});
ui.formWidget('PanelTab', {
link: function (scope, element, attrs) {
var index = element.parent().children().index(element);
var tab = null;
var isHidden = scope.isHidden;
function findTab() {
return tab || (tab = (scope.tabs||[])[index]) || {};
}
scope.handleSelect = function () {
var onTabSelect = scope.$events.onTabSelect;
if (onTabSelect && !element.is(":hidden")) {
onTabSelect();
}
};
scope.isHidden = function () {
var tab = findTab();
return !tab.selected || isHidden.call(scope);
};
scope.$watch("attr('title')", function tabTitleWatch(value) {
var tab = findTab();
tab.title = value;
});
scope.$watch("attr('hidden')", function tabHiddenWatch(hidden, old) {
scope.$evalAsync(function () {
if (hidden) {
return scope.hideTab(index);
}
return scope.showTab(index);
});
});
}
});
})();

View File

@ -0,0 +1,234 @@
/*
* 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 () {
"use strict";
var ui = angular.module('axelor.ui');
var currencySymbols = {
en: '\u0024',
fr: '\u20AC'
};
var thousandSeparator = {
en: ',',
fr: ' '
};
function addCurrency(value, symbol) {
if (value && symbol) {
var val = '' + value;
if (axelor.config['user.lang'] === 'fr' ) {
return val.endsWith(symbol) ? val : val + ' ' + symbol;
}
return val.startsWith(symbol) ? val : symbol + val;
}
return value;
}
function canSetNested(record, name) {
if (record && name && name in record) {
return true;
}
if (name) {
var path = name.split('.');
var val = record || {};
var idx = 0;
while (idx < path.length - 1) {
val = val[path[idx++]];
if (!val) return false;
}
}
return true;
}
function findNested(record, name) {
if (record && name && name in record) {
return record[name] === undefined ? null : record[name];
}
if (name) {
var path = name.split('.');
var val = record || {};
var idx = 0;
while (val && idx < path.length) {
val = val[path[idx++]];
}
if (idx === path.length) {
return val;
}
}
return undefined;
}
function setNested(record, name, value) {
if (!record || !name) return record;
var path = name.split('.');
var nested = record;
var idx = -1;
while (++idx < path.length) {
var key = path[idx];
if (idx !== path.length - 1) {
nested = nested[key] || (nested[key] = {});
} else {
nested[key] = value;
}
}
return record;
}
// override angular.js currency filter
ui.filter('currency', function () {
return addCurrency;
});
function formatNumber(field, value, scale) {
var num = +(value);
if ((value === null || value === undefined) && !field.defaultValue) {
return value;
}
if (num === 0 || num) {
var lang = axelor.config['user.lang'];
var tsep = thousandSeparator[lang] || thousandSeparator.en;
return _.numberFormat(num, scale, '.', tsep);
}
return value;
}
ui.findNested = findNested;
ui.setNested = setNested;
ui.canSetNested = canSetNested;
ui.formatters = {
"string": function(field, value, context) {
if (field.translatable && value && context) {
var key = '$t:' + field.name;
return context[key] || value;
}
return value;
},
"integer": function(field, value) {
return formatNumber(field, value);
},
"decimal": function(field, value, context) {
var scale = (field.widgetAttrs||{}).scale || field.scale || 2;
var currency = (field.widgetAttrs||{}).currency || field.currency;
var text = formatNumber(field, value, scale);
if (text && currency) {
text = addCurrency(text, findNested(context, currency));
}
return text;
},
"boolean": function(field, value) {
return value;
},
"duration": function(field, value) {
return ui.formatDuration(field, value);
},
"date": function(field, value) {
return value ? moment(value).format('DD/MM/YYYY') : "";
},
"time": function(field, value) {
return value ? value : "";
},
"datetime": function(field, value) {
return value ? moment(value).format('DD/MM/YYYY HH:mm') : "";
},
"many-to-one": function(field, value) {
return value
? (field.targetName ? value[field.targetName] : (value.name || value.code || value.id || ""))
: "";
},
"one-to-many": function(field, value) {
return value ? '(' + value.length + ')' : "";
},
"many-to-many": function(field, value) {
return value ? '(' + value.length + ')' : "";
},
"selection": function(field, value) {
var cmp = field.type === "integer" ? function(a, b) { return a == b ; } : _.isEqual;
var res = _.find(field.selectionList, function(item){
return cmp(item.value, value);
}) || {};
return res.title;
}
};
ui.formatters["enum"] = ui.formatters.selection;
function findField(scope, name) {
if (scope.field && scope.field.target) {
return ((scope.field.viewer||{}).fields||{})[name]
|| ((scope.field.editor||{}).fields||{})[name];
}
return (scope.viewItems || scope.fields || {})[name];
}
ui.formatters.$image = function (scope, fieldName, imageName) {
var record = scope.record || {};
var model = scope._model;
if (fieldName) {
var field = (scope.fields||{})[fieldName];
if (field && field.target) {
record = record[fieldName] || {};
model = field.target;
}
}
var v = record.version || record.$version || 0;
var n = record.id;
if (n > 0) {
return "ws/rest/" + model + "/" + n + "/" + imageName + "/download?image=true&v=" + v
+ "&parentId=" + scope.record.id + "&parentModel=" + scope._model;
}
return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
};
ui.formatters.$fmt = function (scope, fieldName, fieldValue, record) {
var context = record || scope.record || {};
var value = arguments.length === 2 ? context[fieldName] : fieldValue;
if (value === undefined || value === null) {
return "";
}
var field = findField(scope, fieldName);
if (!field) {
return value;
}
var type = field.selection ? "selection" : field.type;
var formatter = ui.formatters[type];
if (formatter) {
return formatter(field, value, context);
}
return value;
};
})();

View File

@ -0,0 +1,447 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
var BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
var META_FILE = "com.axelor.meta.db.MetaFile";
var META_JSON_RECORD = "com.axelor.meta.db.MetaJsonRecord";
function makeURL(model, field, recordOrId, version, scope) {
var value = recordOrId;
if (!value) return null;
var id = value.id ? value.id : value;
var ver = version;
if (ver === undefined || ver === null) ver = value.version;
if (ver === undefined || ver === null) ver = value.$version;
if (ver === undefined || ver === null) ver = (new Date()).getTime();
if (!id || id <= 0) return null;
var url = "ws/rest/" + model + "/" + id + "/" + field + "/download?v=" + ver;
if (scope && scope.record) {
var parentId = scope.record.id;
if (!parentId && scope.field && scope._jsonContext && scope._jsonContext.$record) {
parentId = scope._jsonContext.$record.id;
}
url += "&parentId=" + parentId + "&parentModel=" + scope._model;
}
return url;
}
ui.makeImageURL = makeURL;
ui.formInput('ImageLink', {
css: 'image-item',
cssClass: 'from-item image-item',
metaWidget: true,
controller: ['$scope', '$element', '$interpolate', function($scope, $element, $interpolate) {
$scope.parseText = function(text) {
if (!text) return BLANK;
if (!text.match(/{{.*?}}/)) {
return text;
}
return $interpolate(text)($scope.record);
};
}],
init: function(scope) {
var field = scope.field;
var width = field.width || 140;
var height = field.height || '100%';
scope.styles = [{
'width': width,
'max-width': '100%',
'max-height': '100%'
}, {
'width': width,
'height': height,
'max-width': '100%',
'max-height': '100%'
}];
if (field.noframe) {
_.extend(scope.styles[1], {
border: 0,
padding: 0,
background: 'none',
boxShadow: 'none'
});
}
},
link_readonly: function(scope, element, attrs, model) {
var image = element.children('img:first');
var update = scope.$render_readonly = function () {
if (scope.isReadonly()) {
image.get(0).src = scope.parseText(model.$viewValue) || BLANK;
}
};
scope.$watch("record.id", update);
scope.$watch("record.version", update);
scope.$watch("isReadonly()", update);
},
template_editable: '<input type="text">',
template_readonly:
'<div ng-style="styles[0]">'+
'<img class="img-polaroid" ng-style="styles[1]">'+
'</div>'
});
ui.formInput('Image', 'ImageLink', {
init: function(scope) {
this._super.apply(this, arguments);
var field = scope.field;
var isBinary = field.serverType === 'binary';
if (!isBinary && field.target !== META_FILE) {
throw new Error("Invalid field type for Image widget.");
}
scope.parseText = function (value) {
return scope.getLink(value);
};
scope.getLink = function (value) {
var record = scope.record || {};
var model = scope._model;
if (value === null) return BLANK;
if (isBinary) {
if (value) {
return value;
}
if (record.id) {
return makeURL(model, field.name, record, undefined, scope) + "&image=true";
}
return BLANK;
}
return value ? makeURL(META_FILE, "content", (value.id || value), value.version || value.$version, scope) : BLANK;
};
},
link_editable: function(scope, element, attrs, model) {
var field = scope.field;
var input = element.children('input:first');
var image = element.children('img:first');
var buttons = element.children('.btn-group');
var isBinary = field.serverType === 'binary';
var timer = null;
input.add(buttons).hide();
element.on('mouseenter', function (e) {
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
buttons.slideDown();
}, 500);
});
element.on('mouseleave', function (e) {
if (timer) {
clearTimeout(timer);
timer = null;
}
buttons.slideUp();
});
scope.doSelect = function() {
input.click();
};
scope.doSave = function() {
var content = image.get(0).src;
if (content) {
window.open(content);
}
};
scope.doRemove = function() {
image.get(0).src = "";
input.val(null);
update(null);
};
input.change(function(e, ui) {
var file = input.get(0).files[0];
var uploadSize = +(axelor.config["file.upload.size"]) || 0;
// reset file selection
input.get(0).value = null;
if (!file) {
return;
}
if(uploadSize > 0 && file.size > 1048576 * uploadSize) {
return axelor.dialogs.say(_t("You are not allow to upload a file bigger than") + ' ' + uploadSize + 'MB');
}
if (!isBinary) {
return doUpload(file);
}
var reader = new FileReader();
reader.onload = function(e) {
update(e.target.result, file.name);
};
reader.readAsDataURL(file);
});
function doUpload(file) {
var ds = scope._dataSource._new(META_FILE);
var value = field.target === META_FILE ? (scope.getValue()||{}) : {};
var record = {
fileName: file.name,
fileType: file.type,
fileSize: file.size,
id: value.id,
version: value.version || value.$version
};
record.$upload = {
file: file
};
ds.save(record).success(function (saved) {
update(saved);
});
}
function doUpdate(value) {
image.get(0).src = scope.getLink(value);
model.$setViewValue(getData(value));
}
function update(value) {
scope.$applyAsync(function() {
doUpdate(value);
});
}
function getData(value) {
if (!value || isBinary) {
return value;
}
return {
id: value.id
};
}
var updateLink = scope.$render_editable = function() {
image.get(0).src = scope.getLink(model.$viewValue);
};
scope.$watch("record.id", updateLink);
scope.$watch("record.version", updateLink);
if (field.accept) {
input.attr('accept', field.accept);
}
},
template_editable:
'<div ng-style="styles[0]" class="image-wrapper">' +
'<input type="file" accept="image/*">' +
'<img class="img-polaroid" ng-style="styles[1]" style="display: inline-block;">' +
'<div class="btn-group">' +
'<button ng-click="doSelect()" class="btn" type="button"><i class="fa fa-arrow-circle-up"></i></button>' +
'<button ng-click="doRemove()" class="btn" type="button"><i class="fa fa-times"></i></button>' +
'</div>' +
'</div>'
});
ui.formInput('Binary', {
css: 'file-item',
cellCss: 'form-item file-item',
link: function(scope, element, attrs, model) {
var field = scope.field;
var input = element.children('input:first').hide();
scope.doSelect = function() {
input.click();
};
scope.doSave = function() {
var record = scope.record;
var model = scope._model;
var url = makeURL(model, field.name, record, undefined, scope);
ui.download(url, record.fileName || field.name);
};
scope.doRemove = function() {
var record = scope.record;
input.val(null);
model.$setViewValue(null);
record.$upload = null;
if(scope._model === META_FILE) {
record.fileName = null;
record.fileType = null;
}
record.fileSize = null;
};
scope.canDownload = function() {
var record = scope.record || {};
if (!record.id) return false;
if (scope._model === META_FILE) {
return !!record.fileName;
}
return true;
};
input.change(function(e) {
var file = input.get(0).files[0];
var record = scope.record;
// reset file selection
input.get(0).value = null;
if (file) {
record.$upload = {
field: field.name,
file: file
};
if(scope._model === META_FILE) {
record.fileName = file.name;
}
scope.$applyAsync(function() {
record.fileType = file.type;
record.fileSize = file.size;
});
}
});
if (field.accept) {
input.attr('accept', field.accept);
}
},
template_readonly: null,
template_editable: null,
template:
'<div>' +
'<input type="file">' +
'<div class="btn-group">' +
'<button ng-click="doSelect()" ng-show="!isReadonly()" class="btn" type="button"><i class="fa fa-arrow-circle-up"></i></button>' +
'<button ng-click="doSave()" ng-show="canDownload()" class="btn" type="button"><i class="fa fa-arrow-circle-down"></i></button>' +
'<button ng-click="doRemove()" ng-show="!isReadonly()" class="btn" type="button"><i class="fa fa-times"></i></button>' +
'</div>' +
'</div>'
});
ui.formInput('BinaryLink', {
css: 'file-item',
cellCss: 'form-item file-item',
metaWidget: true,
link: function(scope, element, attrs, model) {
var field = scope.field;
var input = element.children('input:first').hide();
if (field.target !== META_FILE) {
throw new Error("BinaryLink widget can be used with MetaFile field only.");
}
scope.doSelect = function() {
input.click();
};
scope.doRemove = function() {
input.val(null);
scope.setValue(null, true);
};
scope.canDownload = function() {
var value = model.$viewValue;
return value && value.id > 0;
};
scope.format = function (value) {
if (value) {
return value.fileName;
}
return value;
};
scope.doSave = function() {
var value = model.$viewValue;
var version = value ? (value.version || value.$version) : undefined;
var url = makeURL(META_FILE, "content", value, version, scope);
ui.download(url, scope.text);
};
input.change(function(e) {
var file = input.get(0).files[0];
// reset file selection
input.get(0).value = null;
if (!file) {
return;
}
var ds = scope._dataSource._new(META_FILE);
var value = scope.getValue() || {};
var record = _.extend({
fileName: file.name,
fileType: file.type,
fileSize: file.size
}, {
id: value.id,
version: value.version || value.$version
});
record.$upload = {
file: file
};
ds.save(record).success(function (rec) {
scope.setValue(rec, true);
});
});
if (field.accept) {
input.attr('accept', field.accept);
}
},
template_readonly: null,
template_editable: null,
template:
'<div>' +
'<input type="file">' +
'<div class="btn-group">' +
'<button ng-click="doSelect()" ng-show="!isReadonly()" class="btn" type="button"><i class="fa fa-arrow-circle-up"></i></button>' +
'<button ng-click="doRemove()" ng-show="canDownload() && !isReadonly()" class="btn" type="button"><i class="fa fa-times"></i></button>' +
'</div> ' +
'<a ng-show="text" href="javascript:" ng-click="doSave()">{{text}}</a>' +
'</div>'
});
})();

View File

@ -0,0 +1,228 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
/**
* The Boolean input widget.
*/
ui.formInput('Boolean', {
css: 'boolean-item',
cellCss: 'form-item boolean-item',
link: function (scope, element, attrs, model) {
element.on('click', 'input:not(.no-toggle)', function (e) {
var value = $(e.target).data('value');
var checked = value === undefined ? e.target.checked : value;
scope.setValue(checked, true);
});
Object.defineProperty(scope, '$value', {
get: function () {
return model.$viewValue;
},
set: function(value) {
model.$setViewValue(value);
}
});
},
template_readonly: null,
template_editable:
"<label class='ibox'>" +
"<input type='checkbox' ng-model='$value' ng-disabled='isReadonly()'>" +
"<span class='box'></span>" +
"</label>"
});
/**
* The Boolean widget with label on right.
*/
ui.formInput('InlineCheckbox', 'Boolean', {
css: 'checkbox-inline',
metaWidget: true,
showTitle: false,
link: function (scope, element, attrs, model) {
this._super.apply(this, arguments);
scope.$watch('attr("title")', function booleanTitleWatch(title) {
scope.label = title;
});
},
template_readonly: null,
template_editable:
"<label class='ibox'>" +
"<input type='checkbox' ng-model='$value' ng-disabled='isReadonly()'>" +
"<div class='box'></div>" +
"<span class='title' ui-help-popover>{{label}}</span>" +
"</label>"
});
ui.formInput('Toggle', 'Boolean', {
cellCss: 'form-item toggle-item',
metaWidget: true,
link: function (scope, element, attrs, model) {
this._super.apply(this, arguments);
var field = scope.field;
var icon = element.find('i');
scope.icon = function () {
return model.$viewValue && field.iconActive ? field.iconActive : field.icon;
};
scope.toggle = function () {
var value = !model.$viewValue;
if (scope.setExclusive && field.exclusive) {
scope.setExclusive(field.name, scope.record);
}
scope.setValue(value, true);
};
if (field.help || field.title) {
element.attr('title', field.help || field.title);
}
},
template_readonly: null,
template_editable:
"<button tabindex='-1' class='btn btn-default' ng-class='{active: $value}' ng-click='toggle()'>" +
"<i class='fa {{icon()}}'></i>" +
"</button>"
});
ui.formInput('BooleanSelect', 'Boolean', {
css: 'form-item boolean-select-item',
metaWidget: true,
init: function (scope) {
var field = scope.field;
var trueText = _t((field.widgetAttrs||{}).trueText) || _t('Yes');
var falseText = _t((field.widgetAttrs||{}).falseText) || _t('No');
scope.$items = [trueText, falseText];
scope.$selection = [{ value: trueText, val: true}, { value: falseText, val: false }];
if (field.nullable) {
scope.$selection.unshift({ value: '', val: null });
}
scope.format = function (value) {
if (field.nullable && (value === null || value === undefined)) {
return "";
}
return value ? scope.$items[0] : scope.$items[1];
};
},
link_editable: function (scope, element, attrs, model) {
var input = element.find('input');
var items = scope.$items;
input.autocomplete({
minLength: 0,
source: scope.$selection,
select: function (e, u) {
scope.setValue(u.item.val, true);
scope.$applyAsync();
}
}).click(function (e) {
input.autocomplete("search" , '');
});
scope.doShowSelect = function () {
input.autocomplete("search" , '');
};
scope.$render_editable = function () {
var value = model.$viewValue;
var text = scope.format(value);
input.val(text);
};
scope.$watch('isReadonly()', function booleanReadonlyWatch(readonly) {
input.autocomplete(readonly ? "disable" : "enable");
input.toggleClass('not-readonly', !readonly);
});
},
template: "<span class='form-item-container'></span>",
template_readonly: '<span>{{text}}</span>',
template_editable: "<span class='picker-input'>" +
"<input type='text' readonly='readonly' class='no-toggle'>" +
"<span class='picker-icons picker-icons-1'>" +
"<i class='fa fa-caret-down' ng-click='doShowSelect()'></i>" +
"</span>" +
"</span>"
});
ui.formInput('BooleanRadio', 'BooleanSelect', {
css: 'form-item boolean-radio-item',
metaWidget: true,
link_editable: function (scope, element, attrs, model) {
var field = scope.field;
if (field.direction === "vertical") {
element.addClass("boolean-radio-vertical");
}
var inputName = _.uniqueId('boolean-radio');
var trueInput = $('<input type="radio" data-value="true" name="' + inputName + '">');
var falseInput = $('<input type="radio" data-value="false" name="' + inputName + '">');
var items = scope.$items;
$('<li>').append(
$('<label class="ibox round">')
.append(trueInput)
.append($('<i class="box">'))
.append($('<span class="title">').text(items[0]))
).appendTo(element);
$('<li>').append(
$('<label class="ibox round">')
.append(falseInput)
.append($('<i class="box">'))
.append($('<span class="title">').text(items[1]))
).appendTo(element);
scope.$render_editable = function () {
var value = model.$viewValue || false;
var input = value ? trueInput : falseInput;
input.prop('checked', true);
};
element.on('change', 'input', function (e) {
var value = $(this).data('value') === true;
scope.setValue(value, true);
scope.$applyAsync();
});
},
template_editable: "<ul class='boolean-radio'></ul>"
});
ui.formInput('BooleanSwitch', 'Boolean', {
css: 'form-item',
metaWidget: true,
template_readonly: null,
template_editable:
"<label class='iswitch'>" +
"<input type='checkbox' ng-model='$value' ng-disabled='isReadonly()'>" +
"<span class='box'></span>" +
"</label>"
});
})();

View File

@ -0,0 +1,635 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
var regional = {
monthNames: [
_t('January'),
_t('February'),
_t('March'),
_t('April'),
_t('May'),
_t('June'),
_t('July'),
_t('August'),
_t('September'),
_t('October'),
_t('November'),
_t('December')],
monthNamesShort: [
_t('Jan'),
_t('Feb'),
_t('Mar'),
_t('Apr'),
_t('May'),
_t('Jun'),
_t('Jul'),
_t('Aug'),
_t('Sep'),
_t('Oct'),
_t('Nov'),
_t('Dec')],
dayNames: [
_t('Sunday'),
_t('Monday'),
_t('Tuesday'),
_t('Wednesday'),
_t('Thursday'),
_t('Friday'),
_t('Saturday')],
dayNamesShort : [_t('Sun'), _t('Mon'), _t('Tue'), _t('Wed'), _t('Thu'), _t('Fri'), _t('Sat')],
dayNamesMin : [_t('Su'), _t('Mo'), _t('Tu'), _t('We'), _t('Th'), _t('Fr'), _t('Sa')],
weekHeader : _t('Wk'),
hourText : _t('Hour'),
minuteText : _t('Minute'),
secondText : _t('Second'),
currentText : _t('Now'),
closeText : _t('Done'),
prevText : _t('Prev'),
nextText : _t('Next'),
firstDay: 1
};
// configure datepicker
$.timepicker.setDefaults(regional);
$.datepicker.setDefaults(regional);
// configure moment.js
moment.locale('en', {
months: regional.monthNames,
monthsShort: regional.monthNamesShort,
weekdays: regional.dayNames,
weekdaysShort: regional.dayNamesShort,
weekdaysMin: regional.dayNamesMin,
calendar : {
sameDay : _t('[Today at] LT'),
nextDay : _t('[Tomorrow at] LT'),
nextWeek : _t('dddd [at] LT'),
lastDay : _t('[Yesterday at] LT'),
lastWeek : _t('[Last] dddd [at] LT'),
sameElse : _t('L')
},
relativeTime : {
future : _t("in %s"),
past : _t("%s ago"),
s : _t("a few seconds"),
m : _t("a minute"),
mm : _t("%d minutes"),
h : _t("an hour"),
hh : _t("%d hours"),
d : _t("a day"),
dd : _t("%d days"),
M : _t("a month"),
MM : _t("%d months"),
y : _t("a year"),
yy : _t("%d years")
}
});
// configure ui.mask
function createTwoDigitDefinition( maximum ) {
return function( value ) {
var number = parseInt( value, 10 );
if (value === "" || /\D/.test(value) || _.isNaN(number)) {
return false;
}
// pad to 2 characters
number = ( number < 10 ? "0" : "" ) + number;
if ( number <= maximum ) {
return number;
}
};
}
function yearsDefinition( value ) {
var temp;
// if the value is empty, or contains a non-digit, it is invalid
if ( value === "" || /\D/.test( value ) ) {
return false;
}
temp = parseInt( value, 10 );
// convert 2 digit years to 4 digits, this allows us to type 80 <right>
if ( value.length <= 2 ) {
// before "32" we assume 2000's otherwise 1900's
if ( temp < 10 ) {
return "200" + temp;
} else if ( temp < 32 ) {
return "20" + temp;
} else {
return "19" + temp;
}
}
if ( temp === 0 ) {
return "2000";
}
if ( value.length === 3 ) {
return "0"+value;
}
if ( value.length === 4 ) {
return value;
}
}
$.extend($.ui.mask.prototype.options.definitions, {
"MM": createTwoDigitDefinition( 12 ),
"DD": createTwoDigitDefinition( 31 ),
"YYYY": yearsDefinition,
"HH": createTwoDigitDefinition( 23 ),
"mm": createTwoDigitDefinition( 59 )
});
// datepicker keyboad navigation hack
var _doKeyDown = $.datepicker._doKeyDown;
$.extend($.datepicker, {
_doKeyDown: function(event) {
var inst = $.datepicker._getInst(event.target),
handled = false;
inst._keyEvent = true;
if ($.datepicker._datepickerShowing) {
switch (event.keyCode) {
case 36: // home
$.datepicker._gotoToday(event.target);
handled = true;
break;
case 37: // left
$.datepicker._adjustDate(event.target, -1, "D");
handled = true;
break;
case 38: // up
$.datepicker._adjustDate(event.target, -7, "D");
handled = true;
break;
case 39: // right
$.datepicker._adjustDate(event.target, +1, "D");
handled = true;
break;
case 40: // down
$.datepicker._adjustDate(event.target, +7, "D");
handled = true;
break;
}
if (handled) {
event.ctrlKey = true;
}
} else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home
$.datepicker._showDatepicker(this);
handled = true;
}
if (handled) {
event.preventDefault();
event.stopPropagation();
} else {
_doKeyDown(event);
}
}
});
var _updateDatepicker = $.datepicker._updateDatepicker;
$.datepicker._updateDatepicker = function(inst) {
if (!$.datepicker._noUpdate) {
return _updateDatepicker.call($.datepicker, inst);
}
};
var _parseDate = $.datepicker.parseDate;
$.datepicker.parseDate = function (format, value, settings) {
if (_.isString(value) && value.indexOf('_') > -1) {
return;
}
return _parseDate.call($.datepicker, format, value, settings);
};
// suppress annoying logs
$.timepicker.log = function () {};
/**
* The DateTime input widget.
*/
ui.formInput('DateTime', {
css : 'datetime-item',
format: 'DD/MM/YYYY HH:mm',
mask: 'DD/MM/YYYY HH:mm',
widgets: ['Datetime'],
init: function(scope) {
var isDate = this.isDate,
format = this.format;
scope.parse = function(value) {
if (angular.isDate(value)) {
return isDate ? moment(value).startOf('day').format('YYYY-MM-DD') : value.toISOString();
}
return value;
};
scope.format = function(value) {
if (value) {
return moment(value).format(format);
}
return value;
};
},
link_editable: function(scope, element, attrs, model) {
var input = element.children('input:first');
var button = element.find('i:first');
var onChange = scope.$events.onChange;
var props = scope.field;
var isDate = this.isDate;
var isShowing = false;
var lastValue = null;
var options = {
dateFormat: 'dd/mm/yy',
showButtonsPanel: false,
showTime: false,
showOn: null,
beforeShow: function (e, ui) {
lastValue = input.mask("value") || '';
isShowing = true;
},
onClose: function (e, ui) {
lastValue = null;
isShowing = false;
},
onSelect: function(dateText, inst) {
input.mask('value', dateText);
updateModel();
if (!inst.timeDefined) {
input.datetimepicker('hide');
setTimeout(function(){
input.focus().select();
});
}
}
};
if (this.isDate) {
options.showTimepicker = false;
}
input.datetimepicker(options);
input.mask({
mask: this.mask
});
var changed = false;
var rendering = false;
input.on('change', function(e, ui){
if (changed) return;
if (isShowing) {
changed = lastValue !== (input.mask("value") || '');
} else {
changed = !rendering;
}
});
input.on('blur', function() {
if (changed) {
changed = false;
updateModel();
}
});
input.on('grid:check', function () {
updateModel();
});
input.on('keydown', function(e){
if (e.isDefaultPrevented()) {
return;
}
if (e.keyCode === $.ui.keyCode.DOWN) {
input.datetimepicker('show');
e.stopPropagation();
e.preventDefault();
return false;
}
if (e.keyCode === $.ui.keyCode.ENTER && $(this).datepicker("widget").is(':visible')) {
e.stopPropagation();
e.preventDefault();
return false;
}
if (e.keyCode === $.ui.keyCode.ENTER || e.keyCode === $.ui.keyCode.TAB) {
if (changed) updateModel();
}
});
button.click(function(e, ui){
if (scope.isReadonly()) {
return;
}
var mobile = axelor.device.mobile;
if (mobile) {
input.attr('disabled', 'disabled')
.addClass('mobile-disabled-input');
}
input.datetimepicker('show');
if (mobile) {
input.removeAttr('disabled')
.removeClass('mobile-disabled-input');
}
});
scope.$onAdjust('size scroll', function () {
if (isShowing) {
input.datepicker('widget').hide();
input.datetimepicker('hide');
}
});
function updateModel() {
var masked = input.mask("value") || '',
value = input.datetimepicker('getDate') || null,
oldValue = scope.getValue() || null;
if (_.isEmpty(masked)) {
value = null;
}
if (!input.mask("valid")) {
model.$setViewValue(value); // force validation
model.$render();
scope.$applyAsync();
return;
}
value = scope.parse(value);
if (angular.equals(value, oldValue)) {
return;
}
scope.setValue(value, true);
scope.$applyAsync();
}
scope.validate = function(value) {
var minSize = props.minSize === 'now' ? moment() : props.minSize,
maxSize = props.maxSize,
val = moment(value),
valid = true;
var masked = input.mask('value');
if (masked && !input.mask("valid")) {
return false;
}
if(isDate) {
if(minSize) minSize = moment(minSize).startOf('day');
if(maxSize) maxSize = moment(maxSize).startOf('day');
}
else {
if(minSize) minSize = moment(minSize);
if(maxSize) maxSize = moment(maxSize);
}
if(minSize) {
if(!val) return false;
valid = !val.isBefore(minSize) ;
}
if(valid && maxSize) {
if(!val) return true;
valid = !val.isAfter(maxSize) ;
}
return valid;
};
scope.$render_editable = function() {
rendering = true;
try {
var value = scope.getText();
if (value) {
input.mask('value', value);
try {
$.datepicker._noUpdate = true;
$.datepicker._setDateDatepicker(input[0], moment(scope.getValue()).toDate());
input.mask('refresh');
} finally {
$.datepicker._noUpdate = false;
}
} else {
input.mask('value', '');
}
} finally {
rendering = false;
}
};
},
template_editable:
'<span class="picker-input">'+
'<input type="text" autocomplete="off">'+
'<span class="picker-icons">'+
'<i class="fa fa-calendar"></i>'+
'</span>'+
'</span>'
});
ui.formInput('Date', 'DateTime', {
format: 'DD/MM/YYYY',
mask: 'DD/MM/YYYY',
isDate: true
});
ui.formInput('Time', 'DateTime', {
css: 'time-item',
mask: 'HH:mm',
init: function(scope) {
this._super(scope);
scope.parse = function(value) {
return value;
};
scope.format = function(value) {
return value;
};
},
link_editable: function(scope, element, attrs, model) {
element.mask({
mask: this.mask
});
element.change(function(e, ui) {
updateModel();
});
element.on('keydown', function(e){
if (e.isDefaultPrevented()) {
return;
}
if (e.keyCode === $.ui.keyCode.ENTER || e.keyCode === $.ui.keyCode.TAB) {
updateModel();
}
});
element.on('grid:check', function () {
updateModel();
});
scope.validate = function(value) {
return !value || /^(\d+:\d+)$/.test(value);
};
function updateModel() {
var masked = element.mask("value") || '',
value = element.val() || '',
oldValue = scope.getValue() || '';
if (value && !element.mask("valid")) {
return model.$setViewValue(value); // force validation
}
if (_.isEmpty(masked)) {
value = null;
}
value = scope.parse(value);
if (angular.equals(value, oldValue)) {
return;
}
scope.setValue(value, true);
scope.$applyAsync();
}
scope.$render_editable = function() {
var value = scope.getText();
if (value) {
element.mask('value', value);
} else {
element.mask('value', '');
}
};
},
template_editable: '<input type="text">'
});
ui.formInput('RelativeTime', 'DateTime', {
metaWidget: true,
init: function(scope) {
this._super(scope);
scope.isReadonly = function() {
return true;
};
scope.format = function(value) {
if (value) {
return moment(value).fromNow();
}
return "";
};
}
});
function formatDuration(field, value) {
if (!value || !_.isNumber(value)) {
return value;
}
var h = Math.floor(value / 3600);
var m = Math.floor((value % 3600) / 60);
var s = Math.floor((value % 3600) % 60);
h = _.str.pad(h, field.big ? 3 : 2, '0');
m = _.str.pad(m, 2, '0');
s = _.str.pad(s, 2, '0');
var text = h + ':' + m;
if (field.seconds) {
text = text + ':' + s;
}
return text;
}
ui.formatDuration = formatDuration;
ui.formInput('Duration', 'Time', {
metaWidget: true,
mask: '99:mm',
init: function(scope) {
this._super(scope);
var field = scope.field;
var pattern = /^\d+:\d+(:\d+)?$/;
scope.format = function(value) {
return formatDuration(field, value);
};
scope.parse = function(value) {
if (!value || !_.isString(value)) {
return value;
}
if (!pattern.test(value)) {
return null;
}
var parts = value.split(':'),
first = +(parts[0]),
next = +(parts[1]),
last = +(parts[2] || 0);
return (first * 60 * 60) + (next * 60) + last;
};
},
link_editable: function(scope, element, attrs, model) {
var field = scope.field || {},
mask = this.mask;
if (field.big) {
mask = "999:mm";
}
if (field.seconds) {
mask = mask + ":mm";
}
this.mask = mask;
this._super.apply(this, arguments);
scope.validate = function(value) {
return !value || _.isNumber(value);
};
}
});
})();

View File

@ -0,0 +1,499 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
function getStylePopup(element, styles) {
return function($popup, $button) {
var $list = $('<div/>').addClass('wysiwyg-plugin-list').attr('unselectable', 'on');
$.each(styles, function(format, name) {
var $link = $('<a/>').attr('href', '#').html(name).click(
function(event) {
$(element).wysiwyg('shell').format(format).closePopup();
event.stopPropagation();
event.preventDefault();
return false;
});
$list.append($link);
});
$popup.append($list);
};
}
function getFontNamePopup(element, fonts) {
return function($popup, $button) {
var $list = $('<div/>').addClass('wysiwyg-plugin-list').attr('unselectable', 'on');
$.each(fonts, function(font, name) {
var $link = $('<a/>').attr('href', '#').html(name).click(
function(event) {
try {
document.execCommand('styleWithCSS', false, false);
$(element).wysiwyg('shell').fontName(font).closePopup();
} finally {
document.execCommand('styleWithCSS', false, true);
}
event.stopPropagation();
event.preventDefault();
return false;
});
$list.append($link);
});
$popup.append($list);
};
}
function getFontSizePopup(element, sizes) {
return function($popup, $button) {
var $list = $('<div/>').addClass('wysiwyg-plugin-list').attr('unselectable', 'on');
$.each(sizes, function(size, name) {
var $link = $('<a/>').attr('href', '#').html(name).click(
function(event) {
$(element).focus();
try {
document.execCommand('styleWithCSS', false, false);
$(element).wysiwyg('shell').fontSize(size).closePopup();
} finally {
document.execCommand('styleWithCSS', false, true);
}
event.stopPropagation();
event.preventDefault();
return false;
});
$list.append($link);
});
$popup.append($list);
};
}
function getButtons(scope, element) {
var lite = scope.field.lite;
return {
style: lite ? false : {
title: _t('Style'),
image: '\uf1dd',
popup: getStylePopup(element, {
'<p>' : _t('Normal'),
'<pre>' : '<pre>' + _t('Formatted') + '</pre>',
'<blockquote>' : '<blockquote>' + _t('Blockquote') + '</blockquote>',
'<h1>': '<h1>' + _t('Header 1') + '</h1>',
'<h2>': '<h2>' + _t('Header 2') + '</h2>',
'<h3>': '<h3>' + _t('Header 3') + '</h3>',
'<h4>': '<h4>' + _t('Header 4') + '</h4>',
'<h5>': '<h5>' + _t('Header 5') + '</h5>',
'<h6>': '<h6>' + _t('Header 6') + '</h6>'
})
},
fontName: lite ? false : {
title: _t('Font'),
image: '\uf031',
popup: getFontNamePopup(element, {
'"Times New Roman", Times, serif': '<span style="font-family: \"Times New Roman\", Times, serif">Times New Roman</span>',
'Arial, Helvetica, sans-serif': '<span style="font-family: Arial, Helvetica, sans-serif">Arial</span>',
'"Courier New", Courier, monospace': '<span style="font-family: \"Courier New\", Courier, monospace">Courier New</span>',
'Comic Sans, Comic Sans MS, cursive': '<span style="font-family: Comic Sans, Comic Sans MS, cursive">Comic Sans</span>',
'Impact, fantasy': '<span style="font-family: Impact, fantasy">Impact</span>'
})
},
fontSize: lite ? false : {
title: _t('Font size'),
image: '\uf035',
popup: getFontSizePopup(element, {
'1': '<span style="font-size: x-small">' + _t('Smaller') + '</span>',
'2': '<span style="font-size: small">' + _t('Small') + '</span>',
'3': '<span style="font-size: medium">' + _t('Medium') + '</span>',
'4': '<span style="font-size: large">' + _t('Large') + '</span>',
'5': '<span style="font-size: x-large">' + _t('Larger') + '</span>'
})
},
d1: lite ? false : {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
bold: {
title: _t('Bold (Ctrl+B)'),
image: '\uf032'
},
italic: {
title: _t('Italic (Ctrl+I)'),
image: '\uf033'
},
underline: {
title: _t('Underline (Ctrl+U)'),
image: '\uf0cd'
},
strikethrough: {
title: _t('Strikethrough (Ctrl+S)'),
image: '\uf0cc'
},
removeformat: {
title: _t('Remove format'),
image: '\uf12d'
},
d2: {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
forecolor: lite ? false : {
title: _t('Text color'),
image: '\uf1fc'
},
highlight: lite ? false : {
title: _t('Background color'),
image: '\uf043'
},
d3: lite ? false : {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
alignleft: {
title: _t('Left'),
image: '\uf036'
},
aligncenter: {
title: _t('Center'),
image: '\uf037'
},
alignright: {
title: _t('Right'),
image: '\uf038'
},
alignjustify: {
title: _t('Justify'),
image: '\uf039'
},
d4: {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
orderedList: {
title: _t('Ordered list'),
image: '\uf0cb'
},
unorderedList: {
title: _t('Unordered list'),
image: '\uf0ca'
},
d6: lite ? false : {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
indent: lite ? false : {
title: _t('Indent'),
image: '\uf03c'
},
outdent: lite ? false : {
title: _t('Outdent'),
image: '\uf03b'
},
d7: lite ? false : {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
insertimage: lite ? false : {
title: _t('Insert image'),
image: '\uf030'
},
insertlink: lite ? false : {
title: _t('Insert link'),
image: '\uf08e'
},
d8: lite ? false : {
html: $('<span class="wysiwyg-toolbar-divider"></span>')
},
normalize: lite ? false : {
title: _t('Normalize'),
image: '\uf0d0',
click: function () {
scope.normalize();
}
},
showCode: lite ? false : {
title: _t('Code'),
image: '\uf121',
click: function () {
scope.toggleCode();
}
}
};
}
ui.formInput('Html', {
css: "html-item",
metaWidget: true,
init: function(scope) {
scope.parse = function(value) {
return value ? value : null;
};
},
link_editable: function(scope, element, attrs, model) {
this._super(scope, element, attrs, model);
var textElement = element.find('textarea');
var buttons = getButtons(scope, textElement);
var props = scope.field,
minSize = +props.minSize,
maxSize = +props.maxSize,
height = +(scope.field.height) || null;
if (height) {
height = Math.max(100, height);
}
function isParagraph(node) {
return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
}
function findParagraph(node) {
if (!node) return null;
if (node.classList && node.classList.contains('wysiwyg-editor')) return null;
if (isParagraph(node)) return node;
return findParagraph(node.parentNode);
}
textElement.wysiwyg({
toolbar: 'top',
buttons: buttons,
submit: {
title: _t('Submit'),
image: '\uf00c'
},
selectImage: _t('Click or drop image'),
placeholderUrl: 'www.example.com',
maxImageSize: [600, 200],
hijackContextmenu: false,
onKeyPress: function(key, character, shiftKey, altKey, ctrlKey, metaKey) {
if (key !== 13 || shiftKey) {
return;
}
var parent = findParagraph(document.getSelection().anchorNode);
if (!parent) {
document.execCommand('formatBlock', false, '<p>');
}
}
});
var shell = textElement.wysiwyg('shell');
var shellElement = $(shell.getElement());
var shellActive = true;
shellElement.addClass('html-content');
if (scope.field.height) {
shellElement.parent().height(height).resizable({
handles: 's',
resize: function () {
shellElement.parent().width('');
}
});
}
function onChange(e) {
var value = shellActive ? shell.getHTML() : textElement.val();
var text = (value || '').trim()
if (text === '<p><br></p>' || text === '<br>') {
value = null;
}
var old = scope.getValue() || null;
var txt = scope.parse(value) || null;
if (old === txt) {
return;
}
scope.setValue(value, true);
scope.$applyAsync();
}
scope.$render_editable = function () {
var value = scope.getValue() || "";
scope.text = scope.format(value);
var current = shellActive ? shell : textElement;
var getter = shellActive ? 'getHTML' : 'val';
var setter = shellActive ? 'setHTML' : 'val';
var html = current[getter]();
if (value !== html) {
current[setter](value);
}
};
textElement.on('input paste change blur', _.debounce(onChange, 100));
textElement.on("focus", _.once(function (e) {
// Chrome and Edge supports this
document.execCommand('defaultParagraphSeparator', false, 'p');
// firefox uses attributes for some commands
document.execCommand('styleWithCSS', false, true);
document.execCommand('insertBrOnReturn', false, false);
}));
scope.toggleCode = function () {
shellActive = !shellActive;
element.parent().find('.wysiwyg-toolbar-icon')
.toggleClass('disabled', !shellActive)
.last().removeClass('disabled');
if (shellActive) {
textElement.hide();
shellElement.show();
shell.setHTML(textElement.val());
} else {
var height = Math.max(100, shellElement.outerHeight());
shellElement.hide();
textElement.show();
if (!scope.field.height) {
textElement.height(height);
}
}
};
scope.normalize = function () {
var html = shell.getHTML();
var div = $('<div>').html(html);
div.find('p').css({
'margin-top': 0,
'margin-bottom': '1em'});
div.find('ol,ul').each(function() {
var el = $(this);
if (el.parents('ol,ul').length) return;
el.css({
'margin-top': 0,
'margin-bottom': '1em'});
});
div.find('blockquote').each(function() {
var el = $(this);
el.css({
'margin': el.parents('blockquote').length ? '0 0 0 2em' : '0 0 1em 2em',
'border': 'none',
'padding': 0
});
});
shellElement.focus();
shell.setHTML(div[0].innerHTML);
div.remove();
};
scope.validate = function(value) {
if (_.isEmpty(value)) {
return true;
}
var length = value.length,
valid = true;
if (minSize) {
valid = length >= minSize;
}
if(valid && maxSize) {
valid = length <= maxSize;
}
return valid;
};
},
template_readonly:
'<div class="form-item-container">'+
'<div class="html-viewer html-content" ui-bind-template x-text="text" x-locals="record" x-live="field.live"></div>'+
'</div>',
template_editable:
'<div class="form-item-container">'+
'<textarea class="html-editor html-content"></textarea>'+
'</div>'
});
ui.directive('uiBindTemplate', ['$interpolate', function($interpolate){
function expand(scope, template) {
if (!template || !template.match(/{{.*?}}/)) {
return template;
}
return $interpolate(template)(scope.locals());
}
return {
terminal: true,
scope: {
locals: "&",
text: "=text",
live: "&"
},
link: function(scope, element, attrs) {
var template = null;
function update() {
var output = expand(scope, template) || "";
element.html(output);
}
scope.$watch("text", function(text, old) {
if (text === template) {
return;
}
template = text;
update();
});
var live = false;
scope.$watch("live()", function(value) {
if (live || !value) {
return;
}
live = true;
scope.$watch("locals()", function(value) {
update();
}, true);
});
}
};
}]);
})();

View File

@ -0,0 +1,431 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
ui.formWidget('PanelJson', {
showTitle: false,
transclude: true,
template: "<div class='panel-json' ui-transclude></div>"
});
ui.formInput('JsonField', 'String', {
showTitle: false,
link: function (scope, element, attrs, model) {
var field = scope.field;
var jsonFields = field.jsonFields || [];
var jsonNames = _.pluck(jsonFields, 'name');
var jsonFix = {};
jsonFields.forEach(function (item) {
if (item.widget && item.showTitle === undefined) {
var widget = ui.getWidgetDef(item.widget);
if (widget) {
item.showTitle = widget.showTitle;
}
}
});
var defaultValues = {};
var parentUnwatch = null;
var selfUnwatch = null;
scope.formPath = scope.formPath ? scope.formPath + "." + field.name : field.name;
scope.record = {};
jsonFields.forEach(function (item) {
if (item.target === 'com.axelor.meta.db.MetaJsonRecord' &&
item.targetName && item.targetName.indexOf('attrs.') === 0) {
jsonFix[item.name] = function (v) {
if (v) {
v[item.targetName.substring(6)] = v[item.targetName];
}
return v;
};
}
if (item.contextField && item.contextFieldValue) {
if (item.showIf === undefined && item.hideIf === undefined && item.hidden) {
return;
}
var condition = "($record." + item.contextField + ".id === " + item.contextFieldValue + ")";
if (item.showIf) condition += " && (" + item.showIf + ")";
if (item.hideIf) condition += " && !(" + item.hideIf + ")";
item.showIf = condition;
item.hideIf = null;
}
});
function getDefaultValues() {
jsonFields.forEach(function (item) {
if (item.defaultValue === undefined) return;
var value = item.defaultValue;
switch(item.type) {
case 'integer':
value = +(value);
break;
case 'date':
case 'datetime':
value = value === 'now' ? new Date() : moment(value).toDate();
break;
}
defaultValues[item.name] = value;
});
return angular.copy(defaultValues);
}
function unwatchParent() {
if (parentUnwatch) {
parentUnwatch();
parentUnwatch = null;
}
}
function unwatchSelf() {
if (selfUnwatch) {
selfUnwatch();
selfUnwatch = null;
}
}
function watchParent() {
unwatchParent();
parentUnwatch = scope.$watch('$parent.record.' + field.name, function jsonParentWatch(value, old) {
if (value === old) return;
onRender();
});
}
function watchSelf() {
unwatchSelf();
selfUnwatch = scope.$watch('record', function jsonRecordWatch(record, old) {
if (record !== old) {
onUpdate();
}
}, true);
}
function format(name, value) {
var func = jsonFix[name];
return func ? func(value) : value;
}
function onUpdate() {
var rec = null;
_.each(scope.record, function (v, k) {
if (k.indexOf('$') === 0 || v === null || v === undefined) return;
if (_.isArray(v)) {
if (v.length == 0) return;
v = v.map(function (x) {
return x.id ? { id: x.id } : x;
});
}
if (rec === null) {
rec = {};
}
rec[k] = format(k, v);
});
unwatchParent();
if (scope.$parent.record[field.name] || rec) {
scope.$parent.record[field.name] = rec ? angular.toJson(rec) : rec;
}
watchParent();
}
function onRender() {
var record = scope.$parent.record || {};
var value = record[field.name];
unwatchSelf();
if (value) {
scope.record = angular.fromJson(value);
} else {
scope.record = getDefaultValues();
if (!_.isEmpty(scope.record)) {
record[field.name] = angular.toJson(scope.record);
}
onUpdate();
}
scope._jsonContext = { '$record': record };
record['$' + field.name] = scope.record;
watchSelf();
}
scope.$on('on:new', onRender);
scope.$on('on:edit', function () {
if (scope.viewType === 'form' || (!scope.viewType && scope._isPopup)) onRender();
});
scope.updateJsonValues = function (values) {
var rec = null;
_.each(values, function (v, k) {
if (jsonNames.indexOf(k) === -1 && scope.fields[k]) {
scope.$parent.record[k] = v;
} else {
if (rec === null) {
rec = {};
}
rec[k] = v;
}
});
if (rec) {
scope.record = _.extend({}, scope.record, rec);
}
};
watchParent();
// hide parent panel if no jsonFields defined
scope.$evalAsync(function () {
var parent = scope.$parent.field || {};
if (parent.type === 'panel' && _.size(parent.items) === 1 && _.isEmpty(field.jsonFields)) {
element.parents('.panel:first').addClass('hide').hide();
}
});
scope.$on('on:update-context', function (e, context) {
if (context && !context[field.name]) {
context[field.name] = angular.toJson(scope.record || {});
}
});
}
});
ui.formInput('JsonRaw', 'String', {
showTitle: false,
link: function (scope, element, attrs, model) {
scope.placeHolderKey = _t('name');
scope.placeHolderVal = _t('value');
scope.items = [];
scope.onAdd = function () {
var last = _.last(scope.items);
if (last && !(_.trim(last.name) && _.trim(last.value))) return;
scope.items.push({});
};
scope.onRemove = function (index) {
if (scope.items.length > 0) {
scope.items.splice(index, 1);
}
};
var unwatch = null;
function doWatch () {
if (unwatch) {
unwatch();
}
unwatch = scope.$watch('items', function jsonItemsWatch(items, old) {
if (items === old) return;
var record = null;
_.each(items, function (item) {
if (!_.trim(item.name) || !_.trim(item.value)) return;
if (record === null) {
record = {};
}
record[item.name] = item.value;
});
model.$setViewValue(record ? angular.toJson(record) : null);
}, true);
}
model.$render = function () {
var value = model.$viewValue;
if (value) {
value = angular.fromJson(value);
} else {
value = {};
}
scope.items = _.map(_.keys(value), function (name) {
return { name: name, value: value[name] || '' };
});
doWatch();
};
},
template_readonly:
"<div class='json-editor'>" +
"<table class='form-layout'>" +
"<tr ng-repeat='(i, item) in items'>" +
"<td class='form-label'>" +
"<strong class='display-text'>{{item.name}}</strong>:" +
"</td>" +
"<td class='form-item'>" +
"<span class='display-text'>{{item.value}}</span>" +
"</td>" +
"</tr>" +
"</table>" +
"</div>",
template_editable:
"<div class='json-editor'>" +
"<table class='form-layout'>" +
"<tr ng-repeat='(i, item) in items'>" +
"<td class='form-item'><span class='form-item-container'>" +
"<input type='text' placeholder='{{placeHolderKey}}' ng-model='item.name'></span>" +
"</td>" +
"<td class='form-item'><span class='form-item-container'>" +
"<input type='text' placeholder='{{placeHolderVal}}' ng-model='item.value'></span>" +
"</td>" +
"<td><a href='' ng-click='onRemove(i)'><i class='fa fa-minus'></i></a></td>" +
"</tr>" +
"</table>" +
"<a href='' ng-click='onAdd()'><i class='fa fa-plus'></i></a>" +
"</div>"
});
ui.formInput('JsonRefSelect', {
css: 'multi-object-select',
controller: ['$scope', 'ViewService', function($scope, ViewService) {
$scope.createElement = function(id, name, selectionList) {
var elemGroup = $('<div ui-group ui-table-layout cols="2" x-widths="150,*"></div>');
var elemSelect = $('<input ui-select showTitle="false">')
.attr("name", name + "$model")
.attr("x-for-widget", id)
.attr("ng-model", "record." + name + ".model");
var elemSelects = $('<div></div>').attr('ng-switch', "record." + name + ".model");
var elemItems = _.map(selectionList, function(s) {
return $('<input ui-json-ref-item ng-switch-when="' + s.value +'">')
.attr('ng-model', 'record.' + name)
.attr('name', name)
.attr('x-target', s.value);
});
elemGroup
.append($('<div></div>').append(elemSelect))
.append(elemSelects.append(elemItems));
return ViewService.compile(elemGroup)($scope);
};
}],
link: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
var name = scope.field.name;
var selectionList = scope.field.selectionList;
scope.fieldsCache = {};
scope.refFireEvent = function (name) {
var handler = scope.$events[name];
if (handler) {
return handler();
}
};
var elem = scope.createElement(element.attr('id'), name, selectionList);
setTimeout(function() {
element.append(elem);
});
scope.$watch("record." + name + ".model", function jsonModelWatch(value, old) {
if (value === old || old === undefined) return;
if (scope.record && scope.record[name]) {
scope.record[name] = _.pick(scope.record[name], 'model');
if (!scope.record[name].model) {
delete scope.record[name];
}
}
});
},
template_editable: null,
template_readonly: null
});
ui.formInput('JsonRefItem', 'ManyToOne', {
showTitle: false,
link: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
if (scope.field.targetName) {
return this._link.apply(this, arguments);
}
var self = this;
var target = element.attr('x-target');
var data = (_.findWhere(scope.$parent.field.selectionList, {value: target})||{}).data || {};
function doLink(fields) {
var name = false,
search = [];
_.each(fields, function(f) {
if (f.nameColumn) name = f.name;
if (f.name === "name") search.push("name");
if (f.name === "code") search.push("code");
});
if (!name && _.contains(search, "name")) {
name = "name";
}
_.extend(scope.field, {
target: scope._model,
targetName: name,
targetSearch: search,
domain: data.domain
});
self._link(scope, element, attrs, model);
}
if (scope.fieldsCache[scope._model]) {
doLink(scope.fieldsCache[scope._model]);
} else {
scope.loadFields().success(function (fields) {
scope.fieldsCache[scope._model] = fields;
doLink(fields);
});
}
},
_link: function(scope, element, attrs, model) {
var name = element.attr('name');
scope.getValue = function () {
return scope.record[name];
};
var __setValue = scope.setValue;
scope.setValue = function (value) {
var val = _.pick(scope.record[name], 'model');
val = _.extend(val, value);
__setValue.call(scope, val);
};
function doSelect() {
var value = (scope.record || {})[name];
scope.select(value);
}
scope.$watch("record", doSelect);
}
});
})();

View File

@ -0,0 +1,252 @@
/*
* 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');
/**
* The Numeric input widget.
*/
ui.formInput('Number', {
css: 'integer-item',
widgets: ['Integer', 'Long', 'Decimal'],
template_readonly: '<span class="display-text">{{localeValue()}}</span>',
link: function(scope, element, attrs, model) {
var props = scope.field,
minSize = +props.minSize,
maxSize = +props.maxSize;
var isDecimal = props.serverType === "decimal" || props.widget === "decimal",
pattern = isDecimal ? /^(-)?\d+(\.\d+)?$/ : /^\s*-?[0-9]*\s*$/;
function scale() {
var value = scope.attr('scale');
if (value) {
return value;
}
if ((props.widgetAttrs||{}).scale) {
return props.widgetAttrs.scale;
}
return props.scale || 2;
}
function precision() {
var value = scope.attr('precision');
if (value) {
return value;
}
if ((props.widgetAttrs||{}).precision) {
return props.widgetAttrs.precision;
}
return props.precision || 18;
}
scope.isNumber = function(value) {
return _.isEmpty(value) || _.isNumber(value) || pattern.test(value);
};
scope.validate = scope.isValid = function(value) {
var valid = scope.isNumber(value);
if (valid && isDecimal && _.isString(value)) {
value = scope.format(value);
valid = _.string.trim(value, '-').length - 1 <= precision();
value = +value;
}
if (valid && (minSize || minSize === 0)) {
valid = value >= minSize;
}
if (valid && (maxSize || maxSize === 0)) {
valid = value <= maxSize;
}
return valid;
};
scope.localeValue = function localeValue() {
var value = scope.getValue();
var field = isDecimal ? _.extend({}, scope.field, {
scale: scale(),
precision: precision()
}) : scope.field;
return isDecimal
? ui.formatters.decimal(field, value)
: ui.formatters.integer(field, value);
};
scope.format = function format(value) {
if (isDecimal && _.isString(value) && value.trim().length > 0) {
return parseFloat(value).toFixed(scale());
}
return value;
};
scope.parse = function(value) {
if (isDecimal) return value;
if (value && _.isString(value)) return +value;
return value;
};
scope.$on("on:attrs-changed", function (e, attr) {
if (attr.name === 'scale' || attr.name === 'precision') {
model.$render();
}
});
},
link_editable: function(scope, element, attrs, model) {
var props = scope.field;
var options = {
step: 1
};
element.on("spin", onSpin);
element.on("spinchange", function(e, row) {
updateModel(element.val());
});
element.on("grid:check", function(e, row) {
updateModel(element.val());
});
var pendingChange = false;
function handleChange(changed) {
var onChange = scope.$events.onChange;
if (onChange && (changed || pendingChange)) {
pendingChange = false;
setTimeout(onChange);
}
}
function equals(a, b) {
if (a === b) return true;
if (angular.equals(a, b)) return true;
if (a === "" && b === undefined) return true;
if (b === "" && a === undefined) return true;
if (a === undefined || b === undefined) return false;
if (a === null || b === null) return false;
if (!scope.isNumber(a) || !scope.isNumber(b)) return false;
a = a === "" ? a : ((+a) || 0);
b = b === "" ? b : ((+b) || 0);
return a === b;
}
function updateModel(value, handle) {
if (!scope.isNumber(value)) {
return model.$setViewValue(value); // force validation
}
var val = scope.parse(value);
var old = scope.getValue();
var text = scope.format(value);
element.val(text);
if (equals(val, old)) {
return handleChange();
}
scope.setValue(val);
scope.$applyAsync();
pendingChange = true;
if (handle !== false) {
handleChange();
}
}
function onSpin(event, ui) {
var text = this.value,
value = ui.value,
orig = element.spinner('value'),
parts, integer, decimal, min, max, dir = 0;
event.preventDefault();
if (!scope.isNumber(text)) {
return false;
}
if (value < orig)
dir = -1;
if (value > orig)
dir = 1;
parts = text.split(/\./);
integer = +parts[0];
decimal = parts[1];
integer += dir;
if (parts.length > 1) {
value = integer + '.' + decimal;
}
min = options.min;
max = options.max;
if (_.isNumber(min) && value < min)
value = min;
if (_.isNumber(max) && value > max)
value = max;
updateModel(value, false);
}
if (props.minSize !== undefined)
options.min = +props.minSize;
if (props.maxSize !== undefined)
options.max = +props.maxSize;
setTimeout(function(){
element.spinner(options);
scope.$elem_editable = element.parent();
model.$render = function() {
var value = model.$viewValue;
if (value) {
value = scope.format(value);
}
element.val(value);
scope.initValue(value);
};
model.$render();
});
}
});
// fix spinner repeat issue
var oldRepeat = $.ui.spinner.prototype._repeat;
$.ui.spinner.prototype._repeat = function () {
if (this.element.scope().isReadonly()) {
return this._stop();
}
return oldRepeat.apply(this, arguments);
};
})();

View File

@ -0,0 +1,115 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
ui.ProgressMixin = {
css: 'progress-item',
cellCss: 'form-item progress-item',
metaWidget: true,
link_readonly: function(scope, element, attrs, model) {
var field = scope.field || {},
that = this;
scope.$watch("getValue()", function progressValueWatch(value, old) {
var props = that.compute(field, value);
scope.cssClasses = 'progress ' + props.css;
scope.styles = {
width: props.width + '%'
};
scope.css = props.css;
scope.width = props.width;
});
},
compute: function(field, value) {
var max = +(field.max) || 100,
min = +(field.min) || 0;
var colors = [
["r", 24], // 00 - 24 (red)
["y", 49], // 25 - 49 (yellow)
["b", 74], // 50 - 74 (blue)
["g", 100] // 75 - 100 (green)
];
if (field.colors) {
colors = _.chain(field.colors.split(/,/)).invoke('split', /:/).value() || [];
}
colors.reverse();
var styles = {
"r": "progress-danger",
"y": "progress-warning",
"b": "progress-primary",
"g": "progress-success"
};
var width = +(value) || 0;
var css = "progress-striped";
width = (width * 100) / (max - min);
width = Math.min(Math.round(width), 100);
var color = "";
for(var i = 0 ; i < colors.length; i++) {
var c = colors[i][0];
var v = +colors[i][1];
if (width <= v) {
color = styles[c] || "";
}
}
css += " " + color;
if (width < 100) {
css += " " + "active";
}
return {
css: css,
width: width
};
},
template_readonly:
'<div ng-class="cssClasses">'+
'<div class="bar" ng-style="styles"></div>'+
'</div>'
};
/**
* The Progress widget with integer/decimal input.
*
*/
ui.formInput('Progress', 'Integer', _.extend({}, ui.ProgressMixin));
/**
* The Progress widget with selection input.
*
*/
ui.formInput('SelectProgress', 'Select', _.extend({}, ui.ProgressMixin));
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,165 @@
/*
* 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() {
/* global Handsontable: true */
"use strict";
var ui = angular.module('axelor.ui');
ui.formInput('Spreadsheet', {
css: "spreadsheet-item",
link: function (scope, element, attrs, model) {
var field = scope.field;
var height = field.height || 580;
var inst;
scope.$timeout(function () {
element.height(height).css({
"position": "relative",
"overflow": "hidden"
});
inst = new Handsontable(element[0], {
colWidths: 60,
rowHeaders: true,
colHeaders: true,
contextMenu: true,
manualColumnResize: true,
manualRowResize: true,
afterChange: function (change, source) {
if (source !== 'loadData') {
update();
}
},
afterCreateCol: update,
afterCreateRow: update,
afterRemoveCol: update,
afterRemoveRow: update
});
model.$render();
});
element.resizable({
handles: 's',
resize: function () {
if (inst) {
inst.render();
}
}
});
function update() {
if (!inst) { return; }
var current = model.$viewValue;
var value = compact(inst.getData());
value = value ? JSON.stringify(value) : value;
if (value === current) {
return;
}
scope.setValue(value, true);
scope.$applyAsync();
}
function compact(items) {
var res = [];
var i;
for (i = 0; i < items.length; i++) {
var item = items[i];
if (Array.isArray(item)) {
item = compact(item);
}
if (item === "" || item === null || item === undefined || item.length === 0) {
continue;
}
res[i] = item;
}
var n = res.length;
for (i = n - 1; i >= 0; i--) {
if (res[i] !== null) {
n = i+1;
break;
}
}
res = res.slice(0, n);
return res.length ? res : null;
}
function fill(data) {
var cols = 0;
var rows = data.length;
var i, row;
for(i = 0; i < data.length; i++) {
row = data[i] || (data[i] = []);
cols = Math.max(row.length, cols);
}
cols = Math.max(50, cols);
rows = Math.max(100, rows);
for(i = 0; i < rows; i++) {
row = data[i] || (data[i] = []);
for (var j = 0; j < cols + 1; j++) {
if (row[j] === undefined) {
row[j] = null;
}
}
}
return data;
}
model.$render = function () {
var value = null;
try {
value = JSON.parse(model.$viewValue) || null;
} catch (e) {
}
if (inst) {
value = fill(value || []);
inst.loadData(value || null);
setTimeout(function () {
inst.render();
}, 300);
}
};
scope.$on("$destroy", function () {
if (inst) {
inst.destroy();
inst = null;
}
});
},
template_editable: null,
template_readonly: null,
template:
"<div></div>"
});
})();

View File

@ -0,0 +1,849 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
var popoverElem = null;
var popoverTimer = null;
function canDisplayPopover(scope, details) {
if (axelor.device.mobile) {
return false;
}
if(!axelor.config['user.technical']) {
return details ? false : scope.field && scope.field.help;
}
return true;
}
function makePopover(scope, element, callback, placement) {
var mode = axelor.config['application.mode'];
var tech = axelor.config['user.technical'];
var doc = $(document);
var table = null;
function addRow(label, text, klass) {
if (table === null) {
table = $('<table class="field-details"></table>');
}
var tr = $('<tr></tr>').appendTo(table);
if (label) {
$('<th></th>').text(label).appendTo(tr);
}
if (klass == null) {
text = '<code>' + text + '</code>';
}
var td = $('<td></td>').html(text).addClass(klass).appendTo(tr);
if (!label) {
td.attr('colspan', 2);
}
return table;
}
element.popover({
html: true,
delay: { show: 1000, hide: 100 },
animate: true,
placement: function() {
if (placement) return placement;
var coord = $(element.get(0)).offset(),
viewport = { height: window.innerHeight, width: window.innerWidth };
if(viewport.height < (coord.top + 100))
return 'top';
if(coord.left > (viewport.width / 2))
return 'left';
return 'right';
},
trigger: 'manual',
container: 'body',
title: function() {
return element.text();
},
content: function() {
if (table) {
table.remove();
table = null;
}
callback(scope, addRow);
if (table) return table;
return "";
}
});
element.on('mouseenter.popover', enter);
element.on('mouseleave.popover', leave);
function selectText(elem) {
var el = $(elem).get(0);
if (document.selection) {
var range = document.body.createTextRange();
range.moveToElementText(el);
range.select();
} else if (window.getSelection) {
var range = document.createRange();
range.selectNodeContents(el);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
}
function enter(e, show) {
if (popoverTimer) {
clearTimeout(popoverTimer);
}
popoverTimer = setTimeout(function () {
if (popoverElem === null) {
popoverElem = element;
popoverElem.popover('show');
if (e.ctrlKey) {
selectText(table.find('.field-name,.model-name').get(0));
}
}
var tip = element.data('popover').$tip;
if (tip) {
tip.attr('tabIndex', 0);
tip.css('outline', 'none');
}
}, (e.ctrlKey || show) ? 0 : 1000);
}
function leave(e) {
if (e.ctrlKey) {
doc.off('mousemove.popover');
doc.on('mousemove.popover', leave);
return;
}
if (popoverTimer) {
clearTimeout(popoverTimer);
popoverTimer = null;
}
if (popoverElem) {
popoverElem.popover('hide');
popoverElem = null;
doc.off('mousemove.popover');
}
}
function destroy() {
if (popoverTimer) {
clearTimeout(popoverTimer);
popoverTimer = null;
}
if (element) {
element.off('mouseenter.popover');
element.off('mouseleave.popover');
element.popover('destroy');
element = null;
}
if (table) {
table.remove();
table = null;
}
doc.off('mousemove.popover');
}
element.on('$destroy', destroy);
scope.$on('$destroy', destroy);
}
function setupPopover(scope, element, getHelp, placement) {
if (!canDisplayPopover(scope, false)) {
return;
}
var timer = null;
element.on('mouseenter.help.setup', function (e) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
element.off('mouseenter.help.setup');
element.off('mouseleave.help.setup');
makePopover(scope, element, getHelp, placement);
element.trigger('mouseenter.popover', true);
}, e.ctrlKey ? 0 : 1000);
});
element.on('mouseleave.help.setup $destroy', function () {
if (timer) {
clearTimeout(timer);
timer = null;
}
});
}
ui.directive('uiTabPopover', function() {
function getHelp(scope, addRow) {
var tab = scope.tab || {};
var type = tab.viewType;
var view = _.findWhere(tab.views, {type: type});
var viewScope = tab.$viewScope;
if (viewScope && viewScope.schema) {
view = viewScope.schema;
}
if (tab.action) {
addRow(_t('Action'), tab.action);
}
if (tab.model) {
addRow(_t('Object'), '<code>' + tab.model + '</code>', 'model-name');
}
if (tab.domain) {
addRow(_t('Domain'), tab.domain);
}
if (view && view.name) {
addRow(_t('View'), view.name);
}
}
return function(scope, element, attrs) {
setupPopover(scope, element, getHelp, 'bottom');
};
});
ui.directive('uiHelpPopover', function() {
function getHelp(scope, addRow) {
var field = scope.field;
var text = field.help;
if (text) {
text = text.replace(/\\n/g, '<br>');
addRow(null, text, 'help-text');
}
if(!canDisplayPopover(scope, true)) {
return;
}
if (text) {
addRow(null, '<hr noshade>', 'help-text');
}
var model = scope._model;
if (model === field.target) {
model = scope._parentModel || scope.$parent._model;
}
addRow(_t('Object'), model);
addRow(_t('Field Name'), '<code>' + field.name + '</code>', 'field-name');
addRow(_t('Field Type'), field.serverType);
if (field.type === 'text') {
return;
}
if (field.domain) {
addRow(_t('Filter'), field.domain);
}
if (field.target) {
addRow(_t('Reference'), field.target);
}
var value = scope.$eval('$$original.' + field.name);
var length;
if (value && /-one$/.test(field.serverType)) {
value = _.compact([value.id, value[field.targetName]]).join(',');
value = '(' + value + ')';
}
if (value && field.type === "password") {
value = _.str.repeat('*', value.length);
}
if (value && /^(string|image|binary)$/.test(field.type)) {
length = value.length;
value = _.first(value, 50);
if (length > 50) {
value.push('...');
}
value = value.join('');
}
if (value && /(panel-related|one-to-many|many-to-many)/.test(field.serverType)) {
length = value.length;
value = _.first(value, 5);
value = _.map(value, function(v){
return v.id;
});
if (length > 5) {
value.push('...');
}
value = value.join(', ');
}
addRow(_t('Orig. Value'), value);
}
function doLink(scope, element, attrs) {
var field = scope.field;
if (field == null) {
return;
}
if (field.help && axelor.config['user.noHelp'] !== true) {
if (element.parent('label').length) {
element.parent('label').addClass('has-help');
} else {
element.addClass('has-help');
}
}
setupPopover(scope, element, getHelp);
}
return function(scope, element, attrs) {
var field = scope.field;
if (!_.isEmpty(field)) {
return doLink(scope, element, attrs);
}
var unwatch = scope.$watch('field', function popoverFieldWatch(field, old) {
if (!field) {
return;
}
unwatch();
doLink(scope, element, attrs);
}, true);
};
});
/**
* The Label widget.
*
*/
ui.formItem('Label', {
css: 'label-item',
cellCss: 'form-label',
transclude: true,
link: function(scope, element, attrs) {
var field = scope.field;
if (field && field.required) {
element.addClass('required');
}
},
template:
"<label><span ui-help-popover ng-transclude></span></label>"
});
ui.directive('uiTranslateIcon', ['$q', function ($q) {
return {
link: function (scope, element) {
var icon = $("<i class='fa fa-flag translate-icon'></i>").attr('title', _t('Show translations.')).appendTo(element);
var toggle = function () {
icon.toggle(!scope.$$readonlyOrig);
};
scope.$watch("$$readonlyOrig", toggle);
scope.$on("on:new", toggle);
scope.$on("on:edit", toggle);
var myDs = scope._dataSource;
var trDs = scope._dataSource._new("com.axelor.meta.db.MetaTranslation");
trDs._sortBy = ["id"];
function saveData(value, data, orig, callback) {
var changed = [];
var removed = [];
data.forEach(function (item) {
var found = _.findWhere(orig, { id: item.id });
if (!angular.equals(found, item)) {
changed.push(item);
}
});
orig.forEach(function (item) {
var found = _.findWhere(data, { id: item.id });
if (!found) {
removed.push(item);
}
});
function saveTranslations() {
var all = [];
if (removed.length) {
all.push(trDs.removeAll(removed));
}
if (changed.length) {
all.push(trDs.saveAll(changed));
}
if (all.length) {
$q.all(all).then(function () {
var lang = axelor.config['user.lang'] || en;
var key = 'value:' + scope.getValue();
var trKey = '$t:' + scope.field.name;
return trDs.search({
domain: "self.key = :key and self.language = :lang",
context: { key: key, lang: lang },
limit: 1
}).success(function (records) {
var record = _.first(records);
if (scope.record) {
scope.record[trKey] = (record||{}).message;
scope.$parent.$parent.text = scope.format(scope.getValue());
var rec = scope._dataSource.get(scope.record.id);
if (rec) {
rec[trKey] = scope.record[trKey];
}
}
});
}).then(callback, callback);
} else {
callback();
}
}
if (value !== scope.getValue()) {
scope.$parent.$parent.setValue(value, true);
scope.waitForActions(function () {
scope.$parent.$parent.onSave().then(saveTranslations, callback);
});
} else {
saveTranslations();
}
}
function showPopup(data) {
if (!data || data.length == 0) {
data = [];
}
var value = scope.getValue();
var orig = angular.copy(data);
var form = $("<form>");
var valueInput = (scope.field.multiline
? $("<textarea class='span12'>")
: $("<input type='text' class='span12'>"))
.prop('name', scope.field.name)
.prop('required', true)
.val(value)
.on('input', function () {
value = this.value;
data.forEach(function (item) {
item.key = 'value:' + value;
});
});
// add value fields
$("<div class='row-fluid'>")
.append($("<label class='span12'>").text(_t("Value")))
.appendTo(form);
$("<div class='row-fluid'>")
.append(valueInput)
.appendTo(form);
form.append('<hr>');
// add translation fields
$("<div class='row-fluid'>")
.append($("<label class='span8'>").text(_t("Translation")))
.append($("<label class='span4'>").text(_t("Language")))
.appendTo(form);
function addRow(item) {
var onchange = function () {
var v = item[this.name];
if (v !== this.value) {
item[this.name] = this.value;
}
};
item.key = item.key || ('value:' + value);
var input1 = (scope.field.multiline
? $("<textarea class='span8'>")
: $("<input type='text' class='span8'>"))
.prop("name", "message")
.prop("required", true)
.val(item.message)
.on("input", onchange);
var input2 = $("<input type='text' class='span4'>")
.prop("name", "language")
.prop("required", true)
.val(item.language)
.on("input", onchange);
var row = $("<div class='row-fluid'>")
.append(input1)
.append(input2)
.appendTo(form);
if (dialog) {
input1.focus();
}
// remove icon
$("<i class='fa fa-times'>")
.add('help', _t('Remove'))
.appendTo(row)
.click(function () {
var i = data.indexOf(item);
data.splice(i, 1);
row.remove();
});
}
function addNew() {
var item = {};
data.push(item);
addRow(item);
}
var dialog;
function validate() {
var empty = html.find('input:text[value=""]');
if (empty.length) {
empty.first().focus();
return false;
}
return true;
}
var html = $("<div>").append(form);
// add icon
$("<i class='fa fa-plus'>")
.attr('help', _t('Add'))
.appendTo(html)
.click(function () {
if (validate()) {
addNew();
}
});
data.forEach(addRow);
if (data.length === 0) {
addNew();
}
function close() {
if (dialog) {
dialog.dialog('close');
}
}
dialog = axelor.dialogs.box(html, {
title: _t('Translations'),
buttons: [{
'text' : _t('Cancel'),
'class' : 'btn',
'click' : close
}, {
'text' : _t('OK'),
'class' : 'btn btn-primary',
'click' : function() {
if (validate()) {
saveData(value, data, orig, close);
}
}
}]
}).addClass('translation-form');
}
icon.click(function (e) {
var value = scope.getValue();
if (value) {
trDs.search({
domain: "self.key = :key",
context: { key: 'value:' + value }
}).success(showPopup);
}
});
}
};
}]);
/**
* The Spacer widget.
*
*/
ui.formItem('Spacer', {
css: 'spacer-item',
template: '<div>&nbsp;</div>'
});
/**
* The Separator widget.
*
*/
ui.formItem('Separator', {
css: 'separator-item',
showTitle: false,
template: '<div>{{field.title}}</div>'
});
/**
* The Static Text widget.
*
*/
ui.formItem('Static', {
css: 'static-item',
link: function (scope, element, attrs, ctrl) {
var field = scope.field;
element.html(field.text);
},
template: '<div></div>'
});
/**
* The Static Label widget.
*
*/
ui.formItem('StaticLabel', {
css: 'static-item',
transclude: true,
template: '<label ng-transclude></label>'
});
/**
* The Help Text widget.
*
*/
ui.formItem('Help', {
css: 'help-item',
link: function (scope, element, attrs, ctrl) {
var field = scope.field;
var css = "alert alert-info";
if (field.css && field.css.indexOf('alert-') > -1) {
css = "alert";
}
element.addClass(css).html(field.text);
},
template: '<div></div>'
});
/**
* The button widget.
*/
ui.formItem('Button', {
css: 'button-item',
transclude: true,
link: function(scope, element, attrs, model) {
var field = scope.field || {};
var icon = field.icon || "";
var iconHover = field.iconHover || "";
var isIcon = icon.indexOf('fa-') === 0;
if (isIcon || icon) {
element.prepend(' ');
}
var css = field.css || '';
if (css.indexOf('btn-') > -1 && css.indexOf('btn-primary') === -1) {
element.removeClass('btn-primary');
}
if (field && field.help && axelor.config['user.noHelp'] !== true) {
element.addClass('has-help');
}
if (isIcon) {
var e = $('<i>').addClass('fa').addClass(icon).prependTo(element);
if (iconHover) {
e.hover(function() {
$(this).removeClass(icon).addClass(iconHover);
}, function() {
$(this).removeClass(iconHover).addClass(icon);
});
}
} else if (icon) {
$('<img>').attr('src', icon).prependTo(element);
}
if (!field.title) {
element.addClass("button-icon");
}
if (_.isString(field.link)) {
element.removeClass('btn btn-primary').addClass('btn-link');
element.attr("href", field.link);
}
element.one('mouseover', function () {
element.tooltip({
html: true,
title: function() {
if (field.help) {
return field.help;
}
if (element.innerWidth() < element[0].scrollWidth) {
return field.title;
}
},
delay: { show: 1000, hide: 100 },
container: 'body'
});
element.on("$destroy", function () {
var t = element.data('tooltip');
if (t) {
t.destroy();
t = null;
}
});
});
element.on("click", function(e) {
if (scope.isReadonlyExclusive() || element.hasClass('disabled')) {
return;
}
function enable() {
scope.ajaxStop(function () {
setDisabled(scope.isReadonlyExclusive());
}, 100);
}
function setEnable(p) {
if (p && p.then) {
p.then(enable, enable);
} else {
scope.ajaxStop(enable, 500);
}
}
function doClick() {
setEnable(scope.fireAction("onClick"));
}
setDisabled(true);
if (scope.waitForActions) {
return scope.waitForActions(doClick);
}
return doClick();
});
function setDisabled(disabled) {
if (disabled || disabled === undefined) {
return element.addClass("disabled").attr('tabindex', -1);
}
return element.removeClass("disabled").removeAttr('tabindex');
}
var readonlySet = false;
scope.$watch('isReadonlyExclusive()', function buttonReadonlyWatch(readonly, old) {
if (readonly === old && readonlySet) return;
readonlySet = true;
return setDisabled(readonly);
});
scope.$watch('attr("title")', function buttonTitleWatch(title, old) {
if (!title || title === old) return;
if (element.is('button')) {
return element.html(title);
}
element.children('.btn-text').html(title);
});
scope.$watch('attr("css")', function buttonCssWatch(css, old) {
var curr = css || field.css || 'btn-success';
var prev = old || field.css || 'btn-success';
if (curr !== prev) {
element.removeClass(prev || '').addClass(curr);
}
});
scope.$watch('attr("icon")', function buttonIconWatch(icon, old) {
if (icon === old || (icon && icon.indexOf('fa-') !== 0)) return;
var iconElem = element.find('i.fa:first');
if (iconElem.length == 0) {
iconElem = $('<i>').addClass('fa').prependTo(element.prepend(' '));
}
iconElem.removeClass(old || '').addClass(icon || field.icon || '');
});
},
template: '<a href="" class="btn btn-primary">'+
'<span class="btn-text" ng-transclude></span>'+
'</a>'
});
ui.formItem('InfoButton', 'Button', {
link: function (scope, element, attrs) {
this._super.apply(this, arguments);
var field = scope.field || {};
scope.title = field.title;
scope.$watch('attr("title")', function infoButtonTitleWatch(title, old) {
if (!title || title === old) return;
scope.title = title;
});
Object.defineProperty(scope, 'value', {
get: function () {
return field.currency
? ui.formatters.decimal(field, (scope.record || {})[field.name], scope.record)
: ui.formatters.$fmt(scope, field.name);
}
});
},
replace: true,
template:
"<div class='btn info-button'>" +
"<div class='info-button-data'>" +
"<span class='info-button-value'>{{value}}</span>" +
"<small class='info-button-title'>{{title}}</small>" +
"</div>" +
"</div>"
});
ui.formItem('ToolButton', 'Button', {
getViewDef: function(element) {
return this.btn;
},
link: function(scope, element, attrs) {
this._super.apply(this, arguments);
var field = scope.field;
if (field == null) {
return;
}
scope.title = field.title;
scope.showTitle = field.showTitle !== false;
scope.btn.isHidden = function() {
return scope.isHidden();
};
},
template:
'<button class="btn" ui-show="!isHidden()" name="{{btn.name}}" ui-actions ui-widget-states>' +
'<span ng-show="showTitle">{{title}}</span>' +
'</button>'
});
})();

View File

@ -0,0 +1,206 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
/**
* The String widget.
*/
ui.formInput('String', {
css: 'string-item',
init: function(scope) {
var field = scope.field;
var isReadonly = scope.isReadonly;
var trKey = "$t:" + field.name;
scope.isReadonly = function () {
scope.$$readonlyOrig = isReadonly.apply(this, arguments);
return (scope.record && scope.record[trKey]) || scope.$$readonlyOrig;
};
scope.format = function (value) {
if ((scope.record && scope.record[trKey])) {
return scope.record[trKey];
}
return value;
};
},
link_editable: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
var field = scope.field,
regex = field.pattern ? new RegExp(field.pattern, 'i') : null,
minSize = +(field.minSize),
maxSize = +(field.maxSize);
scope.validate = function(value) {
if (_.isEmpty(value)) {
return true;
}
var length = value.length,
valid = true;
if (minSize) {
valid = length >= minSize;
}
if(valid && maxSize) {
valid = length <= maxSize;
}
if (valid && regex) {
valid = regex.test(value);
}
return valid;
};
},
template_readonly: '<input type="text" ng-show="text" tabindex="-1" readonly="readonly" class="display-text" value="{{text}}">'
});
/**
* The Email input widget.
*/
ui.formInput('Email', {
css: 'email-item',
metaWidget: true,
pattern: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
link: function(scope, element, attrs, model) {
var pattern = this.pattern;
scope.validate = function(value) {
if(_.isEmpty(value)) {
return true;
}
return pattern.test(value);
};
},
template_editable: '<input type="email">',
template_readonly: '<a target="_blank" ng-show="text" href="mailto:{{text}}">{{text}}</a>'
});
/**
* The URL input widget.
*/
ui.formInput('Url', {
css: 'url-item',
metaWidget: true,
template_editable: '<input type="url">',
template_readonly: '<a target="_blank" ng-show="text" href="{{text}}">{{text}}</a>'
});
/**
* The Phone input widget.
*/
ui.formInput('Phone', 'String', {
css: 'phone-item',
template_editable: '<input type="tel">'
});
/**
* The Text input widget.
*/
ui.formInput('Text', {
css: 'text-item',
link_editable: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
var field = scope.field,
textarea = element.get(0);
textarea.rows = parseInt(field.height) || 8;
//Firefox add one more line
if (axelor.browser.mozilla) {
textarea.rows -= 1;
}
var field = scope.field,
regex = field.pattern ? new RegExp(field.pattern, 'i') : null,
minSize = +(field.minSize),
maxSize = +(field.maxSize);
scope.validate = function(value) {
if (_.isEmpty(value)) {
return true;
}
var length = value.length,
valid = true;
if (minSize) {
valid = length >= minSize;
}
if(valid && maxSize) {
valid = length <= maxSize;
}
if (valid && regex) {
valid = regex.test(value);
}
return valid;
};
},
template_editable: '<textarea></textarea >',
template_readonly: '<pre ng-show="text">{{text}}</pre>'
});
ui.formInput('Password', 'String', {
css: 'password-item',
metaWidget: true,
init: function(scope) {
scope.password = function() {
var value = this.getValue() || "";
return _.str.repeat('*', value.length);
};
},
template_readonly: '<input type="password" ng-show="text" tabindex="-1" readonly="readonly" class="display-text" value="{{password()}}"></input>',
template_editable: '<input type="password" autocomplete="new-password">'
});
ui.directive('uiTextareaAutoSize', function () {
return function (scope, element, attrs) {
if (!element.is('textarea')) return;
function resize() {
var diff = element.outerHeight() - element.innerHeight();
element.css('height', 'auto').css('height', element[0].scrollHeight + diff);
}
element.on('focus keyup input', resize);
setTimeout(resize);
};
});
})();

View File

@ -0,0 +1,602 @@
/*
* 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);
}
});
}
};
}]);
})();

1209
sophal/js/form/form.mail.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,561 @@
/*
* 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() {
"use strict";
var ui = angular.module('axelor.ui');
ui.RefFieldCtrl = RefFieldCtrl;
function RefFieldCtrl($scope, $element, DataSource, ViewService, initCallback) {
var field = $scope.getViewDef($element),
params = {
model: field.target || $element.attr('x-target'),
views: field.views || {},
domain: field.domain,
context: field.context
},
views = {};
if (field.jsonTarget) {
params.context = _.extend({}, params.context, { jsonModel: field.jsonTarget });
}
if (!$element.is('fieldset')) {
_.each(field.views, function(view){
views[view.type] = view;
});
var formView = null,
gridView = null,
summaryView = null;
if (field.summaryView === "" || field.summaryView === "true") {
summaryView = views.form;
}
if (field.gridView) {
gridView = {
type: 'grid',
name: field.gridView
};
}
if (field.formView) {
formView = {
type: 'form',
name: field.formView
};
}
if (field.summaryView === "" || field.summaryView === "true") {
summaryView = views.form || formView || { type: 'form' };
} else if (field.summaryView) {
summaryView = {
type: "form",
name: field.summaryView
};
}
views.form = formView || views.form;
views.grid = gridView || views.grid;
params.summaryView = angular.copy(summaryView);
params.summaryViewDefault = params.summaryView || views.form;
params.views = _.compact([views.grid, views.form]);
$scope._viewParams = params;
}
ui.ViewCtrl($scope, DataSource, ViewService);
$scope.ngModel = null;
$scope.editorCanSave = true;
$scope.editorCanReload = field.canReload;
if (initCallback) {
initCallback.call(this);
}
var editor = null;
var selector = null;
var embedded = null;
$scope.createNestedEditor = function() {
return null;
};
/**
* Show/Hide the nested editor according to the show parameter, if
* undefined then toggle.
*
*/
$scope.showNestedEditor = function showNestedEditor(show) {
if (!params.summaryView) {
return;
}
if (embedded === null) {
embedded = $scope.createNestedEditor();
}
var es = embedded.data('$scope');
if (es !== null) {
es.visible = (show === undefined ? !es.visible : show);
embedded.toggle(es.visible);
}
return embedded;
};
$scope.showPopupEditor = function(record) {
if (!record && this.isReadonly()) {
return;
}
if (editor == null) {
editor = ViewService.compile('<div ui-editor-popup></div>')($scope);
editor.data('$target', $element);
}
var popup = editor.isolateScope();
popup.show(record);
popup._afterPopupShow = function() {
if (record == null) {
popup.$broadcast("on:new");
}
};
};
function _showEditor(record) {
if (!$scope._isPopup && field.editWindow === "blank" && record && record.id > 0) {
var checkVersion = "" + axelor.config["view.form.check-version"];
var context = ($scope.selectedTab || {}).context || {};
if (context.__check_version !== undefined) {
checkVersion = "" + context.__check_version;
}
var tab = {
action: _.uniqueId('$act'),
title: field.title,
model: field.target,
recordId: record.id,
views: [{
type: 'form',
name: field.formView
}, {
type: 'grid',
name: field.gridView
}]
};
if (checkVersion) {
tab.context = { __check_version: checkVersion };
}
return $scope.$root.openTab(tab);
}
if ($scope.editorCanReload && record && record.id) {
var parent = $scope.$parent;
if (parent && parent.canSave()) {
var opts = {
callOnSave: field.callOnSave
};
return parent.onSave(opts).then(function(){
$scope.showPopupEditor(record);
});
}
}
return $scope.showPopupEditor(record);
}
$scope.showEditor = function(record) {
var perm = record ? "read" : "create";
var id = (record||{}).id;
if (perm === 'read' && (!id || id < 0)) {
return _showEditor(record);
}
return $scope.isPermitted(perm, record, function(){
_showEditor(record);
});
};
$scope.parentReload = function() {
var parent = $scope.$parent;
if (parent) {
parent.reload();
}
};
$scope.showSelector = function() {
if (this.isReadonly()) {
return;
}
function doShow() {
if (selector == null) {
selector = $('<div ui-selector-popup></div>').attr('x-select-mode', $scope.selectMode || "multi");
selector = ViewService.compile(selector)($scope);
selector.data('$target', $element);
}
var popup = selector.isolateScope();
popup._domain = $scope._domain; // make sure that popup uses my domain (#1233)
popup.show();
}
var onSelect = this.$events.onSelect;
if (onSelect) {
onSelect().then(function(){
doShow();
});
} else {
doShow();
}
};
$scope.$on("on:edit", function(record){
var domain = ($scope.field||field).domain;
var context = ($scope.field||field).context;
if (domain !== undefined) $scope._domain = domain;
if (context !== undefined) $scope._context = context;
});
$scope.setDomain = function(domain, context) {
if (domain !== undefined) $scope._domain = domain;
if (context !== undefined) $scope._context = context;
};
$scope.getDomain = function() {
return {
_domain: $scope._domain,
_context: $scope._context
};
};
var fetchDS = (function () {
var fds = null;
return function () {
if (fds) return fds;
var ds = $scope._dataSource;
return fds = DataSource.create(ds._model, {
domain: ds._domain,
context: ds._context
});
};
})();
$scope.fetchData = function(value, success) {
var records = $.makeArray(value),
ids = [];
_.each(records, function(item) {
if (_.isNumber(item)) {
return ids.push(item);
}
if (_.isNumber(item.id) && item.id > 0 &&
_.isUndefined(item.version) &&
_.isUndefined(item.$fetched)) {
return ids.push(item.id);
}
});
if (ids.length === 0) {
return success(value);
}
var fields = $scope.selectFields();
function doFetch(view) {
var domain = "self.id in (:_field_ids)";
var context = _.pick($scope.getContext(), ['id', '_model']);
var sortBy = view.sortBy || view.orderBy;
if (sortBy) {
sortBy = sortBy.split(",");
}
if (view.canMove && fields.indexOf('sequence') === -1) {
fields.push('sequence');
}
context._field = field.name;
context._field_ids = ids;
return fetchDS().search({
fields: fields,
sortBy: fetchDS()._sortBy || sortBy,
archived: true,
limit: -1,
domain: domain,
context: context
}).success(function(records, page){
// only edited records should have version property
var items = _.map(records, function(item){
item.$version = item.version;
item.$fetched = false;
delete item.version;
return item;
});
success(items, page);
});
}
if ($scope.isHidden()) {
return doFetch($scope.view || {});
}
return $scope._viewPromise.then(function(view) {
return doFetch(view || {});
});
};
$scope.fetchSelection = function(request, response) {
var fn = fetchSelection.bind(this);
var onSelect = this.$events.onSelect;
if (onSelect) {
return onSelect(true).then(function() {
return fn(request, response);
});
}
return fn(request, response);
};
function fetchSelection(request, response) {
/* jshint validthis: true */
var field = this.field;
var nameField = field.targetName || 'id',
fields = field.targetSearch || [],
filter = {},
limit = field.limit || (axelor.device.small ? 6 : 10),
sortBy = field.orderBy;
fields = ["id", nameField].concat(fields);
fields = _.chain(fields).compact().unique().value();
_.each(fields, function(name){
if (name !== "id" && request.term) {
filter[name] = request.term;
}
});
var domain = this._domain,
context = this._context;
if (domain !== undefined && this.getContext) {
context = _.extend({}, context, this.getContext());
}
if (sortBy) {
sortBy = sortBy.split(",");
}
var params = {
filter: filter,
fields: fields,
sortBy: sortBy,
limit: limit
};
if (domain !== undefined) {
params.domain = domain;
params.context = context;
}
fetchDS().search(params).success(function(records, page){
var trKey = '$t:' + nameField;
var items = _.map(records, function(record) {
return {
label: record[trKey] || record[nameField],
value: record
};
});
response(items, page);
});
}
$scope.createOnTheFly = function (term, popup, onSaveCallback) {
var field = $scope.field;
var targetFields = null;
var requiredFields = (field.create||"").split(/,\s*/);
function createItem(fields, term, popup) {
var ds = $scope._dataSource,
data = { $forceDirty: true }, missing = false;
_.each(fields, function(field) {
if (field.name === "name") return data["name"] = term;
if (field.name === "code") return data["code"] = term;
if (field.nameColumn) return data[field.name] = term;
if (requiredFields.indexOf(field.name) > -1) {
return data[field.name] = term;
}
if (field.required) {
missing = true;
}
});
if (popup || missing || _.isEmpty(data)) {
return $scope.showPopupEditor(data);
}
return ds.save(data).success(onSaveCallback);
}
if (targetFields) {
return createItem(targetFields, term, popup);
}
return $scope.loadView("form").success(function(fields, view){
targetFields = fields;
return createItem(fields, term, popup);
});
};
$scope.attachTagEditor = function attachTagEditor(scope, element, attrs) {
var field = scope.field;
var input = null;
if (!field.target) {
return;
}
function onTagEdit(e, item) {
var elem = $(e.target);
var field = scope.field;
var value = item[field.targetName];
function onKeyDown(e) {
// enter key
if (e.keyCode === 13) {
item[field.targetName] = input.val();
saveAndSelect(item);
hideEditor();
}
// escape
if (e.keyCode === 27) {
hideEditor();
}
}
function hideEditor(forceSave) {
$(document).off('mousedown.tag-editor');
$(input).off('keydown.tag-editor').hide();
if (forceSave && value !== input.val()) {
item[field.targetName] = input.val();
saveAndSelect(item);
}
}
if (input === null) {
input = $('<input class="tag-editor" type="text">').appendTo(element);
}
input.val(value)
.width(element.width() - 6)
.show().focus()
.position({
my: 'left top',
at: 'left+3 top+3',
of: element
});
$(input).on('keydown.tag-editor', onKeyDown);
$(document).on('mousedown.tag-editor', function (e) {
if (!input.is(e.target)) {
hideEditor(true);
}
});
}
function saveAndSelect(record) {
var ds = scope._dataSource;
var data = _.extend({}, record, {
version: record.version || record.$version
});
ds.save(data).success(function (rec) {
scope.select(rec);
});
}
scope.onTagEdit = onTagEdit;
};
$scope.canSelect = function() {
var canSelect = $scope.attr('canSelect');
if (canSelect !== undefined) return canSelect;
if ($scope.selectEnable !== undefined) return $scope.selectEnable;
return true;
};
$scope.canNew = function() {
return $scope.attr('canNew') !== false;
};
$scope.canEdit = function() {
return !$scope.isReadonly();
};
$scope.canView = function() {
return $scope.attr('canView') !== false;
};
$scope.canRemove = function() {
return $scope.attr('canRemove') !== false;
};
$scope.select = function(value) {
};
$scope.onNew = function() {
$scope.showEditor(null);
};
$scope.onEdit = function() {
};
$scope.onSelect = function() {
$scope.showSelector();
};
$scope.onRemove = function() {
};
var hasPermission = $scope.hasPermission;
$scope.hasPermission = function(perm) {
if (hasPermission && !hasPermission.apply($scope, arguments)) {
return false;
}
if (!field.perms) return true;
var perms = field.perms;
var permitted = perms[perm];
if (!permitted) {
return false;
}
return true;
};
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,400 @@
/*
* 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() {
"use strict";
var ui = angular.module("axelor.ui");
var NestedForm = {
scope: true,
controller: [ '$scope', '$element', function($scope, $element) {
ui.FormViewCtrl.call(this, $scope, $element);
$scope.onShow = function(viewPromise) {
};
$scope.$$forceWatch = false;
$scope.$$forceCounter = false;
$scope.$setForceWatch = function () {
$scope.$$forceWatch = true;
$scope.$$forceCounter = true;
};
$scope.registerNested($scope);
$scope.show();
}],
link: function(scope, element, attrs, ctrl) {
},
template: '<div ui-view-form x-handler="this"></div>'
};
ui.EmbeddedEditorCtrl = EmbeddedEditorCtrl;
ui.EmbeddedEditorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
function EmbeddedEditorCtrl($scope, $element, DataSource, ViewService) {
var params = angular.copy($scope._viewParams);
params.views = _.compact([params.summaryView || params.summaryViewDefault]);
$scope._viewParams = params;
ui.ViewCtrl($scope, DataSource, ViewService);
ui.FormViewCtrl.call(this, $scope, $element);
$scope.visible = false;
$scope.onShow = function() {
};
var originalEdit = $scope.edit;
function doEdit(record) {
if (record && record.id > 0 && !record.$fetched) {
$scope.doRead(record.id).success(function(record){
originalEdit(record);
});
} else {
originalEdit(record);
}
}
function doClose() {
if ($scope.isDetailView) {
$scope.edit($scope.getSelectedRecord());
return;
}
$scope.edit(null);
$scope.waitForActions(function () {
$scope.visible = false;
$element.hide();
$element.data('$rel').show();
});
}
$scope.edit = function(record) {
doEdit(record);
$scope.setEditable(!$scope.$parent.$$readonly);
};
$scope.onClose = function() {
$scope.onClear();
doClose();
};
$scope.onOK = function() {
if (!$scope.isValid()) {
return;
}
var record = $scope.record;
if (record) record.$fetched = true;
var event = $scope.$broadcast('on:before-save', record);
if (event.defaultPrevented) {
if (event.error) {
return axelor.dialogs.error(event.error);
}
}
$scope.waitForActions(function () {
$scope.select($scope.record);
$scope.waitForActions(doClose);
});
};
$scope.onAdd = function() {
if (!$scope.isValid() || !$scope.record) {
return;
}
var record = $scope.record;
record.id = null;
record.version = null;
record.$version = null;
$scope.onClear();
function doSelect(rec) {
if (rec) {
$scope.select(rec);
}
return doEdit(rec);
}
if (!$scope.editorCanSave) {
return doSelect(record);
}
$scope.onSave().then(function (rec) {
doSelect(rec);
});
};
$scope.onClear = function() {
if ($scope.$parent.selection) {
$scope.$parent.selection.length = 0;
}
doEdit(null);
};
$scope.canUpdate = function () {
return $scope.record && $scope.record.id;
};
function loadSelected() {
var record = $scope.getSelectedRecord();
if ($scope.isDetailView) {
$scope.edit(record);
}
}
$scope.$on('grid:changed', function(event) {
loadSelected();
});
$scope.$on('on:edit', function(event, record) {
if ($scope.$parent.record === record) {
$scope.waitForActions(loadSelected);
}
});
$scope.$parent.$watch('isReadonly()', function nestedReadonlyWatch(readonly, old) {
if (readonly === old) return;
$scope.setEditable(!readonly);
});
$scope.show();
}
var EmbeddedEditor = {
restrict: 'EA',
css: 'nested-editor',
scope: true,
controller: EmbeddedEditorCtrl,
link: function (scope, element, attrs) {
setTimeout(function () {
var prev = element.prev();
if (prev.is("[ui-slick-grid]")) {
element.zIndex(prev.zIndex() + 1);
}
});
},
template:
'<fieldset class="form-item-group bordered-box" ui-show="visible">'+
'<div ui-view-form x-handler="this"></div>'+
'<div class="btn-toolbar pull-right">'+
'<button type="button" class="btn btn btn-info" ng-click="onClose()" ng-show="isReadonly()"><span x-translate>Back</span></button> '+
'<button type="button" class="btn btn-primary" ng-click="onOK()" ng-show="!isReadonly() && canUpdate()"><span x-translate>OK</span></button>'+
'<button type="button" class="btn btn-primary" ng-click="onAdd()" ng-show="!isReadonly() && !canUpdate()"><span x-translate>Add</span></button> '+
'<button type="button" class="btn btn-danger" ng-click="onClose()" ng-show="!isReadonly()"><span x-translate>Cancel</span></button> '+
'</div>'+
'</fieldset>'
};
ui.NestedEditorCtrl = NestedEditorCtrl;
ui.NestedEditorCtrl.$inject = ['$scope', '$element', 'DataSource', 'ViewService'];
function NestedEditorCtrl($scope, $element, DataSource, ViewService) {
var params = angular.copy($scope._viewParams);
params.views = _.compact([params.summaryView || params.summaryViewDefault]);
$scope._viewParams = params;
ui.ManyToOneCtrl.call(this, $scope, $element, DataSource, ViewService);
$scope.nested = null;
$scope.registerNested = function(scope) {
$scope.nested = scope;
$scope.$watch("isReadonly()", function nestedReadonlyWatch(readonly) {
scope.setEditable(!readonly);
});
};
}
var NestedEditor = {
restrict: 'EA',
css: 'nested-editor',
require: '?ngModel',
scope: true,
controller: NestedEditorCtrl,
link: function(scope, element, attrs, model) {
function setValidity(nested, valid) {
model.$setValidity('valid', nested.isValid());
if (scope.setValidity) {
scope.setValidity('valid', nested.isValid());
}
}
var configure = _.once(function (nested) {
//FIX: select on M2O doesn't apply to nested editor
var unwatchId = scope.$watch(attrs.ngModel + '.id', function nestedRecordIdWatch(id, old){
if (id === old) {
return;
}
unwatchId();
unwatchId = null;
scope.$applyAsync();
});
var unwatchValid = nested.$watch('form.$valid', function nestedValidWatch(valid, old){
if (valid === old) {
return;
}
unwatchValid();
unwatchValid = null;
setValidity(nested, valid);
});
scope.$on("on:check-nested-values", function (e, value) {
if (nested && value) {
var val = scope.getValue() || {};
if (val.$updatedValues === value) {
_.extend(nested.record, value);
}
}
});
var parentAttrs = scope.$parent.field || {};
if (parentAttrs.forceWatch) {
nested.$$forceWatch = true;
}
});
var unwatch = null;
var original = null;
function nestedEdit(record, fireOnLoad) {
var nested = scope.nested;
var counter = 0;
if (!nested) return;
if (unwatch) unwatch();
original = angular.copy(record);
unwatch = nested.$watch('record', function nestedRecordWatch(rec, old) {
if (counter++ === 0 && !nested.$$forceCounter) {
return;
}
var ds = nested._dataSource;
var name = scope.field.name;
// don't process default values
if (ds.equals(rec, nested.defaultValues)) {
return;
}
if (_.isEmpty(rec)) rec = null;
if (_.isEmpty(old)) old = null;
if (rec == old) {
return;
}
if (rec) {
rec.$dirty = !(rec.id > 0 && ds.equals(rec, original));
}
model.$setViewValue(rec);
setValidity(nested, nested.isValid());
}, true);
return nested.edit(record, fireOnLoad);
}
scope.ngModel = model;
scope.visible = false;
scope.onClear = function() {
scope.$parent.setValue(null, true);
scope.$parent.$broadcast('on:new');
};
scope.onClose = function() {
scope.$parent._isNestedOpen = false;
scope.visible = false;
element.hide();
};
scope.canClose = function() {
return scope.canToggle() && scope.canSelect();
};
attrs.$observe('title', function(title){
scope.title = title;
});
model.$render = function() {
var nested = scope.nested,
promise = nested._viewPromise,
oldValue = model.$viewValue;
function doRender() {
var value = model.$viewValue;
if (oldValue !== value) { // prevent unnecessary onLoad
return;
}
if (!value || !value.id || value.$dirty) {
return nestedEdit(value, false);
}
if (value.$fetched && (nested.record||{}).$fetched) return;
return nested.doRead(value.id).success(function(record){
record.$fetched = true;
value.$fetched = true;
return nestedEdit(_.extend({}, value, record));
});
}
if (nested == null) {
return;
}
promise.then(function() {
configure(nested);
nestedEdit(model.$viewValue, false);
scope.waitForActions(doRender, 100);
});
};
},
template:
'<fieldset class="form-item-group bordered-box" ui-show="visible">'+
'<legend>'+
'<span ng-bind-html="title"></span> '+
'<span class="legend-toolbar" style="display: none;" ng-show="!isReadonly()">'+
'<a href="" tabindex="-1" ng-click="onClear()" title="{{\'Clear\' | t}}" ng-show="canShowIcon(\'clear\')"><i class="fa fa-ban"></i></a> '+
'<a href="" tabindex="-1" ng-click="onSelect()" title="{{\'Select\' | t}}" ng-show="canShowIcon(\'select\')"><i class="fa fa-search"></i></a> '+
'<a href="" tabindex="-1" ng-click="onClose()" title="{{\'Close\' | t}}" ng-show="canClose()"><i class="fa fa-times-circle"></i></a>'+
'</span>'+
'</legend>'+
'<div ui-nested-form></div>'+
'</fieldset>'
};
ui.formDirective('uiNestedEditor', NestedEditor);
ui.formDirective('uiEmbeddedEditor', EmbeddedEditor);
ui.formDirective('uiNestedForm', NestedForm);
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,483 @@
/*
* 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() {
"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);
});
};
}]);
})();