a3f31c913b2af85e36989a18ab9317866a0337a357f310bcdf00f616807f5c1ffa0e7b695ca56bf1193902cfac387126bbff7ff9422ae8f405775ea0ca02a5 9.1 KB

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