quill.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import './polyfill';
  2. import Delta from 'quill-delta';
  3. import Editor from './editor';
  4. import Emitter from './emitter';
  5. import Module from './module';
  6. import Parchment from 'parchment';
  7. import Selection, { Range } from './selection';
  8. import extend from 'extend';
  9. import logger from './logger';
  10. import Theme from './theme';
  11. let debug = logger('quill');
  12. class Quill {
  13. static debug(limit) {
  14. if (limit === true) {
  15. limit = 'log';
  16. }
  17. logger.level(limit);
  18. }
  19. static find(node) {
  20. return node.__quill || Parchment.find(node);
  21. }
  22. static import(name) {
  23. if (this.imports[name] == null) {
  24. debug.error(`Cannot import ${name}. Are you sure it was registered?`);
  25. }
  26. return this.imports[name];
  27. }
  28. static register(path, target, overwrite = false) {
  29. if (typeof path !== 'string') {
  30. let name = path.attrName || path.blotName;
  31. if (typeof name === 'string') {
  32. // register(Blot | Attributor, overwrite)
  33. this.register('formats/' + name, path, target);
  34. } else {
  35. Object.keys(path).forEach((key) => {
  36. this.register(key, path[key], target);
  37. });
  38. }
  39. } else {
  40. if (this.imports[path] != null && !overwrite) {
  41. debug.warn(`Overwriting ${path} with`, target);
  42. }
  43. this.imports[path] = target;
  44. if ((path.startsWith('blots/') || path.startsWith('formats/')) &&
  45. target.blotName !== 'abstract') {
  46. Parchment.register(target);
  47. } else if (path.startsWith('modules') && typeof target.register === 'function') {
  48. target.register();
  49. }
  50. }
  51. }
  52. constructor(container, options = {}) {
  53. this.options = expandConfig(container, options);
  54. this.container = this.options.container;
  55. if (this.container == null) {
  56. return debug.error('Invalid Quill container', container);
  57. }
  58. if (this.options.debug) {
  59. Quill.debug(this.options.debug);
  60. }
  61. let html = this.container.innerHTML.trim();
  62. this.container.classList.add('ql-container');
  63. this.container.innerHTML = '';
  64. this.container.__quill = this;
  65. this.root = this.addContainer('ql-editor');
  66. this.root.classList.add('ql-blank');
  67. this.root.setAttribute('data-gramm', false);
  68. this.scrollingContainer = this.options.scrollingContainer || this.root;
  69. this.emitter = new Emitter();
  70. this.scroll = Parchment.create(this.root, {
  71. emitter: this.emitter,
  72. whitelist: this.options.formats
  73. });
  74. this.editor = new Editor(this.scroll);
  75. this.selection = new Selection(this.scroll, this.emitter);
  76. this.theme = new this.options.theme(this, this.options);
  77. this.keyboard = this.theme.addModule('keyboard');
  78. this.clipboard = this.theme.addModule('clipboard');
  79. this.history = this.theme.addModule('history');
  80. this.theme.init();
  81. this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {
  82. if (type === Emitter.events.TEXT_CHANGE) {
  83. this.root.classList.toggle('ql-blank', this.editor.isBlank());
  84. }
  85. });
  86. this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
  87. let range = this.selection.lastRange;
  88. let index = range && range.length === 0 ? range.index : undefined;
  89. modify.call(this, () => {
  90. return this.editor.update(null, mutations, index);
  91. }, source);
  92. });
  93. let contents = this.clipboard.convert(`<div class='ql-editor' style="white-space: normal;">${html}<p><br></p></div>`);
  94. this.setContents(contents);
  95. this.history.clear();
  96. if (this.options.placeholder) {
  97. this.root.setAttribute('data-placeholder', this.options.placeholder);
  98. }
  99. if (this.options.readOnly) {
  100. this.disable();
  101. }
  102. }
  103. addContainer(container, refNode = null) {
  104. if (typeof container === 'string') {
  105. let className = container;
  106. container = document.createElement('div');
  107. container.classList.add(className);
  108. }
  109. this.container.insertBefore(container, refNode);
  110. return container;
  111. }
  112. blur() {
  113. this.selection.setRange(null);
  114. }
  115. deleteText(index, length, source) {
  116. [index, length, , source] = overload(index, length, source);
  117. return modify.call(this, () => {
  118. return this.editor.deleteText(index, length);
  119. }, source, index, -1*length);
  120. }
  121. disable() {
  122. this.enable(false);
  123. }
  124. enable(enabled = true) {
  125. this.scroll.enable(enabled);
  126. this.container.classList.toggle('ql-disabled', !enabled);
  127. }
  128. focus() {
  129. let scrollTop = this.scrollingContainer.scrollTop;
  130. this.selection.focus();
  131. this.scrollingContainer.scrollTop = scrollTop;
  132. this.scrollIntoView();
  133. }
  134. format(name, value, source = Emitter.sources.API) {
  135. return modify.call(this, () => {
  136. let range = this.getSelection(true);
  137. let change = new Delta();
  138. if (range == null) {
  139. return change;
  140. } else if (Parchment.query(name, Parchment.Scope.BLOCK)) {
  141. change = this.editor.formatLine(range.index, range.length, { [name]: value });
  142. } else if (range.length === 0) {
  143. this.selection.format(name, value);
  144. return change;
  145. } else {
  146. change = this.editor.formatText(range.index, range.length, { [name]: value });
  147. }
  148. this.setSelection(range, Emitter.sources.SILENT);
  149. return change;
  150. }, source);
  151. }
  152. formatLine(index, length, name, value, source) {
  153. let formats;
  154. [index, length, formats, source] = overload(index, length, name, value, source);
  155. return modify.call(this, () => {
  156. return this.editor.formatLine(index, length, formats);
  157. }, source, index, 0);
  158. }
  159. formatText(index, length, name, value, source) {
  160. let formats;
  161. [index, length, formats, source] = overload(index, length, name, value, source);
  162. return modify.call(this, () => {
  163. return this.editor.formatText(index, length, formats);
  164. }, source, index, 0);
  165. }
  166. getBounds(index, length = 0) {
  167. let bounds;
  168. if (typeof index === 'number') {
  169. bounds = this.selection.getBounds(index, length);
  170. } else {
  171. bounds = this.selection.getBounds(index.index, index.length);
  172. }
  173. let containerBounds = this.container.getBoundingClientRect();
  174. return {
  175. bottom: bounds.bottom - containerBounds.top,
  176. height: bounds.height,
  177. left: bounds.left - containerBounds.left,
  178. right: bounds.right - containerBounds.left,
  179. top: bounds.top - containerBounds.top,
  180. width: bounds.width
  181. };
  182. }
  183. getContents(index = 0, length = this.getLength() - index) {
  184. [index, length] = overload(index, length);
  185. return this.editor.getContents(index, length);
  186. }
  187. getFormat(index = this.getSelection(true), length = 0) {
  188. if (typeof index === 'number') {
  189. return this.editor.getFormat(index, length);
  190. } else {
  191. return this.editor.getFormat(index.index, index.length);
  192. }
  193. }
  194. getIndex(blot) {
  195. return blot.offset(this.scroll);
  196. }
  197. getLength() {
  198. return this.scroll.length();
  199. }
  200. getLeaf(index) {
  201. return this.scroll.leaf(index);
  202. }
  203. getLine(index) {
  204. return this.scroll.line(index);
  205. }
  206. getLines(index = 0, length = Number.MAX_VALUE) {
  207. if (typeof index !== 'number') {
  208. return this.scroll.lines(index.index, index.length);
  209. } else {
  210. return this.scroll.lines(index, length);
  211. }
  212. }
  213. getModule(name) {
  214. return this.theme.modules[name];
  215. }
  216. getSelection(focus = false) {
  217. if (focus) this.focus();
  218. this.update(); // Make sure we access getRange with editor in consistent state
  219. return this.selection.getRange()[0];
  220. }
  221. getText(index = 0, length = this.getLength() - index) {
  222. [index, length] = overload(index, length);
  223. return this.editor.getText(index, length);
  224. }
  225. hasFocus() {
  226. return this.selection.hasFocus();
  227. }
  228. insertEmbed(index, embed, value, source = Quill.sources.API) {
  229. return modify.call(this, () => {
  230. return this.editor.insertEmbed(index, embed, value);
  231. }, source, index);
  232. }
  233. insertText(index, text, name, value, source) {
  234. let formats;
  235. [index, , formats, source] = overload(index, 0, name, value, source);
  236. return modify.call(this, () => {
  237. return this.editor.insertText(index, text, formats);
  238. }, source, index, text.length);
  239. }
  240. isEnabled() {
  241. return !this.container.classList.contains('ql-disabled');
  242. }
  243. off() {
  244. return this.emitter.off.apply(this.emitter, arguments);
  245. }
  246. on() {
  247. return this.emitter.on.apply(this.emitter, arguments);
  248. }
  249. once() {
  250. return this.emitter.once.apply(this.emitter, arguments);
  251. }
  252. pasteHTML(index, html, source) {
  253. this.clipboard.dangerouslyPasteHTML(index, html, source);
  254. }
  255. removeFormat(index, length, source) {
  256. [index, length, , source] = overload(index, length, source);
  257. return modify.call(this, () => {
  258. return this.editor.removeFormat(index, length);
  259. }, source, index);
  260. }
  261. scrollIntoView() {
  262. this.selection.scrollIntoView(this.scrollingContainer);
  263. }
  264. setContents(delta, source = Emitter.sources.API) {
  265. return modify.call(this, () => {
  266. delta = new Delta(delta);
  267. let length = this.getLength();
  268. let deleted = this.editor.deleteText(0, length);
  269. let applied = this.editor.applyDelta(delta);
  270. let lastOp = applied.ops[applied.ops.length - 1];
  271. if (lastOp != null && typeof(lastOp.insert) === 'string' && lastOp.insert[lastOp.insert.length-1] === '\n') {
  272. this.editor.deleteText(this.getLength() - 1, 1);
  273. applied.delete(1);
  274. }
  275. let ret = deleted.compose(applied);
  276. return ret;
  277. }, source);
  278. }
  279. setSelection(index, length, source) {
  280. if (index == null) {
  281. this.selection.setRange(null, length || Quill.sources.API);
  282. } else {
  283. [index, length, , source] = overload(index, length, source);
  284. this.selection.setRange(new Range(index, length), source);
  285. if (source !== Emitter.sources.SILENT) {
  286. this.selection.scrollIntoView(this.scrollingContainer);
  287. }
  288. }
  289. }
  290. setText(text, source = Emitter.sources.API) {
  291. let delta = new Delta().insert(text);
  292. return this.setContents(delta, source);
  293. }
  294. update(source = Emitter.sources.USER) {
  295. let change = this.scroll.update(source); // Will update selection before selection.update() does if text changes
  296. this.selection.update(source);
  297. return change;
  298. }
  299. updateContents(delta, source = Emitter.sources.API) {
  300. return modify.call(this, () => {
  301. delta = new Delta(delta);
  302. return this.editor.applyDelta(delta, source);
  303. }, source, true);
  304. }
  305. }
  306. Quill.DEFAULTS = {
  307. bounds: null,
  308. formats: null,
  309. modules: {},
  310. placeholder: '',
  311. readOnly: false,
  312. scrollingContainer: null,
  313. strict: true,
  314. theme: 'default'
  315. };
  316. Quill.events = Emitter.events;
  317. Quill.sources = Emitter.sources;
  318. // eslint-disable-next-line no-undef
  319. Quill.version = typeof(QUILL_VERSION) === 'undefined' ? 'dev' : QUILL_VERSION;
  320. Quill.imports = {
  321. 'delta' : Delta,
  322. 'parchment' : Parchment,
  323. 'core/module' : Module,
  324. 'core/theme' : Theme
  325. };
  326. function expandConfig(container, userConfig) {
  327. userConfig = extend(true, {
  328. container: container,
  329. modules: {
  330. clipboard: true,
  331. keyboard: true,
  332. history: true
  333. }
  334. }, userConfig);
  335. if (!userConfig.theme || userConfig.theme === Quill.DEFAULTS.theme) {
  336. userConfig.theme = Theme;
  337. } else {
  338. userConfig.theme = Quill.import(`themes/${userConfig.theme}`);
  339. if (userConfig.theme == null) {
  340. throw new Error(`Invalid theme ${userConfig.theme}. Did you register it?`);
  341. }
  342. }
  343. let themeConfig = extend(true, {}, userConfig.theme.DEFAULTS);
  344. [themeConfig, userConfig].forEach(function(config) {
  345. config.modules = config.modules || {};
  346. Object.keys(config.modules).forEach(function(module) {
  347. if (config.modules[module] === true) {
  348. config.modules[module] = {};
  349. }
  350. });
  351. });
  352. let moduleNames = Object.keys(themeConfig.modules).concat(Object.keys(userConfig.modules));
  353. let moduleConfig = moduleNames.reduce(function(config, name) {
  354. let moduleClass = Quill.import(`modules/${name}`);
  355. if (moduleClass == null) {
  356. debug.error(`Cannot load ${name} module. Are you sure you registered it?`);
  357. } else {
  358. config[name] = moduleClass.DEFAULTS || {};
  359. }
  360. return config;
  361. }, {});
  362. // Special case toolbar shorthand
  363. if (userConfig.modules != null && userConfig.modules.toolbar &&
  364. userConfig.modules.toolbar.constructor !== Object) {
  365. userConfig.modules.toolbar = {
  366. container: userConfig.modules.toolbar
  367. };
  368. }
  369. userConfig = extend(true, {}, Quill.DEFAULTS, { modules: moduleConfig }, themeConfig, userConfig);
  370. ['bounds', 'container', 'scrollingContainer'].forEach(function(key) {
  371. if (typeof userConfig[key] === 'string') {
  372. userConfig[key] = document.querySelector(userConfig[key]);
  373. }
  374. });
  375. userConfig.modules = Object.keys(userConfig.modules).reduce(function(config, name) {
  376. if (userConfig.modules[name]) {
  377. config[name] = userConfig.modules[name];
  378. }
  379. return config;
  380. }, {});
  381. return userConfig;
  382. }
  383. // Handle selection preservation and TEXT_CHANGE emission
  384. // common to modification APIs
  385. function modify(modifier, source, index, shift) {
  386. if (this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) {
  387. return new Delta();
  388. }
  389. let range = index == null ? null : this.getSelection();
  390. let oldDelta = this.editor.delta;
  391. let change = modifier();
  392. if (range != null) {
  393. if (index === true) index = range.index;
  394. if (shift == null) {
  395. range = shiftRange(range, change, source);
  396. } else if (shift !== 0) {
  397. range = shiftRange(range, index, shift, source);
  398. }
  399. this.setSelection(range, Emitter.sources.SILENT);
  400. }
  401. if (change.length() > 0) {
  402. let args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source];
  403. this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
  404. if (source !== Emitter.sources.SILENT) {
  405. this.emitter.emit(...args);
  406. }
  407. }
  408. return change;
  409. }
  410. function overload(index, length, name, value, source) {
  411. let formats = {};
  412. if (typeof index.index === 'number' && typeof index.length === 'number') {
  413. // Allow for throwaway end (used by insertText/insertEmbed)
  414. if (typeof length !== 'number') {
  415. source = value, value = name, name = length, length = index.length, index = index.index;
  416. } else {
  417. length = index.length, index = index.index;
  418. }
  419. } else if (typeof length !== 'number') {
  420. source = value, value = name, name = length, length = 0;
  421. }
  422. // Handle format being object, two format name/value strings or excluded
  423. if (typeof name === 'object') {
  424. formats = name;
  425. source = value;
  426. } else if (typeof name === 'string') {
  427. if (value != null) {
  428. formats[name] = value;
  429. } else {
  430. source = name;
  431. }
  432. }
  433. // Handle optional source
  434. source = source || Emitter.sources.API;
  435. return [index, length, formats, source];
  436. }
  437. function shiftRange(range, index, length, source) {
  438. if (range == null) return null;
  439. let start, end;
  440. if (index instanceof Delta) {
  441. [start, end] = [range.index, range.index + range.length].map(function(pos) {
  442. return index.transformPosition(pos, source !== Emitter.sources.USER);
  443. });
  444. } else {
  445. [start, end] = [range.index, range.index + range.length].map(function(pos) {
  446. if (pos < index || (pos === index && source === Emitter.sources.USER)) return pos;
  447. if (length >= 0) {
  448. return pos + length;
  449. } else {
  450. return Math.max(index, pos + length);
  451. }
  452. });
  453. }
  454. return new Range(start, end - start);
  455. }
  456. export { expandConfig, overload, Quill as default };