Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DNM] Review link preview #150

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 75 additions & 28 deletions components/PostContent.vue
Original file line number Diff line number Diff line change
@@ -1,60 +1,107 @@
<template>
<span class="content" :class="{ decorate }" v-html="content" ref="contentParent" />
<span>
<span class="content"
:class="{ decorate }"
v-html="content"
ref="contentParent" />
<review-preview v-for="(preview, key) in previews" :key="key" :top="preview.top" :left="preview.left" :slug="preview.slug" />
</span>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue';
import ReviewPreview from './ReviewPreview.vue';

type PartialEvent = { preventDefault: Event['preventDefault'], target: Event['target'] };

const getLink = (elm: unknown) => {
if (!(elm instanceof Element)) return null;
if (elm.nodeName !== 'A') {
elm = elm.closest('a');
if (elm === null) return null;
}
return elm as HTMLAnchorElement;
};

const getUrl = (url: string) => {
let host: string, pathname: string;
if (window.URL && window.URL.prototype && ('href' in window.URL.prototype)) {
({host, pathname} = new URL(url));
} else {
// No version of IE supports an instance of URL
const uriRegex = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
const result = url.match(uriRegex);
if (!result) throw Error(`${url} is not a valid URL.`);
host = result[4];
pathname = result[5];
}
const isInternal = host === window.location.host
|| host.endsWith('.audioxide.com')
|| url.startsWith('https://audioxide.com');
return { host, pathname, isInternal };
}

export default Vue.extend({
name: 'PostContent',
components: { ReviewPreview },
props: {
content: { type: String, required: true },
decorate: { type: Boolean, default: false },
},
data: () => ({
// TODO: Add handlers array that can be passed to removeEventListener
handler: null as ((evt: PartialEvent) => void) | null,
// TODO: This might be the wrong approach as PostContent now controls display
previews: [] as ({ slug: string, top: number, left: number })[]
}),
mounted() {
const parent = this.$refs.contentParent;
let preventDefault: PartialEvent['preventDefault'];
this.handler = (evt: PartialEvent) => {
preventDefault = preventDefault || evt.preventDefault.bind(evt);
const elm = evt.target;
if (!(elm instanceof Element)) return;
if (elm.nodeName !== 'A') {
if (parent === elm.parentNode || !this.handler) return;
this.handler({ preventDefault, target: elm.parentNode });
return;
}
methods: {
handleClick(evt: Event) {
const elm = getLink(evt.target);
if (elm === null) return;
const anchorElement = elm as HTMLAnchorElement;
const rawHref = anchorElement.getAttribute('href');
const url = anchorElement.href;
let host: string, pathname: string;
if (window.URL && window.URL.prototype && ('href' in window.URL.prototype)) {
({host, pathname} = new URL(url));
} else {
// No version of IE supports an instance of URL
const uriRegex = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
const result = url.match(uriRegex);
if (!result) return;
host = result[4];
pathname = result[5];
}
const { pathname, isInternal } = getUrl(url);
// Ensure anything on the current or audioxide domain passes except anchor
const isInternal = host === window.location.host
|| host.endsWith('.audioxide.com')
|| url.startsWith('https://audioxide.com');
const isAnchor = rawHref && rawHref.startsWith('#');
if (isInternal && !isAnchor) {
evt.preventDefault();
this.$router.push(pathname);
}
},
handleHover(evt: Event) {
const elm = getLink(evt.target);
if (elm === null) return;
const anchorElement = elm as HTMLAnchorElement;
const rawHref = anchorElement.getAttribute('href');
const url = anchorElement.href;
const { pathname, isInternal } = getUrl(url);
// Ensure anything on the current or audioxide domain passes except current page
const isReview = pathname.startsWith('/reviews');
const isCurrent = pathname === window.location.pathname;
if (!isCurrent && isInternal && isReview) {
const { route } = this.$router.resolve(pathname);
if ('slug' in route.params) {
console.log('gonna show a preview for ', route.params.slug);
const { slug } = route.params;
const { top, left, height } = elm.getBoundingClientRect();
this.previews.push({ slug, top: top + height + window.scrollY, left: left + window.scrollX });
}
}
}
},
mounted() {
const parent = this.$refs.contentParent;
console.log(this.$router);
this.handler = (evt: PartialEvent) => {
// TODO: Test this change works
// TODO: Maybe don't need the portal vue? If we do, how does it work?

};

if (parent instanceof Element) {
parent.addEventListener('click', this.handler, false);
parent.addEventListener('click', this.handleClick, false);
parent.addEventListener('mouseover', this.handleHover, false);
}
},
beforeDestroy() {
Expand Down
81 changes: 81 additions & 0 deletions components/ReviewPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<portal to="global">
<div class="wrapper" :style="{ top: `${top}px`, left: `${left}px`}">
<h3>
<span class="album">Debut</span>
<span class="artist">Bjork</span>
</h3>
<p>‘Björk creates her own identity by combining seemingly contrasting genres and forming something entirely unique. This was the first sign of innovation in her career, breaking the mould of what it means to be a new, exciting artist.’</p>
<p class="score">
<span class="given">25</span>
<span class="total">30</span>
</p>
<div class="img">
<img src="https://picsum.photos/300/300" />
</div>
</div>
</portal>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: "ReviewPreview",
props: {
slug: String,
top: Number,
left: Number
},
created() {
console.log(this.slug, this.top, this.left);
},
});
</script>

<style lang="scss" scoped>
@import "~assets/styles/variables";

.wrapper {
position: absolute;
background: white;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.6);
border-radius: 0.5em;
display: grid;
max-width: 625px;
overflow: hidden;
padding: 20px 37px;
font-family: $base-fontstack;
margin: 0;
flex-shrink: 1;
h3, .score {
font-family: $heading-fontstack;
font-weight: 500;
font-size: 1.4em;
}
h3 {
margin-bottom: $site-content__spacer--large;
span {
display: block;
&.album {
font-style: italic;
}
}
}
p {
@include site-content__body-text;
}
}

.img {
flex-shrink: 0;
width: 300px;
img {
width: 70%;
}
&::before {
content: '';
background-image: 'https://picsum.photos/300/300';
filter: blur(10px) contrast(1.5);
}
}
</style>
1 change: 1 addition & 0 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<audioxide-header />
<nuxt />
<audioxide-footer />
<portal-target name="global" multiple />
</div>
</template>

Expand Down
6 changes: 4 additions & 2 deletions nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export default {
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~plugins/goatcounter.js', mode: 'client' }
{ src: '~plugins/goatcounter.js', mode: 'client' },
{ src: '~plugins/closest-polyfill.js', mode: 'client' }
],
/*
** Nuxt.js dev-modules
Expand All @@ -145,7 +146,8 @@ export default {
modules: [
'@nuxtjs/pwa',
// Doc: https://github.com/nuxt-community/dotenv-module
'@nuxtjs/dotenv'
'@nuxtjs/dotenv',
'portal-vue/nuxt'
],
/*
** Build configuration
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"fetch-ponyfill": "^7.0.0",
"he": "^1.2.0",
"nuxt": "^2.0.0",
"portal-vue": "^2.1.7",
"sitemap-webpack-plugin": "^1.0.0",
"wpapi": "^1.2.1"
},
Expand Down
21 changes: 21 additions & 0 deletions plugins/closest-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default () => {
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.msMatchesSelector ||
Element.prototype.webkitMatchesSelector;
}

if (!Element.prototype.closest) {
Element.prototype.closest = function(s) {
var el = this;

do {
if (Element.prototype.matches.call(el, s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}


}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8297,6 +8297,11 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==

portal-vue@^2.1.7:
version "2.1.7"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4"
integrity sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==

posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
Expand Down