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 building a dashboard using Dash in Python. I have configured all the graphs nicely (it's running on the server here) and the next step is to create a responsive navbar and a footer. Currently looks like this:
And when I shrink the width, it looks like this:
I want to add functionality to this button so it would hide the three links on click. I'm trying to toggle the CSS 'active' attribute using JavaScript with this piece of code:
var toggleButton = document.getElementsByClassName('toggle-button')[0]
var navBarLinks = document.getElementsByClassName('navbar-links')[0]
function toggleFunction() {
navBarLinks.classList.toggle('active')
}
toggleButton.addEventListener('click', toggleFunction)
Basically, when the navbar-links class is active, I want it to be set as display: flex, and when it's not active I want it to be display: none
The HTML elements defined in Python screen are here:
html.Nav([
html.Div('Covid-19 global data Dashboard', className='dashboard-title'),
html.A([html.Span(className='bar'),
html.Span(className='bar'),
html.Span(className='bar')],
href='#', className='toggle-button'),
html.Div(
html.Ul([
html.Li(html.A('Linked-In', href='#')),
html.Li(html.A('Source Code', href='#')),
html.Li(html.A('CSV Data', href='#'))
]),
className='navbar-links'),
], className='navbar')
I didn't expect that there would be issues with accessing elements through JavaScript. After doing some research I found out that JavaScript when executes getElementsByClassName function the returned value is null. That is because the function is run before the page is rendered (as far as I understand). It gives me this error:
This project is getting quite big, so I don't know which parts should I include in this post, but I will share the git repository and the preview of the page. Is there an easy solution to it?
You can defer the execution of JavaScript code until after React has loaded via the DeferScript component from dash-extensions. Here is a small example,
import dash
import dash_html_components as html
from html import unescape
from dash_extensions import DeferScript
mxgraph = r'{"highlight":"#0000ff","nav":true,"resize":true,"toolbar":"zoom layers lightbox","edit":"_blank","xml":"<mxfile host=\"app.diagrams.net\" modified=\"2021-06-07T06:06:13.695Z\" agent=\"5.0 (Windows)\" etag=\"4lPJKNab0_B4ArwMh0-7\" version=\"14.7.6\"><diagram id=\"YgMnHLNxFGq_Sfquzsd6\" name=\"Page-1\">jZJNT4QwEIZ/DUcToOriVVw1JruJcjDxYho60iaFIaUs4K+3yJSPbDbZSzN95qPTdyZgadm/GF7LAwrQQRyKPmBPQRzvktidIxgmwB4IFEaJCUULyNQvEAyJtkpAswm0iNqqegtzrCrI7YZxY7Dbhv2g3r5a8wLOQJZzfU4/lbByoslduPBXUIX0L0cheUrugwk0kgvsVojtA5YaRDtZZZ+CHrXzukx5zxe8c2MGKntNgknk8bs8fsj3+KtuDhxP+HZDVU5ct/RhatYOXgGDbSVgLBIG7LGTykJW83z0dm7kjklbaneLnEnlwFjoL/YZzb93WwNYgjWDC6EEdkuC0cZEO7p3i/6RF1WutL8nxmnkxVx6UcUZJIy/LgP49622mO3/AA==</diagram></mxfile>"}'
app = dash.Dash(__name__)
app.layout = html.Div([
html.Div(className='mxgraph', style={"maxWidth": "100%"}, **{'data-mxgraph': unescape(mxgraph)}),
DeferScript(src='https://viewer.diagrams.net/js/viewer-static.min.js')
])
if __name__ == '__main__':
app.run_server()
Dash callback solution (no Javascript):
import dash
import dash_html_components as html
from dash.dependencies import Output, Input, State
navbar_base_class = "navbar-links"
app = dash.Dash(__name__)
app.layout = html.Nav(
[
html.Div("Covid-19 global data Dashboard", className="dashboard-title"),
html.A(
id="toggle-button",
children=[
html.Span(className="bar"),
html.Span(className="bar"),
html.Span(className="bar"),
],
href="#",
className="toggle-button",
),
html.Div(
id="navbar-links",
children=html.Ul(
children=[
html.Li(html.A("Linked-In", href="#")),
html.Li(html.A("Source Code", href="#")),
html.Li(html.A("CSV Data", href="#")),
],
),
className=navbar_base_class,
),
],
className="navbar",
)
#app.callback(
Output("navbar-links", "className"),
Input("toggle-button", "n_clicks"),
State("navbar-links", "className"),
prevent_initial_call=True,
)
def callback(n_clicks, current_classes):
if "active" in current_classes:
return navbar_base_class
return navbar_base_class + " active"
if __name__ == "__main__":
app.run_server(debug=True)
The idea of the code above is to take the toggle-button click as Input and the current value of navbar-links as State. We can use this state to determine if we should add the active class or remove it. The new className value is returned in the callback.
Javascript solution:
window.addEventListener("load", function () {
var toggleButton = document.getElementsByClassName("toggle-button")[0];
var navBarLinks = document.getElementsByClassName("navbar-links")[0];
function toggleFunction() {
navBarLinks.classList.toggle("active");
}
toggleButton.addEventListener("click", toggleFunction);
});
The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
DOMContentLoaded would be preferable to use, but it only works for me with load.
If you need a pure JS solution you need to use MutationObserver. I've wrote a little helper function we are currently using that did the trick. Another suggestion would be to change the mutation to an element on screen then fire an event to handle the rest
/**
*
* #param {string} id
* #param {*} event
* #param {(this: HTMLElement, ev: any) => any} callback
* #param {boolean | AddEventListenerOptions} options
*/
function attachEventToDash(id, event, callback, options) {
debugger;
var observer = new MutationObserver(function (_mutations, obs) {
var ele = document.getElementById(id);
if (ele) {
debugger;
ele.addEventListener(event, callback, options)
obs.disconnect();
}
});
window.addEventListener('DOMContentLoaded', function () {
observer.observe(document, {
childList: true,
subtree: true
});
})
}
I'm trying to create a React Portal that when mounted, requires running a specific line to load an ActiveReports Designer component.
Here's is my portal code:
constructor(props: IWindowPortalProps) {
super(props);
this.containerEl = document.createElement('div'); // STEP 1: create an empty div
this.containerEl.id = 'designer-host';
this.containerEl.className = styles.designerHost;
this.externalWindow = null;
}
private copyStyles = (sourceDoc: Document, targetDoc: Document) => {
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
if (styleSheet.cssRules) { // true for inline styles
const newStyleEl = sourceDoc.createElement('style');
Array.from(styleSheet.cssRules).forEach(cssRule => {
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
});
targetDoc.head.appendChild(newStyleEl);
} else if (styleSheet.href) { // true for stylesheets loaded from a URL
const newLinkEl = sourceDoc.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
targetDoc.head.appendChild(newLinkEl);
}
});
}
componentDidMount() {
this.externalWindow = window.open('', '', `height=${window.screen.height},width=${window.screen.width}`);
this.externalWindow.document.body.appendChild(this.containerEl);
this.externalWindow.document.title = 'A React portal window';
this.externalWindow.addEventListener('load', () => {
new Designer('#designer-host');
});
}
render() {
return ReactDOM.createPortal(null, this.containerEl);
}
However, when the new window loads, I get the error
Error: Cannot find the host element. at Function.<anonymous>
which indicates that the designer-host div is not there. I think the load function points to the main DOM and not the new window's one.
Alternatively, I tried appending the ActiveReports .js file by doing in my componentDidMount()
s.type = "text/javascript";
s.src = "../node_modules/#grapecity/activereports/lib/node_modules/#grapecity/ar-js-designer/index.js";
this.externalWindow.document.head.append(s);
and then assigning the Designer instantiation on the onLoad property of the element. Again with no luck.
Is there maybe a way I could run JavaScript a code after the portal has been loaded and point to that DOM?
Thank you
I work for GrapeCity. Could you please go to our support portal and submit a ticket. We will need a full code sample for us to be able to answer this question. Please give us a download link to the sample within the ticket.
Thank you
I have a component which displays a data. I have to open this component in a new window on clicking a button/ link from a parent component.
export default class Parent extends Component {
construtor(props) {
super(props);
}
viewData = () => {
window.open('childcomponent.js','Data','height=250,width=250');
}
render() {
return (
<div> <a onclick={this.viewData}>View Data</a></div>
)
}
}
I dont know how to invoke another component and also display it in a new size specified window.
Actually I need to send a props to that child component with which it will fetch me the data from database and render it.
You can use ReactDOM.createPortal to render a component in a new window as David Gilbertson explains in his post:
class MyWindowPortal extends React.PureComponent {
constructor(props) {
super(props);
// STEP 1: create a container <div>
this.containerEl = document.createElement('div');
this.externalWindow = null;
}
render() {
// STEP 2: append props.children to the container <div> that isn't mounted anywhere yet
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
componentDidMount() {
// STEP 3: open a new browser window and store a reference to it
this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
// STEP 4: append the container <div> (that has props.children appended to it) to the body of the new window
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
// STEP 5: This will fire when this.state.showWindowPortal in the parent component becomes false
// So we tidy up by closing the window
this.externalWindow.close();
}
}
The upvoted answer works great!
Just leaving a function component version here in case people are searching for that in the future.
const RenderInWindow = (props) => {
const [container, setContainer] = useState(null);
const newWindow = useRef(null);
useEffect(() => {
// Create container element on client-side
setContainer(document.createElement("div"));
}, []);
useEffect(() => {
// When container is ready
if (container) {
// Create window
newWindow.current = window.open(
"",
"",
"width=600,height=400,left=200,top=200"
);
// Append container
newWindow.current.document.body.appendChild(container);
// Save reference to window for cleanup
const curWindow = newWindow.current;
// Return cleanup function
return () => curWindow.close();
}
}, [container]);
return container && createPortal(props.children, container);
};
This answer is based on David Gilbertson's post. It has been modified to work in Edge. To make this work in Edge div and style elements must be created with the window into which they will be rendered.
class MyWindowPortal extends React.PureComponent {
constructor(props) {
super(props);
this.containerEl = null;
this.externalWindow = null;
}
componentDidMount() {
// STEP 1: Create a new window, a div, and append it to the window. The div
// *MUST** be created by the window it is to be appended to (Edge only)
this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
this.containerEl = this.externalWindow.document.createElement('div');
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
// STEP 2: This will fire when this.state.showWindowPortal in the parent component
// becomes false so we tidy up by just closing the window
this.externalWindow.close();
}
render() {
// STEP 3: The first render occurs before componentDidMount (where we open the
// new window) so container may be null, in this case render nothing.
if (!this.containerEl) {
return null;
}
// STEP 4: Append props.children to the container <div> in the new window
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
The full modified source can be found here https://codepen.io/iamrewt/pen/WYbPWN
You wouldn't open the component directly. You'll need a new page/view that will show the component. When you open the window, you'll then point it at the appropriate URL.
As for size, you provide it as a string in the third parameter of open, which you actually have correct:
window.open('http://example.com/child-path','Data','height=250,width=250');
Note, however, that browsers may, for a variety of reasons, not respect your width and height request. For that reason, it's probably a good idea to also apply appropriate CSS to get a space the right size in case it does open larger than you wanted.
Adding to the current answers - to copy the styles from the original window to the popup window, you can do:
function copyStyles(src, dest) {
Array.from(src.styleSheets).forEach(styleSheet => {
dest.head.appendChild(styleSheet.ownerNode.cloneNode(true))
})
Array.from(src.fonts).forEach(font => dest.fonts.add(font))
}
and then add
copyStyles(window.document, popupWindow.document);
after the call to window.open and once the ref is initialized
For anyone having problem with this answer regarding copying styles and it's not working on production build.
In case you open new window using window.open('', ...), the new window will most likely have about:blank as URL and won't find css files as they have relative paths. You need to set absolute path to href attribute instead:
function copyStyles(src, dest) {
Array.from(src.styleSheets).forEach((styleSheet) => {
const styleElement = styleSheet.ownerNode.cloneNode(true);
styleElement.href = styleSheet.href;
dest.head.appendChild(styleElement);
});
Array.from(src.fonts).forEach((font) => dest.fonts.add(font));
}
If someone is having trouble adding the styles to your new window, the trick is to copy the whole DOM head from the parent to your new popup window.
First you need to stablish the whole html skeleton
newWindow.document.write("<!DOCTYPE html");
newWindow.document.write("<html>");
newWindow.document.write("<head>");
newWindow.document.write("</head>");
newWindow.document.write("<body>");
newWindow.document.write("</body>");
newWindow.document.write("</html>");
// Append the new container to the body of the new window
newWindow.document.body.appendChild(container);
Now in the new window's DOM we have an empty head tag, we traverse the parent head tag and append its children to the new head.
const parentHead= window.document.querySelector("head").childNodes;
parentHead.forEach( item =>{
newWindow.document.head.appendChild(item.cloneNode(true)); // deep copy
})
And that's all. With the new window having the same head children than the parent, all your styles should be working now.
P.S. some styles might be a little stubborn and will not work, the hack that I found worked for those ones is to add a setTimeout in the componentDidMount or in the useEffect with the right time so as you can update your new head with the parent head. Something like this
setTimeout(() => {
updateHead();
}, 5000);
i am using the ARCGIS Javascript API and trying to override the default right click behavior of the vertex points of a shape.
in ESRI's help it does list the onVertexClick event however from here it seems there is no way to determine if this is a right or left click event so i cannot override just the rightclick.
https://developers.arcgis.com/javascript/jsapi/edit.html
I am trying to set the right click behavour to just delete the current node/vertex instead of showing a menu with the option Delete.
EDIT
Here is the current event that exists within the ARCGIS api.
this.eventsList.push(dojo.connect(this._editToolbar, 'onVertexClick', $.proxy(this.addCustomVertexClickEvent, this)));
this event is already in the api however it does not return any way for me to determine left/right click.
your comment "listen for the click event then test the button attribute of the MouseEvent object" would work however i cant actually add a click event to the vertex points directly as these are inside the ARCGIS api code.
For anyone else who is looking for a way to do this without hacking around. You can listen to "contextmenu" (right click) events on the body, set a flag in the "contextmenu" handler to let the application know the current state. Simulate a click event to the "vertex handle" with a "mousedown", "mouseup" combination. In the "vertex-click" handler check for the right click flag set in the "contextmenu" handler
var editToolbar = new Edit(map, options);
var rightClick;
$('body').on('contextmenu', function(e) {
var target = e.target;
if(target.tagName === 'circle') {
// We only care about this event if it targeted a vertex
// which is visualized with an SVG circle element
// Set flag for right click
rightClick = true;
// Simulate click on vertex to allow esri vertex-click
// to fill in the data for us
var mouseDownEvt = new MouseEvent('mousedown', e.originalEvent);
target.dispatchEvent(mouseDownEvt);
var mouseUpEvt = new MouseEvent('mouseup', e.originalEvent);
target.dispatchEvent(mouseUpEvt);
// Since this event will be handled by us lets prevent default
// and stop propagation so the browser context menu doesnt appear
e.preventDefault();
e.stopPropagation();
}
});
editToolbar.on('vertex-click', function(e) {
if(rightClick) {
// Handle the right click on a vertex
rightClick = null;
}
});
after hearing back from ESRI it seems they do not provide this detail in their API so this is not possible yet.
I ended up doing this differently. I wanted to add a UI so the user could enter the XY of the point
// setup to allow editing
this.editToolbar = new EditToolbar(this.map, { allowDeleteVertices: false });
const rcMenuForGraphics = new RightClickVertexContextMenu();
const menu = rcMenuForGraphics.createMenu();
// bind to the map graphics as this is where the vertex editor is
this.map.graphics.on("mouse-over", (evt)=> {
// bind to the graphic underneath the mouse cursor
menu.bindDomNode(evt.graphic.getDojoShape().getNode());
});
this.map.graphics.on("mouse-out", (evt)=> {
menu.unBindDomNode(evt.graphic.getDojoShape().getNode());
});
this.editToolbar.on("vertex-click", (evt2) => {
rcMenuForGraphics.setCurrentTarget(evt2);
// evt2.vertexinfo.graphic.geometry.setX(evt2.vertexinfo.graphic.geometry.x - 1000);
})
// when the graphics layer is clicked start editing
gl.on("click", (evt: any) => {
this.map.setInfoWindowOnClick(false);
// tslint:disable-next-line: no-bitwise
const t: any = EditToolbar.MOVE | EditToolbar.EDIT_VERTICES;
this.editToolbar.deactivate();
this.editToolbar.activate(t, evt.graphic);
})
The code for the menu uses esri's vertex editor to grab the point, change its XY and then manually call the events to refresh the geometry. Only tested with polygon
import Menu = require("dijit/Menu");
import MenuItem = require("dijit/MenuItem");
import Graphic = require("esri/graphic");
import Edit = require("esri/toolbars/edit");
import Point = require("esri/geometry/Point");
class RightClickVertexContextMenu {
private curentTarget: { graphic: Graphic; vertexinfo: any; target: Edit; };
public createMenu() {
const menuForGraphics = new Menu({});
menuForGraphics.addChild(new MenuItem({
label: "Edit",
onClick: () => {
// this is a bit hooky. We grab the verx mover, change the x/y and then call the _moveStopHandler
console.log(this.curentTarget.vertexinfo);
const e: any = this.curentTarget.target;
const mover = e._vertexEditor._findMover(this.curentTarget.vertexinfo.graphic);
const g: Graphic = mover.graphic;
// add in a UI here to allow the user to set the new value. This just shifts the point to the left
g.setGeometry(new Point(mover.point.x - 1000, mover.point.y ))
e._vertexEditor._moveStopHandler(mover, {dx: 15});
this.curentTarget.target.refresh();
}
}));
menuForGraphics.addChild(new MenuItem({
label: "Delete",
onClick: () => {
// call the vertex delete handler
const ct: any = this.curentTarget.target;
ct._vertexEditor._deleteHandler(this.curentTarget.graphic)
}
}));
return menuForGraphics;
}
public setCurrentTarget(evt: { graphic: Graphic; vertexinfo: any; target: Edit; }) {
this.curentTarget = evt;
}
}
export = RightClickVertexContextMenu;