I'm using ng-bootstrap#6.0.0. Some modal dialogs are opening very quickly, while others are opening extremely slowly (sometimes taking 15 seconds to open).
The slow behavior is: The modal window shows with a maximum width regardless of the size selected, but has not modal-body content (yet). Approximately 15 seconds later, the modal window resizes to the requested size, and then populates with the modal-body as expected.
I have compared the modal components that work fine with the modal components that don't, and see very few differences aside from functionality differences like you would expect.
I've tried changing the "backdrop", I've tried setting the "container", I've tried converting the modal between an in-line ng-template and a separate component. Nothing is changing the behavior of the slow modals.
Does anyone have any ideas?
I am using ngx-simplemde, and launching a modal from a custom toolbar item in the markdown editor. For some reason, when launching a modal, popover, or tooltip from within the markdown editor, it behaves very slowly. I had to abstract my logic out of the custom ngx-simplemde toolbar button and trigger the modal being opened from outside the ngx-simplemde custom toolbar item. This was fairly difficult to figure out, as I had to create a hidden button, and then trigger the hidden button using the DOM's .click() method, and the ngClick of the button was setup to initiate the modal popup. Not ideal, fairly hacky, but it works now.
component.html
<simplemde [(ngModel)]="value" [options]="{ toolbar: toolbar }" (ngModelChange)="valueChange.emit($event)" #smde></simplemde>
<button type="button" class="btn btn-primary" (click)="insertImage()" style="display: none" #buttonElement>Test</button>
component.ts
createImageListToolbar() {
return {
name: 'image-list',
className: 'fas fa-images',
title: this.imageListButtonTitle,
// This action is called when the user clicks the button
// It will open the imageListModal that is embedded in the HTML of this component
// When the modal closes, the user will have selected the image they want inserted
action: async () => {
this.buttonEle.nativeElement.click();
}
};
}
async insertImage() {
const image = await this.modalService.getMediaSelection(this.implementationGuideId, this.mediaReferences);
const doc = this.simplemde.Instance.codemirror.getDoc();
const cursor = doc.getCursor();
const altTag = image.title ? `alt="${image.title}" ` : '';
const replaceText = `<table><tr><td><img src="${image.name}" ${altTag}/></td></tr></table>`;
doc.replaceRange(replaceText, cursor);
}
modal.service.ts:
async getMediaSelection(implementationGuideId?: string, mediaReferences?: MediaReference[]): Promise<ImageItem> {
const modalRef = this.modalService.open(MediaSelectionModalComponent, { container: 'body', backdrop: 'static' });
modalRef.componentInstance.implementationGuideId = implementationGuideId;
modalRef.componentInstance.mediaReferences = mediaReferences;
return await modalRef.result;
}
This could be caused by change detection issues.
Try to open modal in change detection zone.
Inject Zone:
constructor(
private modalService: NgbModal,
private zone: NgZone,
) { }
and wrap modalService.open with zone.run:
confirmDelete(user: DSUser): void {
this.zone.run(() => {
const modalRef = this.modalService.open(ConfirmModalComponent, {
backdrop: true,
centered: true,
}).componentInstance;
modalRef.message = `<strong>Are you sure you want to delete <span class="text-primary">"${user.name.firstName} ${user.name.lastName}"</span> profile?</strong>`;
modalRef.confirmCallback = () => this.deleteUser.emit(user);
});
}
Related
On the project where I work (React, TS), we use the viewer and added the Box Selection extension for it.
The first time you activate it with a button in the toolbar, the extension works, the elements are highlighted. Then you can switch to another mode, for example, the orbit mode. And after that, when you click on the button that activates the "box Selection extension", the extension no longer works. The orbit mode remains working.
At the same time, the button is clicked (console.log() is fired) and the loadExtension('Autodesk.Box Selection') method works.
What could be the problem?
I will give some code snippets
This is the extension code:
export default function RectangleSelectionExtension(
this,
viewer,
options,
) {
window.Autodesk.Viewing.Extension.call(this, viewer, options);
}
RectangleSelectionExtension.prototype = (
Object.create(window.Autodesk.Viewing.Extension.prototype)
);
RectangleSelectionExtension.prototype.constructor = RectangleSelectionExtension;
RectangleSelectionExtension.prototype.load = () => true;
RectangleSelectionExtension.prototype.unload = () => true;
RectangleSelectionExtension.prototype.onToolbarCreated = function onToolbarCreated() {
this.group = this.viewer.toolbar.getControl('allExtensionsToolbar');
if (!this.group) {
this.group = new window.Autodesk.Viewing.UI.ControlGroup('allExtensionsToolbar');
this.viewer.toolbar.addControl(this.group);
}
// Add a new button to the toolbar group
this.button = new window.Autodesk.Viewing.UI.Button('RectangleSelectionExtension');
this.button.onClick = async () => {
const boxSelectionExtension = await this.viewer.loadExtension('Autodesk.BoxSelection');
this.viewer.toolController.activateTool(boxSelectionExtension.boxSelectionTool.getName());
boxSelectionExtension.addToolbarButton(this.viewer);
};
this.button.setToolTip('Select within a rectangle area');
this.button.addClass('RectangleSelectionExtension');
this.group.addControl(this.button);
};
window.Autodesk.Viewing.theExtensionManager.registerExtension('BoxSelection', RectangleSelectionExtension);
Next, in the Viewer component, we import and register the extension:
window.Autodesk.Viewing.theExtensionManager.registerExtension('RectangleSelectionExtension', RectangleSelectionExtension);
And this is how we initialize the viewer:
window.Autodesk.Viewing.Initializer(options, () => {
const container = document.getElementById('forgeViewer');
if (container) {
viewer = new window.Autodesk.Viewing.GuiViewer3D(
container,
{
token,
extensions: [
/* ...some extensions */
'RectangleSelectionExtension',
],
},
);
const startedCode = viewer.start();
if (startedCode > 0) {
return;
}
/* ...some eventListeners */
}
I'm not sure I understand the purpose of your RectangleSelectionExtension. From the code it looks like it just adds a button in the toolbar, and clicking that button repeatedly loads another extension (Autodesk.BoxSelection), repeatedly activates the box selection tool, and repeatedly adds the box selection button to the toolbar. That doesn't seem right.
If you're simply interested in the box selection, you can load it (and include it in the toolbar) like so:
// ...
viewer = new window.Autodesk.Viewing.GuiViewer3D(
container,
{
token,
extensions: [
/* ...some extensions */
'Autodesk.BoxSelection',
]
}
);
// and later ...
const boxSelectionExt = viewer.getExtension('Autodesk.BoxSelection');
boxSelectionExt.addToolbarButton(true); // Add the button to the toolbar
boxSelectionExt.addToolbarButton(false); // Remove the button from the toolbar
// ...
I'm in the initial stages of developing a plugin that will allow the user to insert placeholder elements into HTML content that will be processed server-side and used to incorporate some simple logic into a generated PDF document. To this end, I'm attempting to insert a custom element that I've defined using the web components API.
class NSLoop extends HTMLElement {
constructor() {
super();
}
get source() {
return this.getAttribute('source');
}
get as() {
return this.getAttribute('as');
}
}
window.customElements.define('ns-loop', NSLoop);
The contents of loopediting.js:
import Plugin from "#ckeditor/ckeditor5-core/src/plugin";
import Widget from "#ckeditor/ckeditor5-widget/src/widget";
import {viewToModelPositionOutsideModelElement} from "#ckeditor/ckeditor5-widget/src/utils";
import LoopCommand from "./loopcommand";
export default class LoopEditing extends Plugin {
static get requires() {
return [Widget];
}
constructor(editor) {
super(editor);
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add('loop', new LoopCommand(this.editor));
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register('loop', {
isBlock: false,
isLimit: false,
isObject: false,
isInline: false,
isSelectable: false,
isContent: false,
allowWhere: '$block',
allowAttributes: ['for', 'as'],
});
schema.extend( '$text', {
allowIn: 'loop'
} );
schema.extend( '$block', {
allowIn: 'loop'
} );
}
_defineConverters() {
const conversion = this.editor.conversion;
conversion.for('upcast').elementToElement({
view: {
name: 'ns-loop',
},
model: (viewElement, {write: modelWriter}) => {
const source = viewElement.getAttribute('for');
const as = viewElement.getAttribute('as');
return modelWriter.createElement('loop', {source: source, as: as});
}
});
conversion.for('editingDowncast').elementToElement({
model: 'loop',
view: (modelItem, {writer: viewWriter}) => {
const widgetElement = createLoopView(modelItem, viewWriter);
return widgetElement;
}
});
function createLoopView(modelItem, viewWriter) {
const source = modelItem.getAttribute('source');
const as = modelItem.getAttribute('as');
const loopElement = viewWriter.createContainerElement('ns-loop', {'for': source, 'as': as});
return loopElement;
}
}
}
This code works, in the sense that an <ns-loop> element is successfully inserted into the editor content; however, I am not able to edit this element's content. Any keyboard input is inserted into a <p> before the <ns-loop> element, and any text selection disappears once the mouse stops moving. Additionally, it is only possible to place the cursor at the beginning of the element.
If I simply swap out 'ns-loop' as the tag name for 'div' or 'p', I am able to type within the element without issue, so I suspect that I am missing something in the schema definition to make CKEditor aware that this element is "allowed" to be typed in, however I have no idea what I may have missed -- as far as I'm aware, that's what I should be achieving with the schema.extend() calls.
I have tried innumerable variations of allowedIn, allowedWhere, inheritAllFrom, isBlock, isLimit, etc within the schema definition, with no apparent change in behaviour.
Can anyone provide any insight?
Edit: Some additional information I just noticed - when the cursor is within the <ns-loop> element, the Heading/Paragraph dropdown menu is empty. That may be relevant.
Edit 2: Aaand I found the culprit staring me in the face.
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
I'm new to the CKE5 plugin space, and was using other plugins as a reference point, and I guess I copied that code from another plugin. Removing that code solves the problem.
As noted in the second edit, the culprit was the code,
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
which I apparently copied from another plugin I was using for reference. Removing this code has solved the immediate problem.
I'll accept this answer and close the question once the 2-day timer is up.
Is there any way to change font awesome icon dynamically? I want user to be able to select one of font awesome icons dynamically. It works only when you add class first time. The place where I try to do it is - MatDialog. There is form where user have to select icon, background color and category name. To select icon user should open another dialog.
I'm using Angular 9.1.4 and Font Awesome 5.13.0.
That's what I tried:
1. Using ngClass
category-dialog.component.html
<div [ngStyle]="selectedColor">
<i [ngClass]="selectedIcon"></i>
</div>
category-dialog.component.ts
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe(result => {
this.selectedIcon = result;
});
}
This works only first time. But when you try to change icon selectedIcon changes, but UI doesn't refresh element class.
2. Using #ViewChild
#ViewChild('iconElement') iconElement: ElementRef;
constructor(private dialog: MatDialog,
private renderer: Renderer2) { }
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe((result: string) => {
this.iconElement.nativeElement.className = result;
});
}
This also works only first time.
3. Using #ViewChild and Renderer2
category-dialog.component.html
<div #colorElement [ngStyle]="selectedColor">
<i #iconElement></i>
</div>
category-dialog.component.ts
#ViewChild('colorElement') parentElement: ElementRef;
#ViewChild('iconElement') childElement: ElementRef;
constructor(private dialog: MatDialog,
private renderer: Renderer2) { }
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe(result => {
this.replaceIcon(result);
});
}
replaceIcon(iconClass: string): void {
const i = this.renderer.createElement('i');
this.renderer.setProperty(i, 'class', iconClass);
this.renderer.removeChild(this.parentElement.nativeElement, this.childElement);
this.renderer.appendChild(this.parentElement.nativeElement, i);
}
That doesn't work at all.
Is there any way how to change font awesome dynamically?
Resolution
Wasted lot of my free time to investigate how to resolve this issue. Tried everything with Renderer2 and all dirty Javascript methods. But one day I came up with idea to use innerHtml.
Rendering new string of inner HTML changes Font Awesome icons interactively.
category-dialog.component.html
<div [ngStyle]="selectedColor" [innerHtml]="selectedIconHtml"></div>
category-dialog.component.ts
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe((result: string) => {
// EVERY TIME NEW ELEMENT WITH NEW FA CLASS
this.selectedIconHtml = `<i class="${result}"></i>`;
});
}
This solution - on every icon selection's changing <div> element content (inner html).
I solved this issue like this:
<div innerHTML="<i class='{{icon}}'></i>">
</div>
In this case, icon will be rerendered after value changes. innerHTML makes this happen easily. No need any code in TS file.
Is it possible to link two elements together so changing contents of one automatically changes contents of the other using jquery or angular?
I have an app where I want the title of an element, which appears a few times throughout the app, to be the same. So for example, when i click on one button called "open window 1", it opens window 1 and when i click on another button also called "open window 1" it also opens window 1.
What im trying to achieve is if i change the title of 1 button, to try to make it change the title of the other button in real time. Remember i have around 20 different buttons on the app, where each has a twin that should be linked together.
In basic terms i just want to link the two buttons together so they are like a mirror image of each other. Is this some feature/function Angular JS has?
The last time I used Angular was a while ago, and it was mostly with Angularjs rather than Angular, so this is just a brief gist to give you an idea of what I mean, which you'd have to tailor to your needs
You could do something like creating a global module somewhere, such as:
buttons.ts:
interface ButtonInfo {
title: string,
onClick: Function
}
const button1: ButtonInfo = {
title: 'Open Window 1',
onClick: openWindow1()
};
const updateButton1 = update => {
if (update.title) button1.title = update.title;
if (update.onClick) button1.onClick = update.onClick;
};
const button2: ButtonInfo = {
title: 'Open Window 2',
onClick: openWindow2()
};
const updateButton2 = update => {
if (update.title) button2.title = update.title;
if (update.onClick) button2.onClick = update.onClick;
};
export {
button1,
updateButton1,
button2,
updateButton2,
ButtonInfo
};
Then you could import this wherever your pairs of buttons are:
button1.component.ts:
import { button1, updateButton1 } from './buttons';
import type { ButtonInfo } from './buttons';
export class Button1Component {
button1: ButtonInfo = button1;
...
updateButton(update) {
updateButton1(update);
}
}
button1.component.html:
<div>
...
<button (click)="button1.onClick()">
{{button1.title}}
</button>
</div>
Background: my main page opens up an external window (same origin) of another module within my project upon button click.
I also set up a BroadcastChannel so that these two windows can now communicate. Now, if this window is already open & the user clicks the triggering button once again, I want to communicate this to the window:
onAddNewFieldClick() {
if (this.window === null) {
this.window = window.open(window.location.origin + '/wizard', 'Field Wizard', 'resizable,scrollbar');
this.channel = new BroadcastChannel('edit-spec-wizard-channel');
} else {
this.channel.postMessage(1);
}
}
The new window listens on this channel and appends the message data to an array that is used in an ngFor. To be extra safe. I go ahead and create a brand new array each time a new value is pushed to cause a rebind. Here is the logic that powers the component in the new window.
export class EntryComponent implements OnInit, OnDestroy {
newFieldChannel: BroadcastChannel;
newFields: number[] = [];
constructor() { }
ngOnInit() {
this.newFieldChannel = new BroadcastChannel('edit-spec-wizard-channel');
this.newFieldChannel.onmessage = this.newFieldChannelOnMessage.bind(this);
this.newFields.push(1);
}
func() {
this.newFields.push(1);
this.newFields = this.newFields.slice();
}
private newFieldChannelOnMessage(event: MessageEvent) {
this.newFields.push(event.data as number);
this.newFields = this.newFields.slice();
}
ngOnDestroy() {
this.newFieldChannel.close();
}
}
And here is the template HTML:
<div class="row">
<div class="col" *ngFor="let newField of newFields">
<div style="width: 300px; height: 600px; background-color: white;">
NEW FIELD BOX
</div>
</div>
<button class="btn btn-primary" (click)="func()">Click me</button>
</div>
I have also included a button that triggers a function ("func()") that has the exact same logic as the postMessage handler.
Now, when I click the button in this window, I'll get the expected behavior: The correct number of "NEW FIELD BOX" divs will appear in this new window. However, when I press the original button from the main screen that posts a message over the BroadcastChannel, it WILL NOT update the UI to display the right number of "NEW FIELD BOX" divs. Using break points I can see that the array newFields does contain the right number of values, but ngFor does not re-render.
Example: I click the button on the main page that fires the onAddNewFieldClick(). It opens a new window which has one "NEW FIELD BOX" div. I click this button again which posts a message to add another. Still, only one remains on the window. I now click the button within the window that fires the function "func()." This will now render 3 "NEW FIELD BOX" divs (the original one from initialization, the one from the post message that didn't render, and the one from clicking this button).
Any ideas why change detection doesn't seem to happen from a postMessage?
The newFieldChannelOnMessage event handler is probably running outside of the Angular zone and does not trigger change detection. Try wrapping the code in NgZone.run():
import { NgZone } from "#angular/core";
constructor(private ngZone: NgZone) { ... }
private newFieldChannelOnMessage(event: MessageEvent) {
this.ngZone.run(() => {
this.newFields.push(event.data as number);
});
}