toolbar.js 8.5 KB

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