disable checkbox in angular tree component - angular-tree-component

I am unable to find any way to disable checkbox node using angular tree component.
There is no option to mark the tree disabled so that the checkboxes that appear alongwith th etree data should be disabled. Please suggest

Disabling the node can be done by actionMapping inside options attribute https://angular2-tree.readme.io/v1.2.0/docs/options. Here click event of mouse can be overwritten.
<Tree [nodes]="nodes" [options]="treeOptions"></Tree>
In my tree data I kept an attribute isSelectable on each node, which is true|false. In case of true i proceed selecting the node otherwise it does not do anything. Here are full options that I am passing to the tree component.
public options: ITreeOptions = {
isExpandedField: 'expanded',
idField: 'uuid',
getChildren: this.getChildren.bind(this),
actionMapping: {
mouse: {
click: (tree, node, $event) => {
if ( node.data.isSelectable ) {
this.isNodeSelected.emit(node.data);
this.alreadySelected = true;
this.preSelected.tree = tree;
this.preSelected.node = node;
this.preSelected.event = $event;
TREE_ACTIONS.ACTIVATE(this.preSelected.tree, this.preSelected.node, this.preSelected.event);
}
}
}
},
nodeHeight: 23,
allowDrag: (node) => {
return false;
},
allowDrop: (node) => {
return false;
}
};

Related

Hiding DOM elements with a chrome extension without causing a flicker

Preface:
I am aware that there is a duplicate question out there. I am posting it again because it has no answers (and it's from 4 years ago).
General description of what I want:
I want to be able to hide a DOM-element (adding Element.style.display = "none") before the DOM is loaded into the view.
What I've tried:
Other posts point to using a MutationObserver and running it on the document element.
To ensure that we are able to hide an element before the DOM is loaded, we are to run the script containing the MutationObserver as a content_script with "run_at":"document_start".
I did all of this, and I still see a flicker (the elements appear when I load a page and then quickly disappear).
What I'm trying to do:
There's a ul which contains some li with some text on the page I inject my content_script.js into. I populate my popup.html with <text, checkbox> pairs. If the checkbox is checked, the li containing said text is visible, else it is hidden. I want it to persist between refreshes, hence the use of storage.
Things work - but there's a flicker whenever I refresh the page. The elements are there, then they're gone. I don't want them to show up in the first place!
My code:
When I detect that the DOM elements I may remove have loaded, I generate an Object that indicates whether I should hide or keep visible that specific DOM element.
I then set its Element.style.display to none or block accordingly.
/**manifest.json
...
"content_scripts": [
{
"matches": [
"some_website_url"
],
"js": [
"content_script.js"
],
"run_at": "document_start"
}
]
...
*/
///content_script.js
const mutationObserver = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.tagName) {
if (node.querySelector(potentially_hidden_element_selector)) {
chrome.storage.sync.get("courses", ({ courses }) => {
chrome.storage.sync.set({ "courses": generateCourseList(courses) }, () => {
const courseElements = Array.from(node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'))
courseElements.forEach(courseElement => {
const courseName = getCourseName(courseElement)
const isVisible = courses[courseName]
updateCourseElementInSidebar(courseElement, isVisible)
})
})
})
// We found what we were looking for so stop searching
mutationObserver.disconnect()
}
}
}
}
})
mutationObserver.observe(document, { childList: true, subtree: true })
EDIT 1:
My generateCourseList method depends on the DOM elements I may try to hide - so I can't call the chrome.storage.set method before the DOM has loaded I think.
When I refresh the page, a list of courses eventually populates the DOM.
I then populate the storage's courses object based on these course elements' innerText properties. I set each of these elements' visibility to true or false based on one of two factors: if this course is already defined in the courses object, keep its visibility status, if it isn't, set it to true (visible by default).
I can't make certain DOM elements visible/hidden if I don't have reference to them though. So if I try to call generateCourseList before those specific DOM elements have loaded, I end up trying to retrieve all the course elements (document.querySelectorAll('a[data-parent-key="mycourses"]')) and get returned nothing. I end up setting courses in chrome.storage to nothing because of this chrome.storage.sync.set({ "courses": generateCourseList(courses) }....
EDIT 2:
Here is all of my code. I try to chrome.storage.sync.get as soon as I can, and I try to not depend on the result of chrome.storage.sync.set.
I try to delete the elements as soon as I can, but I'm having difficulty doing so. This is because I have difficulty knowing when the content I want to access (the course elements) have fully loaded. Previously, I was detecting when one course element was visible, and when it was, I assumed all were. This was a mistake. I was able to access the one courselement the moment it popped up, but sometimes only 4 of the 6 course elements were actually loaded. I can't hardcode this number, because it changes from person to person. I can't just tackle them one by one, because then I wouldn't know when to disconnect the MutationObserver. I used the debugger and tried to find what element is loaded soon after all 6 course elements are loaded, and that is the header#page-header.row element. I still get a flicker, though less noticeable than before.
Anything I can do to make it even less noticeable?
function start_mutation_observer() {
chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
// The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
if (document.querySelector('header#page-header.row')) {
observer.disconnect()
const generatedCourses = generateCourseList(savedCourses)
const courseElements = getCourseElements()
// Set visibility of course elements
courseElements.forEach(courseElement => {
const courseName = getCourseElementTextContent(courseElement);
const isShown = generatedCourses[courseName];
setCourseElementVisibility(courseElement, isShown);
});
chrome.storage.sync.set({ 'savedCourses': generatedCourses });
return
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
// In case the content script has been injected when some of the DOM has already loaded
onMutation([{ addedNodes: [document.documentElement] }]);
});
}
function getCourseElements() {
const COURSE_ELEMENT_SELECTOR = 'ul > li > a[data-parent-key="mycourses"]'
return Array.from(document.querySelectorAll(COURSE_ELEMENT_SELECTOR))
}
function getCourseElementTextContent(courseElement) {
const COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR = 'a[data-parent-key="mycourses"] > div > div > span.media-body'
return courseElement.querySelector(COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR).textContent
}
function generateCourseList(savedCourses) {
// Turns [[a, b], [b,c]] into {a:b, b:c}
return Object.fromEntries(getCourseElements().map(courseElement => {
const courseName = getCourseElementTextContent(courseElement)
const isShown = savedCourses[courseName] ?? true
return [courseName, isShown]
}))
}
function setCourseElementVisibility(courseElement, isShown) {
if (isShown) {
courseElement.style.display = "block"
} else {
courseElement.style.display = "none"
}
}
start_mutation_observer()
EDIT 3:
I think it's as good as can be now. I only refresh the visibility of the course elements that were just loaded into the DOM. There's essentially no flicker now (there is a slight one, but its' the same amount of flickering without my extension).
Here is the code for the MutationObserver
function start_mutation_observer() {
let handledCourseElements = new Set()
chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
const courseElements = getCourseElements()
const courseElementsAdded = courseElements.length > handledCourseElements.size
// If a courseElement was added, update visibility of those that weren't already processed
if (courseElementsAdded) {
const generatedCourses = generateCourseList(savedCourses)
courseElements
.filter(courseElement => !handledCourseElements.has(courseElement))
.forEach(courseElement => {
const courseName = getCourseElementTextContent(courseElement)
const courseShouldBeVisible = generatedCourses[courseName];
setCourseElementVisibility(courseElement, courseShouldBeVisible);
handledCourseElements.add(courseElement)
})
}
// The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
if (document.querySelector('header#page-header.row')) {
observer.disconnect()
chrome.storage.sync.set({ 'savedCourses': generateCourseList(savedCourses) });
return
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
// In case the content script has been injected when some of the DOM has already loaded
onMutation([{ addedNodes: [document.documentElement] }]);
});
}
Reading storage is slow and asynchronous, so you need to do it at the beginning:
chrome.storage.sync.get('courses', ({ courses }) => {
chrome.storage.sync.set({ 'courses': generateCourseList(courses) });
const observer = new MutationObserver(onMutation);
observer.observe(document, { childList: true, subtree: true });
onMutation([{addedNodes: [document.documentElement]}]);
function onMutation(mutations) {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.tagName && node.querySelector(potentially_hidden_element_selector)) {
observer.disconnect();
processNode(node, courses);
}
}
}
}
});
function processNode(node, courses) {
const courseElements = Array.from(
node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'));
courseElements.forEach(courseElement => {
const courseName = getCourseName(courseElement);
const isVisible = courses[courseName];
updateCourseElementInSidebar(courseElement, isVisible);
});
}

How to disable row group expand functionality on one row?

After a lot of searches in SO without any particular solution, I am compelled to ask this question.
What I want is to hide a row group icon on a single group row. Like in the below picture I have a group row that has only one record, which is already shown in the top row. I want to hide that collapse icon on that single record. Only collapse/expand icon shown when group rows are more than one.
For reference see AG-Grid Master-Detail Section, here they specify which rows to expand. Same functionality I needed here.
I'm using the below versions of AG-Grid Angular (v9)
"#ag-grid-community/core": "^25.3.0",
"#ag-grid-enterprise/row-grouping": "^26.0.0",
"#ag-grid-enterprise/server-side-row-model": "^25.3.0",
"ag-grid-angular": "^25.3.0",
"ag-grid-community": "^25.3.0",
Here is my code:
this.rowModelType = 'serverSide';
this.serverSideStoreType = 'partial';
this.cacheBlockSize = 20;
this.gridOptions = {
rowData: this.loanlist,
columnDefs: this.generateColumns(),
getNodeChildDetails: function(rowItem) {
if (rowItem.orderCount > 1) {
return {
expanded: true
}
} else {
return null;
}
}
}
The issue is the getNodeChildDetails is not accessible. Browser console showing me the below warning and my above code is not working.
This is simple to achieve using a cellRendererSelector on the autoGroupColumnDef. You can specify whether to show the default agGroupCellRenderer or simply return another renderer (or, just return null):
this.autoGroupColumnDef = {
cellRendererSelector: (params) => {
if (params.value == 'United States') {
return null;
} else {
return {
component: 'agGroupCellRenderer',
};
}
},
};
In the example below, we are disabling the row group expand functionality on the United States row.
See this implemented in the following plunkr.
The solution isn't that hard - but could be tough, agreed (one day faced with the same case)
So - the answer is custom cell renderer.
It would look a little bit different (separate column for collapse\expande action) - but you would get all control of it.
Custom rendeder component for this action would look like :
template: `
<em
[ngClass]="{'icon-arrow-down':params.node.expanded, 'icon-arrow-right': !params.node.expanded}"
*ngIf="yourFunctionHere()"
(click)="toggleClick()">
</em>`,
export class MasterDetailActionComponent implements ICellRendererAngularComp {
private params: any;
agInit(params: any): void {
this.params = params;
}
public toggleClick(): void {
this.params.node.setExpanded(!this.params.node.expanded);
}
public yourFunctionHere(): boolean {
// so here you are able to access grid api via params.api
// but anyway params.node - would give you everything related to row also
}
refresh(): boolean {
return false;
}
}
in [ngClass] - you are able to handle the visual part (icons) - modify\customize
and don't forget to add this component in the gridOptions:
frameworkComponents: {
'masterDetailActionCellRenderer': MasterDetailActionComponent,
}
and include this column in your columnDef:
columnDefs: [
headerName: "",
width: 75,
field: "expand",
cellRenderer: "masterDetailActionCellRenderer",
filter: false,
resizable: true,
suppressMenu: true,
sortable: false,
suppressMovable: false,
lockVisible: true,
getQuickFilterText: (params) => { return '' }
]

How to dynamically change gutter icons in VSCode with TextEditor.setDecorations()

I am getting some odd behavior when updating gutter icons in VSCode using TextEditor.setDecorations() https://code.visualstudio.com/api/references/vscode-api#TextEditor.
On activate of my VSCode extension the class below is instantiated and it's constructor called which "one-time" creates 3 TextEditorDecorationType's https://code.visualstudio.com/api/references/vscode-api#TextEditorDecorationType, one for each state that a test result can be in represented by a NONE, PASS, FAIL icon and finally triggerUpdateDecorations() is called to collect the current globalResults within 3 arrays and set the gutter icons with vscode.window.activeTextEditor.setDecorations()
So far everything works as expected. No tests have been run and every test shown in the editor is updated with a NONE gutter icon.
Now as each test is run in the editor, on completion, triggerUpdateDecorations() is called once again to collect results and update the gutter icons.
If there are for example 10 tests in the editor, each with a NONE gutter icon, if I run a single test that test correctly updates with either a PASS or FAIL gutter icon. This behavior repeats itself for all subsequent tests run, except for the last one. The last test run remains set with its NONE gutter icon. It's not tied to a specific test as I can jump around and the behavior follows the last test run.
I've tried adding a dummy NONE icon to a random place in the gutter not tied to a test and that allows all gutter icons tied to a test to be updated with PASS or FAIL gutter icons.
I've been experimenting a lot to try and solve this and can't seem to find the root cause. Greatly appreciate any insights on how to solve this.
Note some of this code stems from the VSCode Samples shown here
https://github.com/microsoft/vscode-extension-samples/blob/main/decorator-sample/src/extension.ts#L58
import * as path from 'path';
import * as vscode from 'vscode';
class ProviderDecorations
{
private timeout: NodeJS.Timeout;
private context: vscode.ExtensionContext;
private activeEditor: vscode.TextEditor;
private readonly decorationNone: vscode.TextEditorDecorationType;
private readonly decorationPass: vscode.TextEditorDecorationType;
private readonly decorationFail: vscode.TextEditorDecorationType;
constructor(context: vscode.ExtensionContext)
{
this.context = context;
this.activeEditor = vscode.window.activeTextEditor;
this.decorationNone = this.getDecorationType(ENTRY_STATE.NONE);
this.decorationPass = this.getDecorationType(ENTRY_STATE.PASS);
this.decorationFail = this.getDecorationType(ENTRY_STATE.FAIL);
vscode.window.onDidChangeActiveTextEditor(editor =>
{
this.activeEditor = editor;
if (editor)
{
this.triggerUpdateDecorations();
}
}, null, this.context.subscriptions);
vscode.workspace.onDidChangeTextDocument(event =>
{
if (this.activeEditor && event.document === this.activeEditor.document)
{
this.triggerUpdateDecorations();
}
}, null, this.context.subscriptions);
this.triggerUpdateDecorations();
}
public updateDecorations()
{
if (!this.activeEditor)
{
return;
}
let rangeNone = [];
let rangePass = [];
let rangeFail = [];
globalResults.forEach((result) =>
{
let range = new vscode.Range(result.line, 0, result.line, 0);
switch (result.state)
{
case ENTRY_STATE.NONE:
rangeNone.push({ range });
break;
case ENTRY_STATE.PASS:
rangePass.push({ range });
break;
case ENTRY_STATE.FAIL:
rangeFail.push({ range });
break;
}
});
if (rangePass.length > 0)
{
this.activeEditor.setDecorations(this.decorationPass, rangePass);
}
if (rangeFail.length > 0)
{
this.activeEditor.setDecorations(this.decorationFail, rangeFail);
}
if (rangeNone.length > 0)
{
this.activeEditor.setDecorations(this.decorationNone, rangeNone);
}
}
private getDecorationType(state: ENTRY_STATE): vscode.TextEditorDecorationType
{
let icon = 'none.svg';
if (state === ENTRY_STATE.PASS)
{
icon = 'pass.svg';
}
else if (state === ENTRY_STATE.FAIL)
{
icon = 'fail.svg';
}
const decorationType = vscode.window.createTextEditorDecorationType(
{
light:
{
gutterIconPath: path.join(__dirname, '..', 'resources', 'light', icon),
gutterIconSize: '85%',
},
dark:
{
gutterIconPath: path.join(__dirname, '..', 'resources', 'dark', icon),
gutterIconSize: '85%'
}
});
return decorationType;
}
public triggerUpdateDecorations()
{
if (this.timeout)
{
clearTimeout(this.timeout);
this.timeout = undefined;
}
this.timeout = setTimeout(() =>
{
this.updateDecorations();
}, 250);
}
}
export default ProviderDecorations;
You never clear a Decorator type, remove the if (rangePass.length > 0) parts
this.activeEditor.setDecorations(this.decorationPass, rangePass);
this.activeEditor.setDecorations(this.decorationFail, rangeFail);
this.activeEditor.setDecorations(this.decorationNone, rangeNone);

Is it possible to have own custom Context Menu in ag-Grid-community

Can't find the exact answer.
If i decide to opt-in for vanilla JavaScript (non-Angular & Co) ag-Grid-community edition, can i have easy to add my own custom context menu an other custom extensions?
As i seen their docs, context menu is only enterprise level feature.
I seen some treads that there is some caveats, but i personally did not dig deeper.
In general, how easy is to implement self-built features in ag-Grid-community. Or it is better to write own grid?
We have a custom context menu component in our Angular project with ag-grid community, so it's definitely possible.
How it works:
We define all grid columns in templates. If you want a context menu, you put an empty column into the column set and put a special directive on it. The directive accepts a context menu template, which is passed into a custom cellRendererFramework (a menu trigger button, basically). The directive also configures the column to ensure consistent look across grid instances.
This might be not what you've been looking for if you require for menu to open with right mouse click anywhere in a row, but I suppose it shouldn't be that hard to trigger the menu from a different event (check out ag-grid events, there might something suitable).
The snippets below should be straightforward to adapt for your framework of choice. Given you opted into vanilla JS, you'll have to use regular functions to do the same, something like this:
const grid = withContextMenu(new Grid(element, gridOptions), menuOptions).
Here's an example of how we use it:
<ag-grid-angular>
<ag-grid-column headerName='ID' field='id'></ag-grid-column>
<ag-grid-column [contextMenu]='menu'>
<mat-menu #menu='matMenu'>
<ng-template matMenuContent let-item='data'>
<button mat-menu-item (click)='restoreSnapshot(item.id)'>Restore From Snapshot</button>
<a mat-menu-item [routerLink]='[item.id, "remove"]'>Remove</a>
</ng-template>
</mat-menu>
</ag-grid-column>
</ag-grid-angular>
The directive that applies the menu:
const WIDTH = 42;
export const CONTEXT_MENU_COLID = 'context-menu';
#Directive({
selector: '[agGridContextMenu]'
})
export class AgGridContextMenuDirective implements AfterViewInit {
constructor(private gridComponent: AgGridAngular) {}
#Input()
agGridContextMenu!: ElementRef<MatMenu>;
ngAfterViewInit() {
if (!this.agGridContextMenu) return;
setTimeout(() => {
this.gridComponent.api.setColumnDefs([
...this.gridComponent.columnDefs,
{
colId: CONTEXT_MENU_COLID,
cellRendererFramework: CellRendererContextMenuComponent,
width: WIDTH,
maxWidth: WIDTH,
minWidth: WIDTH,
cellStyle: {padding: 0},
pinned: 'right',
resizable: false,
cellRendererParams: {
suppressHide: true,
contextMenu: {
menu: this.agGridContextMenu
}
}
}
]);
});
}
}
The cell renderer component:
#Component({
selector: 'cell-renderer-context-menu',
template: `
<ng-container *ngIf='params.data && params.colDef.cellRendererParams.contextMenu.menu'>
<button
type='button'
mat-icon-button
[matMenuTriggerFor]='params.colDef.cellRendererParams.contextMenu.menu'
[matMenuTriggerData]='{data: params.data}'
>
<mat-icon svgIcon='fas:ellipsis-v'></mat-icon>
</button>
</ng-container>
`,
styleUrls: ['./cell-renderer-context-menu.component.scss']
})
export class CellRendererContextMenuComponent implements ICellRendererAngularComp {
params!: ICellRendererParams;
agInit(params: ICellRendererParams) {
this.params = params;
}
refresh() {
return false;
}
}
A screenshot:
I followed this blogpost, using community edition ag-grid, and it worked! I was surprised because previously I had the experience that cell renderers didn't allow content outside of the cell boundaries to be shown, but somehow popper/tippy is getting around that (I think it adds itself to the top of the DOM with this section of code appendTo: document.body).
https://blog.ag-grid.com/creating-popups-in-ag-grid/
basically, in my javascript CellRenderer:
class MyCellRenderer{
// https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/
init(e){
this.isOpen = false;
this.container = document.createElement("span");
let menubutton = document.createElement("button");
menubutton.innerHTML="&#x1F80B"; //downward arrow
this.tippyInstance = tippy(menubutton);
this.tippyInstance.disable();
this.container.appendChild(menubutton);
menubutton.addEventListener('click', that.togglePopup.bind(this));
}
getGui() {
return this.container;
}
togglePopup() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.configureTippyInstance();
this.eMenu = this.createMenuComponent();
this.tippyInstance.setContent(this.eMenu);
} else {
this.tippyInstance.unmount();
}
}
configureTippyInstance() {
this.tippyInstance.enable();
this.tippyInstance.show();
this.tippyInstance.setProps({
trigger: 'manual',
placement: 'bottom-start',
arrow: false,
interactive: true,
appendTo: document.body,
hideOnClick: true,
onShow: (instance) => {
tippy.hideAll({ exclude: instance });
},
onClickOutside: (instance, event) => {
this.isOpen = false;
instance.unmount();
},
});
}
createMenuComponent() {
let menu = document.createElement('div');
menu.classList.add('menu-container');
let options = {};
options['Delete Row'] = this.menuItemClickHandler.bind(this);
options['Popup an Alert!'] = function(){alert("hello!");};
options['Popup an Alert 2!'] = this.menuItemClickHandler.bind(this);
for (const [key, value] of Object.entries(options)) {
let item = document.createElement('div');
item.classList.add('menu-item');
item.setAttribute('data-action', key.toLowerCase());
item.classList.add('hover_changes_color');
item.innerText = `${key}`; // string formatting example
item.addEventListener('click', value);
menu.appendChild(item);
}
return menu;
}
menuItemClickHandler(event) {
this.togglePopup();
const action = event.target.dataset.action;
if (action === 'delete row') {
this.params.api.applyTransaction({ remove: [this.params.data] });
}
if (action === 'popup an alert 2!') {
alert("2");
}
}
}
and in styles.css:
.hover_changes_color:hover {
background-color: dimgrey;
cursor: pointer;
}

How to stop double click event or opening child node when clicking on parent node in jsTree?

Here is the JavaScript code to prevent the double click event
$("#jstree_view").on("dblclick", 'li a', function (e) {
e.preventDefault();
});
but it is not working.
set the $.jstree.defaults.core.dblclick_toggle = false
example:
$('#jstree').jstree({
"core" : {
"dblclick_toggle" : false
}
});
jstree API