61f3b12a27ce3a6758e4b764a32a96acc260a01610cfe5a6925f49b7b1e6bd5ab39c5550c5cb13d800cf6176c077a3bd70b4fad8d64a224c0f7e54b2405db0 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { merge } from 'lodash-es';
  2. import Emitter from '../core/emitter.js';
  3. import Theme from '../core/theme.js';
  4. import ColorPicker from '../ui/color-picker.js';
  5. import IconPicker from '../ui/icon-picker.js';
  6. import Picker from '../ui/picker.js';
  7. import Tooltip from '../ui/tooltip.js';
  8. const ALIGNS = [false, 'center', 'right', 'justify'];
  9. const COLORS = ['#000000', '#e60000', '#ff9900', '#ffff00', '#008a00', '#0066cc', '#9933ff', '#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff', '#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b966', '#66a3e0', '#c285ff', '#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2', '#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466'];
  10. const FONTS = [false, 'serif', 'monospace'];
  11. const HEADERS = ['1', '2', '3', false];
  12. const SIZES = ['small', false, 'large', 'huge'];
  13. class BaseTheme extends Theme {
  14. constructor(quill, options) {
  15. super(quill, options);
  16. const listener = e => {
  17. if (!document.body.contains(quill.root)) {
  18. document.body.removeEventListener('click', listener);
  19. return;
  20. }
  21. if (this.tooltip != null &&
  22. // @ts-expect-error
  23. !this.tooltip.root.contains(e.target) &&
  24. // @ts-expect-error
  25. document.activeElement !== this.tooltip.textbox && !this.quill.hasFocus()) {
  26. this.tooltip.hide();
  27. }
  28. if (this.pickers != null) {
  29. this.pickers.forEach(picker => {
  30. // @ts-expect-error
  31. if (!picker.container.contains(e.target)) {
  32. picker.close();
  33. }
  34. });
  35. }
  36. };
  37. quill.emitter.listenDOM('click', document.body, listener);
  38. }
  39. addModule(name) {
  40. const module = super.addModule(name);
  41. if (name === 'toolbar') {
  42. // @ts-expect-error
  43. this.extendToolbar(module);
  44. }
  45. return module;
  46. }
  47. buildButtons(buttons, icons) {
  48. Array.from(buttons).forEach(button => {
  49. const className = button.getAttribute('class') || '';
  50. className.split(/\s+/).forEach(name => {
  51. if (!name.startsWith('ql-')) return;
  52. name = name.slice('ql-'.length);
  53. if (icons[name] == null) return;
  54. if (name === 'direction') {
  55. // @ts-expect-error
  56. button.innerHTML = icons[name][''] + icons[name].rtl;
  57. } else if (typeof icons[name] === 'string') {
  58. // @ts-expect-error
  59. button.innerHTML = icons[name];
  60. } else {
  61. // @ts-expect-error
  62. const value = button.value || '';
  63. // @ts-expect-error
  64. if (value != null && icons[name][value]) {
  65. // @ts-expect-error
  66. button.innerHTML = icons[name][value];
  67. }
  68. }
  69. });
  70. });
  71. }
  72. buildPickers(selects, icons) {
  73. this.pickers = Array.from(selects).map(select => {
  74. if (select.classList.contains('ql-align')) {
  75. if (select.querySelector('option') == null) {
  76. fillSelect(select, ALIGNS);
  77. }
  78. if (typeof icons.align === 'object') {
  79. return new IconPicker(select, icons.align);
  80. }
  81. }
  82. if (select.classList.contains('ql-background') || select.classList.contains('ql-color')) {
  83. const format = select.classList.contains('ql-background') ? 'background' : 'color';
  84. if (select.querySelector('option') == null) {
  85. fillSelect(select, COLORS, format === 'background' ? '#ffffff' : '#000000');
  86. }
  87. return new ColorPicker(select, icons[format]);
  88. }
  89. if (select.querySelector('option') == null) {
  90. if (select.classList.contains('ql-font')) {
  91. fillSelect(select, FONTS);
  92. } else if (select.classList.contains('ql-header')) {
  93. fillSelect(select, HEADERS);
  94. } else if (select.classList.contains('ql-size')) {
  95. fillSelect(select, SIZES);
  96. }
  97. }
  98. return new Picker(select);
  99. });
  100. const update = () => {
  101. this.pickers.forEach(picker => {
  102. picker.update();
  103. });
  104. };
  105. this.quill.on(Emitter.events.EDITOR_CHANGE, update);
  106. }
  107. }
  108. BaseTheme.DEFAULTS = merge({}, Theme.DEFAULTS, {
  109. modules: {
  110. toolbar: {
  111. handlers: {
  112. formula() {
  113. this.quill.theme.tooltip.edit('formula');
  114. },
  115. image() {
  116. let fileInput = this.container.querySelector('input.ql-image[type=file]');
  117. if (fileInput == null) {
  118. fileInput = document.createElement('input');
  119. fileInput.setAttribute('type', 'file');
  120. fileInput.setAttribute('accept', this.quill.uploader.options.mimetypes.join(', '));
  121. fileInput.classList.add('ql-image');
  122. fileInput.addEventListener('change', () => {
  123. const range = this.quill.getSelection(true);
  124. this.quill.uploader.upload(range, fileInput.files);
  125. fileInput.value = '';
  126. });
  127. this.container.appendChild(fileInput);
  128. }
  129. fileInput.click();
  130. },
  131. video() {
  132. this.quill.theme.tooltip.edit('video');
  133. }
  134. }
  135. }
  136. }
  137. });
  138. class BaseTooltip extends Tooltip {
  139. constructor(quill, boundsContainer) {
  140. super(quill, boundsContainer);
  141. this.textbox = this.root.querySelector('input[type="text"]');
  142. this.listen();
  143. }
  144. listen() {
  145. // @ts-expect-error Fix me later
  146. this.textbox.addEventListener('keydown', event => {
  147. if (event.key === 'Enter') {
  148. this.save();
  149. event.preventDefault();
  150. } else if (event.key === 'Escape') {
  151. this.cancel();
  152. event.preventDefault();
  153. }
  154. });
  155. }
  156. cancel() {
  157. this.hide();
  158. this.restoreFocus();
  159. }
  160. edit() {
  161. let mode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'link';
  162. let preview = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  163. this.root.classList.remove('ql-hidden');
  164. this.root.classList.add('ql-editing');
  165. if (this.textbox == null) return;
  166. if (preview != null) {
  167. this.textbox.value = preview;
  168. } else if (mode !== this.root.getAttribute('data-mode')) {
  169. this.textbox.value = '';
  170. }
  171. const bounds = this.quill.getBounds(this.quill.selection.savedRange);
  172. if (bounds != null) {
  173. this.position(bounds);
  174. }
  175. this.textbox.select();
  176. this.textbox.setAttribute('placeholder', this.textbox.getAttribute(`data-${mode}`) || '');
  177. this.root.setAttribute('data-mode', mode);
  178. }
  179. restoreFocus() {
  180. this.quill.focus({
  181. preventScroll: true
  182. });
  183. }
  184. save() {
  185. // @ts-expect-error Fix me later
  186. let {
  187. value
  188. } = this.textbox;
  189. switch (this.root.getAttribute('data-mode')) {
  190. case 'link':
  191. {
  192. const {
  193. scrollTop
  194. } = this.quill.root;
  195. if (this.linkRange) {
  196. this.quill.formatText(this.linkRange, 'link', value, Emitter.sources.USER);
  197. delete this.linkRange;
  198. } else {
  199. this.restoreFocus();
  200. this.quill.format('link', value, Emitter.sources.USER);
  201. }
  202. this.quill.root.scrollTop = scrollTop;
  203. break;
  204. }
  205. case 'video':
  206. {
  207. value = extractVideoUrl(value);
  208. }
  209. // eslint-disable-next-line no-fallthrough
  210. case 'formula':
  211. {
  212. if (!value) break;
  213. const range = this.quill.getSelection(true);
  214. if (range != null) {
  215. const index = range.index + range.length;
  216. this.quill.insertEmbed(index,
  217. // @ts-expect-error Fix me later
  218. this.root.getAttribute('data-mode'), value, Emitter.sources.USER);
  219. if (this.root.getAttribute('data-mode') === 'formula') {
  220. this.quill.insertText(index + 1, ' ', Emitter.sources.USER);
  221. }
  222. this.quill.setSelection(index + 2, Emitter.sources.USER);
  223. }
  224. break;
  225. }
  226. default:
  227. }
  228. // @ts-expect-error Fix me later
  229. this.textbox.value = '';
  230. this.hide();
  231. }
  232. }
  233. function extractVideoUrl(url) {
  234. let match = url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) || url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/);
  235. if (match) {
  236. return `${match[1] || 'https'}://www.youtube.com/embed/${match[2]}?showinfo=0`;
  237. }
  238. // eslint-disable-next-line no-cond-assign
  239. if (match = url.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/)) {
  240. return `${match[1] || 'https'}://player.vimeo.com/video/${match[2]}/`;
  241. }
  242. return url;
  243. }
  244. function fillSelect(select, values) {
  245. let defaultValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
  246. values.forEach(value => {
  247. const option = document.createElement('option');
  248. if (value === defaultValue) {
  249. option.setAttribute('selected', 'selected');
  250. } else {
  251. option.setAttribute('value', String(value));
  252. }
  253. select.appendChild(option);
  254. });
  255. }
  256. export { BaseTooltip, BaseTheme as default };
  257. //# sourceMappingURL=base.js.map