quill.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. import Delta from 'quill-delta';
  2. import cloneDeep from 'lodash.clonedeep';
  3. import merge from 'lodash.merge';
  4. import * as Parchment from 'parchment';
  5. import Editor from './editor';
  6. import Emitter from './emitter';
  7. import Module from './module';
  8. import Selection, { Range } from './selection';
  9. import instances from './instances';
  10. import logger from './logger';
  11. import Theme from './theme';
  12. const debug = logger('quill');
  13. const defaultNamespace = Symbol('defaultNamespace');
  14. const namespaces = {
  15. [defaultNamespace]: {
  16. registry: null,
  17. imports: {
  18. delta: Delta,
  19. parchment: Parchment,
  20. 'core/module': Module,
  21. 'core/theme': Theme,
  22. },
  23. },
  24. };
  25. Parchment.ParentBlot.uiClass = 'ql-ui';
  26. class Quill {
  27. static debug(limit) {
  28. if (limit === true) {
  29. limit = 'log';
  30. }
  31. logger.level(limit);
  32. }
  33. static getNamespace(namespace = defaultNamespace) {
  34. if (!namespaces[namespace]) {
  35. namespaces[namespace] = { registry: null, imports: {} };
  36. }
  37. if (!namespaces[namespace].registry) {
  38. namespaces[namespace].registry = new Parchment.Registry();
  39. Object.keys(this.defaultDefinitions).forEach(path => {
  40. this.register(path, this.defaultDefinitions[path], { namespace });
  41. });
  42. }
  43. return namespaces[namespace];
  44. }
  45. static find(node) {
  46. return instances.get(node) || this.getNamespace().registry.find(node);
  47. }
  48. static import(name, options = {}) {
  49. const { imports } = this.getNamespace(options.namespace);
  50. if (imports[name] == null) {
  51. debug.error(`Cannot import ${name}. Are you sure it was registered?`);
  52. }
  53. return imports[name];
  54. }
  55. static register(path, target, options = {}) {
  56. if (typeof path !== 'string') {
  57. const name = path.attrName || path.blotName;
  58. if (typeof name === 'string') {
  59. // register(Blot | Attributor, overwrite)
  60. this.register(`formats/${name}`, path, target);
  61. } else {
  62. Object.keys(path).forEach(key => {
  63. this.register(key, path[key], target);
  64. });
  65. }
  66. } else {
  67. if (typeof options === 'boolean') {
  68. options = { overwrite: options };
  69. }
  70. const { overwrite = false, namespace } = options;
  71. const { registry, imports } = this.getNamespace(namespace);
  72. if (imports[path] != null && !overwrite) {
  73. debug.warn(`Overwriting ${path} with`, target);
  74. }
  75. imports[path] = target;
  76. if (
  77. (path.startsWith('blots/') || path.startsWith('formats/')) &&
  78. target.blotName !== 'abstract'
  79. ) {
  80. registry.register(target);
  81. }
  82. if (typeof target.register === 'function') {
  83. target.register(options);
  84. }
  85. }
  86. }
  87. constructor(container, options = {}) {
  88. this.options = expandConfig(container, options);
  89. this.container = this.options.container;
  90. if (this.container == null) {
  91. return debug.error('Invalid Quill container', container);
  92. }
  93. if (this.options.debug) {
  94. Quill.debug(this.options.debug);
  95. }
  96. const html = this.container.innerHTML.trim();
  97. this.container.classList.add('ql-container');
  98. this.container.innerHTML = '';
  99. instances.set(this.container, this);
  100. this.root = this.addContainer('ql-editor');
  101. this.root.classList.add('ql-blank');
  102. this.scrollingContainer = this.options.scrollingContainer || this.root;
  103. this.emitter = new Emitter();
  104. const { registry } = Quill.getNamespace(this.options.namespace);
  105. const ScrollBlot = registry.query(Parchment.ScrollBlot.blotName);
  106. this.scroll = new ScrollBlot(registry, this.root, {
  107. emitter: this.emitter,
  108. });
  109. this.editor = new Editor(this.scroll);
  110. this.selection = new Selection(this.scroll, this.emitter);
  111. this.theme = new this.options.theme(this, this.options); // eslint-disable-line new-cap
  112. this.keyboard = this.theme.addModule('keyboard');
  113. this.clipboard = this.theme.addModule('clipboard');
  114. this.history = this.theme.addModule('history');
  115. this.uploader = this.theme.addModule('uploader');
  116. this.theme.init();
  117. this.emitter.on(Emitter.events.EDITOR_CHANGE, type => {
  118. if (type === Emitter.events.TEXT_CHANGE) {
  119. this.root.classList.toggle('ql-blank', this.editor.isBlank());
  120. }
  121. });
  122. this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
  123. const oldRange = this.selection.lastRange;
  124. const [newRange] = this.selection.getRange();
  125. const selectionInfo =
  126. oldRange && newRange ? { oldRange, newRange } : undefined;
  127. modify.call(
  128. this,
  129. () => this.editor.update(null, mutations, selectionInfo),
  130. source,
  131. );
  132. });
  133. const contents = this.clipboard.convert({
  134. html: `${html}<p><br></p>`,
  135. text: '\n',
  136. });
  137. this.setContents(contents);
  138. this.history.clear();
  139. if (this.options.placeholder) {
  140. this.root.setAttribute('data-placeholder', this.options.placeholder);
  141. }
  142. if (this.options.readOnly) {
  143. this.disable();
  144. }
  145. this.allowReadOnlyEdits = false;
  146. }
  147. addContainer(container, refNode = null) {
  148. if (typeof container === 'string') {
  149. const className = container;
  150. container = document.createElement('div');
  151. container.classList.add(className);
  152. }
  153. this.container.insertBefore(container, refNode);
  154. return container;
  155. }
  156. blur() {
  157. this.selection.setRange(null);
  158. }
  159. deleteText(index, length, source) {
  160. [index, length, , source] = overload(index, length, source);
  161. return modify.call(
  162. this,
  163. () => {
  164. return this.editor.deleteText(index, length);
  165. },
  166. source,
  167. index,
  168. -1 * length,
  169. );
  170. }
  171. disable() {
  172. this.enable(false);
  173. }
  174. editReadOnly(modifier) {
  175. this.allowReadOnlyEdits = true;
  176. const value = modifier();
  177. this.allowReadOnlyEdits = false;
  178. return value;
  179. }
  180. enable(enabled = true) {
  181. this.scroll.enable(enabled);
  182. this.container.classList.toggle('ql-disabled', !enabled);
  183. }
  184. focus() {
  185. const { scrollTop } = this.scrollingContainer;
  186. this.selection.focus();
  187. this.scrollingContainer.scrollTop = scrollTop;
  188. this.scrollIntoView();
  189. }
  190. format(name, value, source = Emitter.sources.API) {
  191. return modify.call(
  192. this,
  193. () => {
  194. const range = this.getSelection(true);
  195. let change = new Delta();
  196. if (range == null) return change;
  197. if (this.scroll.query(name, Parchment.Scope.BLOCK)) {
  198. change = this.editor.formatLine(range.index, range.length, {
  199. [name]: value,
  200. });
  201. } else if (range.length === 0) {
  202. this.selection.format(name, value);
  203. return change;
  204. } else {
  205. change = this.editor.formatText(range.index, range.length, {
  206. [name]: value,
  207. });
  208. }
  209. this.setSelection(range, Emitter.sources.SILENT);
  210. return change;
  211. },
  212. source,
  213. );
  214. }
  215. formatLine(index, length, name, value, source) {
  216. let formats;
  217. // eslint-disable-next-line prefer-const
  218. [index, length, formats, source] = overload(
  219. index,
  220. length,
  221. name,
  222. value,
  223. source,
  224. );
  225. return modify.call(
  226. this,
  227. () => {
  228. return this.editor.formatLine(index, length, formats);
  229. },
  230. source,
  231. index,
  232. 0,
  233. );
  234. }
  235. formatText(index, length, name, value, source) {
  236. let formats;
  237. // eslint-disable-next-line prefer-const
  238. [index, length, formats, source] = overload(
  239. index,
  240. length,
  241. name,
  242. value,
  243. source,
  244. );
  245. return modify.call(
  246. this,
  247. () => {
  248. return this.editor.formatText(index, length, formats);
  249. },
  250. source,
  251. index,
  252. 0,
  253. );
  254. }
  255. getBounds(index, length = 0) {
  256. let bounds;
  257. if (typeof index === 'number') {
  258. bounds = this.selection.getBounds(index, length);
  259. } else {
  260. bounds = this.selection.getBounds(index.index, index.length);
  261. }
  262. const containerBounds = this.container.getBoundingClientRect();
  263. return {
  264. bottom: bounds.bottom - containerBounds.top,
  265. height: bounds.height,
  266. left: bounds.left - containerBounds.left,
  267. right: bounds.right - containerBounds.left,
  268. top: bounds.top - containerBounds.top,
  269. width: bounds.width,
  270. };
  271. }
  272. getContents(index = 0, length = this.getLength() - index) {
  273. [index, length] = overload(index, length);
  274. return this.editor.getContents(index, length);
  275. }
  276. getFormat(index = this.getSelection(true), length = 0) {
  277. if (typeof index === 'number') {
  278. return this.editor.getFormat(index, length);
  279. }
  280. return this.editor.getFormat(index.index, index.length);
  281. }
  282. getIndex(blot) {
  283. return blot.offset(this.scroll);
  284. }
  285. getLength() {
  286. return this.scroll.length();
  287. }
  288. getLeaf(index) {
  289. return this.scroll.leaf(index);
  290. }
  291. getLine(index) {
  292. return this.scroll.line(index);
  293. }
  294. getLines(index = 0, length = Number.MAX_VALUE) {
  295. if (typeof index !== 'number') {
  296. return this.scroll.lines(index.index, index.length);
  297. }
  298. return this.scroll.lines(index, length);
  299. }
  300. getModule(name) {
  301. return this.theme.modules[name];
  302. }
  303. getSelection(focus = false) {
  304. if (focus) this.focus();
  305. this.update(); // Make sure we access getRange with editor in consistent state
  306. return this.selection.getRange()[0];
  307. }
  308. getSemanticHTML(index = 0, length = this.getLength() - index) {
  309. [index, length] = overload(index, length);
  310. return this.editor.getHTML(index, length);
  311. }
  312. getText(index = 0, length = this.getLength() - index) {
  313. [index, length] = overload(index, length);
  314. return this.editor.getText(index, length);
  315. }
  316. hasFocus() {
  317. return this.selection.hasFocus();
  318. }
  319. insertEmbed(index, embed, value, source = Quill.sources.API) {
  320. return modify.call(
  321. this,
  322. () => {
  323. return this.editor.insertEmbed(index, embed, value);
  324. },
  325. source,
  326. index,
  327. );
  328. }
  329. insertText(index, text, name, value, source) {
  330. let formats;
  331. // eslint-disable-next-line prefer-const
  332. [index, , formats, source] = overload(index, 0, name, value, source);
  333. return modify.call(
  334. this,
  335. () => {
  336. return this.editor.insertText(index, text, formats);
  337. },
  338. source,
  339. index,
  340. text.length,
  341. );
  342. }
  343. isEnabled() {
  344. return this.scroll.isEnabled();
  345. }
  346. off(...args) {
  347. return this.emitter.off(...args);
  348. }
  349. on(...args) {
  350. return this.emitter.on(...args);
  351. }
  352. once(...args) {
  353. return this.emitter.once(...args);
  354. }
  355. removeFormat(index, length, source) {
  356. [index, length, , source] = overload(index, length, source);
  357. return modify.call(
  358. this,
  359. () => {
  360. return this.editor.removeFormat(index, length);
  361. },
  362. source,
  363. index,
  364. );
  365. }
  366. scrollIntoView() {
  367. this.selection.scrollIntoView(this.scrollingContainer);
  368. }
  369. setContents(delta, source = Emitter.sources.API) {
  370. return modify.call(
  371. this,
  372. () => {
  373. delta = new Delta(delta);
  374. const length = this.getLength();
  375. // Quill will set empty editor to \n
  376. const delete1 = this.editor.deleteText(0, length);
  377. // delta always applied before existing content
  378. const applied = this.editor.applyDelta(delta);
  379. // Remove extra \n from empty editor initialization
  380. const delete2 = this.editor.deleteText(this.getLength() - 1, 1);
  381. return delete1.compose(applied).compose(delete2);
  382. },
  383. source,
  384. );
  385. }
  386. setSelection(index, length, source) {
  387. if (index == null) {
  388. this.selection.setRange(null, length || Quill.sources.API);
  389. } else {
  390. [index, length, , source] = overload(index, length, source);
  391. this.selection.setRange(new Range(Math.max(0, index), length), source);
  392. if (source !== Emitter.sources.SILENT) {
  393. this.selection.scrollIntoView(this.scrollingContainer);
  394. }
  395. }
  396. }
  397. setText(text, source = Emitter.sources.API) {
  398. const delta = new Delta().insert(text);
  399. return this.setContents(delta, source);
  400. }
  401. update(source = Emitter.sources.USER) {
  402. const change = this.scroll.update(source); // Will update selection before selection.update() does if text changes
  403. this.selection.update(source);
  404. // TODO this is usually undefined
  405. return change;
  406. }
  407. updateContents(delta, source = Emitter.sources.API) {
  408. return modify.call(
  409. this,
  410. () => {
  411. delta = new Delta(delta);
  412. return this.editor.applyDelta(delta, source);
  413. },
  414. source,
  415. true,
  416. );
  417. }
  418. }
  419. Quill.DEFAULTS = {
  420. bounds: null,
  421. modules: {},
  422. placeholder: '',
  423. readOnly: false,
  424. scrollingContainer: null,
  425. theme: 'default',
  426. };
  427. Quill.events = Emitter.events;
  428. Quill.sources = Emitter.sources;
  429. Quill.defaultDefinitions = {};
  430. // eslint-disable-next-line no-undef
  431. Quill.version = typeof QUILL_VERSION === 'undefined' ? 'dev' : QUILL_VERSION;
  432. function expandConfig(container, userConfig) {
  433. userConfig = merge(
  434. {
  435. container,
  436. modules: {
  437. clipboard: true,
  438. keyboard: true,
  439. history: true,
  440. uploader: true,
  441. },
  442. },
  443. userConfig,
  444. );
  445. const { namespace } = userConfig;
  446. if (!userConfig.theme || userConfig.theme === Quill.DEFAULTS.theme) {
  447. userConfig.theme = Theme;
  448. } else {
  449. userConfig.theme = Quill.import(`themes/${userConfig.theme}`, {
  450. namespace,
  451. });
  452. if (userConfig.theme == null) {
  453. throw new Error(
  454. `Invalid theme ${userConfig.theme}. Did you register it?`,
  455. );
  456. }
  457. }
  458. const themeConfig = cloneDeep(userConfig.theme.DEFAULTS);
  459. [themeConfig, userConfig].forEach(config => {
  460. config.modules = config.modules || {};
  461. Object.keys(config.modules).forEach(module => {
  462. if (config.modules[module] === true) {
  463. config.modules[module] = {};
  464. }
  465. });
  466. });
  467. const moduleNames = Object.keys(themeConfig.modules).concat(
  468. Object.keys(userConfig.modules),
  469. );
  470. const moduleConfig = moduleNames.reduce((config, name) => {
  471. const moduleClass = Quill.import(`modules/${name}`, { namespace });
  472. if (moduleClass == null) {
  473. debug.error(
  474. `Cannot load ${name} module. Are you sure you registered it?`,
  475. );
  476. } else {
  477. config[name] = moduleClass.DEFAULTS || {};
  478. }
  479. return config;
  480. }, {});
  481. // Special case toolbar shorthand
  482. if (
  483. userConfig.modules != null &&
  484. userConfig.modules.toolbar &&
  485. userConfig.modules.toolbar.constructor !== Object
  486. ) {
  487. userConfig.modules.toolbar = {
  488. container: userConfig.modules.toolbar,
  489. };
  490. }
  491. userConfig = merge(
  492. {},
  493. Quill.DEFAULTS,
  494. { modules: moduleConfig },
  495. themeConfig,
  496. userConfig,
  497. );
  498. ['bounds', 'container', 'scrollingContainer'].forEach(key => {
  499. if (typeof userConfig[key] === 'string') {
  500. userConfig[key] = document.querySelector(userConfig[key]);
  501. }
  502. });
  503. userConfig.modules = Object.keys(userConfig.modules).reduce(
  504. (config, name) => {
  505. if (userConfig.modules[name]) {
  506. config[name] = userConfig.modules[name];
  507. }
  508. return config;
  509. },
  510. {},
  511. );
  512. return userConfig;
  513. }
  514. // Handle selection preservation and TEXT_CHANGE emission
  515. // common to modification APIs
  516. function modify(modifier, source, index, shift) {
  517. if (
  518. !this.isEnabled() &&
  519. source === Emitter.sources.USER &&
  520. !this.allowReadOnlyEdits
  521. ) {
  522. return new Delta();
  523. }
  524. let range = index == null ? null : this.getSelection();
  525. const oldDelta = this.editor.delta;
  526. const change = modifier();
  527. if (range != null) {
  528. if (index === true) {
  529. index = range.index; // eslint-disable-line prefer-destructuring
  530. }
  531. if (shift == null) {
  532. range = shiftRange(range, change, source);
  533. } else if (shift !== 0) {
  534. range = shiftRange(range, index, shift, source);
  535. }
  536. this.setSelection(range, Emitter.sources.SILENT);
  537. }
  538. if (change.length() > 0) {
  539. const args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source];
  540. this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
  541. if (source !== Emitter.sources.SILENT) {
  542. this.emitter.emit(...args);
  543. }
  544. }
  545. return change;
  546. }
  547. function overload(index, length, name, value, source) {
  548. let formats = {};
  549. if (typeof index.index === 'number' && typeof index.length === 'number') {
  550. // Allow for throwaway end (used by insertText/insertEmbed)
  551. if (typeof length !== 'number') {
  552. source = value;
  553. value = name;
  554. name = length;
  555. length = index.length; // eslint-disable-line prefer-destructuring
  556. index = index.index; // eslint-disable-line prefer-destructuring
  557. } else {
  558. length = index.length; // eslint-disable-line prefer-destructuring
  559. index = index.index; // eslint-disable-line prefer-destructuring
  560. }
  561. } else if (typeof length !== 'number') {
  562. source = value;
  563. value = name;
  564. name = length;
  565. length = 0;
  566. }
  567. // Handle format being object, two format name/value strings or excluded
  568. if (typeof name === 'object') {
  569. formats = name;
  570. source = value;
  571. } else if (typeof name === 'string') {
  572. if (value != null) {
  573. formats[name] = value;
  574. } else {
  575. source = name;
  576. }
  577. }
  578. // Handle optional source
  579. source = source || Emitter.sources.API;
  580. return [index, length, formats, source];
  581. }
  582. function shiftRange(range, index, length, source) {
  583. if (range == null) return null;
  584. let start;
  585. let end;
  586. if (index instanceof Delta) {
  587. [start, end] = [range.index, range.index + range.length].map(pos =>
  588. index.transformPosition(pos, source !== Emitter.sources.USER),
  589. );
  590. } else {
  591. [start, end] = [range.index, range.index + range.length].map(pos => {
  592. if (pos < index || (pos === index && source === Emitter.sources.USER))
  593. return pos;
  594. if (length >= 0) {
  595. return pos + length;
  596. }
  597. return Math.max(index, pos + length);
  598. });
  599. }
  600. return new Range(start, end - start);
  601. }
  602. export { expandConfig, overload, Quill as default };