diff --git a/shared/css/static/modal-permissions.scss b/shared/css/static/modal-permissions.scss
index 6eb5ebac..9c15ecf0 100644
--- a/shared/css/static/modal-permissions.scss
+++ b/shared/css/static/modal-permissions.scss
@@ -64,6 +64,7 @@ permission-editor {
}
}
+ /* legacy */
.entries {
flex-grow: 1;
@@ -252,6 +253,16 @@ permission-editor {
}
}
+ .entry-editor-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ overflow-y: auto;
+
+ min-height: 100px;
+ min-width: 100px;
+ }
+
.container-footer {
margin-top: 10px;
diff --git a/shared/html/templates.html b/shared/html/templates.html
index 9263f51a..312dbd1e 100644
--- a/shared/html/templates.html
+++ b/shared/html/templates.html
@@ -1835,7 +1835,7 @@
{{tr "Granted" /}}
-
+
diff --git a/shared/js/load.ts b/shared/js/load.ts
index 74313c8d..7d7a08c6 100644
--- a/shared/js/load.ts
+++ b/shared/js/load.ts
@@ -570,8 +570,9 @@ const loader_javascript = {
"js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPoke.js",
- "js/ui/modal/ModalPermissionEdit.js",
"js/ui/modal/ModalServerGroupDialog.js",
+ "js/ui/modal/permission/ModalPermissionEdit.js",
+ "js/ui/modal/permission/PermissionEditor.js",
"js/ui/channel.js",
"js/ui/client.js",
diff --git a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts
new file mode 100644
index 00000000..bb3c95c8
--- /dev/null
+++ b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts
@@ -0,0 +1,530 @@
+/**
+ * RIP old HTML based editor (too many nodes, made the browser laggy)
+ */
+namespace unused {
+ namespace PermissionEditor {
+ export interface PermissionEntry {
+ tag: JQuery;
+ tag_value: JQuery;
+ tag_grant: JQuery;
+ tag_flag_negate: JQuery;
+ tag_flag_skip: JQuery;
+
+ id: number;
+ filter: string;
+ is_bool: boolean;
+
+ }
+
+ export interface PermissionValue {
+ remove: boolean; /* if set remove the set permission (value or granted) */
+
+ granted?: number;
+ value?: number;
+
+ flag_skip?: boolean;
+ flag_negate?: boolean;
+ }
+
+ export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise
;
+ }
+
+ enum PermissionEditorMode {
+ VISIBLE,
+ NO_PERMISSION,
+ UNSET
+ }
+
+ class PermissionEditor {
+ readonly permissions: GroupedPermissions[];
+
+ container: JQuery;
+
+ private mode_container_permissions: JQuery;
+ private mode_container_error_permission: JQuery;
+ private mode_container_unset: JQuery;
+
+ /* references within the container tag */
+ private permission_value_map: {[key:number]:PermissionValue} = {};
+ private permission_map: {[key:number]: PermissionEditor.PermissionEntry};
+ private listener_change: PermissionEditor.change_listener_t = () => Promise.resolve();
+ private listener_update: () => any;
+
+ constructor(permissions: GroupedPermissions[]) {
+ this.permissions = permissions;
+ }
+
+ build_tag() {
+ this.permission_map = {};
+
+ this.container = $("#tmpl_permission_editor").renderTag();
+ /* 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.set_mode(PermissionEditorMode.UNSET);
+
+ /* the filter */
+ {
+ const tag_filter_input = this.container.find(".filter-input");
+ const tag_filter_granted = this.container.find(".filter-granted");
+
+ tag_filter_granted.on('change', event => tag_filter_input.trigger('change'));
+ tag_filter_input.on('keyup change', event => {
+ let filter_mask = tag_filter_input.val() as string;
+ let req_granted = tag_filter_granted.prop("checked");
+
+ /* we've to disable this function because its sometimes laggy */
+ const org_fn = $.fn.dropdown && $.fn.dropdown.Constructor ? $.fn.dropdown.Constructor._clearMenus : undefined;
+ if(org_fn)
+ $.fn.dropdown.Constructor._clearMenus = () => {};
+
+ /* update each permission */
+ {
+ const start = Date.now();
+
+ for(const permission_id of Object.keys(this.permission_map)) {
+ const permission: PermissionEditor.PermissionEntry = this.permission_map[permission_id];
+ let shown = filter_mask.length == 0 || permission.filter.indexOf(filter_mask) != -1;
+ if(shown && req_granted) {
+ const value: PermissionValue = this.permission_value_map[permission_id];
+ shown = value && (value.hasValue() || value.hasGrant());
+ }
+
+ permission.tag.attr("match", shown ? 1 : 0);
+ /* this is faster then .hide() or .show() */
+ if(shown)
+ permission.tag.css('display', 'flex');
+ else
+ permission.tag.css('display', 'none');
+ }
+
+ const end = Date.now();
+ console.error("Filter update required %oms", end - start);
+ }
+
+ /* update group visibility (hide empty groups) */
+ {
+ const start = Date.now();
+
+ this.container.find(".group").each((idx, _entry) => {
+ let entry = $(_entry);
+ let target = entry.find(".entry:not(.group)[match=\"1\"]").length > 0;
+ /* this is faster then .hide() or .show() */
+ if(target)
+ entry.css('display', 'flex');
+ else
+ entry.css('display', 'none');
+ });
+
+ const end = Date.now();
+ console.error("Group update required %oms", end - start);
+ }
+
+ if(org_fn)
+ $.fn.dropdown.Constructor._clearMenus = org_fn;
+ });
+ }
+
+ /* update button */
+ {
+ this.container.find(".button-update").on('click', this.trigger_update.bind(this));
+ }
+
+ /* global context menu listener */
+ {
+ this.container.on('contextmenu', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ /* TODO allow collapse and expend all */
+ });
+ }
+
+ /* actual permissions */
+ {
+ const tag_entries = this.container.find(".entries");
+
+ const template_entry = $("#tmpl_permission_entry");
+ const build_group = (group: GroupedPermissions) : JQuery => {
+ const tag_group = template_entry.renderTag({
+ type: "group",
+ name: group.group.name
+ });
+ const tag_group_entries = tag_group.find(".group-entries");
+
+ const update_collapse_status = (status: boolean, recursive: boolean) => {
+ const tag = recursive ? this.container.find(".entry.group") : tag_group;
+
+ /* this is faster then .hide() or .show() */
+ if(status) {
+ tag.find("> .group-entries").css('display', 'block');
+ } else {
+ tag.find("> .group-entries").css('display', 'none');
+ }
+
+ tag.find("> .title .arrow").toggleClass("down", status).toggleClass("right", !status);
+ };
+
+ /* register collapse and context listener */
+ {
+ const tag_arrow = tag_group.find(".arrow");
+ tag_arrow.on('click', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ update_collapse_status(tag_arrow.hasClass("right"), false);
+ });
+
+ const tag_title = tag_group.find(".title");
+ tag_title.on('contextmenu', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ spawn_context_menu(event.pageX, event.pageY, {
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Expend group"),
+ callback: () => update_collapse_status(true, false)
+ }, {
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Expend all"),
+ callback: () => update_collapse_status(true, true)
+ }, {
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Collapse group"),
+ callback: () => update_collapse_status(false, false)
+ }, {
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Collapse all"),
+ callback: () => update_collapse_status(false, true)
+ });
+ });
+ }
+
+ /* build the permissions */
+ {
+ for(const permission of group.permissions) {
+ const tag_permission = template_entry.renderTag({
+ type: "permission",
+ permission_name: permission.name,
+ permission_id: permission.id,
+ permission_description: permission.description,
+ });
+
+ const tag_value = tag_permission.find(".column-value input");
+ const tag_granted = tag_permission.find(".column-granted input");
+ const tag_flag_skip = tag_permission.find(".column-skip input");
+ const tag_flag_negate = tag_permission.find(".column-negate input");
+
+ /* double click listener */
+ {
+ tag_permission.on('dblclick', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ if(tag_permission.hasClass("value-unset")) {
+ tag_flag_skip.prop("checked", false);
+ tag_flag_negate.prop("checked", false);
+
+ tag_permission.removeClass("value-unset");
+ if(permission.name.startsWith("b_")) {
+ tag_permission.find(".column-value input")
+ .prop("checked", true)
+ .trigger('change');
+ } else {
+ /* TODO auto value */
+ tag_value.val('').focus();
+ }
+ } else if(!permission.name.startsWith("b_")) {
+ tag_value.focus();
+ }
+ });
+
+ tag_permission.find(".column-granted").on('dblclick', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ if(tag_permission.hasClass("grant-unset")) {
+ tag_permission.removeClass("grant-unset");
+ tag_granted.focus();
+ }
+ });
+ }
+
+ /* focus out listener */
+ {
+ tag_granted.on('focusout', event => {
+ try {
+ const value = tag_granted.val() as string;
+ if(isNaN(parseInt(value)))
+ throw "";
+ } catch(_) {
+ tag_granted.val("");
+ tag_permission.addClass("grant-unset");
+
+ const element = this.permission_value_map[permission.id];
+ if(element && element.hasGrant()) {
+ this.listener_change(permission, {
+ remove: true,
+ granted: -2
+ }).then(() => {
+ element.granted_value = undefined;
+ }).catch(() => {
+ tag_granted.val(element.granted_value);
+ });
+ }
+ }
+ });
+
+ tag_value.on('focusout', event => {
+ try {
+ if(isNaN(parseInt(tag_value.val() as string)))
+ throw "";
+ } catch(_) {
+ const element = this.permission_value_map[permission.id];
+ if(element && element.hasValue()) {
+ tag_value.val(element.value);
+ } else {
+ tag_value.val("");
+ tag_permission.addClass("value-unset");
+ }
+ }
+ })
+ }
+
+ /* change listener */
+ {
+ tag_flag_negate.on('change', () => tag_value.trigger('change'));
+ tag_flag_skip.on('change', () => tag_value.trigger('change'));
+
+ tag_granted.on('change', event => {
+ const value = parseInt(tag_granted.val() as string);
+ if(isNaN(value)) return;
+
+ this.listener_change(permission, {
+ remove: false,
+ granted: value,
+ }).then(() => {
+ const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission));
+ element.granted_value = value;
+ }).catch(() => {
+ const element = this.permission_value_map[permission.id];
+ tag_granted.val(element && element.hasGrant() ? element.granted_value : "");
+ tag_permission.toggleClass("grant-unset", !element || !element.hasGrant());
+ });
+ });
+
+ tag_value.on('change', event => {
+ const value = permission.is_boolean() ? tag_value.prop("checked") ? 1 : 0 : parseInt(tag_value.val() as string);
+ if(isNaN(value)) return;
+
+ const flag_negate = tag_flag_negate.prop("checked");
+ const flag_skip = tag_flag_skip.prop("checked");
+
+ this.listener_change(permission, {
+ remove: false,
+ value: value,
+ flag_negate: flag_negate,
+ flag_skip: flag_skip
+ }).then(() => {
+ const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission));
+
+ element.value = value;
+ element.flag_skip = flag_skip;
+ element.flag_negate = flag_negate;
+ }).catch(error => {
+ const element = this.permission_value_map[permission.id];
+
+ /* reset or set the fields */
+ if(permission.is_boolean())
+ tag_value.prop('checked', element && element.hasValue() && element.value > 0);
+ else
+ tag_value.val(element && element.hasValue() ? element.value : "");
+ tag_flag_negate.prop("checked", element && element.flag_negate);
+ tag_flag_skip.prop("checked", element && element.flag_skip);
+ tag_permission.toggleClass("value-unset", !element || !element.hasValue());
+ });
+ });
+ }
+
+ /* context menu */
+ {
+ tag_permission.on('contextmenu', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ let entries: ContextMenuEntry[] = [];
+ if(tag_permission.hasClass("value-unset")) {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Add permission"),
+ callback: () => tag_permission.trigger('dblclick')
+ });
+ } else {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Remove permission"),
+ callback: () => {
+ this.listener_change(permission, {
+ remove: true,
+ value: -2
+ }).then(() => {
+ const element = this.permission_value_map[permission.id];
+ if(!element) return; /* This should never happen, if so how are we displaying this permission?! */
+
+ element.value = undefined;
+ element.flag_negate = false;
+ element.flag_skip = false;
+
+ tag_permission.toggleClass("value-unset", true);
+ }).catch(() => {
+ const element = this.permission_value_map[permission.id];
+
+ /* reset or set the fields */
+ tag_value.val(element && element.hasValue() ? element.value : "");
+ tag_flag_negate.prop("checked", element && element.flag_negate);
+ tag_flag_skip.prop("checked", element && element.flag_skip);
+ tag_permission.toggleClass("value-unset", !element || !element.hasValue());
+ });
+ }
+ });
+ }
+
+ if(tag_permission.hasClass("grant-unset")) {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Add grant permission"),
+ callback: () => tag_permission.find(".column-granted").trigger('dblclick')
+ });
+ } else {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Remove grant permission"),
+ callback: () =>
+ tag_granted.val('').trigger('focusout') /* empty values are handled within focus out */
+ });
+ }
+ entries.push(MenuEntry.HR());
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Expend all"),
+ callback: () => update_collapse_status(true, true)
+ });
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Collapse all"),
+ callback: () => update_collapse_status(false, true)
+ });
+ entries.push(MenuEntry.HR());
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Show permission description"),
+ callback: () => {
+ createInfoModal(
+ tr("Permission description"),
+ tr("Permission description for permission ") + permission.name + ":
" + permission.description
+ ).open();
+ }
+ });
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Copy permission name"),
+ callback: () => {
+ copy_to_clipboard(permission.name);
+ }
+ });
+
+ spawn_context_menu(event.pageX, event.pageY, ...entries);
+ });
+ }
+
+ this.permission_map[permission.id] = {
+ tag: tag_permission,
+ id: permission.id,
+ filter: permission.name,
+ tag_flag_negate: tag_flag_negate,
+ tag_flag_skip: tag_flag_skip,
+ tag_grant: tag_granted,
+ tag_value: tag_value,
+ is_bool: permission.is_boolean()
+ };
+
+ tag_group_entries.append(tag_permission);
+ }
+ }
+
+ /* append the subgroups */
+ for(const child of group.children) {
+ tag_group_entries.append(build_group(child));
+ }
+
+ return tag_group;
+ };
+
+ /* build the groups */
+ for(const group of this.permissions)
+ tag_entries.append(build_group(group));
+ }
+ }
+
+ set_permissions(permissions?: PermissionValue[]) {
+ permissions = permissions || [];
+ this.permission_value_map = {};
+
+ for(const permission of permissions)
+ this.permission_value_map[permission.type.id] = permission;
+
+ for(const permission_id of Object.keys(this.permission_map)) {
+ const permission: PermissionEditor.PermissionEntry = this.permission_map[permission_id];
+ const value: PermissionValue = this.permission_value_map[permission_id];
+
+ permission.tag
+ .toggleClass("value-unset", !value || !value.hasValue())
+ .toggleClass("grant-unset", !value || !value.hasGrant());
+
+ if(value && value.hasValue()) {
+ if(value.type.is_boolean())
+ permission.tag_value.prop("checked", value.value);
+ else
+ permission.tag_value.val(value.value);
+ permission.tag_flag_skip.prop("checked", value.flag_skip);
+ permission.tag_flag_negate.prop("checked", value.flag_negate);
+ }
+ if(value && value.hasGrant()) {
+ permission.tag_grant.val(value.granted_value);
+ }
+ }
+ }
+
+ set_listener(listener?: PermissionEditor.change_listener_t) {
+ this.listener_change = listener || (() => Promise.resolve());
+ }
+
+ set_listener_update(listener?: () => any) {
+ this.listener_update = listener;
+ }
+
+ trigger_update() {
+ if(this.listener_update)
+ this.listener_update();
+ }
+
+ 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');
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/js/ui/modal/ModalPermissionEdit.ts b/shared/js/ui/modal/permission/ModalPermissionEdit.ts
similarity index 64%
rename from shared/js/ui/modal/ModalPermissionEdit.ts
rename to shared/js/ui/modal/permission/ModalPermissionEdit.ts
index 4e1daae0..9712b53c 100644
--- a/shared/js/ui/modal/ModalPermissionEdit.ts
+++ b/shared/js/ui/modal/permission/ModalPermissionEdit.ts
@@ -60,17 +60,17 @@ namespace Modals {
/* references within the container tag */
private permission_value_map: {[key:number]:PermissionValue} = {};
- private permission_map: {[key:number]: PermissionEditor.PermissionEntry};
private listener_change: PermissionEditor.change_listener_t = () => Promise.resolve();
private listener_update: () => any;
+ private entry_editor: ui.PermissionEditor;
+
constructor(permissions: GroupedPermissions[]) {
this.permissions = permissions;
+ this.entry_editor = new ui.PermissionEditor(permissions);
}
build_tag() {
- this.permission_map = {};
-
this.container = $("#tmpl_permission_editor").renderTag();
/* search for that as long we've not that much nodes */
this.mode_container_permissions = this.container.find(".container-mode-permissions");
@@ -88,55 +88,19 @@ namespace Modals {
let filter_mask = tag_filter_input.val() as string;
let req_granted = tag_filter_granted.prop("checked");
- /* we've to disable this function because its sometimes laggy */
- const org_fn = $.fn.dropdown && $.fn.dropdown.Constructor ? $.fn.dropdown.Constructor._clearMenus : undefined;
- if(org_fn)
- $.fn.dropdown.Constructor._clearMenus = () => {};
- /* update each permission */
- {
- const start = Date.now();
+ for(const entry of this.entry_editor.permission_entries()) {
+ const permission = entry.permission();
- for(const permission_id of Object.keys(this.permission_map)) {
- const permission: PermissionEditor.PermissionEntry = this.permission_map[permission_id];
- let shown = filter_mask.length == 0 || permission.filter.indexOf(filter_mask) != -1;
- if(shown && req_granted) {
- const value: PermissionValue = this.permission_value_map[permission_id];
- shown = value && (value.hasValue() || value.hasGrant());
- }
-
- permission.tag.attr("match", shown ? 1 : 0);
- /* this is faster then .hide() or .show() */
- if(shown)
- permission.tag.css('display', 'flex');
- else
- permission.tag.css('display', 'none');
+ let shown = filter_mask.length == 0 || permission.name.indexOf(filter_mask) != -1;
+ if(shown && req_granted) {
+ const value: PermissionValue = this.permission_value_map[permission.id];
+ shown = value && (value.hasValue() || value.hasGrant());
}
- const end = Date.now();
- console.error("Filter update required %oms", end - start);
+ entry.hidden = !shown;
}
-
- /* update group visibility (hide empty groups) */
- {
- const start = Date.now();
-
- this.container.find(".group").each((idx, _entry) => {
- let entry = $(_entry);
- let target = entry.find(".entry:not(.group)[match=\"1\"]").length > 0;
- /* this is faster then .hide() or .show() */
- if(target)
- entry.css('display', 'flex');
- else
- entry.css('display', 'none');
- });
-
- const end = Date.now();
- console.error("Group update required %oms", end - start);
- }
-
- if(org_fn)
- $.fn.dropdown.Constructor._clearMenus = org_fn;
+ this.entry_editor.request_draw(true);
});
}
@@ -155,341 +119,169 @@ namespace Modals {
});
}
- /* actual permissions */
{
- const tag_entries = this.container.find(".entries");
+ const tag_container = this.container.find(".entry-editor-container");
+ tag_container.append(this.entry_editor.canvas_container);
- const template_entry = $("#tmpl_permission_entry");
- const build_group = (group: GroupedPermissions) : JQuery => {
- const tag_group = template_entry.renderTag({
- type: "group",
- name: group.group.name
+ tag_container.parent().on('contextmenu', event => {
+ if(event.isDefaultPrevented()) return;
+ event.preventDefault();
+
+ spawn_context_menu(event.pageX, event.pageY, {
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Expend all"),
+ callback: () => this.entry_editor.expend_all()
+ }, {
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Collapse all"),
+ callback: () => this.entry_editor.collapse_all()
});
- const tag_group_entries = tag_group.find(".group-entries");
+ });
+ }
- const update_collapse_status = (status: boolean, recursive: boolean) => {
- const tag = recursive ? this.container.find(".entry.group") : tag_group;
+ /* setup the permissions */
+ for(const entry of this.entry_editor.permission_entries()) {
+ const permission = entry.permission();
+ entry.on_change = () => {
+ const flag_remove = typeof(entry.value) !== "number";
+ this.listener_change(permission, {
+ remove: flag_remove,
+ flag_negate: entry.flag_negate,
+ flag_skip: entry.flag_skip,
+ value: flag_remove ? -2 : entry.value
+ }).then(() => {
+ if(flag_remove) {
+ const element = this.permission_value_map[permission.id];
+ if(!element) return; /* This should never happen, if so how are we displaying this permission?! */
- /* this is faster then .hide() or .show() */
- if(status) {
- tag.find("> .group-entries").css('display', 'block');
+ element.value = undefined;
+ element.flag_negate = false;
+ element.flag_skip = false;
} else {
- tag.find("> .group-entries").css('display', 'none');
+ const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission));
+
+ element.value = entry.value;
+ element.flag_skip = entry.flag_skip;
+ element.flag_negate = entry.flag_negate;
}
- tag.find("> .title .arrow").toggleClass("down", status).toggleClass("right", !status);
- };
+ entry.request_full_draw();
+ this.entry_editor.request_draw(false);
+ }).catch(() => {
+ const element = this.permission_value_map[permission.id];
- /* register collapse and context listener */
- {
- const tag_arrow = tag_group.find(".arrow");
- tag_arrow.on('click', event => {
- if(event.isDefaultPrevented()) return;
- event.preventDefault();
+ entry.value = element && element.hasValue() ? element.value : undefined;
+ entry.flag_skip = element && element.flag_skip;
+ entry.flag_negate = element && element.flag_negate;
- update_collapse_status(tag_arrow.hasClass("right"), false);
- });
-
- const tag_title = tag_group.find(".title");
- tag_title.on('contextmenu', event => {
- if(event.isDefaultPrevented()) return;
- event.preventDefault();
-
- spawn_context_menu(event.pageX, event.pageY, {
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Expend group"),
- callback: () => update_collapse_status(true, false)
- }, {
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Expend all"),
- callback: () => update_collapse_status(true, true)
- }, {
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Collapse group"),
- callback: () => update_collapse_status(false, false)
- }, {
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Collapse all"),
- callback: () => update_collapse_status(false, true)
- });
- });
- }
-
- /* build the permissions */
- {
- for(const permission of group.permissions) {
- const tag_permission = template_entry.renderTag({
- type: "permission",
- permission_name: permission.name,
- permission_id: permission.id,
- permission_description: permission.description,
- });
-
- const tag_value = tag_permission.find(".column-value input");
- const tag_granted = tag_permission.find(".column-granted input");
- const tag_flag_skip = tag_permission.find(".column-skip input");
- const tag_flag_negate = tag_permission.find(".column-negate input");
-
- /* double click listener */
- {
- tag_permission.on('dblclick', event => {
- if(event.isDefaultPrevented()) return;
- event.preventDefault();
-
- if(tag_permission.hasClass("value-unset")) {
- tag_flag_skip.prop("checked", false);
- tag_flag_negate.prop("checked", false);
-
- tag_permission.removeClass("value-unset");
- if(permission.name.startsWith("b_")) {
- tag_permission.find(".column-value input")
- .prop("checked", true)
- .trigger('change');
- } else {
- /* TODO auto value */
- tag_value.val('').focus();
- }
- } else if(!permission.name.startsWith("b_")) {
- tag_value.focus();
- }
- });
-
- tag_permission.find(".column-granted").on('dblclick', event => {
- if(event.isDefaultPrevented()) return;
- event.preventDefault();
-
- if(tag_permission.hasClass("grant-unset")) {
- tag_permission.removeClass("grant-unset");
- tag_granted.focus();
- }
- });
- }
-
- /* focus out listener */
- {
- tag_granted.on('focusout', event => {
- try {
- const value = tag_granted.val() as string;
- if(isNaN(parseInt(value)))
- throw "";
- } catch(_) {
- tag_granted.val("");
- tag_permission.addClass("grant-unset");
-
- const element = this.permission_value_map[permission.id];
- if(element && element.hasGrant()) {
- this.listener_change(permission, {
- remove: true,
- granted: -2
- }).then(() => {
- element.granted_value = undefined;
- }).catch(() => {
- tag_granted.val(element.granted_value);
- });
- }
- }
- });
-
- tag_value.on('focusout', event => {
- try {
- if(isNaN(parseInt(tag_value.val() as string)))
- throw "";
- } catch(_) {
- const element = this.permission_value_map[permission.id];
- if(element && element.hasValue()) {
- tag_value.val(element.value);
- } else {
- tag_value.val("");
- tag_permission.addClass("value-unset");
- }
- }
- })
- }
-
- /* change listener */
- {
- tag_flag_negate.on('change', () => tag_value.trigger('change'));
- tag_flag_skip.on('change', () => tag_value.trigger('change'));
-
- tag_granted.on('change', event => {
- const value = parseInt(tag_granted.val() as string);
- if(isNaN(value)) return;
-
- this.listener_change(permission, {
- remove: false,
- granted: value,
- }).then(() => {
- const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission));
- element.granted_value = value;
- }).catch(() => {
- const element = this.permission_value_map[permission.id];
- tag_granted.val(element && element.hasGrant() ? element.granted_value : "");
- tag_permission.toggleClass("grant-unset", !element || !element.hasGrant());
- });
- });
-
- tag_value.on('change', event => {
- const value = permission.is_boolean() ? tag_value.prop("checked") ? 1 : 0 : parseInt(tag_value.val() as string);
- if(isNaN(value)) return;
-
- const flag_negate = tag_flag_negate.prop("checked");
- const flag_skip = tag_flag_skip.prop("checked");
-
- this.listener_change(permission, {
- remove: false,
- value: value,
- flag_negate: flag_negate,
- flag_skip: flag_skip
- }).then(() => {
- const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission));
-
- element.value = value;
- element.flag_skip = flag_skip;
- element.flag_negate = flag_negate;
- }).catch(error => {
- const element = this.permission_value_map[permission.id];
-
- /* reset or set the fields */
- if(permission.is_boolean())
- tag_value.prop('checked', element && element.hasValue() && element.value > 0);
- else
- tag_value.val(element && element.hasValue() ? element.value : "");
- tag_flag_negate.prop("checked", element && element.flag_negate);
- tag_flag_skip.prop("checked", element && element.flag_skip);
- tag_permission.toggleClass("value-unset", !element || !element.hasValue());
- });
- });
- }
-
- /* context menu */
- {
- tag_permission.on('contextmenu', event => {
- if(event.isDefaultPrevented()) return;
- event.preventDefault();
-
- let entries: ContextMenuEntry[] = [];
- if(tag_permission.hasClass("value-unset")) {
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Add permission"),
- callback: () => tag_permission.trigger('dblclick')
- });
- } else {
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Remove permission"),
- callback: () => {
- this.listener_change(permission, {
- remove: true,
- value: -2
- }).then(() => {
- const element = this.permission_value_map[permission.id];
- if(!element) return; /* This should never happen, if so how are we displaying this permission?! */
-
- element.value = undefined;
- element.flag_negate = false;
- element.flag_skip = false;
-
- tag_permission.toggleClass("value-unset", true);
- }).catch(() => {
- const element = this.permission_value_map[permission.id];
-
- /* reset or set the fields */
- tag_value.val(element && element.hasValue() ? element.value : "");
- tag_flag_negate.prop("checked", element && element.flag_negate);
- tag_flag_skip.prop("checked", element && element.flag_skip);
- tag_permission.toggleClass("value-unset", !element || !element.hasValue());
- });
- }
- });
- }
-
- if(tag_permission.hasClass("grant-unset")) {
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Add grant permission"),
- callback: () => tag_permission.find(".column-granted").trigger('dblclick')
- });
- } else {
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Remove grant permission"),
- callback: () =>
- tag_granted.val('').trigger('focusout') /* empty values are handled within focus out */
- });
- }
- entries.push(MenuEntry.HR());
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Expend all"),
- callback: () => update_collapse_status(true, true)
- });
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Collapse all"),
- callback: () => update_collapse_status(false, true)
- });
- entries.push(MenuEntry.HR());
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Show permission description"),
- callback: () => {
- createInfoModal(
- tr("Permission description"),
- tr("Permission description for permission ") + permission.name + ":
" + permission.description
- ).open();
- }
- });
- entries.push({
- type: MenuEntryType.ENTRY,
- icon: "",
- name: tr("Copy permission name"),
- callback: () => {
- copy_to_clipboard(permission.name);
- }
- });
-
- spawn_context_menu(event.pageX, event.pageY, ...entries);
- });
- }
-
- this.permission_map[permission.id] = {
- tag: tag_permission,
- id: permission.id,
- filter: permission.name,
- tag_flag_negate: tag_flag_negate,
- tag_flag_skip: tag_flag_skip,
- tag_grant: tag_granted,
- tag_value: tag_value,
- is_bool: permission.is_boolean()
- };
-
- tag_group_entries.append(tag_permission);
- }
- }
-
- /* append the subgroups */
- for(const child of group.children) {
- tag_group_entries.append(build_group(child));
- }
-
- return tag_group;
+ entry.request_full_draw();
+ this.entry_editor.request_draw(false);
+ });
};
- /* build the groups */
- for(const group of this.permissions)
- tag_entries.append(build_group(group));
+ entry.on_grant_change = () => {
+ const flag_remove = typeof(entry.granted) !== "number";
+
+ this.listener_change(permission, {
+ remove: flag_remove,
+ granted: flag_remove ? -2 : entry.granted,
+ }).then(() => {
+ if(flag_remove) {
+ const element = this.permission_value_map[permission.id];
+ if (!element) return; /* This should never happen, if so how are we displaying this permission?! */
+
+ element.granted_value = undefined;
+ } else {
+ const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission));
+ element.granted_value = entry.granted;
+ }
+ this.entry_editor.request_draw(false);
+ }).catch(() => {
+ const element = this.permission_value_map[permission.id];
+
+ entry.granted = element && element.hasGrant() ? element.granted_value : undefined;
+ entry.request_full_draw();
+ this.entry_editor.request_draw(false);
+ });
+ };
+
+ entry.on_context_menu = (x, y) => {
+ let entries: ContextMenuEntry[] = [];
+ if(typeof(entry.value) === "undefined") {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Add permission"),
+ callback: () => entry.trigger_value_assign()
+ });
+ } else {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Remove permission"),
+ callback: () => {
+ entry.value = undefined;
+ entry.on_change();
+ }
+ });
+ }
+
+ if(typeof(entry.granted) === "undefined") {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Add grant permission"),
+ callback: () => entry.trigger_grant_assign()
+ });
+ } else {
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Remove grant permission"),
+ callback: () => {
+ entry.granted = undefined;
+ entry.on_grant_change();
+ }
+ });
+ }
+ entries.push(MenuEntry.HR());
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Expend all"),
+ callback: () => this.entry_editor.expend_all()
+ });
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Collapse all"),
+ callback: () => this.entry_editor.collapse_all()
+ });
+ entries.push(MenuEntry.HR());
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Show permission description"),
+ callback: () => {
+ createInfoModal(
+ tr("Permission description"),
+ tr("Permission description for permission ") + permission.name + ":
" + permission.description
+ ).open();
+ }
+ });
+ entries.push({
+ type: MenuEntryType.ENTRY,
+ icon: "",
+ name: tr("Copy permission name"),
+ callback: () => {
+ copy_to_clipboard(permission.name);
+ }
+ });
+
+ spawn_context_menu(x, y, ...entries);
+ }
}
}
@@ -500,26 +292,27 @@ namespace Modals {
for(const permission of permissions)
this.permission_value_map[permission.type.id] = permission;
- for(const permission_id of Object.keys(this.permission_map)) {
- const permission: PermissionEditor.PermissionEntry = this.permission_map[permission_id];
- const value: PermissionValue = this.permission_value_map[permission_id];
-
- permission.tag
- .toggleClass("value-unset", !value || !value.hasValue())
- .toggleClass("grant-unset", !value || !value.hasGrant());
+ for(const entry of this.entry_editor.permission_entries()) {
+ const permission = entry.permission();
+ const value: PermissionValue = this.permission_value_map[permission.id];
if(value && value.hasValue()) {
- if(value.type.is_boolean())
- permission.tag_value.prop("checked", value.value);
- else
- permission.tag_value.val(value.value);
- permission.tag_flag_skip.prop("checked", value.flag_skip);
- permission.tag_flag_negate.prop("checked", value.flag_negate);
+ entry.value = value.value;
+ entry.flag_skip = value.flag_skip;
+ entry.flag_negate = value.flag_negate;
+ } else {
+ entry.value = undefined;
+ entry.flag_skip = false;
+ entry.flag_negate = false;
}
+
if(value && value.hasGrant()) {
- permission.tag_grant.val(value.granted_value);
+ entry.granted = value.granted_value;
+ } else {
+ entry.granted = undefined;
}
}
+ this.entry_editor.request_draw(true);
}
set_listener(listener?: PermissionEditor.change_listener_t) {
@@ -539,11 +332,17 @@ namespace Modals {
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');
+ if(mode == PermissionEditorMode.VISIBLE)
+ this.entry_editor.draw(true);
+ }
+
+ update_ui() {
+ this.entry_editor.draw(true);
}
}
export function spawnPermissionEdit(connection: ConnectionHandler) : Modal {
- const connectModal = createModal({
+ const modal = createModal({
header: function() {
return tr("Server Permissions");
},
@@ -558,7 +357,9 @@ namespace Modals {
const pe_server = tag.find("permission-editor.group-server");
pe_server.append(pe.container); /* fuck off workaround to initialize form listener */
}
-
+ setTimeout(() => {
+ pe.update_ui();
+ }, 500);
apply_server_groups(connection, pe, tag.find(".tab-group-server"));
apply_channel_groups(connection, pe, tag.find(".tab-group-channel"));
@@ -575,12 +376,12 @@ namespace Modals {
full_size: true
});
- const tag = connectModal.htmlTag;
+ const tag = modal.htmlTag;
tag.find(".btn_close").on('click', () => {
- connectModal.close();
+ modal.close();
});
- return connectModal;
+ return modal;
}
function build_channel_tree(connection: ConnectionHandler, channel_list: JQuery, select_callback: (channel: ChannelEntry) => any) {
diff --git a/shared/js/ui/modal/permission/PermissionEditor.ts b/shared/js/ui/modal/permission/PermissionEditor.ts
new file mode 100644
index 00000000..9894a879
--- /dev/null
+++ b/shared/js/ui/modal/permission/PermissionEditor.ts
@@ -0,0 +1,1252 @@
+namespace ui {
+ export namespace scheme {
+ export interface CheckBox {
+ border: string;
+ checkmark: string;
+ checkmark_font: string;
+
+ background_checked: string;
+ background_checked_hovered: string;
+
+ background: string;
+ background_hovered: string;
+ }
+
+ export interface TextField {
+ color: string;
+ font: string;
+
+ background: string;
+ background_hovered: string;
+ }
+
+ export interface ColorScheme {
+ permission: {
+ background: string;
+ background_selected: string;
+
+ name: string;
+ name_unset: string;
+ name_font: string;
+
+ value: TextField;
+ value_b: CheckBox;
+ granted: TextField;
+ negate: CheckBox;
+ skip: CheckBox;
+ }
+
+ group: {
+ name: string;
+ name_font: string;
+ }
+ }
+ }
+
+ enum RepaintMode {
+ NONE,
+ REPAINT,
+ REPAINT_OBJECT_FULL,
+ REPAINT_FULL
+ }
+
+ interface AxisAlignedBoundingBox {
+ x: number;
+ y: number;
+
+ width: number;
+ height: number;
+ }
+
+ enum ClickEventType {
+ SIGNLE,
+ DOUBLE,
+ CONTEXT_MENU
+ }
+
+ interface InteractionClickEvent {
+ type: ClickEventType;
+ consumed: boolean;
+ offset_x: number;
+ offset_y: number;
+ }
+
+ interface InteractionListener {
+ region: AxisAlignedBoundingBox;
+ region_weight: number;
+
+ /**
+ * @return true if a redraw is required
+ */
+ on_mouse_enter?: () => RepaintMode;
+
+ /**
+ * @return true if a redraw is required
+ */
+ on_mouse_leave?: () => RepaintMode;
+
+ /**
+ * @return true if a redraw is required
+ */
+ on_click?: (event: InteractionClickEvent) => RepaintMode;
+
+ mouse_cursor?: string;
+
+ set_full_draw?: () => any;
+ disabled?: boolean;
+ }
+
+ abstract class DrawableObject {
+ abstract draw(context: CanvasRenderingContext2D, full: boolean);
+
+ private _object_full_draw = false;
+ private _width: number = 0;
+ set_width(value: number) {
+ this._width = value;
+ }
+ request_full_draw() {
+ this._object_full_draw = true;
+ }
+ pop_full_draw() {
+ const result = this._object_full_draw;
+ this._object_full_draw = false;
+ return result;
+ }
+
+ width() { return this._width; }
+ abstract height();
+
+ private _transforms: DOMMatrix[] = [];
+ protected push_transform(context: CanvasRenderingContext2D) {
+ this._transforms.push(context.getTransform());
+ }
+
+ protected pop_transform(context: CanvasRenderingContext2D) {
+ const transform = this._transforms.pop();
+ context.setTransform(
+ transform.a,
+ transform.b,
+ transform.c,
+ transform.d,
+ transform.e,
+ transform.f
+ );
+ }
+
+ protected original_x(context: CanvasRenderingContext2D, x: number) {
+ return context.getTransform().e + x;
+ }
+ protected original_y(context: CanvasRenderingContext2D, y: number) {
+ return context.getTransform().f + y;
+ }
+
+ protected colors: scheme.ColorScheme = {} as any;
+ set_color_scheme(scheme: scheme.ColorScheme) {
+ this.colors = scheme;
+ }
+
+ protected manager: PermissionEditor;
+
+ set_manager(manager: PermissionEditor) {
+ this.manager = manager;
+ }
+ abstract initialize();
+ abstract finalize();
+ }
+
+ class PermissionGroup extends DrawableObject {
+ public static readonly HEIGHT = 24;
+ public static readonly ARROW_SIZE = 12;
+
+ group: GroupedPermissions;
+ _sub_elements: PermissionGroup[] = [];
+ _element_permissions: PermissionList;
+
+ collapsed = false;
+ private _listener_colaps: InteractionListener;
+
+ constructor(group: GroupedPermissions) {
+ super();
+
+ this.group = group;
+
+ this._element_permissions = new PermissionList(this.group.permissions);
+ for(const sub of this.group.children)
+ this._sub_elements.push(new PermissionGroup(sub));
+ }
+
+ draw(context: CanvasRenderingContext2D, full: boolean) {
+ const _full = this.pop_full_draw() || full;
+ this.push_transform(context);
+ context.translate(PermissionGroup.ARROW_SIZE + 20, PermissionGroup.HEIGHT);
+
+ let sum_height = 0;
+ /* let first draw the elements, because if the sum height is zero then we could hide ourselves */
+ if(!this.collapsed) { /* draw the next groups */
+ for(const group of this._sub_elements) {
+ group.draw(context, full);
+
+ const height = group.height();
+ sum_height += height;
+ context.translate(0, height);
+ }
+
+ this._element_permissions.draw(context, full);
+ if(sum_height == 0)
+ sum_height += this._element_permissions.height();
+ } else {
+ const process_group = (group: PermissionGroup) => {
+ for(const g of group._sub_elements)
+ process_group(g);
+ group._element_permissions.handle_hide();
+ if(sum_height == 0 && group._element_permissions.height() > 0){
+ sum_height = 1;
+ }
+ };
+ process_group(this);
+ }
+ this.pop_transform(context);
+
+ if(_full && sum_height > 0) {
+ const arrow_stretch = 2/3;
+ if(!full) {
+ context.clearRect(0, 0, this.width(), PermissionGroup.HEIGHT);
+ }
+ context.fillStyle = this.colors.group.name;
+
+ /* arrow */
+ {
+ const x1 = this.collapsed ? PermissionGroup.ARROW_SIZE * arrow_stretch / 2 : 0;
+ const y1 = (PermissionGroup.HEIGHT - PermissionGroup.ARROW_SIZE) / 2 + (this.collapsed ? 0 : PermissionGroup.ARROW_SIZE * arrow_stretch / 2); /* center arrow */
+
+ const x2 = this.collapsed ? x1 + PermissionGroup.ARROW_SIZE * arrow_stretch : x1 + PermissionGroup.ARROW_SIZE / 2;
+ const y2 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE / 2 : y1 + PermissionGroup.ARROW_SIZE * arrow_stretch;
+
+ const x3 = this.collapsed ? x1 : x1 + PermissionGroup.ARROW_SIZE;
+ const y3 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE : y1;
+
+ context.beginPath();
+ context.moveTo(x1, y1);
+
+ context.lineTo(x2, y2);
+ context.lineTo(x3, y3);
+
+ context.moveTo(x2, y2);
+ context.lineTo(x3, y3);
+ context.fill();
+
+ this._listener_colaps.region.x = this.original_x(context, 0);
+ this._listener_colaps.region.y = this.original_y(context, y1);
+ }
+ /* text */
+ {
+ context.font = this.colors.group.name_font;
+ context.textBaseline = "middle";
+ context.textAlign = "start";
+
+ context.fillText(this.group.group.name, PermissionGroup.ARROW_SIZE + 5, PermissionGroup.HEIGHT / 2);
+ }
+ }
+ }
+
+ set_width(value: number) {
+ super.set_width(value);
+ for(const element of this._sub_elements)
+ element.set_width(value - PermissionGroup.ARROW_SIZE - 20);
+ this._element_permissions.set_width(value - PermissionGroup.ARROW_SIZE - 20);
+ }
+
+ set_color_scheme(scheme: scheme.ColorScheme) {
+ super.set_color_scheme(scheme);
+ for(const child of this._sub_elements)
+ child.set_color_scheme(scheme);
+ this._element_permissions.set_color_scheme(scheme);
+ }
+
+ set_manager(manager: PermissionEditor) {
+ super.set_manager(manager);
+ for(const child of this._sub_elements)
+ child.set_manager(manager);
+ this._element_permissions.set_manager(manager);
+ }
+
+ height() {
+ let result = 0;
+
+ if(!this.collapsed) {
+ for(const element of this._sub_elements)
+ result += element.height();
+
+ result += this._element_permissions.height();
+ } else {
+ //We've to figure out if we have permissions
+ const process_group = (group: PermissionGroup) => {
+ if(result == 0 && group._element_permissions.height() > 0){
+ result = 1;
+ } else {
+ for(const g of group._sub_elements)
+ process_group(g);
+ }
+ };
+ process_group(this);
+
+ if(result > 0)
+ return PermissionGroup.HEIGHT;
+
+ return 0;
+ }
+ if(result > 0) {
+ result += PermissionGroup.HEIGHT;
+ return result;
+ } else {
+ return 0;
+ }
+ }
+
+ initialize() {
+ for(const child of this._sub_elements)
+ child.initialize();
+ this._element_permissions.initialize();
+
+
+ this._listener_colaps = {
+ region: {
+ x: 0,
+ y: 0,
+ height: PermissionGroup.ARROW_SIZE,
+ width: PermissionGroup.ARROW_SIZE
+ },
+ region_weight: 10,
+ /*
+ on_mouse_enter: () => {
+ this.collapsed_hovered = true;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_mouse_leave: () => {
+ this.collapsed_hovered = false;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ */
+ on_click: () => {
+ this.collapsed = !this.collapsed;
+ return RepaintMode.REPAINT_FULL;
+ },
+ set_full_draw: () => this.request_full_draw(),
+ mouse_cursor: "pointer"
+ };
+
+ this.manager.intercept_manager().register_listener(this._listener_colaps);
+ }
+
+ finalize() {
+ for(const child of this._sub_elements)
+ child.finalize();
+ this._element_permissions.finalize();
+ }
+
+ collapse_group() {
+ for(const child of this._sub_elements)
+ child.collapse_group();
+
+ this.collapsed = true;
+ }
+
+ expend_group() {
+ for(const child of this._sub_elements)
+ child.expend_group();
+
+ this.collapsed = false;
+ }
+ }
+
+ class PermissionList extends DrawableObject {
+ permissions: PermissionEntry[] = [];
+
+ constructor(permissions: PermissionInfo[]) {
+ super();
+
+ for(const permission of permissions)
+ this.permissions.push(new PermissionEntry(permission));
+ }
+
+ set_width(value: number) {
+ super.set_width(value);
+ for(const entry of this.permissions)
+ entry.set_width(value);
+ }
+
+
+ draw(context: CanvasRenderingContext2D, full: boolean) {
+ this.push_transform(context);
+
+ for(const permission of this.permissions) {
+ permission.draw(context, full);
+ context.translate(0, permission.height());
+ }
+
+ this.pop_transform(context);
+ }
+
+ height() {
+ let height = 0;
+ for(const permission of this.permissions)
+ height += permission.height();
+ return height;
+ }
+
+
+ set_color_scheme(scheme: scheme.ColorScheme) {
+ super.set_color_scheme(scheme);
+ for(const entry of this.permissions)
+ entry.set_color_scheme(scheme);
+ }
+
+ set_manager(manager: PermissionEditor) {
+ super.set_manager(manager);
+
+ for(const entry of this.permissions)
+ entry.set_manager(manager);
+ }
+
+ initialize() {
+ for(const entry of this.permissions)
+ entry.initialize();
+ }
+
+ finalize() {
+ for(const entry of this.permissions)
+ entry.finalize();
+ }
+
+ handle_hide() {
+ for(const entry of this.permissions)
+ entry.handle_hide();
+ }
+ }
+
+ class PermissionEntry extends DrawableObject {
+ public static readonly HEIGHT = 24;
+ public static readonly HALF_HEIGHT = PermissionEntry.HEIGHT / 2;
+ public static readonly CHECKBOX_HEIGHT = PermissionEntry.HEIGHT - 2;
+
+ public static readonly COLUMN_PADDING = 2;
+ public static readonly COLUMN_VALUE = 75;
+ public static readonly COLUMN_GRANTED = 75;
+ //public static readonly COLUMN_NEGATE = 25;
+ //public static readonly COLUMN_SKIP = 25;
+ public static readonly COLUMN_NEGATE = 75;
+ public static readonly COLUMN_SKIP = 75;
+
+ private _permission: PermissionInfo;
+
+ hidden: boolean;
+
+ granted: number = 22;
+ value: number;
+ flag_skip: boolean = true;
+ flag_negate: boolean;
+
+ private _prev_selected = false;
+ selected: boolean;
+
+ flag_skip_hovered = false;
+ flag_negate_hovered = false;
+ flag_value_hovered = false;
+ flag_grant_hovered = false;
+
+ private _listener_checkbox_skip: InteractionListener;
+ private _listener_checkbox_negate: InteractionListener;
+ private _listener_value: InteractionListener;
+ private _listener_grant: InteractionListener;
+ private _listener_general: InteractionListener;
+
+ on_context_menu?: (x: number, y: number) => any;
+ on_grant_change?: () => any;
+ on_change?: () => any;
+
+ constructor(permission: PermissionInfo) {
+ super();
+ this._permission = permission;
+ }
+
+ permission() { return this._permission; }
+
+ draw(ctx: CanvasRenderingContext2D, full: boolean) {
+ if(!this.pop_full_draw() && !full) { /* Note: do not change this order! */
+ /* test for update! */
+ return;
+ }
+ if(this.hidden) {
+ this.handle_hide();
+ return;
+ }
+ ctx.lineWidth = 1;
+
+ /* debug box */
+ if(false) {
+ ctx.fillStyle = "#FF0000";
+ ctx.fillRect(0, 0, this.width(), PermissionEntry.HEIGHT);
+ ctx.fillStyle = "#000000";
+ ctx.strokeRect(0, 0, this.width(), PermissionEntry.HEIGHT);
+ }
+
+ if(!full) {
+ const off = this.selected || this._prev_selected ? ctx.getTransform().e : 0;
+ ctx.clearRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT);
+ }
+
+ if(this.selected)
+ ctx.fillStyle = this.colors.permission.background_selected;
+ else
+ ctx.fillStyle = this.colors.permission.background;
+ const off = this.selected ? ctx.getTransform().e : 0;
+ ctx.fillRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT);
+ this._prev_selected = this.selected;
+
+ /* permission name */
+ {
+ ctx.fillStyle = typeof(this.value) !== "undefined" ? this.colors.permission.name : this.colors.permission.name_unset;
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "start";
+ ctx.font = this.colors.permission.name_font;
+
+ ctx.fillText(this._permission.name, 0, PermissionEntry.HALF_HEIGHT);
+ }
+
+ const original_y = this.original_y(ctx, 0);
+ const original_x = this.original_x(ctx, 0);
+ const width = this.width();
+
+ /* draw granted */
+ let w = width - PermissionEntry.COLUMN_GRANTED;
+ if(typeof(this.granted) === "number") {
+ this._listener_grant.region.x = original_x + w;
+ this._listener_grant.region.y = original_y;
+
+ this._draw_number_field(ctx, this.colors.permission.granted, w, 0, PermissionEntry.COLUMN_VALUE, this.granted, this.flag_grant_hovered);
+ } else {
+ this._listener_grant.region.y = original_y;
+ this._listener_grant.region.x =
+ original_x
+ + width
+ - PermissionEntry.COLUMN_GRANTED;
+ }
+
+ /* draw value and the skip stuff */
+ if(typeof(this.value) === "number") {
+ w -= PermissionEntry.COLUMN_SKIP + PermissionEntry.COLUMN_PADDING;
+ {
+ const x = w + (PermissionEntry.COLUMN_SKIP - PermissionEntry.CHECKBOX_HEIGHT) / 2;
+ const y = 1;
+
+ this._listener_checkbox_skip.region.x = original_x + x;
+ this._listener_checkbox_skip.region.y = original_y + y;
+
+ this._draw_checkbox_field(ctx, this.colors.permission.skip, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_skip, this.flag_skip_hovered);
+ }
+
+ w -= PermissionEntry.COLUMN_NEGATE + PermissionEntry.COLUMN_PADDING;
+ {
+ const x = w + (PermissionEntry.COLUMN_NEGATE - PermissionEntry.CHECKBOX_HEIGHT) / 2;
+ const y = 1;
+
+ this._listener_checkbox_negate.region.x = original_x + x;
+ this._listener_checkbox_negate.region.y = original_y + y;
+
+ this._draw_checkbox_field(ctx, this.colors.permission.negate, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_negate, this.flag_negate_hovered);
+ }
+
+ w -= PermissionEntry.COLUMN_VALUE + PermissionEntry.COLUMN_PADDING;
+ if(this._permission.is_boolean()) {
+ const x = w + PermissionEntry.COLUMN_VALUE - PermissionEntry.CHECKBOX_HEIGHT;
+ const y = 1;
+
+ this._listener_value.region.x = original_x + x;
+ this._listener_value.region.y = original_y + y;
+
+ this._draw_checkbox_field(ctx, this.colors.permission.value_b, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.value > 0, this.flag_value_hovered);
+ } else {
+ this._listener_value.region.x = original_x + w;
+ this._listener_value.region.y = original_y;
+
+ this._draw_number_field(ctx, this.colors.permission.value, w, 0, PermissionEntry.COLUMN_VALUE, this.value, this.flag_value_hovered);
+ }
+ this._listener_value.disabled = false;
+ } else {
+ this._listener_checkbox_skip.region.y = -1e8;
+ this._listener_checkbox_negate.region.y = -1e8;
+
+ this._listener_value.region.y = original_y;
+ this._listener_value.region.x =
+ original_x
+ + width
+ - PermissionEntry.COLUMN_GRANTED
+ - PermissionEntry.COLUMN_NEGATE
+ - PermissionEntry.COLUMN_VALUE
+ - PermissionEntry.COLUMN_PADDING * 4;
+ this._listener_value.disabled = true;
+ }
+
+ this._listener_general.region.y = original_y;
+ this._listener_general.region.x = original_x;
+ }
+
+ handle_hide() {
+ /* so the listener wound get triggered */
+ this._listener_value.region.x = -1e8;
+ this._listener_grant.region.x = -1e8;
+ this._listener_checkbox_negate.region.x = -1e8;
+ this._listener_checkbox_skip.region.x = -1e8;
+ this._listener_general.region.x = -1e8;
+ }
+
+
+ private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean) {
+ ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background;
+ ctx.fillRect(x, y, width, PermissionEntry.HEIGHT);
+
+ ctx.fillStyle = scheme.color;
+ ctx.font = scheme.font; //Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial";
+ ctx.textAlign = "start";
+ ctx.fillText(value + "", x, y + PermissionEntry.HALF_HEIGHT, width);
+
+ ctx.strokeStyle = "#6e6e6e";
+ const line = ctx.lineWidth;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(x, y + PermissionEntry.HEIGHT - 2);
+ ctx.lineTo(x + width, y + PermissionEntry.HEIGHT - 2);
+ ctx.stroke();
+ ctx.lineWidth = line;
+ }
+
+ private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean) {
+ ctx.fillStyle = scheme.border;
+ ctx.strokeRect(x, y, height, height);
+
+
+ ctx.fillStyle = checked ?
+ (hovered ? scheme.background_checked_hovered : scheme.background_checked) :
+ (hovered ? scheme.background_hovered : scheme.background);
+ ctx.fillRect(x + 1, y + 1, height - 2, height - 2);
+
+ if(checked) {
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillStyle = scheme.checkmark;
+ ctx.font = scheme.checkmark_font; //Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial";
+ ctx.fillText("✓", x + height / 2, y + height / 2);
+ }
+ }
+
+ height() {
+ return this.hidden ? 0 : PermissionEntry.HEIGHT;
+ }
+
+ set_width(value: number) {
+ super.set_width(value);
+ this._listener_general.region.width = value;
+ }
+
+ initialize() {
+ this._listener_checkbox_skip = {
+ region: {
+ x: -1e8,
+ y: -1e8,
+ height: PermissionEntry.CHECKBOX_HEIGHT,
+ width: PermissionEntry.CHECKBOX_HEIGHT
+ },
+ region_weight: 10,
+ on_mouse_enter: () => {
+ this.flag_skip_hovered = true;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_mouse_leave: () => {
+ this.flag_skip_hovered = false;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_click: () => {
+ this.flag_skip = !this.flag_skip;
+ if(this.on_change)
+ this.on_change();
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ set_full_draw: () => this.request_full_draw(),
+ mouse_cursor: "pointer"
+ };
+ this._listener_checkbox_negate = {
+ region: {
+ x: -1e8,
+ y: -1e8,
+ height: PermissionEntry.CHECKBOX_HEIGHT,
+ width: PermissionEntry.CHECKBOX_HEIGHT
+ },
+ region_weight: 10,
+ on_mouse_enter: () => {
+ this.flag_negate_hovered = true;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_mouse_leave: () => {
+ this.flag_negate_hovered = false;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_click: () => {
+ this.flag_negate = !this.flag_negate;
+ if(this.on_change)
+ this.on_change();
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ set_full_draw: () => this.request_full_draw(),
+ mouse_cursor: "pointer"
+ };
+ this._listener_value = {
+ region: {
+ x: -1e8,
+ y: -1e8,
+ height: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.HEIGHT,
+ width: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.COLUMN_VALUE
+ },
+ region_weight: 10,
+ on_mouse_enter: () => {
+ this.flag_value_hovered = true;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_mouse_leave: () => {
+ this.flag_value_hovered = false;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_click: () => {
+ if(this._permission.is_boolean()) {
+ this.value = this.value > 0 ? 0 : 1;
+ if(this.on_change)
+ this.on_change();
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ } else {
+ this._spawn_number_edit(
+ this._listener_value.region.x,
+ this._listener_value.region.y,
+ this._listener_value.region.width,
+ this._listener_value.region.height,
+ this.colors.permission.value,
+ this.value || 0,
+ value => {
+ if(typeof(value) === "number") {
+ this.value = value;
+ this.request_full_draw();
+ this.manager.request_draw(false);
+ if(this.on_change)
+ this.on_change();
+ }
+ }
+ )
+ }
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ set_full_draw: () => this.request_full_draw(),
+ mouse_cursor: "pointer"
+ };
+ this._listener_grant = {
+ region: {
+ x: -1e8,
+ y: -1e8,
+ height: PermissionEntry.HEIGHT,
+ width: PermissionEntry.COLUMN_VALUE
+ },
+ region_weight: 10,
+ on_mouse_enter: () => {
+ this.flag_grant_hovered = true;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_mouse_leave: () => {
+ this.flag_grant_hovered = false;
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_click: () => {
+ this._spawn_number_edit(
+ this._listener_grant.region.x,
+ this._listener_grant.region.y,
+ this._listener_grant.region.width,
+ this._listener_grant.region.height,
+ this.colors.permission.granted,
+ this.granted || 0, //TODO use max assignable value?
+ value => {
+ if(typeof(value) === "number") {
+ this.granted = value;
+ this.request_full_draw();
+ this.manager.request_draw(false);
+
+ if(this.on_grant_change)
+ this.on_grant_change();
+ }
+ }
+ );
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ set_full_draw: () => this.request_full_draw(),
+ mouse_cursor: "pointer"
+ };
+
+ this._listener_general = {
+ region: {
+ x: -1e8,
+ y: -1e8,
+ height: PermissionEntry.HEIGHT,
+ width: 0
+ },
+ region_weight: 0,
+ /*
+ on_mouse_enter: () => {
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ on_mouse_leave: () => {
+ return RepaintMode.REPAINT_OBJECT_FULL;
+ },
+ */
+ on_click: (event: InteractionClickEvent) => {
+ this.manager.set_selected_entry(this);
+
+ if(event.type == ClickEventType.DOUBLE && typeof(this.value) === "undefined")
+ return this._listener_value.on_click(event);
+ else if(event.type == ClickEventType.CONTEXT_MENU) {
+ const mouse = this.manager.mouse;
+ if(this.on_context_menu) {
+ this.on_context_menu(mouse.x, mouse.y);
+ event.consumed = true;
+ }
+ }
+ return RepaintMode.NONE;
+ },
+ set_full_draw: () => this.request_full_draw(),
+ };
+
+ this.manager.intercept_manager().register_listener(this._listener_checkbox_negate);
+ this.manager.intercept_manager().register_listener(this._listener_checkbox_skip);
+ this.manager.intercept_manager().register_listener(this._listener_value);
+ this.manager.intercept_manager().register_listener(this._listener_grant);
+ this.manager.intercept_manager().register_listener(this._listener_general);
+ }
+
+ finalize() { }
+
+ private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any) {
+ const element = $.spawn("div");
+ element.prop("contentEditable", true);
+ element
+ .css("pointer-events", "none")
+ .css("background", color.background)
+ .css("display", "block")
+ .css("position", "absolute")
+ .css("top", y)
+ .css("left", x)
+ .css("width", width)
+ .css("height", height)
+ .css("z-index", 1e6);
+ element.text(value);
+ element.appendTo(this.manager.canvas_container);
+ element.focus();
+
+ element.on('focusout', event => {
+ console.log("permission changed to " + element.text());
+ if(!isNaN(parseInt(element.text()))) {
+ callback(parseInt(element.text()));
+ } else {
+ callback(undefined);
+ }
+ element.remove();
+ });
+
+ element.on('keypress', event => {
+ if(event.which == JQuery.Key.Enter)
+ element.trigger('focusout');
+
+ const text = String.fromCharCode(event.which);
+ if (isNaN(parseInt(text)) && text != "-")
+ event.preventDefault();
+
+ if(element.text().length > 7)
+ event.preventDefault();
+ });
+
+ if (window.getSelection) {
+ const selection = window.getSelection();
+ const range = document.createRange();
+ range.selectNodeContents(element[0]);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+
+ trigger_value_assign() {
+ this._listener_value.on_click(undefined);
+ }
+ trigger_grant_assign() {
+ this._listener_grant.on_click(undefined);
+ }
+ }
+
+ export class InteractionManager {
+ private _listeners: InteractionListener[] = [];
+ private _entered_listeners: InteractionListener[] = [];
+
+ register_listener(listener: InteractionListener) {
+ this._listeners.push(listener);
+ }
+
+ remove_listener(listener: InteractionListener) {
+ this._listeners.remove(listener);
+ }
+
+ process_mouse_move(new_x: number, new_y: number) : { repaint: RepaintMode, cursor: string } {
+ let _entered_listeners: InteractionListener[] = [];
+ for(const listener of this._listeners) {
+ const aabb = listener.region;
+
+ if(listener.disabled)
+ continue;
+
+ if(new_x < aabb.x || new_x > aabb.x + aabb.width)
+ continue;
+
+ if(new_y < aabb.y || new_y > aabb.y + aabb.height)
+ continue;
+
+ _entered_listeners.push(listener);
+ }
+
+ let repaint: RepaintMode = RepaintMode.NONE;
+ _entered_listeners.sort((a, b) => (a.region_weight || 0) - (b.region_weight || 0));
+ for(const listener of this._entered_listeners) {
+ if(listener.on_mouse_leave && _entered_listeners.indexOf(listener) == -1) {
+ let mode = listener.on_mouse_leave();
+ if(mode == RepaintMode.REPAINT_OBJECT_FULL) {
+ mode = RepaintMode.REPAINT;
+ if(listener.set_full_draw)
+ listener.set_full_draw();
+ }
+ if(mode > repaint)
+ repaint = mode;
+ }
+ }
+ for(const listener of _entered_listeners) {
+ if(listener.on_mouse_enter && this._entered_listeners.indexOf(listener) == -1) {
+ let mode = listener.on_mouse_enter();
+ if(mode == RepaintMode.REPAINT_OBJECT_FULL) {
+ mode = RepaintMode.REPAINT;
+ if(listener.set_full_draw)
+ listener.set_full_draw();
+ }
+ if(mode > repaint)
+ repaint = mode;
+ }
+ }
+ this._entered_listeners = _entered_listeners;
+
+ let cursor;
+ for(const listener of _entered_listeners)
+ if(typeof(listener.mouse_cursor) === "string") {
+ cursor = listener.mouse_cursor;
+ }
+ return {
+ repaint: repaint,
+ cursor: cursor
+ };
+ }
+
+ private process_click_event(x: number, y: number, event: InteractionClickEvent) : RepaintMode {
+ const move_result = this.process_mouse_move(x, y);
+
+ let repaint: RepaintMode = move_result.repaint;
+ for(const listener of this._entered_listeners)
+ if(listener.on_click) {
+ let mode = listener.on_click(event);
+ if(mode == RepaintMode.REPAINT_OBJECT_FULL){
+ mode = RepaintMode.REPAINT;
+ if(listener.set_full_draw)
+ listener.set_full_draw();
+ }
+ if(mode > repaint)
+ repaint = mode;
+ }
+
+ return repaint;
+ }
+
+ process_click(x: number, y: number) : RepaintMode {
+ const event: InteractionClickEvent = {
+ consumed: false,
+ type: ClickEventType.SIGNLE,
+ offset_x: x,
+ offset_y: y
+ };
+
+ return this.process_click_event(x, y, event);
+ }
+ process_dblclick(x: number, y: number) : RepaintMode {
+ const event: InteractionClickEvent = {
+ consumed: false,
+ type: ClickEventType.DOUBLE,
+ offset_x: x,
+ offset_y: y
+ };
+
+ return this.process_click_event(x, y, event);
+ }
+ process_context_menu(js_event: MouseEvent, x: number, y: number) : RepaintMode {
+ const event: InteractionClickEvent = {
+ consumed: js_event.defaultPrevented,
+ type: ClickEventType.CONTEXT_MENU,
+ offset_x: x,
+ offset_y: y
+ };
+
+ const result = this.process_click_event(x, y, event);
+ if(event.consumed)
+ js_event.preventDefault();
+ return result;
+ }
+ }
+
+ export class PermissionEditor {
+ private static readonly PERMISSION_HEIGHT = 32;
+ private static readonly PERMISSION_GROUP_HEIGHT = 32;
+
+ readonly grouped_permissions: GroupedPermissions[];
+ readonly canvas: HTMLCanvasElement;
+ readonly canvas_container: HTMLDivElement;
+ private _max_height: number = 0;
+
+ private _permission_count: number = 0;
+ private _permission_group_count: number = 0;
+ private _canvas_context: CanvasRenderingContext2D;
+
+ private _selected_entry: PermissionEntry;
+
+ private _draw_requested: boolean = false;
+ private _draw_requested_full: boolean = false;
+
+ private _elements: PermissionGroup[] = [];
+ private _intersect_manager: InteractionManager;
+
+ private _permission_entry_map: {[key: number]:PermissionEntry} = {};
+
+ mouse: {
+ x: number,
+ y: number
+ } = {
+ x: 0,
+ y: 0
+ };
+
+ constructor(permissions: GroupedPermissions[]) {
+ this.grouped_permissions = permissions;
+
+ this.canvas_container = $.spawn("div")
+ .css("position", "relative")
+ .css("user-select", "none")
+ [0];
+ this.canvas = $.spawn("canvas")[0];
+
+ this.canvas_container.appendChild(this.canvas);
+
+ this._intersect_manager = new InteractionManager();
+ this.canvas_container.onmousemove = event => {
+ this.mouse.x = event.pageX;
+ this.mouse.y = event.pageY;
+
+ const draw = this._intersect_manager.process_mouse_move(event.offsetX, event.offsetY);
+ this.canvas_container.style.cursor = draw.cursor || "";
+ this._handle_repaint(draw.repaint);
+ };
+ this.canvas_container.onclick = event => {
+ this._handle_repaint(this._intersect_manager.process_click(event.offsetX, event.offsetY));
+ };
+ this.canvas_container.ondblclick = event => {
+ this._handle_repaint(this._intersect_manager.process_dblclick(event.offsetX, event.offsetY));
+ };
+ this.canvas_container.oncontextmenu = (event: MouseEvent) => {
+ this._handle_repaint(this._intersect_manager.process_context_menu(event, event.offsetX, event.offsetY));
+ };
+
+
+ this.initialize();
+ }
+
+ private _handle_repaint(mode: RepaintMode) {
+ if(mode == RepaintMode.REPAINT || mode == RepaintMode.REPAINT_FULL)
+ this.request_draw(mode == RepaintMode.REPAINT_FULL);
+ }
+
+ request_draw(full?: boolean) {
+ this._draw_requested_full = this._draw_requested_full || full;
+ if(this._draw_requested)
+ return;
+ this._draw_requested = true;
+ requestAnimationFrame(() => {
+ this.draw(this._draw_requested_full);
+ });
+ }
+
+ draw(full?: boolean) {
+ this._draw_requested = false;
+ this._draw_requested_full = false;
+
+ /* clear max height */
+ this.canvas_container.style.overflowY = "shown";
+ this.canvas_container.style.height = undefined;
+
+ const max_height = this._max_height;
+ const max_width = this.canvas_container.clientWidth;
+ const update_width = this.canvas.width != max_width;
+ const full_draw = typeof(full) !== "boolean" || full || update_width;
+
+ if(update_width) {
+ this.canvas.width = max_width;
+ for(const element of this._elements)
+ element.set_width(max_width);
+ }
+
+ console.log("Drawing%s on %dx%d", full_draw ? " full" : "", max_width, max_height);
+ if(full_draw)
+ this.canvas.height = max_height;
+ const ctx = this._canvas_context;
+ ctx.resetTransform();
+ if(full_draw)
+ ctx.clearRect(0, 0, max_width, max_height);
+
+ let sum_height = 0;
+ for(const element of this._elements) {
+ element.draw(ctx, full_draw);
+ const height = element.height();
+ sum_height += height;
+ ctx.translate(0, height);
+ }
+
+ this.canvas_container.style.overflowY = "hidden";
+ this.canvas_container.style.height = sum_height + "px";
+ }
+
+ private initialize() {
+ /* setup the canvas */
+ {
+ const apply_group = (group: GroupedPermissions) => {
+ for(const g of group.children || [])
+ apply_group(g);
+ this._permission_group_count++;
+ this._permission_count += group.permissions.length;
+ };
+ for(const group of this.grouped_permissions)
+ apply_group(group);
+
+ this._max_height = this._permission_count * PermissionEditor.PERMISSION_HEIGHT + this._permission_group_count * PermissionEditor.PERMISSION_GROUP_HEIGHT;
+ console.log("%d permissions and %d groups required %d height", this._permission_count, this._permission_group_count, this._max_height);
+
+ this.canvas.style.width = "100%";
+
+ this.canvas.style.flexShrink = "0";
+ this.canvas_container.style.flexShrink = "0";
+
+ this._canvas_context = this.canvas.getContext("2d");
+ }
+
+ const font = Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial";
+ const font_checkmark = Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial";
+ const checkbox = {
+ background: "#FFFFFF",
+ background_hovered: "#CCCCCC",
+
+ background_checked: "#0000AA",
+ background_checked_hovered: "#0000AA77",
+
+ border: "#000000",
+ checkmark: "#FFFFFF",
+ checkmark_font: font_checkmark
+ };
+ const input: scheme.TextField = {
+ color: "#000000",
+ font: font,
+
+ background_hovered: "#CCCCCCCC",
+ background: "#FFFFFF00"
+ };
+
+ const color_scheme: scheme.ColorScheme = {
+ group: {
+ name: "black",
+ name_font: font
+ },
+
+ permission: {
+ name: "black",
+ name_unset: "#888888",
+ name_font: font,
+
+ background: "#FFFFFF",
+ background_selected: "#00007788",
+
+ value: input,
+ value_b: checkbox,
+ granted: input,
+ negate: checkbox,
+ skip: checkbox
+ }
+ };
+ (window as any).scheme = color_scheme;
+ /* setup elements to draw */
+ {
+ const process_group = (group: PermissionGroup) => {
+ for(const permission of group._element_permissions.permissions)
+ this._permission_entry_map[permission.permission().id] = permission;
+ for(const g of group._sub_elements)
+ process_group(g);
+ };
+
+ for(const group of this.grouped_permissions) {
+ const element = new PermissionGroup(group);
+ element.set_color_scheme(color_scheme);
+ element.set_manager(this);
+ process_group(element);
+ this._elements.push(element);
+ }
+ for(const element of this._elements) {
+ element.initialize();
+ }
+ }
+ }
+
+ intercept_manager() {
+ return this._intersect_manager;
+ }
+
+ set_selected_entry(entry?: PermissionEntry) {
+ if(this._selected_entry === entry)
+ return;
+
+ if(this._selected_entry) {
+ this._selected_entry.selected = false;
+ this._selected_entry.request_full_draw();
+ }
+ this._selected_entry = entry;
+ if(this._selected_entry) {
+ this._selected_entry.selected = true;
+ this._selected_entry.request_full_draw();
+ }
+ this.request_draw(false);
+ }
+
+ permission_entries() : PermissionEntry[] {
+ return Object.keys(this._permission_entry_map).map(e => this._permission_entry_map[e]);
+ }
+
+ collapse_all() {
+ for(const group of this._elements)
+ group.collapse_group();
+ this.request_draw(true);
+ }
+
+ expend_all() {
+ for(const group of this._elements)
+ group.expend_group();
+ this.request_draw(true);
+ }
+ }
+}
\ No newline at end of file