toolbar.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import Delta from 'quill-delta';
  2. import { EmbedBlot, Scope } from 'parchment';
  3. import Quill from '../core/quill';
  4. import logger from '../core/logger';
  5. import Module from '../core/module';
  6. const debug = logger('quill:toolbar');
  7. class Toolbar extends Module {
  8. constructor(quill, options) {
  9. super(quill, options);
  10. if (Array.isArray(this.options.container)) {
  11. const container = document.createElement('div');
  12. addControls(container, this.options.container);
  13. quill.container.parentNode.insertBefore(container, quill.container);
  14. this.container = container;
  15. } else if (typeof this.options.container === 'string') {
  16. this.container = document.querySelector(this.options.container);
  17. } else {
  18. this.container = this.options.container;
  19. }
  20. if (!(this.container instanceof HTMLElement)) {
  21. return debug.error('Container required for toolbar', this.options);
  22. }
  23. this.container.classList.add('ql-toolbar');
  24. this.controls = [];
  25. this.handlers = {};
  26. Object.keys(this.options.handlers).forEach(format => {
  27. this.addHandler(format, this.options.handlers[format]);
  28. });
  29. Array.from(this.container.querySelectorAll('button, select')).forEach(
  30. input => {
  31. this.attach(input);
  32. },
  33. );
  34. this.quill.on(Quill.events.EDITOR_CHANGE, (type, range) => {
  35. if (type === Quill.events.SELECTION_CHANGE) {
  36. this.update(range);
  37. }
  38. });
  39. this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
  40. const [range] = this.quill.selection.getRange(); // quill.getSelection triggers update
  41. this.update(range);
  42. });
  43. }
  44. addHandler(format, handler) {
  45. this.handlers[format] = handler;
  46. }
  47. attach(input) {
  48. let format = Array.from(input.classList).find(className => {
  49. return className.indexOf('ql-') === 0;
  50. });
  51. if (!format) return;
  52. format = format.slice('ql-'.length);
  53. if (input.tagName === 'BUTTON') {
  54. input.setAttribute('type', 'button');
  55. }
  56. if (
  57. this.handlers[format] == null &&
  58. this.quill.scroll.query(format) == null
  59. ) {
  60. debug.warn('ignoring attaching to nonexistent format', format, input);
  61. return;
  62. }
  63. const eventName = input.tagName === 'SELECT' ? 'change' : 'click';
  64. input.addEventListener(eventName, e => {
  65. let value;
  66. if (input.tagName === 'SELECT') {
  67. if (input.selectedIndex < 0) return;
  68. const selected = input.options[input.selectedIndex];
  69. if (selected.hasAttribute('selected')) {
  70. value = false;
  71. } else {
  72. value = selected.value || false;
  73. }
  74. } else {
  75. if (input.classList.contains('ql-active')) {
  76. value = false;
  77. } else {
  78. value = input.value || !input.hasAttribute('value');
  79. }
  80. e.preventDefault();
  81. }
  82. this.quill.focus();
  83. const [range] = this.quill.selection.getRange();
  84. if (this.handlers[format] != null) {
  85. this.handlers[format].call(this, value);
  86. } else if (
  87. this.quill.scroll.query(format).prototype instanceof EmbedBlot
  88. ) {
  89. value = prompt(`Enter ${format}`); // eslint-disable-line no-alert
  90. if (!value) return;
  91. this.quill.updateContents(
  92. new Delta()
  93. .retain(range.index)
  94. .delete(range.length)
  95. .insert({ [format]: value }),
  96. Quill.sources.USER,
  97. );
  98. } else {
  99. this.quill.format(format, value, Quill.sources.USER);
  100. }
  101. this.update(range);
  102. });
  103. this.controls.push([format, input]);
  104. }
  105. update(range) {
  106. const formats = range == null ? {} : this.quill.getFormat(range);
  107. this.controls.forEach(pair => {
  108. const [format, input] = pair;
  109. if (input.tagName === 'SELECT') {
  110. let option;
  111. if (range == null) {
  112. option = null;
  113. } else if (formats[format] == null) {
  114. option = input.querySelector('option[selected]');
  115. } else if (!Array.isArray(formats[format])) {
  116. let value = formats[format];
  117. if (typeof value === 'string') {
  118. value = value.replace(/"/g, '\\"');
  119. }
  120. option = input.querySelector(`option[value="${value}"]`);
  121. }
  122. if (option == null) {
  123. input.value = ''; // TODO make configurable?
  124. input.selectedIndex = -1;
  125. } else {
  126. option.selected = true;
  127. }
  128. } else if (range == null) {
  129. input.classList.remove('ql-active');
  130. } else if (input.hasAttribute('value')) {
  131. // both being null should match (default values)
  132. // '1' should match with 1 (headers)
  133. const isActive =
  134. formats[format] === input.getAttribute('value') ||
  135. (formats[format] != null &&
  136. formats[format].toString() === input.getAttribute('value')) ||
  137. (formats[format] == null && !input.getAttribute('value'));
  138. input.classList.toggle('ql-active', isActive);
  139. } else {
  140. input.classList.toggle('ql-active', formats[format] != null);
  141. }
  142. });
  143. }
  144. }
  145. Toolbar.DEFAULTS = {};
  146. function addButton(container, format, value) {
  147. const input = document.createElement('button');
  148. input.setAttribute('type', 'button');
  149. input.classList.add(`ql-${format}`);
  150. if (value != null) {
  151. input.value = value;
  152. }
  153. container.appendChild(input);
  154. }
  155. function addControls(container, groups) {
  156. if (!Array.isArray(groups[0])) {
  157. groups = [groups];
  158. }
  159. groups.forEach(controls => {
  160. const group = document.createElement('span');
  161. group.classList.add('ql-formats');
  162. controls.forEach(control => {
  163. if (typeof control === 'string') {
  164. addButton(group, control);
  165. } else {
  166. const format = Object.keys(control)[0];
  167. const value = control[format];
  168. if (Array.isArray(value)) {
  169. addSelect(group, format, value);
  170. } else {
  171. addButton(group, format, value);
  172. }
  173. }
  174. });
  175. container.appendChild(group);
  176. });
  177. }
  178. function addSelect(container, format, values) {
  179. const input = document.createElement('select');
  180. input.classList.add(`ql-${format}`);
  181. values.forEach(value => {
  182. const option = document.createElement('option');
  183. if (value !== false) {
  184. option.setAttribute('value', value);
  185. } else {
  186. option.setAttribute('selected', 'selected');
  187. }
  188. input.appendChild(option);
  189. });
  190. container.appendChild(input);
  191. }
  192. Toolbar.DEFAULTS = {
  193. container: null,
  194. handlers: {
  195. clean() {
  196. const range = this.quill.getSelection();
  197. if (range == null) return;
  198. if (range.length === 0) {
  199. const formats = this.quill.getFormat();
  200. Object.keys(formats).forEach(name => {
  201. // Clean functionality in existing apps only clean inline formats
  202. if (this.quill.scroll.query(name, Scope.INLINE) != null) {
  203. this.quill.format(name, false, Quill.sources.USER);
  204. }
  205. });
  206. } else {
  207. this.quill.removeFormat(range, Quill.sources.USER);
  208. }
  209. },
  210. direction(value) {
  211. const { align } = this.quill.getFormat();
  212. if (value === 'rtl' && align == null) {
  213. this.quill.format('align', 'right', Quill.sources.USER);
  214. } else if (!value && align === 'right') {
  215. this.quill.format('align', false, Quill.sources.USER);
  216. }
  217. this.quill.format('direction', value, Quill.sources.USER);
  218. },
  219. indent(value) {
  220. const range = this.quill.getSelection();
  221. const formats = this.quill.getFormat(range);
  222. const indent = parseInt(formats.indent || 0, 10);
  223. if (value === '+1' || value === '-1') {
  224. let modifier = value === '+1' ? 1 : -1;
  225. if (formats.direction === 'rtl') modifier *= -1;
  226. this.quill.format('indent', indent + modifier, Quill.sources.USER);
  227. }
  228. },
  229. link(value) {
  230. if (value === true) {
  231. value = prompt('Enter link URL:'); // eslint-disable-line no-alert
  232. }
  233. this.quill.format('link', value, Quill.sources.USER);
  234. },
  235. list(value) {
  236. const range = this.quill.getSelection();
  237. const formats = this.quill.getFormat(range);
  238. if (value === 'check') {
  239. if (formats.list === 'checked' || formats.list === 'unchecked') {
  240. this.quill.format('list', false, Quill.sources.USER);
  241. } else {
  242. this.quill.format('list', 'unchecked', Quill.sources.USER);
  243. }
  244. } else {
  245. this.quill.format('list', value, Quill.sources.USER);
  246. }
  247. },
  248. },
  249. };
  250. export { Toolbar as default, addControls };