base.js 8.6 KB

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