From 6b3b9a9152c75e316200dec8fc9777da782538d0 Mon Sep 17 00:00:00 2001 From: Mayel de Borniol Date: Sat, 16 Dec 2023 18:15:47 +0000 Subject: [PATCH] https://github.com/bonfire-networks/bonfire-app/issues/626 --- .../hooks/Bonfire.Editor.Milkdown.hooks.js | 540 ++++++++++++++++++ ...onfire.UI.Common.ChangeLocaleLive.hooks.js | 18 + ...onfire.UI.Common.ChangeThemesLive.hooks.js | 85 +++ .../Bonfire.UI.Common.ComposerLive.hooks.js | 221 +++++++ .../Bonfire.UI.Common.LoadMoreLive.hooks.js | 85 +++ ...onfire.UI.Common.NotificationLive.hooks.js | 43 ++ .../Bonfire.UI.Common.ViewCodeLive.hooks.js | 44 ++ 7 files changed, 1036 insertions(+) create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.Editor.Milkdown.hooks.js create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeLocaleLive.hooks.js create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeThemesLive.hooks.js create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ComposerLive.hooks.js create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.LoadMoreLive.hooks.js create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.NotificationLive.hooks.js create mode 100644 flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ViewCodeLive.hooks.js diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.Editor.Milkdown.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.Editor.Milkdown.hooks.js new file mode 100644 index 00000000000..0f43ad30a91 --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.Editor.Milkdown.hooks.js @@ -0,0 +1,540 @@ +/* +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} 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 ` +
  • + +
  • ` +} + +const emojiItemRenderer = (item, text) => { + + return ` +
  • + +
  • ` +} + +// const slashItemRenderer = () => { +// return ` +//
  • +// +//
  • +//
  • +// +//
  • +//
  • +// +//
  • +//
  • +// +//
  • + +//
  • +// +//
  • +// ` + +// } + + + +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') + .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(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-[9999]', + }); + + + 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$) + } +} + diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeLocaleLive.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeLocaleLive.hooks.js new file mode 100644 index 00000000000..c0adbe5971f --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeLocaleLive.hooks.js @@ -0,0 +1,18 @@ +/* +This file was generated by the Surface compiler. +*/ + +function date_future(days) { + let date = new Date() + // Set expiry to X days + date.setDate(date.getDate() + days) + return date.toGMTString() +} + +export default { + destroyed() { + console.log(this.el.value) + document.cookie = `locale=${this.el.value}; path=/; expires=${date_future(90)}` + // Cookie.set("locale", this.el.value) + } +} diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeThemesLive.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeThemesLive.hooks.js new file mode 100644 index 00000000000..648caef9060 --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ChangeThemesLive.hooks.js @@ -0,0 +1,85 @@ +/* +This file was generated by the Surface compiler. +*/ + + +// import { themeChange } from "theme-change" +import 'vanilla-colorful/hex-color-picker.js'; +import 'vanilla-colorful/hex-input.js'; + +// run to load previously chosen theme when first loading any page (note: not need if using data-theme param on HTML wrapper instead) +// themeChange() + +// let Themeable = { + +// mounted() { +// // run on a view/component with theme-changing controls (wrapper should have phx-hook="Themeable") +// themeChange(false) +// }, + +// } + +let ColourPicker = { + + mounted() { + const id = this.el.id; + const picker = this.el.querySelector('hex-color-picker'); + const input = this.el.querySelector('hex-input'); + const preview = this.el.querySelector('.colour_preview'); + const scope = this.el.dataset.scope; + var count = 0; + var debounceCount = 0; + var debounce; + + let value = input.color.replace("#", "") + picker.color = value; + // this.el.style.backgroundColor = "#" + value; + + + const maybe_set = function(hook) { + console.log("maybe_set") + // Alpine.debounce(() => this.set(), 500) + // Alpine.throttle(() => this.set(), 500) + + // Update the count by 1 + count++; + + // Clear any existing debounce event + clearTimeout(debounce); + + // Update and log the counts after 3 seconds + debounce = setTimeout(function() { + + // Update the debounceCount + debounceCount++; + + // do the thing + console.log("set") + hook.pushEvent("Bonfire.Common.Settings:put", { keys: "ui:theme:custom:" + id, values: value, scope: scope }) + + }, 1000); + + } + + picker.addEventListener('color-changed', (event) => { + value = event.detail.value; + console.log(value) + preview.style.backgroundColor = value; + console.log(preview) + input.color = value; + maybe_set(this) + }); + + input.addEventListener('color-changed', (event) => { + value = event.detail.value; + picker.color = value; + preview.style.backgroundColor = value; + maybe_set(this) + }); + + + }, + +} + +export { ColourPicker } diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ComposerLive.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ComposerLive.hooks.js new file mode 100644 index 00000000000..ff7d215eece --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ComposerLive.hooks.js @@ -0,0 +1,221 @@ +/* +This file was generated by the Surface compiler. +*/ + +import insertText from 'insert-text-at-cursor'; +import getCaretCoordinates from 'textarea-caret'; +// import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'; + + +const mentionItemRenderer = (item, text) => { + return ` +
  • + +
  • ` +} + +const topicItemRenderer = (item) => { + return ` +
  • + +
  • ` +} + + +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) => { + let values = data.map((item) => ({ + id: item.id, + value: item.name, + // link: item.link, + icon: item.icon + })); + resolve(values); + }) + .catch((error) => { + console.error("There has been a problem with the tag search:", error); + resolve([]); + }); + }); + } else return []; +} + + +export default { + + mounted() { + const MIN_PREFIX_LENGTH = 2 + // Technically mastodon accounts allow dots, but it would be weird to do an autosuggest search if it ends with a dot. + // Also this is rare. https://github.com/tootsuite/mastodon/pull/6844 + const VALID_CHARS = '[\\w\\+_\\-:]' + const MENTION_PREFIX = '(?:@)' + // const HASH_PREFIX = '(?:#)' + const TOPIC_PREFIX = '(?:\\+)' + const MENTION_REGEX = new RegExp(`(?:\\s|^)(${MENTION_PREFIX}${VALID_CHARS}{${MIN_PREFIX_LENGTH},})$`) + const TOPIC_REGEX = new RegExp(`(?:\\s|^)(${TOPIC_PREFIX}${VALID_CHARS}{${MIN_PREFIX_LENGTH},})$`) + + const textarea = this.el.querySelector("textarea") + // console.log(textarea) + const suggestions_menu = this.el.querySelector(".menu") + const container = document.querySelector("#smart_input"); + + + setFileInput = function(data, input, name, defaultType = "image/jpeg") { + // console.log(data) + var split = data.toString().split(";base64,"); + var type = data.type || defaultType; + var ext = type.split("/")[1]; + // console.log(split) + file = new File([split[1] || data], name + "." + ext, { + type: type, + }); + console.log(file); + let container = new DataTransfer(); + container.items.add(file); + input.files = container.files; + var event = document.createEvent("HTMLEvents"); // bubble up to LV + event.initEvent("input", true, true); + input.dispatchEvent(event); + } + + // Detect if the user is holding shift and the key is enter + textarea.addEventListener("keydown", (e) => { + if (e.key === "Enter" && e.shiftKey) { + // Prevent the default behavior + e.preventDefault(); + return; + } + }) + + // Add a listener to the textarea to detect when the user paste an image + textarea.addEventListener("paste", (e) => { + // Get the clipboard data + const clipboardData = e.clipboardData || window.clipboardData; + // Get the image from the clipboard + const image = clipboardData.items[0].getAsFile(); + const input = container.querySelector("input[type=file]"); + // If the image is not null + if (image) { + setFileInput(image, input, "image") + } + }) + + // Add a listener to the textarea to detect when the user drag and drop an image + // textarea.addEventListener("drop", (e) => { + // // Get the image from the clipboard + // const image = e.dataTransfer.files[0]; + // const input = container.querySelector("input[type=file]"); + // // If the image is not null + // if (image) { + // setFileInput(image, input, "image") + // } + // }) + + suggestions_menu.addEventListener("click", (e) => { + // get the data-id attribute from the button child element + const id = e.target.closest('button').dataset.id + const inputText = e.target.closest('button').dataset.input + + // Remove the previous text from the current cursor position untill a space is found + const text = textarea.value + const pos = textarea.selectionStart + const before = text.substring(0, pos) + const beforeSpace = before.lastIndexOf(' ') + const beforeText = before.substring(0, beforeSpace) + textarea.value = beforeText + ' ' + // Insert the id of the selected user + + + insertText(textarea, id + " ") + textarea.focus() + }) + + textarea.addEventListener("input", (e) => { + + // Get the input text from the textarea + const inputText = textarea.value; + // console.log(inputText) + + // Get the mentions from the input text, only if the character is followed by a word character and not an empty space + const mentions = inputText.match(MENTION_REGEX) + const topics = inputText.match(TOPIC_REGEX) + + let list = '' + const menu = this.el.querySelector('.menu') + if (mentions) { + const text = mentions[0].split('@').pop() + getFeedItems(text, '@').then(res => { + // if suggestions is greater than 0 append below textarea a menu with the suggestions + if (res.length > 0) { + menu.classList.remove("hidden", false) + + var caret = getCaretCoordinates(textarea, textarea.selectionEnd); + menu.style.top = caret.top + caret.height + 'px' + menu.style.left = caret.left + 'px' + + let counter = 0; + res.some((item) => { + if (counter >= 4) { + return true; // Stops the iteration + } + + list += topicItemRenderer(item); + counter++; + }); + + // let maxItems = 4 + // for (let i = 0; i < res.length && i < maxItems; i++) { + // list += topicItemRenderer(res[i]); + // } + // res.forEach((item) => { + // list += mentionItemRenderer(item, text) + // }) + } else { + menu.classList.add("hidden", false) + list += ` ` + } + menu.innerHTML = list + }) + + } else if (topics) { + // console.log(topics) + const text = topics[0].split('+').pop() + getFeedItems(text, '+').then(res => { + // if suggestions is greater than 0 append below textarea a menu with the suggestions + if (res.length > 0) { + console.log(res.length) + menu.classList.remove("hidden", false) + var caret = getCaretCoordinates(textarea, textarea.selectionEnd); + menu.style.top = caret.top + caret.height + 'px' + menu.style.left = caret.left + 'px' + let maxItems = 4 + for (let i = 0; i < res.length && i < maxItems; i++) { + list += topicItemRenderer(res[i]); + } + } else { + list += ` ` + } + menu.innerHTML = list + }) + + } else { + // no suggestions + list += ` ` + menu.innerHTML = list + menu.classList.add("hidden", false) + } + }) + } +} diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.LoadMoreLive.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.LoadMoreLive.hooks.js new file mode 100644 index 00000000000..4e01e8f562f --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.LoadMoreLive.hooks.js @@ -0,0 +1,85 @@ +/* +This file was generated by the Surface compiler. +*/ + +let Ignore = { + mounted() { + // nothing + } +} +let LoadMore = { + + page() { return this.el.dataset.page; }, + getPhxValues(el) { + return el + .getAttributeNames() + .filter(name => name.startsWith("phx-value-")) + .reduce((obj, name) => ({ + ...obj, + [name.substring(10)]: el.getAttribute(name) + }), {}) + }, + loadMore(entries) { + const target = entries[0]; + // console.log(this.el.dataset.entryCount) + if (target.isIntersecting && this.pending == this.page()) { + let entryCount = this.el.dataset.entryCount + if (undefined == entryCount || entryCount == "0") { + let event = this.el.getAttribute("phx-scroll") + if (event) { + // add the disabled attribute to this.el + this.el.disabled = true + this.el.classList.add("btn-disabled") + this.el.getElementsByTagName("span")[0].innerHTML = "Loading more..."; + this.pending = this.page() + 1; + this.pushEventTo(this.el.getAttribute("phx-target"), event, this.getPhxValues(this.el)); + } else { + console.log("skip loading more because no phx-scroll event") + } + } else { + console.log("skip loading more because entryCount is: " + entryCount) + } + } + }, + mounted() { + this.pending = this.page(); + this.observer = new IntersectionObserver( + (entries) => this.loadMore(entries), + { + root: null, // window by default + rootMargin: "400px", + threshold: 0.1, + } + ); + this.observer.observe(this.el); + + this.el.addEventListener("click", e => { + let entryCount = this.el.dataset.entryCount + if (undefined != entryCount || entryCount != "0") { + let items = document.getElementsByClassName("infinite_scroll_hidden") + if (items.length != 0) { + for (let element of items) { + element.style.display = "block"; + } + e.preventDefault(); + } else { + console.log("no infinite_scroll_hidden") + } + this.el.getElementsByTagName("span")[0].innerHTML = "Load more"; + this.el.dataset.entryCount = 0; + + } else { + console.log("no entryCount") + } + + }); + }, + destroyed() { + this.observer.unobserve(this.el); + }, + updated() { + this.pending = this.page(); + }, +} + +export { LoadMore, Ignore } \ No newline at end of file diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.NotificationLive.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.NotificationLive.hooks.js new file mode 100644 index 00000000000..7af4cbac2b3 --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.NotificationLive.hooks.js @@ -0,0 +1,43 @@ +/* +This file was generated by the Surface compiler. +*/ + + +export default { + mounted() { + if (Notification.permission === "default") { + this.pushEvent("Bonfire.UI.Common.Notifications:request") // ask the server to ask the client to ask the user for permission + console.debug("notification permission should be requested ") + } else { + console.debug("notification permission is already granted ") + } + this.handleEvent("notify", ({ title, message, url, icon }) => sendNotification(title, message, url, icon)); + } +} + +function sendNotification(title, message, url, icon) { + console.debug("notification received: " + title) + // console.debug(title + message + icon) + console.debug("notification permission: " + Notification.permission) + if (Notification.permission === "granted") { + try { + n = new Notification(title, { + body: message, + icon: icon, + requireInteraction: false + }); + console.debug("notification attempted...") + // Hide the notification after 5 seconds + setTimeout(n.close.bind(n), 2000); + if (url && url != "") { + n.onclick = function() { + window.location.href = url; + }; + } + } catch (e) { + console.debug("notification error: " + e) + } + } else { + Notification.requestPermission(); + } +} diff --git a/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ViewCodeLive.hooks.js b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ViewCodeLive.hooks.js new file mode 100644 index 00000000000..ab27cccb387 --- /dev/null +++ b/flavours/classic/config/flavour_assets/hooks/Bonfire.UI.Common.ViewCodeLive.hooks.js @@ -0,0 +1,44 @@ +/* +This file was generated by the Surface compiler. +*/ + + +function update_hash() { + let hash = location.hash + // clear existing highlighted lines + Array.from(document.getElementsByClassName("highlighted")).forEach(function (n, i) { n.classList.remove('highlighted') }) + + if (hash.startsWith("#L")) { + let id = hash.slice(1) + let line = document.getElementById(id) + if (line) { line.classList.add("highlighted"); } + } +} + +let loadHash = { + mounted() { + let loc = window.location; + let url = loc.protocol + '//' + loc.host + loc.pathname + '#L' + this.el.dataset.lineNumber; + history.pushState(history.state, document.title, url); + update_hash() + + window.onhashchange = function () { + update_hash() + } + + } +} + +let updateHash = { + mounted() { + this.el.addEventListener("click", e => { + let loc = window.location; + let url = loc.protocol + '//' + loc.host + loc.pathname + '#L' + this.el.dataset.lineNumber; + history.pushState(history.state, document.title, url); + update_hash() + e.preventDefault(); + }); + } +} + +export { loadHash, updateHash }