refactor core into composables & components

This commit is contained in:
riqo 2021-01-30 16:05:59 -06:00 • committed by Enrique Hernandez
commit a84019da0d
37 changed files with 409 additions and 469 deletions

View file

@ -0,0 +1,66 @@
<template>
<svg
id="messenger"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
@click="emittMessage"
>
<MessageVisualizer />
<InputVisualizer />
</svg>
</template>
<script lang="ts">
// mock messages
import Messages from "./mockMessages";
// child components
import MessageVisualizer from "./messageVisualizer/index.vue";
import InputVisualizer from "@/components/inputVisualizer/index.vue";
// event emitter
import { Mitt } from "@/types/mitt/index";
import { defineComponent, ref, inject } from "vue";
export default defineComponent({
name: "Messenger",
components: { MessageVisualizer, InputVisualizer },
setup() {
const messages = Messages;
const count = ref(0);
// imports mitt Emitter safely
let emitter: Mitt;
const emitterInject: Mitt | undefined = inject("mitt");
if (emitterInject) emitter = emitterInject;
function emittMessage() {
if (count.value < messages.length) {
emitter.emit("renderMessage", messages[count.value]);
count.value++;
return;
}
}
window.ipcRenderer.on("render-message", (event, payload) => {
console.log(payload.message);
emitter.emit("renderMessage", payload.message);
});
return {
emittMessage,
};
},
});
</script>
<style lang="scss" scoped>
#messenger {
width: 350px;
height: 500px;
background-color: #e6e6e6;
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.3);
// border-radius: 20px;
}
</style>

View file

@ -0,0 +1,73 @@
import { computed, onMounted, ref } from "vue";
/**
* single message svg item computed properties
*/
export default function useComputedBubble(m: string, id: string) {
const x = ref(0)
const paddingLeft = 30;
const winW = 350;
const messageWidth = ref(0);
const messageMarginTop = 75;
const textFill = computed(() => {
switch (m) {
case "sf":
return "#fff";
default:
return "#000";
}
});
const bubbleFill = computed(() => {
switch (m) {
case "sf":
return "#61b4f4";
default:
return "#fff";
}
});
const setMessageWidthRef = async () => {
const msg = await document.getElementById(id);
if (msg) {
messageWidth.value = msg.getBoundingClientRect().width;
}
}
const selfMessageMarginRight = 2.5;
const calculateXoffset = async () => {
switch (m) {
case "sf":
x.value = winW - messageWidth.value - selfMessageMarginRight;
break;
default:
x.value = paddingLeft;
// to center a message
// x = messageMargin + (winW -messageWidth) / 2;
}
}
const startingOffset = computed(() => {
const parentView = document.getElementById('messageVisualizer');
if (parentView) {
const rect = parentView.getBoundingClientRect();
let Y = 600;
if (rect.height > 500) {
Y = rect.height + messageMarginTop;
}
return `translate(${x.value} ${Y})`;
}
});
onMounted(async () => {
await setMessageWidthRef();
calculateXoffset();
})
return {
textFill,
bubbleFill,
startingOffset
};
}

View file

@ -0,0 +1,50 @@
import { ref, onMounted } from "vue";
import { Ref } from "@/types/vueRef/index";
/**
* handles visualization of single message svg items
*/
export default function useBubbleDrawer({
mr,
tr,
}: {
mr: Ref<string>;
tr: Ref<string>;
}) {
const signatureTranslate = ref("");
let messageRect: SVGSVGElement & SVGRectElement;
let messageText: SVGSVGElement & SVGTextElement;
onMounted(async () => {
messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement;
messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement;
});
function adjustMessageRect(): void {
// must get dimensions of text box to fit bubble around
const padding = 12.5;
const textBox = messageText.getBBox();
messageRect.setAttribute("x", String(textBox.x - padding));
messageRect.setAttribute("y", String(textBox.y - padding));
messageRect.setAttribute("width", String(textBox.width + 2 * padding));
messageRect.setAttribute("height", String(textBox.height + 2 * padding));
}
function translateMsgSignature(): void {
if (messageRect.getAttribute("height") !== null) {
const height = Number(messageRect.getAttribute("height"));
signatureTranslate.value = `translate(-10 ${height})`;
}
}
async function drawMessage() {
// ignore lint error: await needed for proper render
await adjustMessageRect();
translateMsgSignature();
}
return {
drawMessage,
signatureTranslate,
};
}

View file

@ -0,0 +1,83 @@
<template>
<g :id="item.id" :class="item.modifier" :transform="startingOffset">
<rect ref="messageRect" x="30" y="10" width="196" height="63" rx="10" :fill="bubbleFill" />
<text font-size="14" font-family="SF Pro" ref="messageText" :fill="textFill">
<tspan dy="16" x="0" v-for="(item, index) in textArr" :key="index">{{ item }}</tspan>
</text>
<g :transform="signatureTranslate">
<text
transform="translate(0, 5)"
fill="#000"
font-size="10"
font-family="SFCompactDisplay-Bold, SF Compact Display"
font-weight="700"
>
<tspan xml:space="preserve">{{ item.context }}</tspan>
</text>
<text
transform="translate(0, 20)"
fill="#6f6f6f"
font-size="9"
font-family="SFCompactDisplay, SF Compact Display"
>
<tspan v-if="item.subContext">{{item.subContext}}</tspan>
</text>
</g>
</g>
</template>
<script lang="ts">
import useComputedBubble from "./bubbleDetails/computed";
import useBubbleDrawer from "./bubbleDetails/drawer";
// text wrap composition
import useTextWrap from "@/composables/textWrap";
import { defineComponent, onMounted, ref, reactive } from "vue";
export default defineComponent({
name: "MessageItem",
props: {
item: {
type: Object,
required: true,
},
},
setup(props) {
let textArr: string[] = reactive([]);
const msgID = ref(props.item.id);
const messageRect = ref(`message${msgID.value}`);
const messageText = ref(`text${msgID.value}`);
const { textFill, bubbleFill, startingOffset } = useComputedBubble(
props.item.modifier,
props.item.id
);
const { textWrap } = useTextWrap();
const { drawMessage, signatureTranslate } = useBubbleDrawer({
mr: messageRect,
tr: messageText,
});
// wraps message text before render
textArr = textWrap({
s: props.item.content,
w: 32,
});
onMounted(() => {
drawMessage();
});
return {
textFill,
bubbleFill,
startingOffset,
textArr,
signatureTranslate,
messageText,
messageRect,
};
},
});
</script>

View file

@ -0,0 +1,80 @@
<template>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
:viewBox="scrollView"
>
<g id="messageVisualizer" transform="translate(0, 5)">
<g
v-for="(item, index) in renderArr"
:key="index"
class="messageList"
transform="translate(0, 25)"
preserverAspectRatio="none"
>
<transition
appear
v-on:before-appear="beforeEnter"
v-on:appear="enter"
v-on:after-enter="afterEnter"
>
<MessageItem :item="item" :key="index" />
</transition>
</g>
</g>
</svg>
</template>
<script lang="ts">
import useMessageVisualizer from "./viewDetails/visualizer";
import useScrollView from "@/composables/scrollView";
import MessageItem from "../messageBubble/index.vue";
import { Message } from "@/types/message/index";
import { Mitt } from "@/types/mitt/index";
import { defineComponent, reactive, inject } from "vue";
export default defineComponent({
name: "MessageVisualizer",
components: { MessageItem },
setup() {
const renderArr: Message[] = reactive([]);
// imports mitt Emitter safely
let emitter: Mitt;
const emitterInject: Mitt | undefined = inject("mitt");
if (emitterInject) {
emitter = emitterInject;
emitter.on("renderMessage", (payload: Message | undefined) => {
if (payload) renderArr.push(payload);
});
}
const { beforeEnter, enter, afterEnter } = useMessageVisualizer();
const { scrollView } = useScrollView({
targetId: "messageVisualizer",
});
return {
renderArr,
beforeEnter,
enter,
afterEnter,
scrollView,
};
},
});
</script>
<style lang="scss" scoped>
svg {
height: 1%;
overflow: hidden;
}
#messageRenderer {
margin: 0;
}
</style>

View file

@ -0,0 +1,80 @@
import { inject } from "vue";
import AnimeFunc from "@/types/animejs/index";
import { MessageAnimeFunc } from '@/types/message/index';
/**
* anime js animations used by message renderer
*/
export default function useMessageViewAnims(): {
shiftAnimate: MessageAnimeFunc;
stackAnimate: MessageAnimeFunc;
} {
// imports animejs safely
let anime: AnimeFunc;
const animeInject: AnimeFunc | undefined = inject("animejs");
if (animeInject) anime = animeInject;
// shifts message list by a given value
function shiftList({ targetList, sv }: { targetList: NodeList | undefined; sv: number | undefined }) {
if (targetList) {
for (const element of targetList) {
const el = element.parentNode as HTMLElement;
anime({
targets: el,
transform: `translate(0 ${sv})`,
easing: "easeInOutQuad",
duration: 500,
});
}
}
}
// adds new message at the bottom of the list and shifts existing message list
const shiftAnimate: MessageAnimeFunc = (el, transform, shiftValue, targetList) => {
anime({
targets: el,
transform: transform.initialTranslate,
easing: "easeInOutQuad",
duration: 500,
begin: function() {
shiftList({
targetList,
sv: shiftValue
});
},
complete: function() {
anime({
targets: el,
transform: transform.finalTranslate,
easing: "easeInOutQuad",
duration: 500,
offset: 1000
});
}
});
}
// stacks message from top to bottom
const stackAnimate: MessageAnimeFunc = (el, transform) => {
anime({
targets: el,
transform: transform.initialTranslate,
easing: "easeInOutQuad",
duration: 1000,
complete: function() {
anime({
targets: el,
transform: transform.finalTranslate,
easing: "easeInOutQuad",
duration: 500,
offset: 1000
});
}
});
}
return {
shiftAnimate,
stackAnimate
};
}

View file

@ -0,0 +1,41 @@
import { ref } from "vue";
/**
* Calculates vertical and horizontal message offsets
*/
export default function useOffsetCalculator() {
const xOffset = ref(0);
const yOffset = ref(0);
const firstChildMarginTop = 0;
let previousMessageHeight = 0;
const messagePadding = 15;
function calculateX(message: HTMLElement): void {
const computedQuery = window.getComputedStyle(message);
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
xOffset.value = matrix.m41;
}
function calculateY(messageHeight: number): void {
if (previousMessageHeight === 0) {
yOffset.value = firstChildMarginTop;
} else {
yOffset.value += previousMessageHeight + messagePadding;
}
previousMessageHeight = messageHeight;
}
const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => {
const msgRect = msg.getBoundingClientRect();
calculateX(msg);
calculateY(msgRect.height);
const offsets = {
x: xOffset.value as number,
y: yOffset.value as number
};
return offsets;
}
return {
calculateMessageOffsets,
}
}

View file

@ -0,0 +1,72 @@
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
import useMessageViewAnims from "./anime";
import useOffsetCalculator from './offsetCalculator';
/**
* handles visualization of view messages through
* vue transition callbacks and anime-js animations
*/
export default function useMessageVisualizer(): {
beforeEnter: VueTransitionCallback;
enter: VueTransitionCallback;
afterEnter: VueTransitionCallback;
} {
// uses offsetCalculator logic and renderer animations
const { calculateMessageOffsets } = useOffsetCalculator();
const { shiftAnimate, stackAnimate } = useMessageViewAnims();
const messagePadding = 15;
let messageList: NodeList;
let message: HTMLElement | null;
let messageOffset = { x: 0, y: 0 };
let shift = false;
let shiftOffset = 0;
// animates message item to 'enter' renderView
const animateMessage = (el: SVGGElement): void => {
const transform = {
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
}
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
}
const beforeEnter: VueTransitionCallback = () => {
const queryResult = document.querySelectorAll(".messageList");
if (queryResult.length) messageList = queryResult;
}
const enter: VueTransitionCallback = (el) => {
// query DOM for latest message as soon as it enters
if (el) {
message = document.getElementById(el.id);
}
}
const afterEnter: VueTransitionCallback = (el) => {
// get message positioning & shift state before animating
if (message) {
messageOffset = calculateMessageOffsets(message);
shift = messageOffset.y > 400 ? true : false;
}
if (el) {
if (shift) {
console.log(messageOffset.y)
if (messageOffset.y < 445) {
shiftOffset -= 15;
} else {
shiftOffset -= el.getBBox().height + messagePadding;
}
}
animateMessage(el);
}
}
return {
beforeEnter,
enter,
afterEnter
};
}

View file

@ -0,0 +1,161 @@
import { Message } from "@/types/message/index";
const Messages: Message[] = [
{
content: "Welcome",
context: "ai",
subContext: "",
modifier: "ai",
id: "2",
time: ""
},
{
content:
"add friend",
context: "launch",
subContext: "",
modifier: "sf",
id: "1",
time: ""
},
{
content: "Did you mean to addcontact?",
context: "addcontact",
subContext: "",
modifier: "ai",
id: "5",
time: ""
},
{
content: "yes",
context: "fulfill",
subContext: "",
modifier: "sf",
id: "6",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Mesage from Tommy McConville",
subContext: "New Conversation",
modifier: "ai",
id: "3",
time: ""
},
{
content: "Hey do you know Enrique Hernandez?",
context: "Message from Crimata",
subContext: "",
modifier: "ai",
id: "7",
time: ""
},
{
content: "Yes posting it online soon.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "4",
time: ""
},
{
content: "Hey do you know Enrique Hernandez?",
context: "Message from Crimata",
subContext: "",
modifier: "ai",
id: "8",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "9",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "10",
time: ""
},
{
content: "Yes posting it online soon.",
context: "Message from Tommy McConville",
subContext: "",
modifier: "sf",
id: "11",
time: ""
},
{
content:
"I can be there at 5 pm tomorrow to greet you at a Chick file with my almond and chive salad.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "12",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "13",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "14",
time: ""
},
{
content: "Yes posting it online soon.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "15",
time: ""
},
{
content: "Sweet, thanks!",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "16",
time: ""
},
{
content: "Hey do you know Enrique Hernandez?",
context: "Message from Crimata AI",
subContext: "",
modifier: "ai",
id: "17",
time: ""
},
{
content: "Yes posting it online soon.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "18",
time: ""
},
{
content: "Sweet, thanks!",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "19",
time: ""
},
];
export default Messages;