bonfire-app/flavours/cooperation/config/flavour_assets/hooks/Bonfire.Editor.Milkdown.hooks.js
Mayel de Borniol 6617e15fae deps
2024-04-01 16:28:15 +01:00

541 lines
17 KiB
JavaScript

/*
This file was generated by the Surface compiler.
*/
import { defaultValueCtx, editorViewOptionsCtx, Editor, editorViewCtx, commandsCtx, rootCtx } from '@milkdown/core';
import { $prose, replaceAll, insert} from '@milkdown/utils';
import { commonmark, wrapInHeadingCommand, toggleStrongCommand, toggleEmphasisCommand, remarkInlineLinkPlugin } from '@milkdown/preset-commonmark';
import {gfm} from "@milkdown/preset-gfm";
import { emoji } from '@milkdown/plugin-emoji';
import { listener, listenerCtx } from '@milkdown/plugin-listener';
import { SlashProvider } from '@milkdown/plugin-slash'
import { slashFactory } from '@milkdown/plugin-slash';
import { gemoji } from "gemoji";
import { clipboard } from '@milkdown/plugin-clipboard';
import { createPopup } from '@picmo/popup-picker';
import { Plugin, PluginKey } from '@milkdown/prose/state';
import { Decoration, DecorationSet } from '@milkdown/prose/view';
const PlaceholderPlugin = new Plugin({
key: new PluginKey('milkdown-placeholder'),
props: {
decorations: (state) => {
const element = document.createElement('span')
element.classList.add('milkdown-placeholder')
element.style.position = "absolute";
element.style.opacity = "0.5";
element.innerText = "Write something...";
const placeholderDecoration = Decoration.widget(0, element, { key: 'milkdown-placeholder', side: 0 });
if (state.doc.textContent.trim().length === 0) {
return DecorationSet.create(state.doc, [placeholderDecoration])
}
}
}
});
const placeholder = $prose(() => PlaceholderPlugin);
const MIN_PREFIX_LENGTH = 2
const VALID_CHARS = '[\\w\\+_\\-:]'
const MENTION_PREFIX = '(?:@)'
const EMOJI_PREFIX = '(?::)'
const MENTION_REGEX = new RegExp(`(?:\\s|^)(${MENTION_PREFIX}${VALID_CHARS}{${MIN_PREFIX_LENGTH},})$`)
const EMOJI_REGEX = new RegExp(`(?:\\s|^)(${EMOJI_PREFIX}${VALID_CHARS}{${MIN_PREFIX_LENGTH},})$`)
import '@milkdown/theme-nord/style.css';
const markdown = ``
function mentionsPluginView(view) {
const content = document.createElement('ul');
content.tabIndex = 1;
content.className = 'm-0 p-0 menu w-72 bg-base-100 shadow-lg ring-2';
let list = ''
const provider = new SlashProvider({
content,
shouldShow: (view, prevState) => {
// get the current content of the editor
const { state } = view;
const { doc } = state;
const currentText = doc.textContent;
if (currentText === '') {
return false;
}
const mentions = currentText.match(MENTION_REGEX)
// Display the menu if the last character is `@` followed by 2 chars.
if (mentions) {
// get the characters that follows the `@` in currentText
const text = mentions[1].split('@').pop()
return getFeedItems(text, '@').then(res => {
list = ''
if (res.length > 0) {
// Add max 4 items to the menu
let maxItems = 4
for (let i = 0; i < res.length && i < maxItems; i++) {
list += mentionItemRenderer(res[i], text);
}
content.innerHTML = list
return true
} else {
content.innerHTML = ''
return false
}
})
}
return false;
},
trigger: '@',
});
return {
update: (updatedView, prevState) => {
provider.update(updatedView, prevState);
},
destroy: () => {
provider.destroy();
content.remove();
}
}
}
function emojisPluginView() {
const content = document.createElement('ul');
content.tabIndex = 1;
content.className = 'm-0 p-0 menu w-72 bg-base-100 shadow-lg ring-2';
let list = ''
const provider = new SlashProvider({
content,
shouldShow: (view, prevState) => {
// get the current content of the editor
const { state } = view;
const { doc } = state;
const currentText = doc.textContent;
if (currentText === '') {
return false;
}
const emojis = currentText.match(EMOJI_REGEX)
// Display the menu if the last character is `@` followed by 2 chars.
if (emojis) {
// get the characters that follows the `@` in currentText
const text = emojis[1].split(':').pop()
const index = gemoji.findIndex((emoji) => {
return emoji.names.some((name) => name.includes(text));
});
list = ''
if (index > 0) {
// Add max 4 items to the menu
gemoji
.filter((emoji) => {
return emoji.names.some((name) => name.includes(text));
})
.slice(0, 6)
.map((emoji) => {
list += emojiItemRenderer(emoji, text);
})
content.innerHTML = list
return true
} else {
content.innerHTML = ''
return false
}
}
return false;
},
trigger: ':',
});
return {
update: (updatedView, prevState) => {
provider.update(updatedView, prevState);
},
destroy: () => {
provider.destroy();
content.remove();
}
}
}
// function slashPluginView(view) {
// const content = document.createElement('ul');
// content.tabIndex = 1;
// content.className = 'm-0 p-0 menu w-72 bg-base-100 shadow-lg ring-2';
// let list = slashItemRenderer()
// content.innerHTML = list
// const provider = new SlashProvider({
// content,
// trigger: '/',
// });
// return {
// update: (updatedView, prevState) => {
// provider.update(updatedView, prevState);
// },
// destroy: () => {
// provider.destroy();
// content.remove();
// }
// }
// }
function getFeedItems(queryText, prefix) {
// console.log(prefix)
if (queryText && queryText.length > 0) {
return new Promise((resolve) => {
// this requires the bonfire_tag extension
fetch("/api/tag/autocomplete/ck5/" + prefix + "/" + queryText)
.then((response) => response.json())
.then((data) => {
console.log("data")
console.log(data)
let values = data.map((item) => ({
id: item.id,
value: item.name,
icon: item.icon
}));
resolve(values);
})
.catch((error) => {
console.error("There has been a problem with the tag search:", error);
resolve([]);
});
});
} else return [];
}
const mentionItemRenderer = (item, text) => {
return `
<li class="rounded-none">
<button type="button" data-mention="${item.id}" data-text="${text}" class="mention_btn rounded-none w-full flex items-center">
<div class="flex items-center gap-3 w-full pointer-events-none">
<div class="flex-shrink-0">
<img class="h-6 w-6 rounded-full" src="${item.icon}" alt="">
</div>
<div class="gap-0 items-start flex flex-col" data-id="${item.id}" data-input="${text}">
<div class="text-sm truncate max-w-[240px] text-base-content font-semibold">${item.value}</div>
<div class="text-xs truncate max-w-[240px] text-base-content/70 font-regular">${item.id}</div>
</div>
</div>
</button>
</li>`
}
const emojiItemRenderer = (item, text) => {
return `
<li class="rounded-none">
<button type="button" data-text="${text}" data-emoji=${item.emoji} class="emoji_btn gap-3 rounded-none w-full flex items-center">
<div class="pointer-events-none flex items-baseline w-full gap-2">
<span>${item.emoji}</span> <span class="truncate max-w-[220px]">:${item.names[0]}:</span>
</div>
</button>
</li>`
}
// const slashItemRenderer = () => {
// return `
// <li class="rounded-none">
// <button type="button" class="heading_btn rounded-none flex items-center gap-2">
// <span class="pointer-events-none material-symbols-outlined text-nord-10 dark:text-nord-9">
// <svg class="w-5 h-5 shrink-0 flex-1 text-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6 17q-.425 0-.713-.288T5 16V8q0-.425.288-.713T6 7q.425 0 .713.288T7 8v3h4V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v8q0 .425-.288.713T12 17q-.425 0-.713-.288T11 16v-3H7v3q0 .425-.288.713T6 17Zm12 0q-.425 0-.713-.288T17 16V9h-1q-.425 0-.713-.288T15 8q0-.425.288-.713T16 7h2q.425 0 .713.288T19 8v8q0 .425-.288.713T18 17Z"/></svg>
// </span>
// Heading
// </button>
// </li>
// <li class="rounded-none">
// <button type="button" class="bold_btn rounded-none flex items-center gap-2">
// <span class="material-symbols-outlined text-nord-10 dark:text-nord-9">
// <svg class="w-5 h-5 shrink-0 flex-1 text-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6 17q-.425 0-.713-.288T5 16V8q0-.425.288-.713T6 7q.425 0 .713.288T7 8v3h4V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v8q0 .425-.288.713T12 17q-.425 0-.713-.288T11 16v-3H7v3q0 .425-.288.713T6 17Zm12 0q-.425 0-.713-.288T17 16V9h-1q-.425 0-.713-.288T15 8q0-.425.288-.713T16 7h2q.425 0 .713.288T19 8v8q0 .425-.288.713T18 17Z"/></svg>
// </span>
// Bold
// </button>
// </li>
// <li class="rounded-none">
// <button type="button" class="italic_btn rounded-none flex items-center gap-2">
// <span class="material-symbols-outlined text-nord-10 dark:text-nord-9">
// <svg class="w-5 h-5 shrink-0 flex-1 text-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6 17q-.425 0-.713-.288T5 16V8q0-.425.288-.713T6 7q.425 0 .713.288T7 8v3h4V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v8q0 .425-.288.713T12 17q-.425 0-.713-.288T11 16v-3H7v3q0 .425-.288.713T6 17Zm12 0q-.425 0-.713-.288T17 16V9h-1q-.425 0-.713-.288T15 8q0-.425.288-.713T16 7h2q.425 0 .713.288T19 8v8q0 .425-.288.713T18 17Z"/></svg>
// </span>
// Italic
// </button>
// </li>
// <li class="rounded-none">
// <button type="button" class="link_btn rounded-none flex items-center gap-2">
// <span class="material-symbols-outlined text-nord-10 dark:text-nord-9">
// <svg class="w-5 h-5 shrink-0 flex-1 text-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6 17q-.425 0-.713-.288T5 16V8q0-.425.288-.713T6 7q.425 0 .713.288T7 8v3h4V8q0-.425.288-.713T12 7q.425 0 .713.288T13 8v8q0 .425-.288.713T12 17q-.425 0-.713-.288T11 16v-3H7v3q0 .425-.288.713T6 17Zm12 0q-.425 0-.713-.288T17 16V9h-1q-.425 0-.713-.288T15 8q0-.425.288-.713T16 7h2q.425 0 .713.288T19 8v8q0 .425-.288.713T18 17Z"/></svg>
// </span>
// Link
// </button>
// </li>
// <li class="rounded-none">
// <button type="button" class="divider_btn rounded-none flex items-center gap-2">
// <span class="material-symbols-outlined text-nord-10 dark:text-nord-9">
// <svg class="w-5 h-5 shrink-0 flex-1 text-info" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M2 7.5a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1h-10a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>
// </span>
// Divider
// </button>
// </li>
// `
// }
const mentionSlash = slashFactory('mentions-slash');
const emojisSlash = slashFactory('emojis-slash');
// const slash = slashFactory('slash');
let isUpdatingMarkdown = false;
const createEditor = async (_this, hidden_input, composer$) => {
const editor = await Editor
.make()
.config(ctx => {
ctx.set(rootCtx, '#editor')
ctx.set(defaultValueCtx, markdown)
ctx.set(mentionSlash.key, {
view: mentionsPluginView
})
ctx.set(emojisSlash.key, {
view: emojisPluginView
})
ctx.get(listenerCtx)
.markdownUpdated((ctx, markdown, prevMarkdown) => {
const transformedMarkdown = markdown
.replace(/!\[(.*?)\]\(.*?\)/g, '$1');
hidden_input.value = transformedMarkdown;
console.log(hidden_input.value);
const inputEvent = new Event('input', {
bubbles: true,
});
hidden_input.dispatchEvent(inputEvent);
})
ctx.update(editorViewOptionsCtx, (prev) => ({
...prev,
attributes: {
placeholder: "Type your text here...",
class: 'editor prose prose-sm h-full p-2 focus:outline-none composer w-full max-w-full',
spellcheck: 'false' },
}))
})
// .config(nord)
.use(commonmark)
.use(remarkInlineLinkPlugin)
.use(gfm)
.use(emoji)
.use(listener)
.use(mentionSlash)
.use(emojisSlash)
.use(clipboard)
.use(placeholder)
// .use(slash)
.create()
const trigger = document.querySelector('.emoji-button');
trigger.addEventListener('click', () => {
picker.toggle();
});
const picker = createPopup({}, {
referenceElement: trigger,
triggerElement: trigger,
emojiSize: '1.75rem',
className: 'z-[99999999999999999999]',
});
picker.addEventListener('emoji:select', event => {
console.log(event.emoji)
editor.action((ctx) => {
const view = ctx.get(editorViewCtx);
const { state } = view;
const { selection } = state;
view.dispatch(
view.state.tr
.insertText(event.emoji + ' ')
);
view.focus()
})
});
const submit_btn = document.getElementById('submit_btn');
const heading_btn = document.getElementById('heading_btn');
const bold_btn = document.getElementById('bold_btn');
const italic_btn = document.getElementById('italic_btn');
// const quote_btn = document.getElementById('quote_btn');
// const strike_btn = document.getElementById('strike_btn');
// const table_btn = document.getElementById('table_btn');
_this.handleEvent("smart_input:reset", ({text}) => {
editor.action(replaceAll(''))
})
// submit_btn.addEventListener('click', (e) => {
// editor.action(replaceAll(''))
// })
_this.handleEvent("mention_suggestions", ({text}) => {
// replace the current text with the text from the event
editor.action(replaceAll(''))
if (text != null) {
editor.action((ctx) => {
const view = ctx.get(editorViewCtx);
view.dispatch(
view.state.tr
.insertText(text + ' ')
);
view.focus()
})
}
})
heading_btn.addEventListener('click', (e) => {
e.preventDefault();
editor.action((ctx) => {
const commandManager = ctx.get(commandsCtx);
const view = ctx.get(editorViewCtx);
commandManager.call(wrapInHeadingCommand.key, 3);
view.focus()
});
})
bold_btn.addEventListener('click', (e) => {
e.preventDefault();
editor.action((ctx) => {
const commandManager = ctx.get(commandsCtx);
const view = ctx.get(editorViewCtx);
commandManager.call(toggleStrongCommand.key);
view.focus()
});
})
italic_btn.addEventListener('click', (e) => {
e.preventDefault();
editor.action((ctx) => {
const commandManager = ctx.get(commandsCtx);
const view = ctx.get(editorViewCtx);
commandManager.call(toggleEmphasisCommand.key);
view.focus()
});
})
// quote_btn.addEventListener('click', (e) => {
// e.preventDefault();
// editor.action((ctx) => {
// const commandManager = ctx.get(commandsCtx);
// const view = ctx.get(editorViewCtx);
// commandManager.call(wrapInBlockquoteCommand.key);
// view.focus()
// });
// })
// strike_btn.addEventListener('click', (e) => {
// e.preventDefault();
// editor.action((ctx) => {
// const commandManager = ctx.get(commandsCtx);
// const view = ctx.get(editorViewCtx);
// commandManager.call(toggleStrikethroughCommand.key);
// view.focus()
// });
// })
// table_btn.addEventListener('click', (e) => {
// e.preventDefault();
// editor.action((ctx) => {
// const commandManager = ctx.get(commandsCtx);
// const view = ctx.get(editorViewCtx);
// commandManager.call(insertTableCommand.key);
// view.focus()
// });
// })
composer$.addEventListener('click', (e) => {
if (e.target.matches('.emoji_btn')) {
e.preventDefault();
const emoji = e.target.dataset.emoji;
const text = e.target.dataset.text
editor.action((ctx) => {
const view = ctx.get(editorViewCtx);
const { state } = view;
const { selection } = state;
view.dispatch(
view.state.tr
.delete(selection.from - text.length - 1, selection.from)
.insertText(emoji + ' ')
);
view.focus()
})
}
if (e.target.matches('.mention_btn')) {
e.preventDefault();
const mention = e.target.dataset.mention;
const text = e.target.dataset.text
editor.action((ctx) => {
const view = ctx.get(editorViewCtx);
const { state } = view;
const { selection } = state;
// Calculate the start position for deletion
const startPos = selection.from - text.length -1;
view.dispatch(
view.state.tr
.delete(startPos, selection.from)
.insertText(`${mention} ` + `\u200B ` ) // add a space character after the mention variable
);
view.focus()
})
}
})
return editor;
}
export default {
mounted() {
const hidden_input = document.getElementById('editor_hidden_input');
const composer$ = this.el.querySelector('#editor')
createEditor(this, hidden_input, composer$)
}
}