I can move and transfer the rectangles with the code below.
I used konva library at vue.js
This works well.
But I want to get the x,y position to save into local-storage after moving it
Could you teach how to get that?
And I am sorry for the long code
You can attach this code at '.vue' which works well without problem.
It moves and transfer well, but I can 't see the value of position moving it
<template>
<div>
<v-stage ref="stage" :config="stageSize" #mousedown="handleStageMouseDown">
<v-layer ref="layer">
<v-rect v-for="item in rectangles" :key="item.id" :config="item"/>
<v-transformer ref="transformer"/>
</v-layer>
</v-stage>
<div>
<p>{{ rectangles[0].x }}</p>
<button #click="addCounter">+</button>
<button #click="subCounter">-</button>
<button #click="position">SAVE</button>
</div>
</div>
</template>
<script>
const width = window.innerWidth;
const height = window.innerHeight;
export default {
data() {
return {
stageSize: {
width: width,
height: height
},
rectangles: [
{
x: 150,
y: 100,
width: 100,
height: 100,
fill: "red",
name: "rect1",
draggable: true
},
{
x: 150,
y: 150,
width: 100,
height: 100,
fill: "green",
name: "rect2",
draggable: true
}
],
selectedShapeName: ""
};
},
methods: {
position() {
localStorage.setItem(this.rectangles[0].x, true);
},
addCounter() {
this.rectangles[0].x++;
},
subCounter() {
this.rectangles[0].x--;
},
handleStageMouseDown(e) {
// clicked on stage - cler selection
if (e.target === e.target.getStage()) {
this.selectedShapeName = "";
this.updateTransformer();
return;
}
// clicked on transformer - do nothing
const clickedOnTransformer =
e.target.getParent().className === "Transformer";
if (clickedOnTransformer) {
return;
}
// find clicked rect by its name
const name = e.target.name();
const rect = this.rectangles.find(r => r.name === name);
if (rect) {
this.selectedShapeName = name;
} else {
this.selectedShapeName = "";
}
this.updateTransformer();
},
updateTransformer() {
// here we need to manually attach or detach Transformer node
const transformerNode = this.$refs.transformer.getStage();
const stage = transformerNode.getStage();
const { selectedShapeName } = this;
const selectedNode = stage.findOne("." + selectedShapeName);
// do nothing if selected node is already attached
if (selectedNode === transformerNode.node()) {
return;
}
if (selectedNode) {
// attach to another node
transformerNode.attachTo(selectedNode);
} else {
// remove transformer
transformerNode.detach();
}
transformerNode.getLayer().batchDraw();
}
}
};
</script>
You can use dragmove and transform events.
<v-rect
v-for="item in rectangles"
:key="item.id"
:config="item"
#dragmove="handleRectChange"
#transform="handleRectChange"
/>
handleRectChange(e) {
console.log(e.target.x(), e.target.y()); // will log current position
},
Demo: https://codesandbox.io/s/lp53194w59
Related
We're building an app using Ionic 6 / Vue 3 / Capacitor as our framework. On one of the pages we need to display a form which, among other inputs, contain 2 map components. The user can tap to pinpoint a geographical location in each of the maps. This is our map component:
<template>
<ion-item>
<ion-label position="stacked" color="tertiary" style="margin-bottom: 10px"
>{{ controlLabel }} (Tap to choose position)</ion-label
>
<div class="mapBox">
<div
:id="'map'+randomId"
class="map"
#click="getCoord($event)"
></div>
</div>
<div>
<ion-label color="tertiary" position="stacked">{{
$lang.Lengdegrad
}}</ion-label>
<ion-input
v-model="lon"
#change="setMarker($event)"
:controlIdLat="controlIdLat"
:data-value="valueLat"
/>
</div>
<div>
<ion-label color="tertiary" position="stacked">{{
$lang.Breddegrad
}}</ion-label>
<ion-input
v-model="lat"
#change="setMarker($event)"
:controlIdLon="controlIdLon"
:data-value="valueLon"
/>
</div>
</ion-item>
</template>
<script>
import { defineComponent } from "#vue/runtime-core";
import { IonInput, IonItem, IonLabel } from "#ionic/vue";
import Map from "ol/Map";
import View from "ol/View";
import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import { Style, Icon } from "ol/style";
import { OSM, Vector as VectorSource } from "ol/source";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { fromLonLat, toLonLat } from "ol/proj";
import XYZ from "ol/source/XYZ";
import "ol/ol.css";
import PinImg from "../../resources/icons8-pin-48.png";
import { Geolocation } from "#capacitor/geolocation";
export default defineComponent({
props: [
"controlLabel",
"controlIdLat",
"controlIdLon",
"lonLatFields",
"valueLat",
"valueLon",
"setStartMarker",
],
components: {
IonInput,
IonItem,
IonLabel,
},
data() {
return {
mainMap: null,
lat: null,
lon: null,
pinLayer: null,
pinFeat: null,
$lang: this.$lang,
isOnline: this.$isOnline,
isVisible: false,
randomId: Math.random().toString(36).substr(2, 5),
};
},
mounted() {
console.log("randomid",this.randomId)
this.source = new VectorSource();
setTimeout(async () => {
if (document.readyState === "loading") {
document.addEventListener(
"DOMContentLoaded",
await this.getLocation()
);
} else {
await this.getLocation();
}
this.myMap();
this.$nextTick(() => {
if (this.setStartMarker) {
this.setMarker();
}
view.setCenter(fromLonLat([this.lon, this.lat]));
});
}, 100);
},
methods: {
async getLocation() {
const position = await Geolocation.getCurrentPosition();
this.lat = position.coords.latitude.toFixed(3);
this.lon = position.coords.longitude.toFixed(3);
},
getCoord(event) {
let lonlat = toLonLat(this.mainMap.getEventCoordinate(event));
this.lat = lonlat[1].toFixed(3);
this.lon = lonlat[0].toFixed(3);
this.$nextTick(() => {
this.setMarker();
});
},
myMap() {
this.mainMap = new Map({
layers: [
new TileLayer({
source: new OSM(),
}),
new TileLayer({
source: new XYZ({
url: "https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}",
attributions:
'Kartverket',
}),
}),
(this.pinLayer = new VectorLayer({
source: new VectorSource({
features: [],
}),
style: new Style({
image: new Icon({
anchor: [0.5, 46],
anchorXUnits: "fraction",
anchorYUnits: "pixels",
src: PinImg,
}),
}),
})),
],
target: "map" + this.randomId,
view: view,
});
console.log("map", this.mainMap);
},
setMarker() {
let p = new Point(fromLonLat([this.lon, this.lat]));
console.log("setmarker", p);
if (!this.pinLayer.getSource().getFeatures().length) {
this.pinFeat = new Feature({
geometry: p,
});
this.pinLayer.getSource().addFeature(this.pinFeat);
} else {
this.pinFeat.setGeometry(p);
}
let vals = {
[this.controlIdLat]: this.lat,
[this.controlIdLon]: this.lon,
};
this.$emit("input", vals);
console.log(p.getCoordinates());
view.setCenter(p.getCoordinates());
},
},
});
const view = new View({
center: fromLonLat([13.486, 68.131]),
zoom: 10,
constrainResolution: true,
});
</script>
<style scoped>
div.map {
border: 5px solid white;
margin: 0 auto;
height: 100%;
width: 100%;
}
div.mapBox {
width: 100%;
height: 40vh;
}
</style>
Testing in the browser, this kind of works. The user can tap each of the two maps and two separate locations will be saved to the variables. However, when panning or zooming one of the maps, the other one follows so that they both show exactly the same map area.
As you can see, I've tried assigning a random id to each map to separate them from each other. That didn't work.
Testing on an Android phone, only the latter of the two maps are displayed. The first mainMap is left undefined.
My guess is that if I can figure out the first nuisance, then the second problem will also be solved. Any tips?
Edit: I tried making a new identical map component and use one for each map. Now the maps work as expected in the browser, being controlled separately. However the first map is still undefined on Android.
I'm working on Vue 3 app where I would like to use multiple instances of one component. Each component should have its instance of D3 for displaying various SVG images. In my case D3 works as intended only on first instance of Vue component.
Dots are random generated by D3. I can see when inspecting elements that none of dots has been appended in second instance of component. Screenshot of the problem may be found here.
My component with D3 looks like this:
<template>
<div class="fill-100">
<svg ref="svgRef" width="400" height=667>
<g></g>
</svg>
</div>
</template>
<script>
import {ref, onMounted} from "#vue/runtime-core";
import {select, zoom} from "d3";
export default {
name: "SldSvgD3",
props: ["id"],
setup() {
const svgRef = ref(null);
onMounted(() =>{
const svg = select(svgRef.value);
svg.append("svg")
let data = [], width = 400, height = 667, numPoints = 100;
let zoom3 = zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
select('svg g')
.attr('transform', e.transform);
}
function initZoom() {
select('svg')
.call(zoom3);
}
function updateData() {
data = [];
for(let i=0; i<numPoints; i++) {
data.push({
id: i,
x: Math.random() * width,
y: Math.random() * height
});
}
}
function update() {
select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3);
}
initZoom();
updateData();
update();
});
return {svgRef}
}
}
</script>
<style lang="scss">
.fill-100{
width: 100%;
height: 100%;
}
</style>
Implementation of D3 zoom and pan taken from this site
What I didn't know is that scope of d3.select() call is global for the whole app. Solution in my case was just creating unique id for root div and selecting this div before any manipulation.
This question was very helpful to me.
Complete code:
<template>
<div class="fill-100" :id="'sld_div'+this.id">
</div>
</template>
<script>
import {ref, onMounted} from "#vue/runtime-core";
import * as d3 from "d3";
export default {
name: "SldSvgD3",
props: ["id"],
setup(props) {
const svgRef = ref(null);
const svg_width = 400;
const svg_height = 667;
onMounted(() =>{
const svg = d3
.select("#sld_div"+props.id)
svg.append("svg")
.attr("id","sld_root"+props.id)
.attr("width", svg_width)
.attr("height", svg_height)
.append("g")
.attr("id","sld_root_g"+props.id)
let data = [], width = 600, height = 400, numPoints = 100;
let zoom = d3.zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
d3.select("#sld_div"+props.id)
.select('svg g')
.attr('transform', e.transform);
}
function initZoom() {
d3.select("#sld_div"+props.id)
.select('svg')
.call(zoom);
}
function updateData() {
data = [];
for(let i=0; i<numPoints; i++) {
data.push({
id: i,
x: Math.random() * width,
y: Math.random() * height
});
}
}
function update() {
d3.select("#sld_div"+props.id)
.select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3);
}
initZoom();
updateData();
update();
});
return {svgRef}
}
}
</script>
<style lang="scss">
.fill-100{
width: 100%;
height: 100%;
}
</style>
I'm try do reactive change style HTML element from range input, but there is a problem style params font-size, height, width and more with px
<span v-bind:style="myobj.css">Hi all</span>
var vm = new Vue({
el: '#app',
data: {
myobj: {
css: {
color: '#999999',
fontSize: '18',
}
}
}
})
<input v-model="myobj.css.fontSize" type="range" min="10" max="32" step="1">
but needly - "font-size: 18px, how i can do it? I'm try use filter, but is doesnt work with obj
You could use a computed property for your v-model:
computed: {
fontSize: {
get() {
const fontPx = /(<?value>\d+(?:\.\d+)?)px/
if (!fontPx.test(newValue))
return 0
const { groups: { value } } = newValue.match(fontPx)
return value
},
set(newValue) {
this.myobj.css.fontsize = newValue + 'px'
}
}
}
and in your template:
<input v-model="fontSize" type="range" min="10" max="32" step="1">
EDIT - I just re-read your question. I had the get and set flipped around. The edited answer should do what you need.
Alternatively, you could use a computed property for the style instead:
computed: {
style() {
const style = { ...this.myobj.css }
style.fontsize = style.fontsize + 'px'
return style
}
}
<span v-bind:style="style">Hi all</span>
I have a <b-modal> from VueBootstrap, inside of which I'm trying to render a <GmapMap> (https://www.npmjs.com/package/gmap-vue)
It's rendering a grey box inside the modal, but outside the modal it renders the map just fine.
All the searching I've done leads to the same solution which I'm finding in some places is google.maps.event.trigger(map, 'resize') which is not working. Apparently, it's no longer part of the API [Source: https://stackoverflow.com/questions/13059034/how-to-use-google-maps-event-triggermap-resize]
<template>
<div class="text-center">
<h1>{{ title }}</h1>
<div class="row d-flex justify-content-center">
<div class="col-md-8">
<GmapMap
ref="topMapRef"
class="gmap"
:center="{ lat: 42, lng: 42 }"
:zoom="7"
map-type-id="terrain"
/>
<b-table
bordered
dark
fixed
hover
show-empty
striped
:busy.sync="isBusy"
:items="items"
:fields="fields"
>
<template v-slot:cell(actions)="row">
<b-button
size="sm"
#click="info(row.item, row.index, $event.target)"
>
Map
</b-button>
</template>
</b-table>
<b-modal
:id="mapModal.id"
:title="mapModal.title"
#hide="resetInfoModal"
ok-only
>
<GmapMap
ref="modalMapRef"
class="gmap"
:center="{ lat: 42, lng: 42 }"
:zoom="7"
map-type-id="terrain"
/>
</b-modal>
</div>
</div>
</div>
</template>
<script>
// import axios from "axios";
import { gmapApi } from 'gmap-vue';
export default {
name: "RenderList",
props: {
title: String,
},
computed: {
google() {
return gmapApi();
},
},
updated() {
console.log(this.$refs.modalMapRef);
console.log(window.google.maps);
this.$refs.modalMapRef.$mapPromise.then((map) => {
map.setCenter(new window.google.maps.LatLng(54, -2));
map.setZoom(2);
window.google.maps.event.trigger(map, 'resize');
})
},
data: function () {
return {
items: [
{ id: 1, lat: 42, long: 42 },
{ id: 2, lat: 42, long: 42 },
{ id: 3, lat: 42, long: 42 },
],
isBusy: false,
fields: [
{
key: "id",
sortable: true,
class: "text-left",
},
{
key: "text",
sortable: true,
class: "text-left",
},
"lat",
"long",
{
key: "actions",
label: "Actions"
}
],
mapModal: {
id: "map-modal",
title: "",
item: ""
}
}
},
methods: {
// dataProvider() {
// this.isBusy = true;
// let promise = axios.get(process.env.VUE_APP_LIST_DATA_SERVICE);
// return promise.then((response) => {
// this.isBusy = false
// return response.data;
// }).catch(error => {
// this.isBusy = false;
// console.log(error);
// return [];
// })
// },
info(item, index, button) {
this.mapModal.title = `Label: ${item.id}`;
this.mapModal.item = item;
this.$root.$emit("bv::show::modal", this.mapModal.id, button);
},
resetInfoModal() {
this.mapModal.title = "";
this.mapModal.content = "";
},
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
margin-bottom: 60px;
}
.gmap {
width: 100%;
height: 300px;
margin-bottom: 60px;
}
</style>
Does anyone know how to get the map to display properly in the modal?
Surely, I'm not the first to try this?
Had this problem, in my case it was solved by providing the following options to google maps:
mapOptions: {
center: { lat: 10.365365, lng: -66.96667 },
clickableIcons: false,
streetViewControl: false,
panControlOptions: false,
gestureHandling: 'cooperative',
mapTypeControl: false,
zoomControlOptions: {
style: 'SMALL'
},
zoom: 14
}
However you can probably make-do with just center and zoom.
Edit: Try using your own google maps components, follow this tutorial:
https://v2.vuejs.org/v2/cookbook/practical-use-of-scoped-slots.html#Base-Example
You can use the package described in the tutorial to load the map, dont be scared by the big red "deprecated" warning on the npm package page.
However for production, you should use the package referenced by the author, which is the one backed by google:
https://googlemaps.github.io/js-api-loader/index.html
The only big difference between the two:
The 'google' object is not returned by the non-deprecated loader, it is instead attached to the window. See my answer here for clarification:
'google' is not defined Using Google Maps JavaScript API Loader
Happy coding!
in the latest version of ant-design-vue, we're no longer able to upload bigger image.
<template>
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
#change="handleChange"
>
<img v-if="imageUrl" :src="imageUrl" alt="avatar" />
<div v-else>
<a-icon :type="loading ? 'loading' : 'plus'" />
<div class="ant-upload-text">
Upload
</div>
</div>
</a-upload>
</template>
<script>
function getBase64(img, callback) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result));
reader.readAsDataURL(img);
}
export default {
data() {
return {
loading: false,
imageUrl: '',
};
},
methods: {
handleChange(info) {
if (info.file.status === 'uploading') {
this.loading = true;
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, imageUrl => {
this.imageUrl = imageUrl;
this.loading = false;
});
}
},
beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
this.$message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
this.$message.error('Image must smaller than 2MB!');
}
return isJpgOrPng && isLt2M;
},
},
};
</script>
<style>
.avatar-uploader > .ant-upload {
width: 128px;
height: 128px;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>
it'll return
error POST https://www.mocky.io/v2/5cc8019d300000980a055e76 net::ERR_CONNECTION_RESET will produced after upload file. only file around 10-50 mb is allowed.
i've try by leave action as empty but it'll use base domain with path null as post api.
is there any other way to to leave action as empty so that it can run straight to #change instead stuck in action other than create own api?