diff --git a/app/src/assets/scss/base.scss b/app/src/assets/scss/base.scss index 2e947266b..2263476b9 100644 --- a/app/src/assets/scss/base.scss +++ b/app/src/assets/scss/base.scss @@ -34,6 +34,7 @@ @import "business/custom"; @import "business/resize"; @import "business/av"; +@import "business/emojis"; /* .status: 2 diff --git a/app/src/assets/scss/business/_emojis.scss b/app/src/assets/scss/business/_emojis.scss new file mode 100644 index 000000000..c8f8d265e --- /dev/null +++ b/app/src/assets/scss/business/_emojis.scss @@ -0,0 +1,107 @@ + +.emojis { + word-break: break-all; + white-space: normal; + display: flex; + flex-direction: column; + padding: 8px 0; + height: 100%; + box-sizing: border-box; + + &__tabheader { + display: flex; + border-bottom: 1px solid var(--b3-border-color); + padding: 0 8px 8px; + } + + &__tabbody { + flex: 1; + overflow: auto; + + div[data-type="tab-emoji"] { + display: flex; + flex-direction: column; + height: 100%; + } + } + + .emoji__dynamic { + &-item { + width: 73px; + margin: 8px; + cursor: pointer; + } + + &-color { + padding: 8px 8px 4px 4px; + } + } + + &__item { + font-size: 24px; + line-height: .9em; // windows 需要这样设置 + font-family: var(--b3-font-family-emoji); + text-align: center; + height: 28px; + padding: 2px 4px; + cursor: pointer; + display: inline-block; + transition: var(--b3-transition); + background-color: transparent; + border: 0; + margin: 1px; + overflow: hidden; + border-radius: var(--b3-border-radius); + + img, svg { + height: 24px; + display: block; + width: 24px; + } + + &--current, + &:hover { + background: var(--b3-list-hover); + } + } + + &__title { + color: var(--b3-theme-on-surface); + padding: 8px 4px 4px 4px; + } + + &__panel { + flex: 1; + overflow: auto; + padding: 0 8px; + } + + &__content { + display: flex; + flex-wrap: wrap; + } + + &__type { + cursor: pointer; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + height: 28px; + line-height: 28px; + transition: var(--b3-list-hover); + font-size: 16px; + background-color: transparent; + border: 0; + padding: 0; + + &:hover { + background-color: var(--b3-theme-surface-lighter); + } + + svg { + height: 16px; + width: 16px; + } + } +} diff --git a/app/src/assets/scss/component/_menu.scss b/app/src/assets/scss/component/_menu.scss index 6378acf26..e6dab7c42 100644 --- a/app/src/assets/scss/component/_menu.scss +++ b/app/src/assets/scss/component/_menu.scss @@ -366,81 +366,3 @@ } } } - -.emojis { - word-break: break-all; - white-space: normal; - display: flex; - flex-direction: column; - padding: 8px 0; - height: 100%; - box-sizing: border-box; - - &__item { - font-size: 24px; - line-height: .9em; // windows 需要这样设置 - font-family: var(--b3-font-family-emoji); - text-align: center; - height: 28px; - padding: 2px 4px; - cursor: pointer; - display: inline-block; - transition: var(--b3-transition); - background-color: transparent; - border: 0; - margin: 1px; - overflow: hidden; - border-radius: var(--b3-border-radius); - - img, svg { - height: 24px; - display: block; - width: 24px; - } - - &--current, - &:hover { - background: var(--b3-list-hover); - } - } - - &__title { - color: var(--b3-theme-on-surface); - padding: 8px 4px 4px 4px; - } - - &__panel { - flex: 1; - overflow: auto; - padding: 0 8px; - } - - &__content { - display: flex; - flex-wrap: wrap; - } - - &__type { - cursor: pointer; - flex: 1; - display: flex; - align-items: center; - justify-content: center; - height: 28px; - line-height: 28px; - transition: var(--b3-list-hover); - font-size: 16px; - background-color: transparent; - border: 0; - padding: 0; - - &:hover { - background-color: var(--b3-theme-surface-lighter); - } - - svg { - height: 16px; - width: 16px; - } - } -} diff --git a/app/src/assets/scss/mobile.scss b/app/src/assets/scss/mobile.scss index ff31b132d..5fd9a6660 100644 --- a/app/src/assets/scss/mobile.scss +++ b/app/src/assets/scss/mobile.scss @@ -27,6 +27,7 @@ @import "business/custom"; @import "business/av"; @import "business/search"; +@import "business/emojis"; .block__popover { width: 80vw; diff --git a/app/src/constants.ts b/app/src/constants.ts index 2915e5c74..e60598f09 100644 --- a/app/src/constants.ts +++ b/app/src/constants.ts @@ -123,6 +123,7 @@ export abstract class Constants { public static readonly LOCAL_OUTLINE = "local-outline"; public static readonly LOCAL_PLUGIN_DOCKS = "local-plugin-docks"; public static readonly LOCAL_IMAGES = "local-images"; + public static readonly LOCAL_EMOJIS = "local-emojis"; // dialog public static readonly DIALOG_OPENCARD = "dialog-opencard"; diff --git a/app/src/emoji/index.ts b/app/src/emoji/index.ts index 0c92fe43b..232fdf3da 100644 --- a/app/src/emoji/index.ts +++ b/app/src/emoji/index.ts @@ -1,6 +1,5 @@ import {getRandom, isMobile} from "../util/functions"; import {fetchPost} from "../util/fetch"; -import {hasClosestByClassName} from "../protyle/util/hasClosest"; import {Constants} from "../constants"; import {Files} from "../layout/dock/Files"; /// #if !MOBILE @@ -11,6 +10,8 @@ import {getAllEditor} from "../layout/getAll"; import {setNoteBook} from "../util/pathName"; import {Dialog} from "../dialog"; import {setPosition} from "../util/setPosition"; +import {setStorageVal} from "../protyle/util/compatibility"; +import * as dayjs from "dayjs"; export const getRandomEmoji = () => { const emojis = window.siyuan.emojis[getRandom(0, window.siyuan.emojis.length - 1)]; @@ -27,6 +28,8 @@ export const unicode2Emoji = (unicode: string, className = "", needSpan = false, let emoji = ""; if (unicode.indexOf(".") > -1) { emoji = ``; + } else if (unicode.startsWith("api/icon/getDynamicIcon")) { + emoji = ``; } else { try { unicode.split("-").forEach(item => { @@ -198,28 +201,37 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi window.siyuan.menus.menu.removeScrollEvent(); } + const dynamicURL = 'api/icon/getDynamicIcon?' const dialog = new Dialog({ disableAnimation: true, transparent: true, hideCloseIcon: true, - width: isMobile() ? "80vw" : "360px", + width: isMobile() ? "80vw" : "368px", height: "50vh", content: `
-
- - - - - - - +
+
+
+
+
+
-
${filterEmoji()}
-
- ${[ +
+
+
+
+ + + + + +
+
${filterEmoji()}
+
+ ${[ ["2b50", window.siyuan.languages.recentEmoji], ["1f527", getEmojiTitle(0)], ["1f60d", getEmojiTitle(1)], @@ -233,6 +245,75 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi ].map(([unicode, title], index) => `
${unicode2Emoji(unicode)}
` ).join("")} +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + ${window.siyuan.languages.language} + + + +
+
+
+ + ${window.siyuan.languages.date} + + + +
+
+
+ + ${window.siyuan.languages.format} + + + +
+
+ + + + + + + +
+
+
+ + ${window.siyuan.languages.custom} + + + +
+
+ +
+
` }); @@ -241,42 +322,45 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi const dialogElement = dialog.element.querySelector(".b3-dialog") as HTMLElement; dialogElement.style.justifyContent = "inherit"; dialogElement.style.alignItems = "inherit"; + const currentTab = window.siyuan.storage[Constants.LOCAL_EMOJIS].currentTab; + dialog.element.querySelector(`.emojis__tabheader [data-type="tab-${currentTab}"]`).classList.add("block__icon--active"); + dialog.element.querySelector(`.emojis__tabbody [data-type="tab-${currentTab}"]`).classList.remove("fn__none"); setPosition(dialog.element.querySelector(".b3-dialog__container"), position.x, position.y, position.h, position.w); dialog.element.querySelector(".emojis__item").classList.add("emojis__item--current"); - const inputElement = dialog.element.querySelector(".b3-text-field") as HTMLInputElement; + const emojiSearchInputElement = dialog.element.querySelector('[data-type="tab-emoji"] .b3-text-field') as HTMLInputElement; const emojisContentElement = dialog.element.querySelector(".emojis__panel"); - inputElement.addEventListener("compositionend", () => { - emojisContentElement.innerHTML = filterEmoji(inputElement.value); - if (inputElement.value) { + emojiSearchInputElement.addEventListener("compositionend", () => { + emojisContentElement.innerHTML = filterEmoji(emojiSearchInputElement.value); + if (emojiSearchInputElement.value) { emojisContentElement.nextElementSibling.classList.add("fn__none"); } else { emojisContentElement.nextElementSibling.classList.remove("fn__none"); } emojisContentElement.scrollTop = 0; dialog.element.querySelector(".emojis__item")?.classList.add("emojis__item--current"); - if (inputElement.value === "") { + if (emojiSearchInputElement.value === "") { lazyLoadEmoji(dialog.element); } lazyLoadEmojiImg(dialog.element); }); - inputElement.addEventListener("input", (event: InputEvent) => { + emojiSearchInputElement.addEventListener("input", (event: InputEvent) => { if (event.isComposing) { return; } - emojisContentElement.innerHTML = filterEmoji(inputElement.value); - if (inputElement.value) { + emojisContentElement.innerHTML = filterEmoji(emojiSearchInputElement.value); + if (emojiSearchInputElement.value) { emojisContentElement.nextElementSibling.classList.add("fn__none"); } else { emojisContentElement.nextElementSibling.classList.remove("fn__none"); } emojisContentElement.scrollTop = 0; dialog.element.querySelector(".emojis__item")?.classList.add("emojis__item--current"); - if (inputElement.value === "") { + if (emojiSearchInputElement.value === "") { lazyLoadEmoji(dialog.element); } lazyLoadEmojiImg(dialog.element); }); - inputElement.addEventListener("keydown", (event: KeyboardEvent) => { + emojiSearchInputElement.addEventListener("keydown", (event: KeyboardEvent) => { if (event.isComposing) { return; } @@ -379,7 +463,7 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi } if (newCurrentElement) { newCurrentElement.classList.add("emojis__item--current"); - const inputHeight = inputElement.clientHeight + 6; + const inputHeight = emojiSearchInputElement.clientHeight + 6; if (newCurrentElement.offsetTop - inputHeight < emojisContentElement.scrollTop) { emojisContentElement.scrollTop = newCurrentElement.offsetTop - inputHeight - 6; } else if (newCurrentElement.offsetTop - inputHeight - emojisContentElement.clientHeight + newCurrentElement.clientHeight > emojisContentElement.scrollTop) { @@ -387,95 +471,160 @@ export const openEmojiPanel = (id: string, type: "doc" | "notebook" | "av", posi } } }); - if (!isMobile()) { - inputElement.focus(); + if (!isMobile() && currentTab === "emoji") { + emojiSearchInputElement.focus(); } lazyLoadEmoji(dialog.element); lazyLoadEmojiImg(dialog.element); // 不能使用 getEventName 否则 https://github.com/siyuan-note/siyuan/issues/5472 dialog.element.addEventListener("click", (event) => { - const eventTarget = event.target as HTMLElement; - const typeElement = hasClosestByClassName(eventTarget, "emojis__type"); - if (typeElement) { - const titleElement = emojisContentElement.querySelector(`[data-type="${typeElement.getAttribute("data-type")}"]`) as HTMLElement; - if (titleElement) { - const index = titleElement.nextElementSibling.getAttribute("data-index"); - if (index) { - let html = ""; - window.siyuan.emojis[parseInt(index)].items.forEach(emoji => { - html += ``; - }); - titleElement.nextElementSibling.innerHTML = html; - titleElement.nextElementSibling.removeAttribute("data-index"); - } + }); + titleElement.nextElementSibling.innerHTML = html; + titleElement.nextElementSibling.removeAttribute("data-index"); + } - emojisContentElement.scrollTo({ - top: titleElement.offsetTop - 34, - // behavior: "smooth" 不能使用,否则无法定位 - }); - } - return; - } - const iconElement = hasClosestByClassName(eventTarget, "block__icon"); - if (iconElement && iconElement.getAttribute("aria-label") === window.siyuan.languages.remove) { - if (type === "notebook") { - fetchPost("/api/notebook/setNotebookIcon", { - notebook: id, - icon: "" - }, () => { - dialog.destroy(); - updateFileTreeEmoji("", id, "iconFilesRoot"); - }); - } else if (type === "doc") { - fetchPost("/api/attr/setBlockAttrs", { - id: id, - attrs: {"icon": ""} - }, () => { - dialog.destroy(); - updateFileTreeEmoji("", id); - updateOutlineEmoji("", id); - }); - } else { - avCB(""); - } - return; - } - const emojiElement = hasClosestByClassName(eventTarget, "emojis__item"); - if (emojiElement || iconElement) { - let unicode = ""; - if (emojiElement) { - unicode = emojiElement.getAttribute("data-unicode"); - if (type !== "av") { - dialog.destroy(); + emojisContentElement.scrollTo({ + top: titleElement.offsetTop - 77, + // behavior: "smooth" 不能使用,否则无法定位 + }); } - } else { - // 随机 - unicode = getRandomEmoji(); + break; + } else if (target.getAttribute("data-action") === "remove") { + if (type === "notebook") { + fetchPost("/api/notebook/setNotebookIcon", { + notebook: id, + icon: "" + }, () => { + dialog.destroy(); + updateFileTreeEmoji("", id, "iconFilesRoot"); + }); + } else if (type === "doc") { + fetchPost("/api/attr/setBlockAttrs", { + id: id, + attrs: {"icon": ""} + }, () => { + dialog.destroy(); + updateFileTreeEmoji("", id); + updateOutlineEmoji("", id); + }); + } else { + avCB(""); + } + break; + } else if (target.classList.contains("emojis__item") || target.getAttribute("data-action") === "random" || target.classList.contains("emoji__dynamic-item")) { + let unicode = ""; + if (target.classList.contains("emojis__item")) { + unicode = target.getAttribute("data-unicode"); + if (type !== "av") { + dialog.destroy(); + } + } else if (target.classList.contains("emoji__dynamic-item")) { + unicode = target.getAttribute("src"); + } else { + // 随机 + unicode = getRandomEmoji(); + } + if (type === "notebook") { + fetchPost("/api/notebook/setNotebookIcon", { + notebook: id, + icon: unicode + }, () => { + addEmoji(unicode); + updateFileTreeEmoji(unicode, id, "iconFilesRoot"); + }); + } else if (type === "doc") { + fetchPost("/api/attr/setBlockAttrs", { + id, + attrs: {"icon": unicode} + }, () => { + addEmoji(unicode); + updateFileTreeEmoji(unicode, id); + updateOutlineEmoji(unicode, id); + }); + } else { + avCB(unicode); + } + break + } else if (target.getAttribute("data-type")?.startsWith("tab-")) { + dialogElement.querySelectorAll('.emojis__tabheader [data-type|="tab"]').forEach((item: HTMLElement) => { + if (item.dataset.type === target.dataset.type) { + item.classList.add("block__icon--active") + } else { + item.classList.remove("block__icon--active") + } + }) + dialogElement.querySelectorAll('.emojis__tabbody > div').forEach((item: HTMLElement) => { + if (item.dataset.type === target.dataset.type) { + item.classList.remove("fn__none") + } else { + item.classList.add("fn__none") + } + }) + window.siyuan.storage[Constants.LOCAL_EMOJIS].currentTab = target.dataset.type.replace("tab-", ""); + setStorageVal(Constants.LOCAL_EMOJIS, window.siyuan.storage[Constants.LOCAL_EMOJIS]); + break + } else if (target.classList.contains("color__square")) { + dynamicTextElements[0].value = target.getAttribute("style").replace("background-color:", ""); + dynamicTextElements[0].style.backgroundColor = dynamicTextElements[0].value; + dynamicTextElements[0].dispatchEvent(new CustomEvent("input")) + break; } - if (type === "notebook") { - fetchPost("/api/notebook/setNotebookIcon", { - notebook: id, - icon: unicode - }, () => { - addEmoji(unicode); - updateFileTreeEmoji(unicode, id, "iconFilesRoot"); - }); - } else if (type === "doc") { - fetchPost("/api/attr/setBlockAttrs", { - id, - attrs: {"icon": unicode} - }, () => { - addEmoji(unicode); - updateFileTreeEmoji(unicode, id); - updateOutlineEmoji(unicode, id); - }); - } else { - avCB(unicode); - } - return; + target = target.parentElement; } }); + const dynamicLangElements: NodeListOf = dialog.element.querySelectorAll('[data-type="tab-dynamic"] .b3-select') + dynamicLangElements[0].addEventListener("change", () => { + dialog.element.querySelectorAll(".fn__flex-wrap .emoji__dynamic-item").forEach(item => { + const url = new URLSearchParams(item.getAttribute("src").replace(dynamicURL, "")); + if (dynamicLangElements[0].value) { + url.set("lang", dynamicLangElements[0].value); + } else { + url.delete("lang"); + } + item.setAttribute("src", dynamicURL + url.toString()); + }) + }); + dynamicLangElements[1].addEventListener("change", () => { + dialog.element.querySelectorAll(".fn__flex-wrap .emoji__dynamic-item").forEach(item => { + const url = new URLSearchParams(item.getAttribute("src").replace(dynamicURL, "")); + url.set("weekdayType", dynamicLangElements[1].value); + item.setAttribute("src", dynamicURL + url.toString()); + }) + }); + const dynamicDateElement = dialog.element.querySelector('[data-type="tab-dynamic"] [type="date"]') as HTMLInputElement + dynamicDateElement.addEventListener("change", () => { + dialog.element.querySelectorAll(".fn__flex-wrap .emoji__dynamic-item").forEach(item => { + const url = new URLSearchParams(item.getAttribute("src").replace(dynamicURL, "")); + url.set("date", dynamicDateElement.value ? dayjs(dynamicDateElement.value).format("YYYY-MM-DD") : ""); + item.setAttribute("src", dynamicURL + url.toString()); + }) + }); + const dynamicTextElements: NodeListOf = dialog.element.querySelectorAll('[data-type="tab-dynamic"] [type="text"]') + const dynamicTextImgElement = dialog.element.querySelector('.emoji__dynamic-item[data-type="text"]') + dynamicTextElements[0].addEventListener("input", () => { + dialog.element.querySelectorAll(".emoji__dynamic-item").forEach(item => { + const url = new URLSearchParams(item.getAttribute("src").replace(dynamicURL, "")); + url.set("color", dynamicTextElements[0].value); + item.setAttribute("src", dynamicURL + url.toString()); + dynamicTextElements[0].style.backgroundColor = dynamicTextElements[0].value; + }) + }); + dynamicTextElements[1].addEventListener("input", () => { + const url = new URLSearchParams(dynamicTextImgElement.getAttribute("src").replace(dynamicURL, "")); + url.set("content", dynamicTextElements[1].value); + dynamicTextImgElement.setAttribute("src", dynamicURL + url.toString()); + }); }; export const updateOutlineEmoji = (unicode: string, id: string) => { diff --git a/app/src/protyle/util/compatibility.ts b/app/src/protyle/util/compatibility.ts index 597b43dc9..e11871d61 100644 --- a/app/src/protyle/util/compatibility.ts +++ b/app/src/protyle/util/compatibility.ts @@ -251,6 +251,9 @@ export const getLocalStorage = (cb: () => void) => { note: "1f5c3", folder: "1f4d1" }; + defaultStorage[Constants.LOCAL_EMOJIS] = { + currentTab: "emoji" + }; defaultStorage[Constants.LOCAL_FONTSTYLES] = []; defaultStorage[Constants.LOCAL_FILESPATHS] = []; // filesPath[] defaultStorage[Constants.LOCAL_SEARCHDATA] = { @@ -288,7 +291,7 @@ export const getLocalStorage = (cb: () => void) => { Constants.LOCAL_PLUGINTOPUNPIN, Constants.LOCAL_SEARCHASSET, Constants.LOCAL_FLASHCARD, Constants.LOCAL_DIALOGPOSITION, Constants.LOCAL_SEARCHUNREF, Constants.LOCAL_HISTORY, Constants.LOCAL_OUTLINE, Constants.LOCAL_FILEPOSITION, Constants.LOCAL_FILESPATHS, Constants.LOCAL_IMAGES, - Constants.LOCAL_PLUGIN_DOCKS].forEach((key) => { + Constants.LOCAL_PLUGIN_DOCKS, Constants.LOCAL_EMOJIS].forEach((key) => { if (typeof response.data[key] === "string") { try { const parseData = JSON.parse(response.data[key]);