Files
ERP/sophal/js/form/form.input.select.js

1012 lines
26 KiB
JavaScript

/*
* Axelor Business Solutions
*
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function() {
"use strict";
var ui = angular.module('axelor.ui');
function acceptNumber(value) {
if (value === null || value === undefined) {
return value;
}
if (_.isNumber(value)) {
return +value;
}
if (/^(-)?\d+(\.\d+)?$/.test(value)) {
return +value;
}
return value;
}
function parseNumber(field, value) {
if (value === null || value === undefined) {
return value;
}
if (!field || ['integer', 'long'].indexOf(field.serverType) === -1) {
return value;
}
var num = +value;
if (isNaN(num)) {
return value;
}
return num;
}
ui.formWidget('BaseSelect', {
findInput: function(element) {
return element.find('input:first:not([ui-panel-editor] input)');
},
init: function(scope) {
scope.loadSelection = function(request, response) {
};
scope.parse = function(value) {
return value;
};
scope.format = function(value) {
return this.formatItem(value);
};
scope.formatItem = function(item) {
return item;
};
},
link_editable: function (scope, element, attrs, model) {
var input = this.findInput(element);
var showing = false;
var willShow = false;
scope.showSelection = function(delay) {
if (scope.isReadonly() || showing || willShow) {
return;
}
if (input.is('.x-focus')) {
input.removeClass('.x-focus');
return;
}
willShow = true;
input.addClass('.x-focus');
doSetup(input);
setTimeout(function () {
if (input.is(':focus')) {
input.autocomplete("search" , '');
input.removeClass('.x-focus');
}
willShow = false;
}, delay || 100);
};
scope.handleClear = function(e) {
scope.setValue(null, true);
};
scope.handleDelete = function(e) {
};
scope.handleEnter = function(e) {
};
scope.handleSelect = function(e, ui) {
};
scope.handleClose = function(e, ui) {
};
scope.handleOpen = function(e, ui) {
};
function renderItem(ul, item) {
var el = $("<li>").append( $("<a>").html(item.label)).appendTo(ul);
if (item.click) {
el.addClass("tag-select-action");
ul.addClass("tag-select-action-menu");
}
return el;
}
var doSetup = _.once(function (input) {
var loading = false;
var pending = null;
function doLoad(request, response) {
if (loading) {
return pending = _.partial(doLoad, request, response);
}
loading = true;
scope.loadSelection(request, function() {
loading = false;
response.apply(null, arguments);
if (pending) {
pending();
pending = null;
}
});
}
input.autocomplete({
minLength: 0,
position: { collision: "flip" },
source: doLoad,
focus: function(event, ui) {
return false;
},
select: function(event, ui) {
// do not select with tab key, to prevent unexpected result on editable grid
if (event.keyCode === 9) {
return false;
}
var ret = scope.handleSelect(event, ui);
if (ret !== undefined) {
return ret;
}
return false;
},
open: function(event, ui) {
showing = true;
scope.handleOpen(event, ui);
},
close: function(event, ui) {
showing = false;
scope.handleClose(event, ui);
}
});
input.data('ui-autocomplete')._renderItem = scope.renderSelectItem || renderItem;
scope.$onAdjust('size scroll', function (e) {
if (e.type === 'adjust:size' && e.target !== document) {
return;
}
if (showing) {
input.autocomplete('close');
}
});
});
input.focus(function(e) {
element.addClass('focus');
doSetup(input);
}).blur(function() {
element.removeClass('focus');
if (showing) {
input.autocomplete('close');
}
}).keyup(function(e) {
// if TAB key
if (e.which === 9) {
scope.showSelection(300);
}
}).keydown(function(e) {
var KEY = $.ui.keyCode;
switch(e.keyCode) {
case KEY.DELETE:
case KEY.BACKSPACE:
scope.handleDelete(e);
break;
case KEY.ENTER:
scope.handleEnter(e);
break;
}
}).click(function() {
scope.showSelection();
});
if (axelor.browser.mozilla) {
input.mousedown(function () {
if (!input.is(':focus')) {
scope.showSelection(300);
}
});
}
},
template_editable:
'<span class="picker-input">'+
'<input type="text" autocomplete="off">'+
'<span class="picker-icons picker-icons-2">'+
'<i class="fa fa-times" ng-show="text" ng-click="handleClear()"></i>'+
'<i class="fa fa-caret-down" ng-click="showSelection()"></i>'+
'</span>'+
'</span>'
});
function filterSelection(scope, field, selection, current) {
var selectionIn = scope.attr('selection-in') || field.selectionIn;
if (_.isEmpty(selection)) return selection;
if (_.isEmpty(selectionIn)) return selection;
var context = (scope.getContext || angular.noop)() || {};
var list = selectionIn;
if (_.isString(selectionIn)) {
var expr = selectionIn.trim();
if (expr.indexOf('[') !== 0) {
expr = '[' + expr + ']';
}
list = axelor.$eval(scope, expr, context);
}
var value = acceptNumber(current);
if (_.isEmpty(list)) {
return selection;
}
list = _.map(list, acceptNumber);
return _.filter(selection, function (item) {
var val = acceptNumber(item.value);
return val === value || list.indexOf(val) > -1;
});
}
ui.formInput('Select', 'BaseSelect', {
css: 'select-item',
cellCss: 'form-item select-item',
init: function(scope) {
this._super(scope);
var field = scope.field,
selectionList = field.selectionList || [],
selectionMap = {};
var data = _.map(selectionList, function(item) {
var value = "" + item.value;
selectionMap[value] = item.title;
return {
value: value,
label: item.title || "&nbsp;"
};
});
var dataSource = null;
function getDataSource() {
if (dataSource || !field.selection || !field.domain) {
return dataSource;
}
return dataSource = scope._dataSource._new('com.axelor.meta.db.MetaSelectItem', {
domain: "(self.select.name = :_select) AND (" + field.domain + ")",
context: {
_select: field.selection
}
});
}
scope.loadSelection = function(request, response) {
var ds = getDataSource();
function select(records) {
var items = _.filter(records, function(item) {
var label = item.label || "",
term = request.term || "";
return label.toLowerCase().indexOf(term.toLowerCase()) > -1;
});
items = filterSelection(scope, field, items);
return response(items);
}
if (ds) {
return ds.search({
fields: ['value', 'title'],
context: scope.getContext ? scope.getContext() : undefined
}).success(function (records) {
_.each(records, function (item) {
item.label = selectionMap[item.value] || item.title;
});
return select(records);
});
}
return select(data);
};
scope.formatItem = function(item) {
var key = _.isNumber(item) ? "" + item : item;
if (!key) {
return item;
}
if (_.isString(key)) {
return selectionMap[key] || "";
}
return item.label;
};
if (field.enumType) {
var __enumValues = {};
var __hasValue = false;
_.each(selectionList, function (item) {
__enumValues[item.value] = (item.data || {}).value;
__hasValue = __hasValue || __enumValues[item.value] !== undefined;
});
if (__hasValue) {
scope.$watch('record.' + field.name, function selectFieldNameWatch(value, old) {
if (value && value !== old) {
var enumValue = __enumValues[value];
if (scope.record && enumValue !== value) {
scope.record[field.name + '$value'] = enumValue;
}
}
});
}
}
},
link_editable: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
var input = this.findInput(element);
function update(value) {
var val = parseNumber(scope.field, value);
scope.setValue(val, true);
scope.$applyAsync();
}
scope.handleDelete = function(e) {
if (e.keyCode === 46) { // DELETE
update(null);
}
if (e.keyCode === 8) { // BACKSPACE
var value = scope.getValue();
if (value || (e.target.value||'').length < 2) {
update(null);
}
}
};
scope.handleSelect = function(e, ui) {
update(ui.item.value);
};
scope.$render_editable = function() {
input.val(this.getText());
};
scope.$on('on:edit', function () {
// force update input text, fixes #5965
scope.$render_editable();
});
}
});
ui.formInput('Enum', 'Select');
ui.formInput('ImageSelect', 'Select', {
metaWidget: true,
BLANK: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
link: function(scope, element, attrs) {
this._super(scope, element, attrs);
var field = scope.field;
var formatItem = scope.formatItem;
var selectIcons = {};
_.each(field.selectionList, function (item) {
selectIcons[item.value] = item.icon || item.value;
});
scope.canShowText = function () {
return field.labels === undefined || field.labels;
};
scope.formatItem = function (item) {
if (scope.canShowText()) {
return formatItem(item);
}
return "";
};
scope.findImage = function (value) {
return selectIcons[value] || this.BLANK;
};
scope.$watch('getValue()', function selectFieldValueWatch(value, old) {
scope.image = scope.findImage(value);
scope.isIcon = scope.image && scope.image.indexOf('fa-') === 0;
element.toggleClass('empty', !value);
}.bind(this));
},
link_editable: function(scope, element, attrs) {
this._super(scope, element, attrs);
var input = this.findInput(element);
var selects = {};
_.each(scope.field.selectionList, function (item) {
selects[item.value] = (item.data||{}).icon || item.value;
});
scope.renderSelectItem = function(ul, item) {
var a = $("<a>");
var el = $("<li>").addClass("image-select-item").append(a).appendTo(ul);
var image = scope.findImage(item.value);
if (image && image.indexOf('fa-') === 0) {
a.append($("<i>").addClass("fa").addClass(image));
} else {
a.append($("<img>").attr("src", image));
}
if (scope.canShowText()) {
a.append($("<span></span>").html(item.label));
}
return el;
};
var $render_editable = scope.$render_editable;
scope.$render_editable = function () {
$render_editable.apply(scope, arguments);
setTimeout(function () {
element.find('input').css('padding-left', element.find('i.image,img').width());
});
};
},
template_readonly:
'<span class="image-select readonly">'+
'<i ng-if="isIcon" class="fa" ng-class="image"></i>'+
'<img ng-if="!isIcon" ng-src="{{image}}"></img> <span ng-show="canShowText()">{{text}}</span>' +
'</span>',
template_editable:
'<span class="picker-input image-select">'+
'<i ng-if="isIcon" class="fa" ng-class="image"></i>'+
'<img ng-if="!isIcon" ng-src="{{image}}"></img>' +
'<input type="text" autocomplete="off">'+
'<span class="picker-icons">'+
'<i class="fa fa-caret-down" ng-click="showSelection()"></i>'+
'</span>'+
'</span>'
});
ui.formInput('MultiSelect', 'Select', {
css: 'multi-select-item',
cellCss: 'form-item multi-select-item',
metaWidget: true,
init: function(scope) {
this._super(scope);
var __parse = scope.parse;
scope.parse = function(value) {
if (_.isArray(value)) {
return value.join(', ');
}
return __parse(value);
};
scope.format = function(value) {
var items = value,
values = [];
if (!value) {
scope.items = [];
return value;
}
if (!_.isArray(items)) items = items.split(/,\s*/);
values = _.map(items, function(item) {
return {
value: item,
title: scope.formatItem(item)
};
});
scope.items = values;
return _.pluck(values, 'title').join(', ');
};
scope.matchValues = function(a, b) {
if (a === b) return true;
if (!a) return false;
if (!b) return false;
if (_.isString(a)) return a === b;
return a.value === b.value;
};
scope.getSelection = function() {
return this.items;
};
var max = +(scope.field.max);
scope.limited = function(items) {
if (max && items && items.length > max) {
scope.more = _t("and {0} more", items.length - max);
return _.first(items, max);
}
scope.more = null;
return items;
};
},
link_editable: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
var input = this.findInput(element);
input.focus(function() {
scaleInput();
}).blur(function() {
scaleInput(50);
input.val('');
});
var placeholder = null;
if (scope.field.placeholder) {
placeholder = $('<span class="tag-select-placeholder hidden"></span>')
.text(scope.field.placeholder)
.appendTo(element)
.click(function (e) {
scope.showSelection();
});
}
function scaleInput(width) {
var elem = element.find('.tag-selector'),
pos = elem.position();
if (width) {
input.css('position', '');
elem.width('');
return input.width(width);
}
var top = pos.top,
left = pos.left;
width = element.innerWidth() - left;
elem.width(50);
input.css('width', width - 24);
}
function update(value) {
var val = parseNumber(scope.field, value);
scope.setValue(val, true);
scope.$applyAsync();
setTimeout(function () {
scaleInput(50);
});
}
scope.removeItem = function(item) {
var items = this.getSelection(),
value = _.isString(item) ? item : (item||{}).value;
items = _.chain(items)
.pluck('value')
.filter(function(v){
return !scope.matchValues(v, value);
})
.value();
update(items);
};
scope.onShowSelection = function(e) {
if (e && $(e.target || e.srcElement).is('input,li,i,span.tag-text')) {
return;
}
input.focus();
setTimeout(function() {
scope.showSelection();
});
};
scope.handleDelete = function(e) {
if (input.val()) {
return;
}
var items = this.getSelection();
this.removeItem(_.last(items));
};
scope.handleSelect = function(e, ui) {
var items = this.getSelection(),
values = _.pluck(items, 'value');
var found = _.find(values, function(v){
return scope.matchValues(v, ui.item.value);
});
if (found) {
return false;
}
values.push(ui.item.value);
update(values);
scaleInput(50);
};
scope.handleOpen = function(e, ui) {
input.data('autocomplete')
.menu
.element
.position({
my: "left top",
at: "left bottom",
of: element
})
.width(element.width() - 4);
};
scope.$render_editable = function() {
if (placeholder) {
placeholder.toggleClass('hidden', !!scope.getValue());
}
return input.val('');
};
input.on("input blur", function () {
if (placeholder) {
placeholder.toggleClass('hidden', !!(input.val() || scope.getValue()));
}
});
scope.$watch('items.length', function selectItemsLengthWatch(value, old) {
setTimeout(function () {
scaleInput(50);
});
});
},
template_editable:
'<div class="tag-select picker-input" ng-click="onShowSelection($event)">'+
'<ul>'+
'<li class="tag-item label label-primary" ng-repeat="item in items">'+
'<span ng-class="{\'tag-link\': handleClick}" class="tag-text" ng-click="handleClick($event, item.value)">{{item.title}}</span> '+
'<i class="fa fa-times fa-small" ng-click="removeItem(item)"></i>'+
'</li>'+
'<li class="tag-selector">'+
'<input type="text" autocomplete="off">'+
'</li>'+
'</ul>'+
'<span class="picker-icons">'+
'<i class="fa fa-caret-down" ng-click="onShowSelection()"></i>'+
'</span>'+
'</div>',
template_readonly:
'<div class="tag-select">'+
'<span class="label label-primary" ng-repeat="item in limited(items)">'+
'<span ng-class="{\'tag-link\': handleClick}" class="tag-text" ng-click="handleClick($event, item.value)">{{item.title}}</span>'+
'</span>'+
'<span ng-show="more"> {{more}}</span>'+
'</div>'
});
ui.formInput('SelectQuery', 'Select', {
link_editable: function(scope, element, attrs, model) {
this._super.apply(this, arguments);
var current = {};
function update(value) {
scope.setValue(value);
scope.$applyAsync();
}
scope.format = function(value) {
if (!value) return "";
if (_.isString(value)) {
return current.label || value;
}
current = value;
return value.label;
};
scope.parse = function(value) {
if (!value || _.isString(value)) return value;
return value.value;
};
scope.handleSelect = function(e, ui) {
update(ui.item);
};
var query = scope.$eval(attrs.query);
scope.loadSelection = function(request, response) {
return query(request, response);
};
}
});
ui.formInput('RadioSelect', {
css: "radio-select",
metaWidget: true,
link: function(scope, element, attrs, model) {
var field = scope.field;
var selection = field.selectionList || [];
scope.getSelection = function () {
return filterSelection(scope, field, selection, scope.getValue());
};
element.on("change", ":input", function(e) {
var val = parseNumber(scope.field, $(e.target).val());
scope.setValue(val, true);
scope.$applyAsync();
});
if (field.direction === "vertical" || field.dir === "vert") {
setTimeout(function(){
element.addClass("radio-select-vertical");
});
}
},
template_editable: null,
template_readonly: null,
template:
'<ul ng-class="{ readonly: isReadonly() }">'+
'<li ng-repeat="select in getSelection()">'+
'<label class="ibox round">'+
'<input type="radio" name="radio_{{$parent.$id}}" value="{{select.value}}"'+
' ng-disabled="isReadonly()"'+
' ng-checked="getValue() == select.value">'+
'<span class="box"></span>'+
'<span class="title">{{select.title}}</span>'+
'</label>'+
'</li>'+
'</ul>'
});
ui.formInput('CheckboxSelect', {
css: "checkbox-select",
metaWidget: true,
link: function(scope, element, attrs, model) {
var field = scope.field;
var selection = field.selectionList || [];
scope.getSelection = function () {
return filterSelection(scope, field, selection, scope.getValue());
};
scope.isSelected = function (select) {
var value = scope.getValue();
var current = ("" + value).split(",").map(function (val) {
return parseNumber(scope.field, val);
});
return current.indexOf(select.value) > -1;
};
element.on("change", ":input", function(e) {
var all = element.find("input:checked");
var selected = [];
all.each(function () {
var val = parseNumber(scope.field, $(this).val());
selected.push(val);
});
var value = selected.length === 0 ? null : selected.join(",");
scope.setValue(value, true);
scope.$applyAsync();
});
if (field.direction === "vertical" || field.dir === "vert") {
setTimeout(function(){
element.addClass("checkbox-select-vertical");
});
}
},
template_editable: null,
template_readonly: null,
template:
'<ul ng-class="{ readonly: isReadonly() }">'+
'<li ng-repeat="select in getSelection()">'+
'<label class="ibox">'+
'<input type="checkbox" value="{{select.value}}"'+
' ng-disabled="isReadonly()"'+
' ng-checked="isSelected(select)">'+
'<span class="box"></span>'+
'<span class="title">{{select.title}}</span>'+
'</label>'+
'</li>'+
'</ul>'
});
ui.formInput('NavSelect', {
css: "nav-select",
metaWidget: true,
link: function(scope, element, attrs, model) {
var field = scope.field;
var selection = field.selectionList || [];
scope.getSelection = function () {
return filterSelection(scope, field, selection, scope.getValue()) || [];
};
scope.$watch('text', function navSelectTextWatch(text, old) {
adjust();
});
scope.onSelect = function(select) {
if (scope.attr('readonly')) {
return;
}
var val = parseNumber(scope.field, select.value);
this.setValue(val, true);
elemNavs.removeClass('open');
elemMenu.removeClass('open');
// if selection change is used to show/hide some elements
// the layout should be adjusted
axelor.$adjustSize();
};
scope.isSelected = function (select) {
return select && scope.getValue() == select.value;
};
var lastWidth = 0;
var lastValue = null;
var elemNavs = null;
var elemMenu = null;
var elemMenuTitle = null;
var elemMenuItems = null;
function setup() {
elemNavs = element.children('.nav-steps').children('li:not(.dropdown,.ignore)');
elemMenu = element.children('.nav-steps').children('li.dropdown');
elemMenuTitle = elemMenu.find('a.nav-label > span');
elemMenuItems = elemMenu.find('li');
adjust();
}
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 (elemNavs === null || element.is(":hidden")) {
return;
}
var currentValue = scope.getValue();
var parentWidth = element.width() - 16;
if (parentWidth === lastWidth && currentValue === lastValue) {
return;
}
lastWidth = parentWidth;
lastValue = currentValue;
elemNavs.parent().css('visibility', 'hidden');
elemNavs.show();
elemMenu.hide();
if (elemNavs.parent().width() <= parentWidth) {
elemNavs.parent().css('visibility', '');
return;
}
var navs = scope.getSelection();
var selected = _.find(navs, scope.isSelected.bind(scope));
var selectedIndex = navs.indexOf(selected);
var elem = null;
var index = navs.length;
setMenuTitle(null);
while (elemNavs.parent().width() > parentWidth) {
elem = $(elemNavs[--index]);
elem.hide();
if (index === selectedIndex) {
setMenuTitle(selected);
}
}
elemMenuItems.hide();
while(index < navs.length) {
$(elemMenuItems[index++]).show();
}
elemNavs.parent().css('visibility', '');
}
scope.$onAdjust(adjust);
scope.$callWhen(setup, function () {
return element.is(':visible');
});
},
template_editable: null,
template_readonly: null,
template:
"<div class='nav-select'>" +
"<ul class='nav-steps' style='display: inline-flex; visibility: hidden;'>" +
"<li class='nav-step' ng-repeat='select in getSelection()' ng-class='{ active: isSelected(select), last: $last }'>" +
"<a href='' class='nav-label' ng-click='onSelect(select)' ng-bind-html='select.title'></a>" +
"</li>" +
"<li class='nav-step dropdown'>" +
"<a href='' class='nav-label dropdown-toggle' data-toggle='dropdown'><span></span></a>" +
"<ul class='dropdown-menu pull-right'>" +
"<li ng-repeat='select in getSelection()' ng-class='{active: getValue() == select.value}'>" +
"<a tabindex='-1' href='' ng-click='onSelect(select)' ng-bind-html='select.title'></a>" +
"</li>" +
"</ul>" +
"</li>" +
"</ul>"+
"</div>"
});
ui.formInput('ThemeSelect', 'Select', {
init: function (scope) {
scope.field.selectionList = _.map(axelor.config['application.themes'], function (name) {
return { value: name, title: _.titleize(name) };
});
scope.field.selectionList.unshift({
value: "default",
title: "Default"
});
this._super(scope);
}
});
ui.formInput('WidgetSelect', 'Select', {
init: function (scope) {
scope.field.selectionList = _.map(ui.getMetaWidgets(), function (name) {
return { value: name, title: name };
});
this._super(scope);
}
});
})();