@nextcloud/vue
Version:
Nextcloud vue components
952 lines (951 loc) • 32.9 kB
JavaScript
import '../assets/NcRichContenteditable-BuaWt3Xn.css';
import debounce from "debounce";
import Tribute from "tributejs/dist/tribute.esm.js";
import { useIsDarkTheme } from "../composables/useIsDarkTheme/index.mjs";
import { g as getAvatarUrl } from "./NcMentionBubble.vue_vue_type_style_index_0_scoped_45238efd_lang-D6LzDiYf.mjs";
import { N as NcUserStatusIcon } from "./NcUserStatusIcon-CGEf7fej.mjs";
import { createElementBlock, openBlock, normalizeClass, createElementVNode, normalizeStyle, toDisplayString, createApp, resolveComponent, createBlock, createCommentVNode, mergeProps, withModifiers, withKeys } from "vue";
import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.mjs";
import { e as emojiSearch, a as emojiAddRecent } from "./emoji-BY_D0V5K.mjs";
import { r as register, k as t37, p as t34, a as t, q as n } from "./_l10n-DrTiip5c.mjs";
import escapeHTML from "escape-html";
import stripTags from "striptags";
import { c as createElementId } from "./createElementId-DhjFt1I9.mjs";
import { l as logger } from "./logger-D3RVzcfQ.mjs";
import "@nextcloud/auth";
import "@nextcloud/axios";
import "@nextcloud/router";
import "@nextcloud/sharing/public";
import "@vueuse/core";
import "vue-router";
import "./legacy-DcjXBL_t.mjs";
import "./NcButton-Dc8V4Urj.mjs";
import { g as getLinkWithPicker, s as searchProvider } from "./referencePickerModal-DmD3-xYB.mjs";
import "./customPickerElements-4pQTZUnk.mjs";
import "./autolink-U5pBzLgI.mjs";
import "./NcRichText-CBMtJzE_.mjs";
import "./NcEmptyContent-B8-90BSI.mjs";
import "./NcHighlight.vue_vue_type_script_lang-DnWQDM_2.mjs";
import "./NcSelect-Czzsi3P_.mjs";
import "./NcLoadingIcon-b_ajZ_nQ.mjs";
import "./NcTextField.vue_vue_type_script_setup_true_lang-D1y_LfGJ.mjs";
import "dompurify";
import "./NcIconSvgWrapper-BvLanNaW.mjs";
import "./NcInputField-Bwsh2aHY.mjs";
import "@nextcloud/event-bus";
import "focus-trap";
import "./NcActions-DWmvh7-Y.mjs";
import "../composables/useFormatDateTime/index.mjs";
import "../composables/useHotKey/index.mjs";
import "../composables/useIsFullscreen/index.mjs";
import "../composables/useIsMobile/index.mjs";
import "./NcModal-MC_HktJd.mjs";
import "./rtl-v0UOPAM7.mjs";
const _sfc_main$2 = {
name: "NcMentionBubble",
/* eslint vue/require-prop-comment: warn -- TODO: Add a proper doc block about what this props do */
props: {
/**
* Id of the bubble
*/
id: {
type: String,
required: true
},
/**
* The main text
*/
label: {
type: String,
required: false,
default: null
},
/**
* Icon to be applied
*/
icon: {
type: String,
required: true
},
/**
* URL of the icon
*/
iconUrl: {
type: [String, null],
default: null
},
source: {
type: String,
required: true
},
/**
* Is the bubble shown as primary
*/
primary: {
type: Boolean,
default: false
}
},
setup() {
const isDarkTheme = useIsDarkTheme();
return {
isDarkTheme
};
},
computed: {
avatarUrl() {
if (this.iconUrl) {
return this.iconUrl;
}
return this.id && this.source === "users" ? getAvatarUrl(this.id, { isDarkTheme: this.isDarkTheme }) : null;
},
mentionText() {
return !this.id.includes(" ") && !this.id.includes("/") ? `@${this.id}` : `@"${this.id}"`;
}
}
};
const _hoisted_1$2 = { class: "mention-bubble__wrapper" };
const _hoisted_2$2 = { class: "mention-bubble__content" };
const _hoisted_3$1 = ["title"];
const _hoisted_4$1 = {
role: "none",
class: "mention-bubble__select"
};
function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
return openBlock(), createElementBlock("span", {
class: normalizeClass(["mention-bubble", { "mention-bubble--primary": $props.primary }]),
contenteditable: "false"
}, [
createElementVNode("span", _hoisted_1$2, [
createElementVNode("span", _hoisted_2$2, [
createElementVNode("span", {
class: normalizeClass([[$props.icon, `mention-bubble__icon--${$options.avatarUrl ? "with-avatar" : ""}`], "mention-bubble__icon"]),
style: normalizeStyle($options.avatarUrl ? { backgroundImage: `url(${$options.avatarUrl})` } : null)
}, null, 6),
createElementVNode("span", {
role: "heading",
class: "mention-bubble__title",
title: $props.label
}, null, 8, _hoisted_3$1)
]),
createElementVNode("span", _hoisted_4$1, toDisplayString($options.mentionText), 1)
])
], 2);
}
const NcMentionBubble = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2], ["__scopeId", "data-v-45238efd"]]);
const MENTION_START = /(?=[a-z0-9_\-@.'])\B/.source;
const MENTION_SIMPLE = /(@[a-z0-9_\-@.']+)/.source;
const MENTION_GUEST = /@"(?:guest|email){1}\/[a-f0-9]+"/.source;
const MENTION_PREFIXED = /@"(?:federated_)?(?:group|team|user){1}\/[a-z0-9_\-@.' /:]+"/.source;
const MENTION_WITH_SPACE = /@"[a-z0-9_\-@.' ]+"/.source;
const MENTION_COMPLEX = `(${MENTION_GUEST}|${MENTION_PREFIXED}|${MENTION_WITH_SPACE})`;
const USERID_REGEX = new RegExp(`${MENTION_START}${MENTION_SIMPLE}`, "gi");
const USERID_REGEX_WITH_SPACE = new RegExp(`${MENTION_START}${MENTION_COMPLEX}`, "gi");
const richEditor = {
props: {
userData: {
type: Object,
default: () => ({})
}
},
methods: {
/**
* Convert the value string to html for the inner content
*
* @param {string} value the content without html
* @return {string} rendered html
*/
renderContent(value) {
const sanitizedValue = escapeHTML(value);
const splitValue = sanitizedValue.split(USERID_REGEX).map((part) => part.split(USERID_REGEX_WITH_SPACE)).flat();
return splitValue.map((part) => {
if (!part.startsWith("@")) {
return part;
}
const id = part.slice(1).replace(/"/gi, "");
return this.genSelectTemplate(id);
}).join("").replace(/\n/gmi, "<br>").replace(/&/gmi, "&");
},
/**
* Convert the innerHtml content to a string with mentions as text
*
* @param {string} content the content without html
* @return {string}
*/
parseContent(content) {
let text = content;
text = text.replace(/<br>/gmi, "\n");
text = text.replace(/ /gmi, " ");
text = text.replace(/&/gmi, "&");
text = text.replace(/<\/div>/gmi, "\n");
text = stripTags(text, "<div>");
text = stripTags(text);
return text;
},
/**
* Generate an autocompletion popup entry template
*
* @param {string} value the value to match against the userData
* @return {string}
*/
genSelectTemplate(value) {
if (typeof value === "undefined") {
return `${this.autocompleteTribute.current.collection.trigger}${this.autocompleteTribute.current.mentionText}`;
}
const data = this.userData[value];
if (!data) {
return [" ", "/", ":"].every((char) => !value.includes(char)) ? `@${value}` : `@"${value}"`;
}
return this.renderComponentHtml(data, NcMentionBubble).replace(/[\n\t]/gmi, "").replace(/>\s+</g, "><");
},
/**
* Render a component and return its html content
*
* @param {object} props the props to pass to the component
* @param {object} component the component to render
* @return {string} the rendered html
*/
renderComponentHtml(props, component) {
const Item = createApp(component, {
...props
});
const mount = document.createElement("div");
mount.style.display = "none";
document.body.appendChild(mount);
Item.mount(mount);
const renderedHtml = mount.innerHTML;
Item.unmount();
mount.remove();
return renderedHtml;
}
}
};
const _sfc_main$1 = {
name: "NcAutoCompleteResult",
components: {
NcUserStatusIcon
},
/* eslint vue/require-prop-comment: warn -- TODO: Add a proper doc block about what this props do */
props: {
/**
* The label text
*/
label: {
type: String,
required: false,
default: null
},
/**
* The secondary line of text if any
*/
subline: {
type: String,
default: null
},
/**
* Unique id
*/
id: {
type: String,
default: null
},
/**
* The icon class
*/
icon: {
type: String,
required: true
},
/**
* Icon as external URL
*/
iconUrl: {
type: String,
default: null
},
source: {
type: String,
required: true
},
status: {
type: [Object, Array],
default: () => ({})
}
},
setup() {
const isDarkTheme = useIsDarkTheme();
return {
isDarkTheme
};
},
computed: {
avatarUrl() {
if (this.iconUrl) {
return this.iconUrl;
}
return this.id && this.source === "users" ? getAvatarUrl(this.id, { isDarkTheme: this.isDarkTheme }) : null;
}
}
};
const _hoisted_1$1 = { class: "autocomplete-result" };
const _hoisted_2$1 = {
key: 0,
class: "autocomplete-result__status autocomplete-result__status--icon"
};
const _hoisted_3 = { class: "autocomplete-result__content" };
const _hoisted_4 = ["title"];
const _hoisted_5 = {
key: 0,
class: "autocomplete-result__subline"
};
function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
const _component_NcUserStatusIcon = resolveComponent("NcUserStatusIcon");
return openBlock(), createElementBlock("div", _hoisted_1$1, [
createElementVNode("div", {
class: normalizeClass([[$props.icon, `autocomplete-result__icon--${$options.avatarUrl ? "with-avatar" : ""}`], "autocomplete-result__icon"]),
style: normalizeStyle($options.avatarUrl ? { backgroundImage: `url(${$options.avatarUrl})` } : null)
}, [
$props.status.icon ? (openBlock(), createElementBlock("span", _hoisted_2$1, toDisplayString($props.status && $props.status.icon || ""), 1)) : $props.status.status && $props.status.status !== "offline" ? (openBlock(), createBlock(_component_NcUserStatusIcon, {
key: 1,
class: "autocomplete-result__status",
status: $props.status.status
}, null, 8, ["status"])) : createCommentVNode("", true)
], 6),
createElementVNode("span", _hoisted_3, [
createElementVNode("span", {
class: "autocomplete-result__title",
title: $props.label
}, toDisplayString($props.label), 9, _hoisted_4),
$props.subline ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString($props.subline), 1)) : createCommentVNode("", true)
])
]);
}
const NcAutoCompleteResult = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render$1], ["__scopeId", "data-v-ca83b679"]]);
register(t34, t37);
const style1 = {
"material-design-icon": "_material-design-icon_1xkrb_12",
"tribute-container": "_tribute-container_1xkrb_20",
"tribute-container__item": "_tribute-container__item_1xkrb_41",
"tribute-container--focus-visible": "_tribute-container--focus-visible_1xkrb_55",
"tribute-container-autocomplete": "_tribute-container-autocomplete_1xkrb_59",
"tribute-container-emoji": "_tribute-container-emoji_1xkrb_65",
"tribute-container-link": "_tribute-container-link_1xkrb_66",
"tribute-item": "_tribute-item_1xkrb_71",
"tribute-item__title": "_tribute-item__title_1xkrb_86",
"tribute-item__icon": "_tribute-item__icon_1xkrb_91"
};
const smilesCharacters = ["d", "D", "p", "P", "s", "S", "x", "X", ")", "(", "|", "/"];
const textSmiles = [];
smilesCharacters.forEach((char) => {
textSmiles.push(":" + char);
textSmiles.push(":-" + char);
});
const _sfc_main = {
name: "NcRichContenteditable",
mixins: [richEditor],
inheritAttrs: false,
props: {
/**
* The ID attribute of the content editable
*/
id: {
type: String,
default: () => createElementId()
},
/**
* Visual label of the contenteditable
*/
label: {
type: String,
default: ""
},
/**
* The text content
*/
modelValue: {
type: String,
required: true
},
/**
* Placeholder to be shown if empty
*/
placeholder: {
type: String,
default: t("Write a message …")
},
/**
* Auto complete function
*/
autoComplete: {
type: Function,
default: () => []
},
/**
* The containing element for the menu popover
*/
menuContainer: {
type: Element,
default: () => document.body
},
/**
* Make the contenteditable looks like a textarea or not.
* Default looks like a single-line input.
* This also handle the default enter/shift+enter behaviour.
* if multiline, enter = newline; otherwise enter = submit
* shift+enter always add a new line. ctrl+enter always submits
*/
multiline: {
type: Boolean,
default: false
},
/**
* Is the content editable ?
*/
contenteditable: {
type: Boolean,
default: true
},
/**
* Disable the editing and show specific disabled design
*/
disabled: {
type: Boolean,
default: false
},
/**
* Max allowed length
*/
maxlength: {
type: Number,
default: null
},
/**
* Enable or disable emoji autocompletion
*/
emojiAutocomplete: {
type: Boolean,
default: true
},
/**
* Enable or disable link autocompletion
*/
linkAutocomplete: {
type: Boolean,
default: true
},
/**
* CSS class to apply to the root element.
*/
class: {
type: [String, Array, Object],
default: ""
}
},
emits: [
"paste",
"update:modelValue",
"smartPickerSubmit",
"submit"
],
setup() {
const segmenter = new Intl.Segmenter();
return {
// Constants
labelId: createElementId(),
tributeId: createElementId(),
segmenter,
/**
* Non-reactive property to store Tribute instance
*
* @type {import('tributejs').default | null}
*/
tribute: null,
tributeStyleMutationObserver: null
};
},
data() {
return {
// Represent the raw untrimmed text of the contenteditable
// serves no other purpose than to check whether the
// content is empty or not
localValue: this.modelValue,
// Is in text composition session in IME
isComposing: false,
// Tribute autocomplete
isAutocompleteOpen: false,
autocompleteActiveId: void 0,
isTributeIntegrationDone: false
};
},
computed: {
/**
* Is the current trimmed value empty?
*
* @return {boolean}
*/
isEmptyValue() {
return !this.localValue || this.localValue.trim() === "";
},
/**
* Is the current value over maxlength?
*
* @return {boolean}
*/
isOverMaxlength() {
if (this.isEmptyValue || !this.maxlength) {
return false;
}
const length = [...this.segmenter.segment(this.localValue)].length;
return length > this.maxlength;
},
/**
* Tooltip to show if characters count is over limit
*
* @return {string}
*/
tooltipString() {
if (!this.isOverMaxlength) {
return null;
}
return n("Message limit of %n character reached", "Message limit of %n characters reached", this.maxlength);
},
/**
* Edit is only allowed when contenteditableis true and disabled is false
*
* @return {boolean}
*/
canEdit() {
return this.contenteditable && !this.disabled;
},
/**
* Compute debounce function for the autocomplete function
*/
debouncedAutoComplete() {
return debounce(async (search, callback) => {
this.autoComplete(search, callback);
}, 100);
}
},
watch: {
/**
* If the parent value change, we compare the plain text rendering
* If it's different, we render everything and update the main content
*/
modelValue() {
const html = this.$refs.contenteditable.innerHTML;
if (this.modelValue.trim() !== this.parseContent(html).trim()) {
this.updateContent(this.modelValue);
}
}
},
mounted() {
this.initializeTribute();
this.updateContent(this.modelValue);
this.$refs.contenteditable.contentEditable = this.canEdit;
},
beforeUnmount() {
if (this.tribute) {
this.tribute.detach(this.$refs.contenteditable);
}
if (this.tributeStyleMutationObserver) {
this.tributeStyleMutationObserver.disconnect();
}
},
methods: {
/**
* Focus the richContenteditable
*
* @public
*/
focus() {
this.$refs.contenteditable.focus();
},
initializeTribute() {
const renderMenuItem = (content) => `<div id="${createElementId()}" class="${this.$style["tribute-item"]}" role="option">${content}</div>`;
const tributesCollection = [];
tributesCollection.push({
fillAttr: "id",
// Search against id and label (display name) (fallback to title for v8.0.0..8.6.1 compatibility)
lookup: (result) => `${result.id} ${result.label ?? result.title}`,
requireLeadingSpace: true,
// Popup mention autocompletion templates
menuItemTemplate: (item) => renderMenuItem(this.renderComponentHtml(item.original, NcAutoCompleteResult)),
// Hide if no results
noMatchTemplate: () => '<span class="hidden"></span>',
// Inner display of mentions
selectTemplate: (item) => this.genSelectTemplate(item?.original?.id),
// Autocompletion results
values: this.debouncedAutoComplete,
// Class added to the menu container
containerClass: `${this.$style["tribute-container"]} ${this.$style["tribute-container-autocomplete"]}`,
// Class added to each list item
itemClass: this.$style["tribute-container__item"]
});
if (this.emojiAutocomplete) {
tributesCollection.push({
trigger: ":",
// Don't use the tribute search function at all
// We pass search results as values (see below)
lookup: (result, query) => query,
requireLeadingSpace: true,
// Popup mention autocompletion templates
menuItemTemplate: (item) => {
if (textSmiles.includes(item.original)) {
return item.original;
}
return renderMenuItem(`<span class="${this.$style["tribute-item__emoji"]}">${item.original.native}</span> :${item.original.short_name}`);
},
// Hide if no results
noMatchTemplate: () => t("No emoji found"),
// Display raw emoji along with its name
selectTemplate: (item) => {
if (textSmiles.includes(item.original)) {
return item.original;
}
emojiAddRecent(item.original);
return item.original.native;
},
// Pass the search results as values
values: (text, cb) => {
const emojiResults = emojiSearch(text);
if (textSmiles.includes(":" + text)) {
emojiResults.unshift(":" + text);
}
cb(emojiResults);
},
// Class added to the menu container
containerClass: `${this.$style["tribute-container"]} ${this.$style["tribute-container-emoji"]}`,
// Class added to each list item
itemClass: this.$style["tribute-container__item"]
});
}
if (this.linkAutocomplete) {
tributesCollection.push({
trigger: "/",
// Don't use the tribute search function at all
// We pass search results as values (see below)
lookup: (result, query) => query,
requireLeadingSpace: true,
// Popup mention autocompletion templates
menuItemTemplate: (item) => renderMenuItem(`<img class="${this.$style["tribute-item__icon"]}" src="${item.original.icon_url}"> <span class="${this.$style["tribute-item__title"]}">${item.original.title}</span>`),
// Hide if no results
noMatchTemplate: () => t("No link provider found"),
selectTemplate: this.getLink,
// Pass the search results as values
values: (text, cb) => cb(searchProvider(text)),
// Class added to the menu container
containerClass: `${this.$style["tribute-container"]} ${this.$style["tribute-container-link"]}`,
// Class added to each list item
itemClass: this.$style["tribute-container__item"]
});
}
this.tribute = new Tribute({
collection: tributesCollection,
// FIXME: tributejs doesn't support allowSpaces as a collection option, only as a global one
// Requires to fork a library to allow spaces only in the middle of mentions ('@' trigger)
allowSpaces: false,
// Where to inject the menu popup
menuContainer: this.menuContainer
});
this.tribute.attach(this.$refs.contenteditable);
},
getLink(item) {
getLinkWithPicker(item.original.id).then((result) => {
const tmpElem = document.getElementById("tmp-smart-picker-result-node");
const eventData = {
result,
insertText: true
};
this.$emit("smartPickerSubmit", eventData);
if (eventData.insertText) {
const newElem = document.createTextNode(result);
tmpElem.replaceWith(newElem);
this.setCursorAfter(newElem);
this.updateValue(this.$refs.contenteditable.innerHTML);
} else {
tmpElem.remove();
}
}).catch((error) => {
logger.debug("[NcRichContenteditable] Smart picker promise rejected:", { error });
const tmpElem = document.getElementById("tmp-smart-picker-result-node");
this.setCursorAfter(tmpElem);
tmpElem.remove();
});
return '<span id="tmp-smart-picker-result-node"></span>';
},
setCursorAfter(element) {
const range = document.createRange();
range.setEndAfter(element);
range.collapse();
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
moveCursorToEnd() {
if (!document.createRange) {
return;
}
if (window.getSelection().rangeCount > 0 && this.$refs.contenteditable.contains(window.getSelection().getRangeAt(0).commonAncestorContainer)) {
return;
}
const range = document.createRange();
range.selectNodeContents(this.$refs.contenteditable);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
/**
* Re-emit the input event to the parent
*
* @param {Event} event the input event
*/
onInput(event) {
this.updateValue(event.target.innerHTML);
},
/**
* When pasting, sanitize the content, extract text
* and render it again
*
* @param {Event} event the paste event
* @fires Event paste the original paste event
*/
onPaste(event) {
if (!this.canEdit) {
return;
}
event.preventDefault();
const clipboardData = event.clipboardData;
this.$emit("paste", event);
if (clipboardData.files.length !== 0 || !Object.values(clipboardData.items).find((item) => item?.type.startsWith("text"))) {
return;
}
const text = clipboardData.getData("text");
const selection = window.getSelection();
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
range.collapse(false);
this.updateValue(this.$refs.contenteditable.innerHTML);
},
/**
* Update the value text from the provided html
*
* @param {string} htmlOrText the html content (or raw text with @mentions)
*/
updateValue(htmlOrText) {
const text = this.parseContent(htmlOrText).replace(/^\n$/, "");
this.localValue = text;
this.$emit("update:modelValue", text);
},
/**
* Update content and local value
*
* @param {string} value the message value
*/
updateContent(value) {
const renderedContent = this.renderContent(value);
this.$refs.contenteditable.innerHTML = renderedContent;
this.localValue = value;
},
/**
* Enter key pressed. Submits if not multiline
*
* @param {Event} event the keydown event
*/
onEnter(event) {
if (this.multiline || this.isOverMaxlength || this.tribute.isActive || this.isComposing) {
return;
}
event.preventDefault();
event.stopPropagation();
this.$emit("submit", event);
},
/**
* Ctrl + Enter key pressed is used to submit
*
* @param {Event} event the keydown event
*/
onCtrlEnter(event) {
if (this.isOverMaxlength) {
return;
}
this.$emit("submit", event);
},
onKeyUp(event) {
event.stopImmediatePropagation();
},
onKeyEsc(event) {
if (this.tribute && this.isAutocompleteOpen) {
event.stopImmediatePropagation();
this.tribute.hideMenu();
}
},
/**
* Get HTML element with Tribute.js container
*
* @return {HTMLElement}
*/
getTributeContainer() {
return this.tribute.menu;
},
/**
* Get the currently selected item element id in Tribute.js container
*
* @return {HTMLElement}
*/
getTributeSelectedItem() {
return this.getTributeContainer().querySelector('.highlight [id^="nc-rich-contenteditable-tribute-item-"]');
},
/**
* Handle Tribute activation
*
* @param {boolean} isActive - is active
*/
onTributeActive(isActive) {
this.isAutocompleteOpen = isActive;
if (isActive) {
this.getTributeContainer().setAttribute("class", this.tribute.current.collection.containerClass || this.$style["tribute-container"]);
this.setupTributeIntegration();
document.removeEventListener("click", this.hideTribute, true);
} else {
this.debouncedAutoComplete.clear();
this.autocompleteActiveId = void 0;
this.setTributeFocusVisible(false);
}
},
onTributeArrowKeyDown() {
if (!this.isAutocompleteOpen) {
return;
}
this.setTributeFocusVisible(true);
this.onTributeSelectedItemWillChange();
},
onTributeSelectedItemWillChange() {
requestAnimationFrame(() => {
this.autocompleteActiveId = this.getTributeSelectedItem()?.id;
});
},
setupTributeIntegration() {
if (this.isTributeIntegrationDone) {
return;
}
this.isTributeIntegrationDone = true;
const tributeContainer = this.getTributeContainer();
tributeContainer.id = this.tributeId;
tributeContainer.setAttribute("role", "listbox");
const ul = tributeContainer.children[0];
ul.setAttribute("role", "presentation");
this.tributeStyleMutationObserver = new MutationObserver(([{ target }]) => {
if (target.style.display !== "none") {
this.onTributeSelectedItemWillChange();
}
}).observe(tributeContainer, {
attributes: true,
attributeFilter: ["style"]
});
tributeContainer.addEventListener("mousemove", () => {
this.setTributeFocusVisible(false);
this.onTributeSelectedItemWillChange();
}, { passive: true });
},
/**
* Set tribute-container--focus-visible class on the Tribute container when the user navigates the listbox via keyboard.
*
* Because the real focus is kept on the textbox, we cannot use the :focus-visible pseudo-class
* to style selected options in the autocomplete listbox.
*
* @param {boolean} withFocusVisible - should the focus-visible class be added
*/
setTributeFocusVisible(withFocusVisible) {
if (withFocusVisible) {
this.getTributeContainer().classList.add(this.$style["tribute-container--focus-visible"]);
} else {
this.getTributeContainer().classList.remove(this.$style["tribute-container--focus-visible"]);
}
},
/**
* Show tribute menu programmatically.
*
* @param {string} trigger - trigger character, can be '/', '@', or ':'
*
* @public
*/
showTribute(trigger) {
this.focus();
const index = this.tribute.collection.findIndex((collection) => collection.trigger === trigger);
this.tribute.showMenuForCollection(this.$refs.contenteditable, index);
this.updateValue(this.$refs.contenteditable.innerHTML);
document.addEventListener("click", this.hideTribute, true);
},
/**
* Hide tribute menu programmatically
*
*/
hideTribute() {
this.tribute.hideMenu();
document.removeEventListener("click", this.hideTribute, true);
}
}
};
const _hoisted_1 = ["id", "contenteditable", "aria-labelledby", "aria-placeholder", "aria-controls", "aria-expanded", "aria-activedescendant", "title"];
const _hoisted_2 = ["id"];
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return openBlock(), createElementBlock("div", {
class: normalizeClass(["rich-contenteditable", _ctx.$props.class])
}, [
createElementVNode("div", mergeProps({
id: $props.id,
ref: "contenteditable",
class: [{
"rich-contenteditable__input--empty": $options.isEmptyValue,
"rich-contenteditable__input--multiline": $props.multiline,
"rich-contenteditable__input--has-label": $props.label,
"rich-contenteditable__input--overflow": $options.isOverMaxlength,
"rich-contenteditable__input--disabled": $props.disabled
}, "rich-contenteditable__input"],
contenteditable: $options.canEdit,
"aria-labelledby": $props.label ? $setup.labelId : void 0,
"aria-placeholder": $props.placeholder,
"aria-multiline": "true",
role: "textbox",
"aria-haspopup": "listbox",
"aria-autocomplete": "inline",
"aria-controls": $setup.tributeId,
"aria-expanded": $data.isAutocompleteOpen ? "true" : "false",
"aria-activedescendant": $data.autocompleteActiveId,
title: $options.tooltipString
}, _ctx.$attrs, {
onFocus: _cache[0] || (_cache[0] = (...args) => $options.moveCursorToEnd && $options.moveCursorToEnd(...args)),
onInput: _cache[1] || (_cache[1] = (...args) => $options.onInput && $options.onInput(...args)),
onCompositionstart: _cache[2] || (_cache[2] = ($event) => $data.isComposing = true),
onCompositionend: _cache[3] || (_cache[3] = ($event) => $data.isComposing = false),
onKeydownCapture: _cache[4] || (_cache[4] = withKeys((...args) => $options.onKeyEsc && $options.onKeyEsc(...args), ["esc"])),
onKeydown: [
_cache[5] || (_cache[5] = withKeys(withModifiers((...args) => $options.onEnter && $options.onEnter(...args), ["exact"]), ["enter"])),
_cache[6] || (_cache[6] = withKeys(withModifiers((...args) => $options.onCtrlEnter && $options.onCtrlEnter(...args), ["ctrl", "exact", "stop", "prevent"]), ["enter"])),
_cache[9] || (_cache[9] = withKeys(withModifiers((...args) => $options.onTributeArrowKeyDown && $options.onTributeArrowKeyDown(...args), ["exact", "stop"]), ["up"])),
_cache[10] || (_cache[10] = withKeys(withModifiers((...args) => $options.onTributeArrowKeyDown && $options.onTributeArrowKeyDown(...args), ["exact", "stop"]), ["down"]))
],
onPaste: _cache[7] || (_cache[7] = (...args) => $options.onPaste && $options.onPaste(...args)),
onKeyupCapture: _cache[8] || (_cache[8] = withModifiers((...args) => $options.onKeyUp && $options.onKeyUp(...args), ["stop", "prevent"])),
onTributeActiveTrue: _cache[11] || (_cache[11] = ($event) => $options.onTributeActive(true)),
onTributeActiveFalse: _cache[12] || (_cache[12] = ($event) => $options.onTributeActive(false))
}), null, 16, _hoisted_1),
$props.label ? (openBlock(), createElementBlock("div", {
key: 0,
id: $setup.labelId,
class: "rich-contenteditable__label"
}, toDisplayString($props.label), 9, _hoisted_2)) : createCommentVNode("", true)
], 2);
}
const cssModules = {
"$style": style1
};
const NcRichContenteditable = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__cssModules", cssModules], ["__scopeId", "data-v-faef642b"]]);
export {
NcMentionBubble as N,
NcAutoCompleteResult as a,
NcRichContenteditable as b
};
//# sourceMappingURL=NcRichContenteditable-0w6dbJeG.mjs.map