diff --git a/package.json b/package.json index b14b5239..0d010681 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "rollup": "^2.32.1", "rollup-plugin-babel": "^4.4.0", "tslib": "^2.2.0", + "twemoji": "^13.1.0", "typescript": "^4.2.4" } } diff --git a/src/iconsPickerModal.ts b/src/iconsPickerModal.ts index 04da1e33..155fc881 100644 --- a/src/iconsPickerModal.ts +++ b/src/iconsPickerModal.ts @@ -1,6 +1,7 @@ +import twemoji from 'twemoji'; import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian'; import IconFolderPlugin from './main'; -import { addToDOM, getEnabledIcons, getIcon } from './util'; +import { addToDOM, getEnabledIcons, getIcon, isEmoji } from './util'; export interface Icon { name: string; @@ -15,6 +16,23 @@ export default class IconsPickerModal extends FuzzySuggestModal { super(app); this.plugin = plugin; this.path = path; + + this.inputEl.addEventListener('input', (e) => { + const inputVal = (e.target as HTMLInputElement).value; + if (isEmoji(inputVal)) { + this.resultContainerEl.querySelector('.suggestion-empty').remove(); + + const suggestionItem = this.resultContainerEl.createDiv(); + suggestionItem.className = 'suggestion-item'; + suggestionItem.textContent = 'Use twemoji Emoji'; + suggestionItem.addEventListener('click', () => { + const codepoint = twemoji.convert.toCodePoint(inputVal); + this.onChooseItem(codepoint); + this.close(); + }); + this.resultContainerEl.appendChild(suggestionItem); + } + }); } onOpen() { @@ -42,8 +60,12 @@ export default class IconsPickerModal extends FuzzySuggestModal { return iconKeys; } - onChooseItem(item: Icon): void { - addToDOM(this.plugin, this.path, item.name); + onChooseItem(item: Icon | string): void { + if (typeof item === 'object') { + addToDOM(this.plugin, this.path, item.name); + } else { + addToDOM(this.plugin, this.path, item); + } this.plugin.addFolderIcon(this.path, item); } diff --git a/src/main.ts b/src/main.ts index d966ff50..63456f91 100644 --- a/src/main.ts +++ b/src/main.ts @@ -69,7 +69,7 @@ export default class IconFolderPlugin extends Plugin { const modal = new IconsPickerModal(this.app, this, file.path); modal.open(); // manipulate `onChooseItem` method to get custom functioanlity for inheriting icons - modal.onChooseItem = (icon: Icon) => { + modal.onChooseItem = (icon: Icon | string) => { this.saveInheritanceData(file.path, icon); addInheritanceForFolder(this, file.path); }; @@ -124,7 +124,7 @@ export default class IconFolderPlugin extends Plugin { }); } - private saveInheritanceData(folderPath: string, icon: Icon | null): void { + private saveInheritanceData(folderPath: string, icon: Icon | string | null): void { const currentValue = this.data[folderPath]; // if icon is null, it will remove the inheritance icon from the data if (icon === null && currentValue && typeof currentValue === 'object') { @@ -144,20 +144,20 @@ export default class IconFolderPlugin extends Plugin { if (typeof currentValue === 'string') { this.data[folderPath] = { iconName: currentValue as string, - inheritanceIcon: icon.name, + inheritanceIcon: typeof icon === 'object' ? icon.name : icon, }; } // check if it has already a inheritance icon else if (folderPath !== 'settings') { this.data[folderPath] = { ...(currentValue as FolderIconObject), - inheritanceIcon: icon.name, + inheritanceIcon: typeof icon === 'object' ? icon.name : icon, }; } } else { this.data[folderPath] = { iconName: null, - inheritanceIcon: icon.name, + inheritanceIcon: typeof icon === 'object' ? icon.name : icon, }; } } @@ -197,8 +197,8 @@ export default class IconFolderPlugin extends Plugin { this.saveIconFolderData(); } - addFolderIcon(path: string, icon: Icon): void { - this.data[path] = icon.name; + addFolderIcon(path: string, icon: Icon | string): void { + this.data[path] = typeof icon === 'object' ? icon.name : icon; this.saveIconFolderData(); } diff --git a/src/util.ts b/src/util.ts index afbc36c1..d8d02dab 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import twemoji from 'twemoji'; import * as remixicons from '../remixicons'; import * as faLine from '../fontawesome/index-line'; import * as faFill from '../fontawesome/index-fill'; @@ -71,11 +72,11 @@ export const getEnabledIcons = (plugin: IconFolderPlugin): string[] => { * * @public * @param {string} name - The icon name. - * @returns {string} The correct transformed svg. + * @returns {string | null} The transformed svg or null if it cannot find any iconpack. */ -export const getIcon = (name: string): string => { +export const getIcon = (name: string): string | null => { const prefix = name.substr(0, 2); - let iconSvg: string = ''; + let iconSvg: string = null; if (prefix === 'Fa') { if (name.toLowerCase().substr(name.length - 4) === 'line') { iconSvg = faLine[name.substr(2)]; @@ -86,8 +87,6 @@ export const getIcon = (name: string): string => { } } else if (prefix === 'Ri') { iconSvg = remixicons[name.substr(2)]; - } else { - iconSvg = remixicons[name.substr(2)]; } return iconSvg; @@ -101,21 +100,18 @@ export const getIcon = (name: string): string => { * * @public * @param {IconFolderPlugin} plugin - The main plugin. - * @param {string} iconSvg - The to be styled icon svg. + * @param {string} icon - The to be styled icon. * @param {HTMLElement} el - The element that will include the padding from the user settings. * @returns {string} The svg with the customized css settings. */ -export const customizeIconStyle = (plugin: IconFolderPlugin, iconSvg: string, el: HTMLElement): string => { +export const customizeIconStyle = (plugin: IconFolderPlugin, icon: string, el: HTMLElement): string => { // Allow custom font size const sizeRe = new RegExp(/width="\d+" height="\d+"/g); - iconSvg = iconSvg.replace( - sizeRe, - `width="${plugin.getSettings().fontSize}" height="${plugin.getSettings().fontSize}"`, - ); + icon = icon.replace(sizeRe, `width="${plugin.getSettings().fontSize}" height="${plugin.getSettings().fontSize}"`); // Allow custom icon color const colorRe = new RegExp(/fill="(\w|#)+"/g); - iconSvg = iconSvg.replace(colorRe, `fill="${plugin.getSettings().iconColor ?? 'currentColor'}"`); + icon = icon.replace(colorRe, `fill="${plugin.getSettings().iconColor ?? 'currentColor'}"`); // Change padding of icon if (plugin.getSettings().extraPadding) { @@ -124,7 +120,7 @@ export const customizeIconStyle = (plugin: IconFolderPlugin, iconSvg: string, el }px ${plugin.getSettings().extraPadding.bottom ?? 2}px ${plugin.getSettings().extraPadding.left ?? 2}px`; } - return iconSvg; + return icon; }; /** @@ -168,7 +164,8 @@ export const addIconsToDOM = ( if (iconName) { const iconNode = titleEl.createDiv(); iconNode.classList.add('obsidian-icon-folder-icon'); - iconNode.innerHTML = customizeIconStyle(plugin, getIcon(iconName), iconNode); + + insertIconToNode(plugin, iconName, iconNode); titleEl.insertBefore(iconNode, titleInnerEl); } @@ -181,7 +178,8 @@ export const addIconsToDOM = ( const inheritanceFileItem = fileExplorer.view.fileItems[f.path]; const iconNode = inheritanceFileItem.titleEl.createDiv(); iconNode.classList.add('obsidian-icon-folder-icon'); - iconNode.innerHTML = customizeIconStyle(plugin, getIcon(inheritanceIconName), iconNode); + + insertIconToNode(plugin, inheritanceIconName, iconNode); inheritanceFileItem.titleEl.insertBefore(iconNode, inheritanceFileItem.titleInnerEl); } @@ -210,7 +208,8 @@ export const addInheritanceIconToFile = ( if (fileItem) { const iconNode = fileItem.titleEl.createDiv(); iconNode.classList.add('obsidian-icon-folder-icon'); - iconNode.innerHTML = customizeIconStyle(plugin, getIcon(iconName), iconNode); + + insertIconToNode(plugin, iconName, iconNode); fileItem.titleEl.insertBefore(iconNode, fileItem.titleInnerEl); } @@ -268,9 +267,9 @@ export const removeFromDOM = (path: string): void => { * @public * @param {IconFolderPlugin} plugin - The main plugin. * @param {string} path - The path in the DOM where the icon will be added. - * @param {string} iconId - The icon id that will be added to the DOM. + * @param {string} icon - The icon that will be added to the DOM - can be an icon id or codepoint for twemoji. */ -export const addToDOM = (plugin: IconFolderPlugin, path: string, iconId: string): void => { +export const addToDOM = (plugin: IconFolderPlugin, path: string, icon: string): void => { if (plugin.getData()[path]) { removeFromDOM(path); } @@ -299,11 +298,29 @@ export const addToDOM = (plugin: IconFolderPlugin, path: string, iconId: string) const iconNode = document.createElement('div'); iconNode.classList.add('obsidian-icon-folder-icon'); - iconNode.innerHTML = customizeIconStyle(plugin, getIcon(iconId), iconNode); + + insertIconToNode(plugin, icon, iconNode); node.insertBefore(iconNode, titleNode); }; +const insertIconToNode = (plugin: IconFolderPlugin, icon: string, node: HTMLElement): void => { + const possibleIcon = getIcon(icon); + if (possibleIcon) { + node.innerHTML = customizeIconStyle(plugin, possibleIcon, node); + } else { + const emoji = twemoji.parse(twemoji.convert.fromCodePoint(icon), { + folder: 'svg', + ext: '.svg', + attributes: () => ({ + width: '16px', + height: '16px', + }), + }); + node.innerHTML = customizeIconStyle(plugin, emoji, node); + } +}; + /** * This function will add inheritance functionality to a specific folder. * It will add the inheritance icon to all child files. @@ -351,3 +368,15 @@ export const removeInheritanceForFolder = (plugin: IconFolderPlugin, folderPath: } }); }; + +export const isEmoji = (str: string): boolean => { + const ranges = [ + '(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])', // U+1F680 to U+1F6FF + ]; + + if (str.match(ranges.join('|'))) { + return true; + } else { + return false; + } +}; diff --git a/yarn.lock b/yarn.lock index 762551a2..623b22cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1094,6 +1094,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1421,6 +1430,22 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922" + integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w== + dependencies: + universalify "^0.1.2" + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -2268,6 +2293,21 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +twemoji-parser@13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4" + integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg== + +twemoji@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-13.1.0.tgz#65bb71e966dae56f0d42c30176f04cbdae109913" + integrity sha512-e3fZRl2S9UQQdBFLYXtTBT6o4vidJMnpWUAhJA+yLGR+kaUTZAt3PixC0cGvvxWSuq2MSz/o0rJraOXrWw/4Ew== + dependencies: + fs-extra "^8.0.1" + jsonfile "^5.0.0" + twemoji-parser "13.1.0" + universalify "^0.1.2" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -2310,6 +2350,11 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.3.tgz#c0f25dfea1e8e5323eccf59610be08b6043c15cf" integrity sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g== +universalify@^0.1.0, universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"