Is there a Vue way of creating a drag ghost image? - vue.js

I need to create a custom ghost image for a dragged element. And also I need to change it's style a bit. This is my code:
const onDragStart = (e) => {
const ghost = e.target.cloneNode(true);
ghost.setAttribute("id", "ghost");
ghost.style.position = "absolute";
ghost.style.right = "-999px";
// counter is an element that should not be displayed on a ghost image
ghost.getElementsByClassName("counter")[0].remove();
document.body.appendChild(ghost);
e.dataTransfer.setDragImage(ghost, 20, 20);
};
const onDragEnd = () => {
const ghost = document.getElementById("ghost");
if (ghost) {
ghost.remove();
}
};
Since I use Vue, manipulating DOM directly seems like a bad idea. Is there any way to do the same result, but in a Vue-way?

Related

How to work with EventListeners for draggable scrolling?

I'm trying to build a calendar in which you can scroll. I would like to achieve the scrolling also by dragging. So I use EventListeners.
mousemove continuously triggers, after the first mousedown, even if I already released the mouse. So the removeEventListeners don't really work. I don't quite understand what's wrong or how to get the interactions between all Listeners to work correctly.
Here is my CodeSandBox
mounted() {
this.initScrollCalendar()
},
methods: {
initScrollCalendar() {
const calendar = this.$refs.calendar
calendar.scrollLeft = this.position.left;
calendar.addEventListener('mousedown', (e) => this.mouseDownHandler())
},
mouseDownHandler() {
const calendar = this.$refs.calendar
calendar.addEventListener('mousemove', (e) => this.mouseMoveHandler(e) )
calendar.addEventListener('mouseup', this.mouseUpHandler());
},
mouseMoveHandler(e) {
console.log("Move")
const calendar = this.$refs.calendar
const rect = calendar.getBoundingClientRect()
const clientX = e.clientX - rect.left
const dx = clientX - this.position.x
calendar.scrollLeft = this.position.left + dx
},
mouseUpHandler() {
console.log("Up")
const calendar = this.$refs.calendar
calendar.removeEventListener('mousemove', (e) => this.mouseMoveHandler(e));
calendar.removeEventListener('mouseup', this.mouseUpHandler());
}
}
I ended up using vue-dragscroll. Works like a charm and the setup is totally simple. If needed, I can also set a starting position of my scroll container.
Don't forget overflow: auto in css
<div ref="scrollContainer" v-dragscroll>
...
</div>
// Setting starting point
const scrollContainer = this.$refs.scrollContainer
scrollContainer.scrollLeft = 500;

Manipulate innerText of a CKEditor ViewElement

I am creating a little custom plugin for the CKEditor5 for the #neoscms.
Neos is using the #ckeditor5 but with a custom view.
The plugin is more or less a placeholder plugin. The user can configure a data-source with a key value store for items (identifier and labels). The dropdown in the CKEditor is filled with the items and when the user selects an item from the dropdown, it creates a placeholder element that should end in a span element with some data-attributes.
The main idea was to have an empty element and just data-attributes to identify the element and being able to assign live data. But it turns out that the live data thing is tricky. When I manipulate the span with an extra JS snippet on the Website, the CKEditor cannot handle this.
Is it possible to manipulate a view element in the DOM and still have a working Editor?
The Plugin works fine if I just add inner Text in the downCasting and don't replace something. But the live data would be nice.
Neos Backend with a element
Maybe that code gives an idea of the package.
It is not ready yet as this is more or less the main feature ;)
import {Plugin, toWidget, viewToModelPositionOutsideModelElement, Widget,} from "ckeditor5-exports";
import PlaceholderCommand from "./placeHolderCommand";
export default class PlaceholderEditing extends Plugin {
static get requires() {
return [Widget];
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
"placeholder",
new PlaceholderCommand(this.editor)
);
this.editor.editing.mapper.on(
"viewToModelPosition",
viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) =>
viewElement.hasClass("internezzo-placeholder")
)
);
this.editor.config.define("placeholderProps", {
types: ["name", "node", "nodePath"],
});
this.editor.config.define("placeholderBrackets", {
open: "[",
close: "]",
});
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register("placeholder", {
allowWhere: "$text",
isInline: true,
isObject: true,
allowAttributes: [
"name",
"node",
"nodePath",
"data-placeholder-identifier",
"data-node-identifier",
"data-node-path",
],
});
}
_defineConverters() {
const conversion = this.editor.conversion;
const config = this.editor.config;
conversion.for("upcast").elementToElement({
view: {
name: "span",
classes: ["foobar-placeholder"],
},
model: (viewElement, writer) => {
const name = viewElement.getAttribute('data-placeholder-identifier');
const node = viewElement.getAttribute('data-node-identifier');
const nodePath = viewElement.getAttribute('data-node-path');
const modelWriter = writer.writer || writer;
return modelWriter.createElement("placeholder", {name, node, nodePath, editable: false});
},
});
conversion.for("editingDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
const widgetElement = createPlaceholderView(modelItem, viewWriter);
return toWidget(widgetElement, viewWriter);
},
});
conversion.for("dataDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
return createPlaceholderView(modelItem, viewWriter);
},
});
// Helper method for downcast converters.
function createPlaceholderView(modelItem, viewWriter) {
const name = modelItem.getAttribute("name");
const node = modelItem.getAttribute("node");
const nodePath = modelItem.getAttribute("nodePath");
const placeholderView = viewWriter.createContainerElement("span", {
class: "foobar-placeholder",
"data-placeholder-identifier": name,
"data-node-identifier": node,
"data-node-path": nodePath,
});
// Would be nice to remove that and have just empty spans that get dynamic data
let innerText = config.get("placeholderBrackets.open") + name;
innerText += config.get("placeholderBrackets.close");
viewWriter.insert(
viewWriter.createPositionAt(placeholderView, 0),
viewWriter.createText(innerText)
);
return placeholderView;
}
}
}
So, the extra JS snippet that is used by the website is searching for spans with the class foobar-placeholder and writes a value with live data into the span. That works in the frontend, of course, but the backend of the CMS that uses CKEditor has issues with the changing data.
I could not find a solution with docs of CKEditor, and maybe I misuse the API somehow, but I now found a working solution for me.
My website snippet is now communicating with the Plugin via Broadcast messages. And then I search for placeholder elements and check if I need to change an attribute.
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.postMessage({identifier: name, value});
And in the plugin
// Receive new values for placeholder via broadcast
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.onmessage = (message) => {
const identifier = get('data.identifier', message);
const newValue = get('data.value', message);
this.editor.model.change( writer => {
if (identifier) {
this._replaceAttribute(writer, identifier, newValue);
}
});
};
Only downside now is that I need to reload the page, but already read that this is maybe cause by my element down casting and I change attributes.

Vuejs v-for so laggy when infinite scrolling

I have this weird vuejs effect where when I am adding a new object data, the v-for re-renders all the object even if its already rendered.
I am implementing an infinite scroll like facebook.
The Code
To explain this code, I am fetching a new data from firebase and then push the data into the data object when it reaches the bottom of the screen
var vueApp = new Vue({
el: '#root',
data: {
postList: [],
isOkaytoLoad: false,
isRoomPostEmpty: false,
},
mounted: function() {
// Everytime user scroll, call handleScroll() method
window.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll: function()
{
var d = document.documentElement;
var offset = d.scrollTop + window.innerHeight;
var height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height)
{
this.isOkaytoLoad = false;
(async()=>{
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
roomSnapshot.forEach((doc) => {
const postID = doc.id;
(async()=>{
// Put the new data at the postList object
this.postList = [...this.postList, doc];
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc['commentCount'] = commentSnapshot.size;
//this.postList.push(doc);
console.log(this.postList);
setTimeout(()=>{ this.isOkaytoLoad = true }, 1000);
})();
});
})();
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div v-if="postList.length > 0" class="card-containers">
<!-- I have a component `Postcard` in my js file and it works well -->
<Postcard
v-for="post in postList"
:key="post.id"
:post-id="post.id"
:owner-name="post.data().owner_displayName"
:owner-uid="post.data().owner_id"
:post-type="post.data().post_type"
:image-url="post.data().image_url"
:post-content="truncateString(linkify(post.data().post_content))"
:room="post.data().room"
:time="post.data().time.toDate()"
:likers="post.data().likers"
:comment-count="post.commentCount"
:file-url="post.data().file_url"
:file-name="post.data().file_name"
:downloads="post.data().downloads">
</Postcard>
</div>
Now, the problem is here ...
Look at this screen record, FOCUS AT THE MOUSE, it's lagging and I can't even click on those buttons when vuejs is adding and loading the new data
Here is the code That I used
What I suspect
I am suspecting that everytime I add a new data, the VueJS re-renders it all, which does that effect. How can I force vueJS to not re-render those data that is already rendered in the screen?
You've got two unnecessary async IIFE; the second one inside the forEach is particularly problematic because the async code inside it will be executed concurrently for each loop iteration, which has implications:
getDocs() will be fired all at once for each loop ieration, potentially spamming the server (assuming this is performing a network request). Was this your intention? It looks like you're only fetching at most 5 new posts, so this is probably OK.
The async function updates some state which will trigger Vue to re-render for each doc. This should be batched together at the end so Vue does as minimal updates as possible.
Also don't use var; use const or let instead. There's almost no good reason to use var anymore, let it die.
I can't say this will improve your performance substantially, but I recommend making the following changes (untested):
async handleScroll() {
const d = document.documentElement;
const offset = d.scrollTop + window.innerHeight;
const height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height) {
// Prevent loading while we load more posts
this.isOkaytoLoad = false;
try {
// Get new posts
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
// Fetch comments of each post. Do this all at once for each post.
// TODO: This can probably be optimized into a single query
// for all the posts, instead of one query per post.
await Promise.all(roomSnapshot.docs.map(async doc => {
const postID = doc.id;
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc.commentCount = commentSnapshot.size;
}));
// Append the new posts to the list
this.postList.push(...roomSnapshot.docs);
} catch (ex) {
// TODO: Handle error
} finally {
// Wait a bit to re-enable loading
setTimeout(() => { this.isOkaytoLoad = true }, 1000);
}
}
}
Doing :post-content="truncateString(linkify(post.data().post_content))" in the template means linkify will be executed during each re-render. I suspect linkify may be potentially slow for long lists? Can this be pre-calculated for each post ahead of time?
You're registering a window scroll event listener when the component is mounted. If the component is ever destroyed, you need to unregister the event listener otherwise it'll still fire whenever the window is scrolled. This may not be an issue in your case, but for reusable components it must be done.

i18n won't translate correctly when inside array or object in React Native

I'm trying to use i18n-js to translate some strings into other languages. If I have my code in normal code, it works. Ex:
//Displays "Something" (no quotes) where I want it
<Text> translate("Something"); </Text>
But if I put it inside an array or object, then call it later, it stops working and shows a missing message instead of the text I want translated. Ex:
const messages = {
something: translate("Something"),
// other translations...
}
// later on
// Displays "[missing "en.Something" translation]" (no quotes) where I want it
<Text> messages.something </Text>
The following is my code for my translate function, as well as the config for i18n. I'm using lodash-memoize, but that is unrelated to the issue. I've already checked that the text being passed to i18n.t() is the same (including type) no matter if it's in normal code or in the array, but it still doesn't return the correct thing. I have some error checking written up to avoid getting the missing message on screen, but that still doesn't fix the issue that it can't find the translation.
export const translationGetters = ({
en: () => require('./translations/en.json'),
es: () => require('./translations/es.json')
});
export const translate = memoize(
(key, config) => {
text = i18n.t(key, config)
return text
},
(key, config) => (config ? key + JSON.stringify(config) : key)
);
export const setI18nConfig = () => {
// fallback if no available language fits
const fallback = { languageTag: "en", isRTL: false };
const { languageTag, isRTL } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback;
// clear translation cache
translate.cache.clear();
// update layout direction
I18nManager.forceRTL(isRTL);
// set i18n-js config
i18n.translations = { [languageTag]: translationGetters[languageTag]() };
i18n.locale = languageTag;
};
I have no idea where to go on this. Any advice would be appreciated!
Same problem here, workaround is to return array/object from inside a function:
Don't work
export const translations = [i18.t('path')]
Works
export function getTranslations() {
const translations = [i18.t('path')]
return translations
}

Trying to load obj & mtl file with Three.js in React Native

Main objective : Load animated models exported from Maya into React Native app
Exported files : obj, mtl & png file
I have setup https://github.com/react-community/react-native-webgl in my React Native project and it is working properly.
Now, when I am trying to load the MTL file using the MTLLoader, I am getting following error:
Can't find variable: document
Apparently, the MTLLoader is calling TextureLoader which internally calls some load function which has 'document' reference. So what could be the solution to this ?
Here are the two files that I am using:
three.js
const THREE = require("three");
global.THREE = THREE;
if (!window.addEventListener)
window.addEventListener = () => { };
// require("three/examples/js/renderers/Projector");
require("three/examples/js/loaders/MTLLoader");
require("three/examples/js/loaders/OBJLoader");
export default THREE;
ThreeView.js
import React, { Component } from "react";
import { StyleSheet, View } from "react-native";
import { WebGLView } from "react-native-webgl";
import THREE from "./three";
import { image } from "src/res/image";
export default class ThreeView extends Component {
requestId: *;
componentWillUnmount() {
cancelAnimationFrame(this.requestId);
}
onContextCreate = (gl: WebGLRenderingContext) => {
const rngl = gl.getExtension("RN");
const { drawingBufferWidth: width, drawingBufferHeight: height } = gl;
const renderer = new THREE.WebGLRenderer({
canvas: {
width,
height,
style: {},
addEventListener: () => { },
removeEventListener: () => { },
clientHeight: height
},
context: gl
});
renderer.setSize(width, height);
renderer.setClearColor(0xffffff, 1);
let camera, scene;
let cube;
function init() {
camera = new THREE.PerspectiveCamera(75, width / height, 1, 1100);
camera.position.y = 150;
camera.position.z = 500;
scene = new THREE.Scene();
var mtlLoader = new THREE.MTLLoader();
mtlLoader.load('female-croupier-2013-03-26.mtl', function (materials) {
materials.preload();
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
objLoader.load('female-croupier-2013-03-26.obj', function (object) {
scene.add(object);
}, onLoading, onErrorLoading);
}, onLoading, onErrorLoading);
}
const onLoading = (xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
};
const onErrorLoading = (error) => {
console.log('An error happened', error);
};
const animate = () => {
this.requestId = requestAnimationFrame(animate);
renderer.render(scene, camera);
// cube.rotation.y += 0.05;
gl.flush();
rngl.endFrame();
};
init();
animate();
};
render() {
return (
<View style={styles.container}>
<WebGLView
style={styles.webglView}
onContextCreate={this.onContextCreate}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
},
webglView: {
width: 300,
height: 300
}
});
This error is as others have said caused by threejs trying to use features from a browser which react-native does not have.
I've gotten so far as to be able to load the textures (which is the stage you're getting the error from) by monkey patching the texture loader to use the loader in react-native-webgl. Add this in your init function (right near the top preferably).
//make sure you have defined renderer and rngl
/*
const renderer = new THREE.WebGLRenderer(...)
const rngl = gl.getExtension("RN");
*/
const loadTexture = async function(url, onLoad, onProgress, onError) {
let textureObject = new THREE.Texture();
console.log("loading",url,'with fancy texture loader');
let properties = renderer.properties.get(textureObject);
var texture = await rngl.loadTexture({yflip: false, image: url});
/*
rngl.loadTexture({ image: url })
.then(({ texture }) => {
*/
console.log("Texture [" + url + "] Loaded!")
texture.needsUpdate = true;
properties.__webglTexture = texture;
properties.__webglInit = true;
console.log(texture);
if (onLoad !== undefined) {
//console.warn('loaded tex', texture);
onLoad(textureObject);
}
//});
return textureObject;
}
THREE.TextureLoader.prototype.load = loadTexture;
This solves the problem of loading textures and I can see them load in Charles but they still don't render on a model so I'm stuck past that point. Technically a correct answer but you'll be stuck as soon as you've implemented it. I'm hoping you can comment back and tell me you've gotten further.
I had a similar setup and encountered same issue. My option was to switch to JSONLoader which doesn’t need document to render in react-native. So, I just loaded my model in Blender with a three-js addon, then exported it as json. Just check out this process of adding a three-js adon to Blender
https://www.youtube.com/watch?v=mqjwgTAGQRY
All the best
this might get you closer:
The GLTF format supports embedding texture images (as base64). If your asset pipeline allows it, you could convert to GLTF and then load into three/react-native.
I had to provide some "window" polyfills for "decodeUriComponent" and "atob" because GLTFLoader uses FileLoader to parse the base64:
I've successfully loaded embedded buffers, but you'll need more polyfills to load textures. TextureLoader uses ImageLoader, which uses document.createElementNS
You are using the MTLLoader which uses TextureLoader, and the TextureLoader uses the ImageLoader.
The imageloader uses the document.createElementNS() function.
what i did to solve this was to directly call the THREEjs TextureLoader:
let texture = new THREE.Texture(
url //URL = a base 64 JPEG string in this case
);
(for the use of Texture check the Texture documentation)
Then i used the Image class from React native (instead of the THREEjs Image, which requires the DOM to be constructed) to give that to the Texture as a property:
import { Image } from 'react-native';
var img = new Image(128, 128);
img.src = url;
texture.normal = img;
And then finally map the texture over the target material:
const mat = new THREE.MeshPhongMaterial();
mat.map = texture;
In the react native documentation it will explain how the react native Image element can be used, it supports base64 encoded JPEG.
Maybe there's a way for you to single out the part where it calls for the TextureLoader and replace that part with this answer. Let me know how it works out.
side note, i havent tried to display this yet in my webGLView, but in the logs it looked like normal threejs objects, it's worth the try
Use TextureLoader from expo-three
import { TextureLoader } from "expo-three";
export function loadTexture(resource) {
if (textureCache[resource]) {
return textureCache[resource].clone();
}
const texture = new TextureLoader().load(resource);
texture.magFilter = NearestFilter;
texture.minFilter = NearestFilter;
textureCache[resource] = texture;
return texture;
}
Source: https://github.com/EvanBacon/Expo-Crossy-Road/blob/master/src/Node/Generic.js