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

View File

@ -1,8 +1,8 @@
<template> <template>
<button <button
v-if="link.canDisplay" 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> </template>
<script> <script>
@ -12,11 +12,11 @@ export default {
link: Object, link: Object,
}, },
methods: { methods: {
onClick: function() { onClick() {
this.link.shown = !this.link.shown; this.link.shown = !this.link.shown;
this.$parent.$emit("linkPreviewToggle", this.link, this.$parent.message); this.$parent.$emit("linkPreviewToggle", this.link, this.$parent.message);
} },
} },
}; };
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

@ -3,130 +3,14 @@
const $ = require("jquery"); const $ = require("jquery");
const debounce = require("lodash/debounce"); const debounce = require("lodash/debounce");
const Mousetrap = require("mousetrap"); const Mousetrap = require("mousetrap");
const options = require("./options");
const socket = require("./socket");
const templates = require("../views"); const templates = require("../views");
const chat = $("#chat");
const {togglePreviewMoreButtonsIfNeeded} = require("./utils"); 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 // On resize, previews in the current channel that are expanded need to compute
// their "More" button. Debounced handler to avoid performance cost. // their "More" button. Debounced handler to avoid performance cost.
$(window).on("resize", debounce(togglePreviewMoreButtonsIfNeeded, 150)); $(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 */ /* Image viewer */
const imageViewer = $("#image-viewer"); const imageViewer = $("#image-viewer");