Source

frontend/components/emoji_button_trigger.js

  1. /* This file is part of Ezra Bible App.
  2. Copyright (C) 2019 - 2023 Ezra Bible App Development Team <contact@ezrabibleapp.net>
  3. Ezra Bible App is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. Ezra Bible App is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with Ezra Bible App. See the file LICENSE.
  13. If not, see <http://www.gnu.org/licenses/>. */
  14. const i18nController = require('../controllers/i18n_controller.js');
  15. const eventController = require('../controllers/event_controller.js');
  16. const { html, sleep, waitUntilIdle } = require('../helpers/ezra_helper.js');
  17. var emojiPicker; // to keep only one instance of the picker
  18. const template = html`
  19. <style>
  20. :host {
  21. position: absolute;
  22. right: 0;
  23. bottom: auto;
  24. width: 1em;
  25. margin-right: 1.8em;
  26. padding: 3px;
  27. fill: #5f5f5f;
  28. display: inline-block;
  29. cursor: pointer;
  30. z-index: 1000;
  31. }
  32. </style>
  33. <!-- using Font Awesome icon -->
  34. <svg viewBox="0 0 496 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm105.6-151.4c-25.9 8.3-64.4 13.1-105.6 13.1s-79.6-4.8-105.6-13.1c-9.9-3.1-19.4 5.4-17.7 15.3 7.9 47.1 71.3 80 123.3 80s115.3-32.9 123.3-80c1.6-9.8-7.7-18.4-17.7-15.3zM168 240c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32z"/></svg>
  35. `;
  36. /**
  37. * The emoji-picker component adds the possibility to insert emojis to the tag names and notes
  38. * on desktop platforms (electron app). It provides an emoji picker trigger button and inserts
  39. * emoji in the previous DOM text input or codeMirror editor.
  40. *
  41. * The Emoji Picker dialog is implemented via this external library:
  42. * https://github.com/zhuiks/emoji-button
  43. * This library is a customized version of the original:
  44. * https://github.com/joeattardi/emoji-button
  45. *
  46. * The library instance is kept inside the module variable picker.
  47. *
  48. * @category Component
  49. */
  50. class EmojiButtonTrigger extends HTMLElement {
  51. constructor() {
  52. super();
  53. if (hasNativeEmoji()) {
  54. this.style.display = 'none';
  55. return;
  56. }
  57. this.editor = null;
  58. const shadow = this.attachShadow({ mode: 'open' });
  59. shadow.appendChild(template.content.cloneNode(true));
  60. this.addEventListener('click', () => this._handleClick());
  61. }
  62. connectedCallback() {
  63. if (hasNativeEmoji()) {
  64. return;
  65. }
  66. this.parentNode.style.position = 'relative'; // emoji trigger position relative to the parent
  67. if (emojiPicker === undefined) {
  68. emojiPicker = initPicker(); // attach picker early, it would be a promise at first and it won't block the startup flow
  69. }
  70. }
  71. disconnectedCallback() {
  72. this.editor = null;
  73. if (emojiPicker && typeof emojiPicker.hidePicker === 'function') {
  74. emojiPicker.hidePicker();
  75. }
  76. }
  77. /**
  78. * Appends emoji picker button to codeMirror
  79. * @param codeMirror - active codeMirror editor instance
  80. */
  81. attachEditor(codeMirror) {
  82. if (hasNativeEmoji()) {
  83. return;
  84. }
  85. this.editor = codeMirror;
  86. this.style.bottom = '0.8em';
  87. }
  88. /**
  89. * Insert emoji into input or codeMirror depending on the context in the DOM
  90. * To be called only by the Picker library in the "emoji" event callback
  91. * @param {string} emoji - active codeMirror editor instance
  92. */
  93. insertEmoji(emoji) {
  94. const input = this.previousElementSibling;
  95. if (input && input.nodeName === 'INPUT') {
  96. input.value += emoji;
  97. } else if (this.editor && this.editor.getDoc()) {
  98. this.editor.getDoc().replaceSelection(emoji);
  99. } else {
  100. console.error('EmojiButtonTrigger: Input is not detected. Can\'t add emoji', emoji);
  101. }
  102. }
  103. restoreFocus() {
  104. const input = this.previousElementSibling;
  105. if (input && input.nodeName === 'INPUT') {
  106. input.focus();
  107. } else if (this.editor && this.editor.getDoc()) {
  108. this.editor.getInputField().focus();
  109. }
  110. }
  111. async _handleClick() {
  112. (await emojiPicker).togglePicker(this);
  113. }
  114. }
  115. customElements.define('emoji-button-trigger', EmojiButtonTrigger);
  116. module.exports.EmojiButtonTrigger = EmojiButtonTrigger;
  117. function hasNativeEmoji() {
  118. return platformHelper.isCordova();
  119. }
  120. async function initPicker(locale=i18nController.getLocale()) {
  121. await sleep(3000); // delay init as emoji picker is not a priority
  122. await waitUntilIdle();
  123. var emojiHelper = null;
  124. try {
  125. emojiHelper = require('../helpers/emoji_helper.js');
  126. } catch (e) {
  127. console.warn("Could not initialize emoji picker!");
  128. return;
  129. }
  130. const EmojiButton = emojiHelper.getEmojiButtonLib();
  131. // FIXME: get data from state instead of config option
  132. const nightModeOption = app_controller.optionsMenu._nightModeOption;
  133. const isNightMode = nightModeOption && nightModeOption.isChecked;
  134. const picker = new EmojiButton({ // https://emoji-button.js.org/docs/api
  135. emojiData: emojiHelper.getLocalizedData(locale),
  136. showPreview: false,
  137. showVariants: false,
  138. showAnimation: false,
  139. categories: ['smileys', 'people', 'animals', 'food', 'activities', 'travel', 'objects', 'symbols'],
  140. i18n: i18n.t('emoji', { returnObjects: true }),
  141. theme: isNightMode ? 'dark' : 'light',
  142. emojiSize: '1.3em',
  143. position: 'auto',
  144. zIndex: 10000,
  145. styleProperties: {
  146. '--background-color': '#f2f5f7',
  147. '--dark-background-color': '#1e1e1e',
  148. }
  149. });
  150. picker.on('emoji', ({ emoji, name, trigger }) => {
  151. if (!trigger || trigger.nodeName !== 'EMOJI-BUTTON-TRIGGER') {
  152. console.log('EmojiButtonTrigger: Something wrong. Trigger element is not detected :( But emoji was clicked:', emoji, name);
  153. return;
  154. }
  155. trigger.insertEmoji(emoji);
  156. });
  157. picker.on('hidden', ({ trigger }) => {
  158. if (trigger) {
  159. trigger.restoreFocus();
  160. }
  161. });
  162. subscribePicker();
  163. return picker;
  164. }
  165. var emojiPickerSubscribed = false; // subscribe to the state update only once
  166. function subscribePicker() {
  167. if (emojiPickerSubscribed || !emojiPicker) {
  168. return;
  169. }
  170. emojiPickerSubscribed = true;
  171. eventController.subscribe('on-locale-changed', async locale => {
  172. // FIXME: Handle properly
  173. try {
  174. (await emojiPicker).destroyPicker();
  175. } catch (e) {
  176. console.log('EmojiButtonTrigger: Got the following error when destroying external emojiPicker after locale change:', e);
  177. }
  178. emojiPicker = await initPicker(locale);
  179. });
  180. eventController.subscribe('on-theme-changed', async theme => {
  181. const picker = await emojiPicker;
  182. switch (theme) {
  183. case 'dark':
  184. picker.setTheme('dark');
  185. break;
  186. case 'regular':
  187. picker.setTheme('light');
  188. break;
  189. default:
  190. console.error('Unknown theme ' + theme);
  191. }
  192. });
  193. }