Port 'more' button in previews to Vue
This commit is contained in:
parent
1cd28a5ccf
commit
d83dcc35e2
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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) => {
|
||||||
|
@ -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");
|
||||||
|
@ -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");
|
||||||
|
Loading…
Reference in New Issue
Block a user