TeaWeb/shared/js/ui/modal/permission/HTMLPermissionEditor.ts

860 lines
32 KiB
TypeScript
Raw Normal View History

2019-08-21 10:00:01 +02:00
namespace pe {
import PermissionEditorMode = Modals.PermissionEditorMode;
import KeyDownEvent = JQuery.KeyDownEvent;
2019-08-21 10:00:01 +02:00
class HTMLPermission {
readonly handle: HTMLPermissionEditor;
readonly group: HTMLPermissionGroup;
readonly permission: PermissionInfo;
readonly index: number;
2019-08-21 10:00:01 +02:00
tag: JQuery;
tag_name: JQuery;
tag_container_value: JQuery;
tag_container_granted: JQuery;
tag_container_skip: JQuery;
tag_container_negate: JQuery;
2019-08-21 10:00:01 +02:00
hidden: boolean;
2019-08-21 10:00:01 +02:00
/* the "actual" values */
private _mask = 0; /* fourth bit: hidden by filer | third bit: value type | second bit: grant shown | first bit: value shown */
2019-08-21 10:00:01 +02:00
private _tag_value: JQuery;
private _tag_value_input: JQuery;
2019-08-21 10:00:01 +02:00
private _tag_granted: JQuery;
private _tag_granted_input: JQuery;
2019-08-21 10:00:01 +02:00
private _tag_skip: JQuery;
private _tag_skip_input: JQuery;
2019-08-21 10:00:01 +02:00
private _tag_negate: JQuery;
private _tag_negate_input: JQuery;
2019-08-21 10:00:01 +02:00
private _value: number | undefined;
private _grant: number | undefined;
private flags: number; /* 0x01 := Skip | 0x02 := Negate */
2019-08-21 10:00:01 +02:00
constructor(handle: HTMLPermissionEditor, group: HTMLPermissionGroup, permission: PermissionInfo, index: number) {
this.handle = handle;
this.permission = permission;
this.index = index;
this.group = group;
2019-08-21 10:00:01 +02:00
this.build_tag();
}
2019-08-21 10:00:01 +02:00
private static build_checkbox() : {tag: JQuery, input: JQuery} {
let tag, input;
tag = $.spawn("label").addClass("switch").append([
input = $.spawn("input").attr("type", "checkbox"),
$.spawn("span").addClass("slider").append(
$.spawn("div").addClass("dot")
)
]);
return {tag: tag, input: input};
}
2019-08-21 10:00:01 +02:00
private static number_filter_re = /^[-+]?([0-9]{0,9})$/;
private static number_filter = (event: KeyDownEvent) => {
if(event.ctrlKey)
return;
2019-08-21 10:00:01 +02:00
const target = <HTMLInputElement>event.target;
if(event.key === "Enter") {
target.blur();
return;
}
2019-08-21 10:00:01 +02:00
if('keyCode' in event) {
/* everything under 46 is a control key except 32 its space */
if(event.keyCode < 46 && event.keyCode != 32)
return;
2019-08-21 10:00:01 +02:00
if(!HTMLPermission.number_filter_re.test(target.value + String.fromCharCode(event.keyCode))) {
event.preventDefault();
return;
}
} else {
const e = <KeyDownEvent>event; /* for some reason typescript deducts the event type to "never" */
if(!HTMLPermission.number_filter_re.test(e.key)) {
e.preventDefault();
return;
}
}
};
private build_tag() {
this.tag = $.spawn("div").addClass("entry permission").css('padding-left', this.index + "em").append([
this.tag_name = $.spawn("div").addClass("column-name").text(this.permission.name),
this.tag_container_value = $.spawn("div").addClass("column-value"),
this.tag_container_skip = $.spawn("div").addClass("column-skip"),
this.tag_container_negate = $.spawn("div").addClass("column-negate"),
this.tag_container_granted = $.spawn("div").addClass("column-granted")
]);
if(this.permission.is_boolean()) {
let value = HTMLPermission.build_checkbox();
this._tag_value = value.tag;
this._tag_value_input = value.input;
this._tag_value_input.on('change', event => {
const value = this._tag_value_input.prop('checked') ? 1 : 0;
this.handle.trigger_change(this.permission, {
remove: false,
value: value,
flag_skip: (this.flags & 0x01) > 0,
flag_negate: (this.flags & 0x02) > 0
}).then(() => {
this._value = value;
}).catch(error => {
this._reset_value();
});
});
2019-08-21 10:00:01 +02:00
this._mask |= 0x04;
} else {
this._tag_value = $.spawn("input").addClass("number");
this._tag_value_input = this._tag_value;
this._tag_value_input.on('keydown', HTMLPermission.number_filter);
this._tag_value_input.on('change', event => {
const str_value = this._tag_value_input.val() as string;
const value = parseInt(str_value);
if(!HTMLPermission.number_filter_re.test(str_value) || value == NaN) {
console.warn(tr("Failed to parse given permission value string: %s"), this._tag_value_input.val());
this._reset_value();
return;
}
this.handle.trigger_change(this.permission, {
remove: false,
value: value,
flag_skip: (this.flags & 0x01) > 0,
flag_negate: (this.flags & 0x02) > 0
}).then(() => {
this._value = value;
this._update_active_class();
}).catch(error => {
this._reset_value();
});
});
}
2019-08-21 10:00:01 +02:00
{
let skip = HTMLPermission.build_checkbox();
this._tag_skip = skip.tag;
this._tag_skip_input = skip.input;
this._tag_skip_input.on('change', event => {
const value = this._tag_skip_input.prop('checked');
this.handle.trigger_change(this.permission, {
remove: false,
value: this._value,
flag_skip: value,
flag_negate: (this.flags & 0x02) > 0
}).then(() => {
if(value)
this.flags |= 0x01;
else
this.flags &= ~0x1;
this._update_active_class();
}).catch(error => {
this._reset_value();
});
});
}
2019-08-21 10:00:01 +02:00
{
let negate = HTMLPermission.build_checkbox();
this._tag_negate = negate.tag;
this._tag_negate_input = negate.input;
this._tag_negate_input.on('change', event => {
const value = this._tag_negate_input.prop('checked');
console.log("Negate value: %o", value);
this.handle.trigger_change(this.permission, {
remove: false,
value: this._value,
flag_skip: (this.flags & 0x01) > 0,
flag_negate: value
}).then(() => {
if(value)
this.flags |= 0x02;
else
this.flags &= ~0x2;
this._update_active_class();
}).catch(error => {
this._reset_value();
});
});
}
2019-08-21 10:00:01 +02:00
{
this._tag_granted = $.spawn("input").addClass("number");
this._tag_granted_input = this._tag_granted;
this._tag_granted_input.on('keydown', HTMLPermission.number_filter);
this._tag_granted_input.on('change', event => {
const str_value = this._tag_granted_input.val() as string;
const value = parseInt(str_value);
if(!HTMLPermission.number_filter_re.test(str_value) || value == NaN) {
console.warn(tr("Failed to parse given permission granted value string: %s"), this._tag_granted_input.val());
this._reset_value();
return;
}
2019-08-21 10:00:01 +02:00
this.handle.trigger_change(this.permission, {
remove: false,
granted: value
}).then(() => {
this._grant = value;
this._update_active_class();
}).catch(error => {
this._reset_grant();
});
});
}
2019-08-21 10:00:01 +02:00
/* double click handler */
{
2019-08-21 10:00:01 +02:00
this.tag.on('dblclick', event => this._trigger_value_assign())
}
2019-08-21 10:00:01 +02:00
/* context menu */
{
2019-08-21 10:00:01 +02:00
this.tag.on('contextmenu', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
2019-08-21 10:00:01 +02:00
let entries: contextmenu.MenuEntry[] = [];
if(typeof(this._value) === "undefined") {
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Add permission"),
callback: () => this._trigger_value_assign()
});
} else {
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Remove permission"),
callback: () => {
this.handle.trigger_change(this.permission, {
remove: true,
value: 0
}).then(() => {
this.value(undefined);
}).catch(error => {
//We have to do nothing
});
}
});
}
if(typeof(this._grant) === "undefined") {
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Add grant permission"),
callback: () => this._trigger_grant_assign()
});
} else {
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Remove grant permission"),
callback: () => {
this.handle.trigger_change(this.permission, {
remove: true,
granted: 0
}).then(() => {
this.granted(undefined);
}).catch(error => {
//We have to do nothing
});
}
});
}
entries.push(contextmenu.Entry.HR());
if(this.group.collapsed)
entries.push({ /* This could never happen! */
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Expend group"),
callback: () => this.group.expend()
});
else
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Collapse group"),
callback: () => this.group.collapse()
});
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Expend all"),
callback: () => this.handle.expend_all()
});
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Collapse all"),
callback: () => this.handle.collapse_all()
});
entries.push(contextmenu.Entry.HR());
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Show permission description"),
callback: () => {
createInfoModal(
tr("Permission description"),
tr("Permission description for permission ") + this.permission.name + ": <br>" + this.permission.description
).open();
}
});
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Copy permission name"),
callback: () => {
copy_to_clipboard(this.permission.name);
}
});
contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries);
});
}
2019-08-21 10:00:01 +02:00
}
2019-08-21 10:00:01 +02:00
private _trigger_value_assign() {
if(typeof(this._value) === "undefined")
this.value(this._grant || 1, false, false); //TODO: Use max granted value?
this._tag_value_input.focus();
if(this.permission.is_boolean())
this._tag_value_input.trigger('change');
}
2019-08-21 10:00:01 +02:00
private _trigger_grant_assign() {
this.granted(1); //TODO: Use max granted value?
this._tag_granted_input.focus();
}
2019-08-21 10:00:01 +02:00
hide() {
this._mask &= ~0x08;
for(const element of this.tag)
(<HTMLElement>element).style.display = 'none';
}
2019-08-21 10:00:01 +02:00
show() {
this._mask |= 0x08;
for(const element of this.tag)
(<HTMLElement>element).style.display = 'flex';
}
2019-08-21 10:00:01 +02:00
is_filtered() : boolean {
return (this._mask & 0x10) > 0;
}
2019-08-21 10:00:01 +02:00
set_filtered(flag: boolean) {
if(flag)
this._mask |= 0x10;
else
this._mask &= ~0x10;
}
2019-08-21 10:00:01 +02:00
is_set() : boolean {
return (this._mask & 0x03) > 0;
}
2019-08-21 10:00:01 +02:00
value(value: number | undefined, skip?: boolean, negate?: boolean) {
if(typeof value === "undefined") {
this._tag_value.detach();
this._tag_negate.detach();
this._tag_skip.detach();
2019-08-21 10:00:01 +02:00
this._value = undefined;
this.flags = 0;
2019-08-21 10:00:01 +02:00
this._update_active_class();
this._mask &= ~0x1;
return;
}
2019-08-21 10:00:01 +02:00
if((this._mask & 0x1) == 0) {
this._tag_value.appendTo(this.tag_container_value);
this._tag_negate.appendTo(this.tag_container_negate);
this._tag_skip.appendTo(this.tag_container_skip);
2019-08-21 10:00:01 +02:00
this._update_active_class();
this._mask |= 0x01;
}
2019-08-21 10:00:01 +02:00
if((this._mask & 0x04) > 0)
this._tag_value_input.prop('checked', !!value);
else
this._tag_value_input.val(value);
this._tag_skip_input.prop('checked', !!skip);
this._tag_negate_input.prop('checked', !!negate);
2019-08-21 10:00:01 +02:00
this._value = value;
this.flags = (!!skip ? 0x01 : 0) | (!!negate ? 0x2 : 0);
}
2019-08-21 10:00:01 +02:00
granted(value: number | undefined) {
if(typeof value === "undefined") {
this._tag_granted.detach();
2019-08-21 10:00:01 +02:00
this._update_active_class();
this._grant = undefined;
this._mask &= ~0x2;
return;
}
2019-08-21 10:00:01 +02:00
if((this._mask & 0x2) == 0) {
this._mask |= 0x02;
this._tag_granted.appendTo(this.tag_container_granted);
this._update_active_class();
}
this._tag_granted_input.val(value);
this._grant = value;
}
2019-08-21 10:00:01 +02:00
reset() {
this._mask &= ~0x03;
2019-08-21 10:00:01 +02:00
this._tag_value.detach();
this._tag_negate.detach();
this._tag_skip.detach();
2019-08-21 10:00:01 +02:00
this._tag_granted.detach();
this._value = undefined;
this._grant = undefined;
this.flags = 0;
const tag = this.tag[0] as HTMLDivElement;
tag.classList.remove("active");
}
private _reset_value() {
if(typeof(this._value) === "undefined") {
if((this._mask & 0x1) != 0)
this.value(undefined);
} else {
this.value(this._value, (this.flags & 0x1) > 1, (this.flags & 0x2) > 1);
}
}
2019-08-21 10:00:01 +02:00
private _reset_grant() {
if(typeof(this._grant) === "undefined") {
if((this._mask & 0x2) != 0)
this.granted(undefined);
} else {
this.granted(this._grant);
}
}
2019-08-21 10:00:01 +02:00
private _update_active_class() {
const value = typeof(this._value) !== "undefined" || typeof(this._grant) !== "undefined";
const tag = this.tag[0] as HTMLDivElement;
if(value)
tag.classList.add("active");
else
tag.classList.remove("active");
}
}
2019-08-21 10:00:01 +02:00
class HTMLPermissionGroup {
readonly handle: HTMLPermissionEditor;
readonly group: PermissionGroup;
readonly index: number;
2019-08-21 10:00:01 +02:00
private _tag_arrow: JQuery;
2019-08-21 10:00:01 +02:00
permissions: HTMLPermission[] = [];
children: HTMLPermissionGroup[] = [];
tag: JQuery;
visible: boolean;
collapsed: boolean;
parent_collapsed: boolean;
constructor(handle: HTMLPermissionEditor, group: PermissionGroup, index: number) {
this.handle = handle;
this.group = group;
this.index = index;
this._build_tag();
}
private _build_tag() {
this.tag = $.spawn("div").addClass("entry group").css('padding-left', this.index + "em").append([
$.spawn("div").addClass("column-name").append([
this._tag_arrow = $.spawn("div").addClass("arrow down"),
$.spawn("div").addClass("group-name").text(this.group.name)
]),
$.spawn("div").addClass("column-value"),
$.spawn("div").addClass("column-skip"),
$.spawn("div").addClass("column-negate"),
$.spawn("div").addClass("column-granted")
]);
this.tag.on('contextmenu', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
const entries: contextmenu.MenuEntry[] = [];
if(this.collapsed)
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Expend group"),
callback: () => this.expend(),
});
else
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Collapse group"),
callback: () => this.collapse(),
});
entries.push(contextmenu.Entry.HR());
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Expend all"),
callback: () => this.handle.expend_all()
});
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Collapse all"),
callback: () => this.handle.collapse_all()
});
contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries);
});
this._tag_arrow.on('click', event => {
if(this.collapsed)
this.expend();
else
this.collapse();
})
}
update_visibility() {
let flag = false;
if (!flag) {
for (const group of this.children) {
if (group.visible) {
flag = true;
break;
}
}
2019-08-21 10:00:01 +02:00
}
if (!flag) {
for (const permission of this.permissions) {
if (!permission.is_filtered()) {
flag = true;
break;
}
}
}
2019-08-21 10:00:01 +02:00
this.visible = flag;
flag = flag && !this.parent_collapsed;
for(const element of this.tag)
(<HTMLElement>element).style.display = flag ? 'flex' : 'none';
const arrow_node = this._tag_arrow[0];
arrow_node.classList.remove(this.collapsed ? "down" : "right");
arrow_node.classList.add(!this.collapsed ? "down" : "right");
}
collapse() {
this.collapsed = true;
const children = [...this.children];
while (true) {
const child = children.pop();
if(!child) break;
child.parent_collapsed = true;
children.push(...child.children);
}
this.handle.update_view();
}
expend() {
this.collapsed = false;
if(this.parent_collapsed)
return;
const children = [...this.children];
while (true) {
const child = children.pop();
if(!child) break;
child.parent_collapsed = false;
if(!child.collapsed)
children.push(...child.children);
}
this.handle.update_view();
}
}
export class HTMLPermissionEditor extends Modals.AbstractPermissionEditor {
container: JQuery;
private mode_container_permissions: JQuery;
private mode_container_error_permission: JQuery;
private mode_container_unset: JQuery;
private filter_input: JQuery;
private filter_grant: JQuery;
private button_toggle: JQuery;
private even_list: ({ visible() : boolean; set_even(flag: boolean); })[];
private permission_map: Array<HTMLPermission>;
private permission_groups: HTMLPermissionGroup[];
constructor() {
super();
}
initialize(permissions: GroupedPermissions[]) {
this._permissions = permissions;
this.build_tag();
}
private update_filter() {
const value = (this.filter_input.val() as string).toLowerCase();
const grant = !!this.filter_grant.prop('checked');
const _filter = (permission: HTMLPermission) => {
if(value && permission.permission.name.indexOf(value) == -1) return false;
if(grant && !permission.is_set()) return false;
return true;
};
for(let id = 1; id < this.permission_map.length; id++) {
const permission = this.permission_map[id];
let flag = _filter(permission);
permission.set_filtered(!flag);
flag = flag && !permission.group.collapsed && !permission.group.parent_collapsed; /* hide when parent is filtered */
if(flag) permission.show();
else permission.hide();
}
/* run in both directions, to update the parent visibility and the actiual visibility */
for(const group of this.permission_groups)
group.update_visibility();
for(const group of this.permission_groups.slice().reverse())
group.update_visibility();
let index = 0;
for(const entry of this.even_list) {
if(!entry.visible()) continue;
entry.set_even((index++ & 0x1) == 0);
}
}
2019-08-21 10:00:01 +02:00
private build_tag() {
this.container = $("#tmpl_permission_editor_html").renderTag();
this.container.find("input").on('change', event => {
$(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value);
});
/* search for that as long we've not that much nodes */
this.mode_container_permissions = this.container.find(".container-mode-permissions");
this.mode_container_error_permission = this.container.find(".container-mode-no-permissions");
this.mode_container_unset = this.container.find(".container-mode-unset");
this.filter_input = this.container.find(".filter-input");
this.filter_input.on('change keyup', event => this.update_filter());
this.filter_grant = this.container.find(".filter-granted");
this.filter_grant.on('change', event => this.update_filter());
this.button_toggle = this.container.find(".button-toggle-clients");
this.button_toggle.on('click', () => {
if(this._toggle_callback)
this.button_toggle.text(this._toggle_callback());
});
this.container.find(".button-update").on('click', event => this.trigger_update());
/* allocate array space */
{
let max_index = 0;
let tmp: GroupedPermissions[] = [];
while(true) {
const entry = tmp.pop();
if(!entry) break;
for(const permission of entry.permissions)
if(permission.id > max_index)
max_index = permission.id;
tmp.push(...entry.children);
}
this.permission_map = new Array(max_index + 1);
}
this.permission_groups = [];
this.even_list = [];
{
const container_permission = this.mode_container_permissions.find(".container-permission-list .body");
const build_group = (pgroup: HTMLPermissionGroup, group: GroupedPermissions, index: number) => {
const hgroup = new HTMLPermissionGroup(this, group.group, index);
hgroup.tag.appendTo(container_permission);
this.even_list.push({
set_even(flag: boolean) {
if(flag)
hgroup.tag[0].classList.add('even');
else
hgroup.tag[0].classList.remove('even');
},
visible(): boolean {
return !hgroup.parent_collapsed && hgroup.visible;
}
});
if(pgroup)
pgroup.children.push(hgroup);
this.permission_groups.push(hgroup);
index++;
for(const child of group.children)
build_group(hgroup, child, index);
for(const permission of group.permissions) {
const perm = new HTMLPermission(this, hgroup, permission, index);
this.permission_map[perm.permission.id] = perm;
perm.tag.appendTo(container_permission);
hgroup.permissions.push(perm);
this.even_list.push({
set_even(flag: boolean) {
if(flag)
perm.tag[0].classList.add('even');
else
perm.tag[0].classList.remove('even');
},
visible(): boolean {
return !perm.is_filtered() && !perm.group.collapsed && !perm.group.parent_collapsed;
}
});
}
};
for(const group of this._permissions)
build_group(undefined, group, 0);
}
this.mode_container_permissions.on('contextmenu', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
const entries: contextmenu.MenuEntry[] = [];
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Expend all"),
callback: () => this.expend_all()
});
entries.push({
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Collapse all"),
callback: () => this.collapse_all()
});
contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries);
});
this.set_mode(PermissionEditorMode.UNSET);
}
2019-08-21 10:00:01 +02:00
html_tag(): JQuery<HTMLElement> {
return this.container;
}
2019-08-21 10:00:01 +02:00
set_permissions(u_permissions?: PermissionValue[]) {
const permissions = new Array<PermissionValue>(this.permission_map.length);
/* initialize update array, boundary checks are already made by js */
for(const perm of u_permissions)
permissions[perm.type.id] = perm;
/* there is no permission with id 0 */
for(let id = 1; id < permissions.length; id++) {
const new_permission = permissions[id];
const permission_handle = this.permission_map[id];
if(!new_permission) {
permission_handle.reset();
continue;
}
permission_handle.value(new_permission.value, new_permission.flag_skip, new_permission.flag_negate);
permission_handle.granted(new_permission.granted_value);
}
this.update_filter();
}
set_mode(mode: PermissionEditorMode) {
this.mode_container_permissions.css('display', mode == PermissionEditorMode.VISIBLE ? 'flex' : 'none');
this.mode_container_error_permission.css('display', mode == PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none');
this.mode_container_unset.css('display', mode == PermissionEditorMode.UNSET ? 'block' : 'none');
}
2019-08-21 10:00:01 +02:00
trigger_change(permission: PermissionInfo, value?: Modals.PermissionEditor.PermissionValue) : Promise<void> {
if(this._listener_change)
return this._listener_change(permission, value);
return Promise.reject();
}
collapse_all() {
for(const group of this.permission_groups) {
group.collapsed = true;
for(const child of group.children)
child.parent_collapsed = true;
}
this.update_filter(); /* update display state of all entries */
}
expend_all() {
for(const group of this.permission_groups) {
group.collapsed = false;
group.parent_collapsed = false;
}
this.update_filter(); /* update display state of all entries */
}
update_view() { return this.update_filter(); }
set_toggle_button(callback: () => string, initial: string) {
this._toggle_callback = callback;
if(this._toggle_callback) {
this.button_toggle.text(initial);
this.button_toggle.show();
} else {
this.button_toggle.hide();
}
}
}
}