lib/editor/tiny/plugins/link/amd/src/link.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle 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 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Link helper for Tiny Link plugin.
  17. *
  18. * @module tiny_link/link
  19. * @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import Templates from 'core/templates';
  23. import Pending from 'core/pending';
  24. import Selectors from 'tiny_link/selectors';
  25. /**
  26. * Handle insertion of a new link, or update of an existing one.
  27. *
  28. * @param {Element} currentForm
  29. * @param {TinyMCE} editor
  30. */
  31. export const setLink = (currentForm, editor) => {
  32. const input = currentForm.querySelector(Selectors.elements.urlEntry);
  33. let value = input.value;
  34. if (value !== '') {
  35. const pendingPromise = new Pending('tiny_link/setLink');
  36. // We add a prefix if it is not already prefixed.
  37. value = value.trim();
  38. const expr = new RegExp(/^[a-zA-Z]*\.*\/|^#|^[a-zA-Z]*:/);
  39. if (!expr.test(value)) {
  40. value = 'http://' + value;
  41. }
  42. // Add the link.
  43. setLinkOnSelection(currentForm, editor, value).then(pendingPromise.resolve);
  44. }
  45. };
  46. /**
  47. * Handle unlink of a link
  48. *
  49. * @param {TinyMCE} editor
  50. */
  51. export const unSetLink = (editor) => {
  52. if (editor.hasPlugin('rtc', true)) {
  53. editor.execCommand('unlink');
  54. } else {
  55. const dom = editor.dom;
  56. const selection = editor.selection;
  57. const bookmark = selection.getBookmark();
  58. const rng = selection.getRng().cloneRange();
  59. const startAnchorElm = dom.getParent(rng.startContainer, 'a[href]', editor.getBody());
  60. const endAnchorElm = dom.getParent(rng.endContainer, 'a[href]', editor.getBody());
  61. if (startAnchorElm) {
  62. rng.setStartBefore(startAnchorElm);
  63. }
  64. if (endAnchorElm) {
  65. rng.setEndAfter(endAnchorElm);
  66. }
  67. selection.setRng(rng);
  68. editor.execCommand('unlink');
  69. selection.moveToBookmark(bookmark);
  70. }
  71. };
  72. /**
  73. * Final step setting the anchor on the selection.
  74. *
  75. * @param {Element} currentForm
  76. * @param {TinyMCE} editor
  77. * @param {String} url URL the link will point to.
  78. */
  79. const setLinkOnSelection = async(currentForm, editor, url) => {
  80. const urlText = currentForm.querySelector(Selectors.elements.urlText);
  81. const target = currentForm.querySelector(Selectors.elements.openInNewWindow);
  82. let textToDisplay = urlText.value.replace(/(<([^>]+)>)/gi, "").trim();
  83. if (textToDisplay === '') {
  84. textToDisplay = url;
  85. }
  86. const context = {
  87. url: url,
  88. newwindow: target.checked,
  89. };
  90. if (urlText.getAttribute('data-link-on-element')) {
  91. context.title = textToDisplay;
  92. context.name = editor.selection.getNode().outerHTML;
  93. } else {
  94. context.name = textToDisplay;
  95. }
  96. const {html} = await Templates.renderForPromise('tiny_link/embed_link', context);
  97. const currentLink = getSelectedLink(editor);
  98. if (currentLink) {
  99. currentLink.outerHTML = html;
  100. } else {
  101. editor.insertContent(html);
  102. }
  103. };
  104. /**
  105. * Get current link data.
  106. *
  107. * @param {TinyMCE} editor
  108. * @returns {{}}
  109. */
  110. export const getCurrentLinkData = (editor) => {
  111. let properties = {};
  112. const link = getSelectedLink(editor);
  113. if (link) {
  114. const url = link.getAttribute('href');
  115. const target = link.getAttribute('target');
  116. const textToDisplay = link.innerText;
  117. const title = link.getAttribute('title');
  118. if (url !== '') {
  119. properties.url = url;
  120. }
  121. if (target === '_blank') {
  122. properties.newwindow = true;
  123. }
  124. if (title && title !== '') {
  125. properties.urltext = title.trim();
  126. } else if (textToDisplay !== '') {
  127. properties.urltext = textToDisplay.trim();
  128. }
  129. } else {
  130. // Check if the user is selecting some text before clicking on the Link button.
  131. const selectedNode = editor.selection.getNode();
  132. if (selectedNode) {
  133. const textToDisplay = getTextSelection(editor);
  134. if (textToDisplay !== '') {
  135. properties.urltext = textToDisplay.trim();
  136. properties.hasTextToDisplay = true;
  137. properties.hasPlainTextSelected = true;
  138. } else {
  139. if (selectedNode.getAttribute('data-mce-selected')) {
  140. properties.setLinkOnElement = true;
  141. }
  142. }
  143. }
  144. }
  145. return properties;
  146. };
  147. /**
  148. * Get selected link.
  149. *
  150. * @param {TinyMCE} editor
  151. * @returns {Element}
  152. */
  153. const getSelectedLink = (editor) => {
  154. return getAnchorElement(editor);
  155. };
  156. /**
  157. * Get anchor element.
  158. *
  159. * @param {TinyMCE} editor
  160. * @param {Element} selectedElm
  161. * @returns {Element}
  162. */
  163. const getAnchorElement = (editor, selectedElm) => {
  164. selectedElm = selectedElm || editor.selection.getNode();
  165. return editor.dom.getParent(selectedElm, 'a[href]');
  166. };
  167. /**
  168. * Get only the selected text.
  169. * In some cases, window.getSelection() is not run as expected. We should only get the text value
  170. * For ex: <img src="" alt="XYZ">Some text here
  171. * window.getSelection() will return XYZSome text here
  172. *
  173. * @param {TinyMCE} editor
  174. * @return {string} Selected text
  175. */
  176. const getTextSelection = (editor) => {
  177. let selText = '';
  178. const sel = editor.selection.getSel();
  179. const rangeCount = sel.rangeCount;
  180. if (rangeCount) {
  181. let rangeTexts = [];
  182. for (let i = 0; i < rangeCount; ++i) {
  183. rangeTexts.push('' + sel.getRangeAt(i));
  184. }
  185. selText = rangeTexts.join('');
  186. }
  187. return selText;
  188. };
  189. /**
  190. * Check the current selected element is an anchor or not.
  191. *
  192. * @param {TinyMCE} editor
  193. * @param {Element} selectedElm
  194. * @returns {boolean}
  195. */
  196. const isInAnchor = (editor, selectedElm) => getAnchorElement(editor, selectedElm) !== null;
  197. /**
  198. * Change state of button.
  199. *
  200. * @param {TinyMCE} editor
  201. * @param {function()} toggler
  202. * @returns {function()}
  203. */
  204. const toggleState = (editor, toggler) => {
  205. editor.on('NodeChange', toggler);
  206. return () => editor.off('NodeChange', toggler);
  207. };
  208. /**
  209. * Change the active state of button.
  210. *
  211. * @param {TinyMCE} editor
  212. * @returns {function(*): function(): *}
  213. */
  214. export const toggleActiveState = (editor) => (api) => {
  215. const updateState = () => api.setActive(!editor.mode.isReadOnly() && isInAnchor(editor, editor.selection.getNode()));
  216. updateState();
  217. return toggleState(editor, updateState);
  218. };