Port 'more' button in previews to Vue

This commit is contained in:
Pavel Djundik 2018-07-12 11:26:12 +03:00 committed by Pavel Djundik
parent 1cd28a5ccf
commit d83dcc35e2
7 changed files with 52 additions and 145 deletions

View File

@ -1,8 +1,11 @@
<template>
<div
v-if="link.shown && link.canDisplay"
ref="container"
class="preview">
<div :class="['toggle-content', 'toggle-type-' + link.type]">
<div
ref="content"
:class="['toggle-content', 'toggle-type-' + link.type, { opened: isContentShown }]">
<template v-if="link.type === 'link'">
<a
v-if="link.thumb"
@ -28,14 +31,12 @@
</div>
<button
v-if="showMoreButton"
:aria-expanded="isContentShown"
:aria-label="moreButtonLabel"
class="more"
aria-expanded="false"
aria-label="More"
data-closed-text="More"
data-opened-text="Less"
>
<span class="more-caret"/>
</button>
@click="onMoreClick"
><span class="more-caret"/></button>
</div>
<div class="body overflowable">
@ -105,11 +106,10 @@
</div>
<button
:aria-expanded="isContentShown"
:aria-label="moreButtonLabel"
class="more"
aria-expanded="false"
aria-label="More"
data-closed-text="More"
data-opened-text="Less"
@click="onMoreClick"
><span class="more-caret"/></button>
</template>
</template>
@ -123,14 +123,40 @@ export default {
props: {
link: Object,
},
data() {
return {
showMoreButton: false,
isContentShown: false,
};
},
computed: {
moreButtonLabel() {
return this.isContentShown ? "Less" : "More";
},
},
mounted() {
const options = require("../js/options");
this.$set(this.link, "canDisplay", this.link.type !== "loading" && options.shouldOpenMessagePreview(this.link.type));
if (this.link.type !== "link") {
return;
}
this.$nextTick(() => {
if (!this.$refs.content) {
return;
}
this.showMoreButton = this.$refs.content.offsetWidth >= this.$refs.container.offsetWidth;
});
},
methods: {
onPreviewReady() {
},
onMoreClick() {
this.isContentShown = !this.isContentShown;
},
},
};
</script>

View File

@ -1,8 +1,8 @@
<template>
<button
v-if="link.canDisplay"
@click="onClick"
:class="['toggle-button', 'toggle-preview', { opened: link.shown }]"/>
:class="['toggle-button', 'toggle-preview', { opened: link.shown }]"
@click="onClick"/>
</template>
<script>
@ -12,11 +12,11 @@ export default {
link: Object,
},
methods: {
onClick: function() {
onClick() {
this.link.shown = !this.link.shown;
this.$parent.$emit("linkPreviewToggle", this.link, this.$parent.message);
}
}
},
},
};
</script>

View File

@ -35,8 +35,8 @@
<Message
v-else
:message="message"
@linkPreviewToggle="onLinkPreviewToggle"
:key="message.id"/>
:key="message.id"
@linkPreviewToggle="onLinkPreviewToggle"/>
</template>
</div>
</template>

View File

@ -2,8 +2,8 @@
const parse = require("../js/libs/handlebars/parse");
export default {
functional: true,
name: "ParsedMessage",
functional: true,
props: {
message: Object,
},

View File

@ -101,7 +101,7 @@ function createVueFragment(fragment, createElement) {
return createElement("span", {
class: classes,
style: {
color: `#${fragment.hexColor}`,
"color": `#${fragment.hexColor}`,
"background-color": fragment.hexBgColor ? `#${fragment.hexBgColor}` : null,
},
}, fragment.text);
@ -157,16 +157,14 @@ module.exports = function parse(text, message = null, createElement = null) {
link: preview,
},
}, fragments)];
//`<button class="toggle-button toggle-preview" data-url="${escapedLink}" hidden></button>`;
} else if (textPart.channel) {
return createElement("span", {
class: [
"inline-channel",
],
attrs: {
role: "button",
tabindex: 0,
"role": "button",
"tabindex": 0,
"data-chan": textPart.channel,
},
}, fragments);
@ -176,7 +174,7 @@ module.exports = function parse(text, message = null, createElement = null) {
"emoji",
],
attrs: {
role: "img",
"role": "img",
"aria-label": emojiMap[textPart.emoji] ? `Emoji: ${emojiMap[textPart.emoji]}` : null,
},
}, fragments);
@ -187,7 +185,7 @@ module.exports = function parse(text, message = null, createElement = null) {
colorClass(textPart.nick),
],
attrs: {
role: "button",
"role": "button",
"data-name": textPart.nick,
},
}, fragments);
@ -196,7 +194,7 @@ module.exports = function parse(text, message = null, createElement = null) {
return fragments;
});
}
// Merge the styling information with the channels / URLs / nicks / text objects and
// generate HTML strings with the resulting fragments
return merge(parts, styleFragments, cleanText).map((textPart) => {

View File

@ -176,7 +176,6 @@ window.vueMounted = () => {
.addClass("active")
.trigger("show");
utils.togglePreviewMoreButtonsIfNeeded();
utils.updateTitle();
const type = chan.data("type");

View File

@ -3,130 +3,14 @@
const $ = require("jquery");
const debounce = require("lodash/debounce");
const Mousetrap = require("mousetrap");
const options = require("./options");
const socket = require("./socket");
const templates = require("../views");
const chat = $("#chat");
const {togglePreviewMoreButtonsIfNeeded} = require("./utils");
module.exports = renderPreview;
function renderPreview(preview, msg) {
if (preview.type === "loading") {
return;
}
preview.shown = preview.shown && options.shouldOpenMessagePreview(preview.type);
const template = $(templates.msg_preview({preview}));
const image = template.find("img, video, audio").first();
if (image.length === 0) {
return appendPreview(preview, msg, template);
}
const loadEvent = image.prop("tagName") === "IMG" ? "load" : "canplay";
// If there is an image in preview, wait for it to load before appending it to DOM
// This is done to prevent problems keeping scroll to the bottom while images load
image.on(`${loadEvent}.preview`, () => {
image.off(".preview");
appendPreview(preview, msg, template);
});
// If the image fails to load, remove it from DOM and still render the preview
if (preview.type === "link") {
image.on("abort.preview error.preview", () => {
image.parent().remove();
appendPreview(preview, msg, template);
});
}
}
function appendPreview(preview, msg, template) {
const escapedLink = preview.link.replace(/["\\]/g, "\\$&");
const previewContainer = msg.find(`.preview[data-url="${escapedLink}"]`);
// This is to fix a very rare case of rendering a preview twice
// This happens when a very large amount of messages is being sent to the client
// and they get queued, so the `preview` object on the server has time to load before
// it actually gets sent to the server, which makes the loaded preview sent twice,
// once in `msg` and another in `msg:preview`
if (!previewContainer.is(":empty")) {
return;
}
const container = msg.closest(".chat");
const channel = container.closest(".chan");
const channelId = channel.data("id") || -1;
const activeChannelId = chat.find(".chan.active").data("id") || -2;
msg.find(`.text a[href="${escapedLink}"]`)
.first()
.after(templates.msg_preview_toggle({preview}).trim());
previewContainer.append(template);
const moreBtn = previewContainer.find(".more");
const previewContent = previewContainer.find(".toggle-content");
// Depending on the size of the preview and the text within it, show or hide a
// "More" button that allows users to expand without having to open the link.
// Warning: Make sure to call this only on active channel, link previews only,
// expanded only.
const showMoreIfNeeded = () => {
const isVisible = moreBtn.is(":visible");
const shouldShow = previewContent[0].offsetWidth >= previewContainer[0].offsetWidth;
if (!isVisible && shouldShow) {
moreBtn.show();
} else if (isVisible && !shouldShow) {
togglePreviewMore(moreBtn, false);
moreBtn.hide();
}
};
// "More" button only applies on text previews
if (preview.type === "link") {
// This event is triggered when a side menu is opened/closed, or when the
// preview gets expanded/collapsed.
previewContent.on("showMoreIfNeeded",
() => window.requestAnimationFrame(showMoreIfNeeded)
);
}
if (activeChannelId === channelId) {
// If this preview is in active channel, hide "More" button if necessary
previewContent.trigger("showMoreIfNeeded");
}
}
// On resize, previews in the current channel that are expanded need to compute
// their "More" button. Debounced handler to avoid performance cost.
$(window).on("resize", debounce(togglePreviewMoreButtonsIfNeeded, 150));
$("#chat").on("click", ".toggle-content .more", function() {
togglePreviewMore($(this));
return false;
});
function togglePreviewMore(moreBtn, state = undefined) {
moreBtn.closest(".toggle-content").toggleClass("opened", state);
const isExpanded = moreBtn.closest(".toggle-content").hasClass("opened");
moreBtn.attr("aria-expanded", isExpanded);
if (isExpanded) {
moreBtn.attr("aria-label", moreBtn.data("opened-text"));
} else {
moreBtn.attr("aria-label", moreBtn.data("closed-text"));
}
}
/* Image viewer */
const imageViewer = $("#image-viewer");