cloning-medium-with-parchment.mdx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. ---
  2. title: Cloning Medium with Parchment
  3. ---
  4. To provide a consistent editing experience, you need both consistent data and predictable behaviors. The DOM unfortunately lacks both of these. The solution for modern editors is to maintain their own document model to represent their contents. [Parchment](https://github.com/quilljs/parchment/) is that solution for Quill. It is organized in its own codebase with its own API layer. Through Parchment you can customize the content and formats Quill recognizes, or add entirely new ones.
  5. In this guide, we will use the building blocks provided by Parchment and Quill to replicate the editor on Medium. We will start with the bare bones of Quill, without any themes, extraneous modules, or formats. At this basic level, Quill only understands plain text. But by the end of this guide, links, videos, and even tweets will be understood.
  6. ### Groundwork
  7. Let's start without even using Quill, with just a textarea and button, hooked up to a dummy event listener. We'll use jQuery for convenience throughout this guide, but neither Quill nor Parchment depends on this. We'll also add some basic styling, with the help of [Google Fonts](https://fonts.google.com/) and [Font Awesome](https://fontawesome.io/). None of this has anything to do with Quill or Parchment, so we'll move through quickly.
  8. <Sandpack
  9. externalResources={scope.externalResources}
  10. defaultShowPreview
  11. showFileTree
  12. files={{
  13. 'index.html': `
  14. ${scope.basicHTML}
  15. <textarea id="editor">Tell your story...</textarea>
  16. <script type="module" src="/index.js"></script>
  17. `,
  18. 'styles.css': `
  19. #editor {
  20. display: block;
  21. font-family: 'Open Sans', Helvetica, sans-serif;
  22. font-size: 1.2em;
  23. height: 180px;
  24. margin: 0 auto;
  25. width: 450px;
  26. }
  27. #tooltip-controls, #sidebar-controls {
  28. text-align: center;
  29. }
  30. button {
  31. background: transparent;
  32. border: none;
  33. cursor: pointer;
  34. display: inline-block;
  35. font-size: 18px;
  36. padding: 0;
  37. height: 32px;
  38. width: 32px;
  39. text-align: center;
  40. }
  41. button:active, button:focus {
  42. outline: none;
  43. }
  44. `,
  45. 'index.js': `
  46. document.querySelectorAll('button').forEach((button) => {
  47. button.addEventListener('click', () => {
  48. alert('Click!');
  49. });
  50. });
  51. `
  52. }}
  53. />
  54. ### Adding Quill Core
  55. Next, we'll replace the textarea with Quill core, absent of themes, formats and extraneous modules. Open up your developer console to inspect the demo while you type into the editor. You can see the basic building blocks of a Parchment document at work.
  56. <Sandpack
  57. externalResources={scope.externalResources}
  58. showFileTree
  59. defaultShowPreview
  60. files={{
  61. 'index.html': scope.html,
  62. 'styles.css': `
  63. ${scope.basicCSS}
  64. `,
  65. 'index.js': `
  66. document.querySelectorAll('button').forEach((button) => {
  67. button.addEventListener('click', () => {
  68. alert('Click!');
  69. });
  70. });
  71. const quill = new Quill('#editor');
  72. `
  73. }}
  74. />
  75. Like the DOM, a Parchment document is a tree. Its nodes, called Blots, are an abstraction over DOM Nodes. A few blots are already defined for us: Scroll, Block, Inline, Text and Break. As you type, a Text blot is synchronized with the corresponding DOM Text node; enters are handled by creating a new Block blot. In Parchment, Blots that can have children must have at least one child, so empty Blocks are filled with a Break blot. This makes handling leaves simple and predictable. All this is organized under a root Scroll blot.
  76. You cannot observe an Inline blot by just typing at this point since it does not contribute meaningful structure or formatting to the document. A valid Quill document must be canonical and compact. There is only one valid DOM tree that can represent a given document, and that DOM tree contains the minimal number of nodes.
  77. Since `<p><span>Text</span></p>` and `<p>Text</p>` represent the same content, the former is invalid and it is part of Quill's optimization process to unwrap the `<span>`. Similarly, once we add formatting, `<p><em>Te</em><em>st</em></p>` and `<p><em><em>Test</em></em></p>` are also invalid, as they are not the most compact representation.
  78. Because of these constraints, **Quill cannot support arbitrary DOM trees and HTML changes**. But as we will see, the consistency and predicability this structure provides enables us to easily build rich editing experiences.
  79. ### Basic Formatting
  80. We mentioned earlier that an Inline does not contribute formatting. This is the exception, rather than the rule, made for the base Inline class. The base Block blot works the same way for block level elements.
  81. To implement bold and italics, we need only to inherit from Inline, set the `blotName` and `tagName`, and register it with Quill. For a compelete reference of the signatures of inherited and static methods and variables, take a look at [Parchment](https://github.com/quilljs/parchment/).
  82. ```js
  83. const Inline = Quill.import('blots/inline');
  84. class BoldBlot extends Inline {
  85. static blotName = 'bold';
  86. static tagName = 'strong';
  87. }
  88. class ItalicBlot extends Inline {
  89. static blotName = 'italic';
  90. static tagName = 'em';
  91. }
  92. Quill.register(BoldBlot);
  93. Quill.register(ItalicBlot);
  94. ```
  95. We follow Medium's example here in using `strong` and `em` tags but you could just as well use `b` and `i` tags. The name of the blot will be used as the name of the format by Quill. By registering our blots, we can now use Quill's full API on our new formats:
  96. ```js
  97. Quill.register(BoldBlot);
  98. Quill.register(ItalicBlot);
  99. const quill = new Quill('#editor');
  100. quill.insertText(0, 'Test', { bold: true });
  101. quill.formatText(0, 4, 'italic', true);
  102. // If we named our italic blot "myitalic", we would call
  103. // quill.formatText(0, 4, 'myitalic', true);
  104. ```
  105. Let's get rid of our dummy button handler and hook up the bold and italic buttons to Quill's [`format()`](/docs/api/#format). We will hardcode `true` to always add formatting for simplicity. In your application, you can use [`getFormat()`](/docs/api/#getformat) to retrieve the current formatting over a arbitrary range to decide whether to add or remove a format. The [Toolbar](/docs/modules/toolbar/) module implements this for Quill, and we will not reimplement it here.
  106. Open your developer console and try out Quill's [APIs](/docs/api) on your new bold and italic formats! Make sure to set the context to the correct CodePen iframe to be able to access the `quill` variable in the demo.
  107. <Sandpack
  108. externalResources={scope.externalResources}
  109. showFileTree
  110. defaultShowPreview
  111. activeFile="index.js"
  112. files={{
  113. 'index.html': scope.html,
  114. 'styles.css': `
  115. ${scope.basicCSS}
  116. `,
  117. 'formats/boldBlot.js': scope.boldBlot,
  118. 'formats/italicBlot.js': scope.italicBlot,
  119. 'index.js': `
  120. import './formats/boldBlot.js';
  121. import './formats/italicBlot.js';
  122. const onClick = (selector, callback) => {
  123. document.querySelector(selector).addEventListener('click', callback);
  124. };
  125. onClick('#bold-button', () => {
  126. quill.format('bold', true);
  127. });
  128. onClick('#italic-button', () => {
  129. quill.format('italic', true);
  130. });
  131. const quill = new Quill('#editor');
  132. `
  133. }}
  134. />
  135. Note that if you apply both bold and italic to some text, regardless of what order you do so, Quill wraps the `<strong>` tag outside of the `<em>` tag, in a consistent order.
  136. ### Links
  137. Links are slightly more complicated, since we need more than a boolean to store the link url. This affects our Link blot in two ways: creation and format retrieval. We will represent the url as a string value, but we could easily do so in other ways, such as an object with a url key, allowing for other key/value pairs to be set and define a link. We will demonstrate this later with [images](#images).
  138. ```js
  139. class LinkBlot extends Inline {
  140. static blotName = 'link';
  141. static tagName = 'a';
  142. static create(value) {
  143. const node = super.create();
  144. // Sanitize url value if desired
  145. node.setAttribute('href', value);
  146. // Okay to set other non-format related attributes
  147. // These are invisible to Parchment so must be static
  148. node.setAttribute('target', '_blank');
  149. return node;
  150. }
  151. static formats(node) {
  152. // We will only be called with a node already
  153. // determined to be a Link blot, so we do
  154. // not need to check ourselves
  155. return node.getAttribute('href');
  156. }
  157. }
  158. Quill.register(LinkBlot);
  159. ```
  160. Now we can hook our link button up to a fancy `prompt`, again to keep things simple, before passing to Quill's `format()`.
  161. <Sandpack
  162. externalResources={scope.externalResources}
  163. showFileTree
  164. defaultShowPreview
  165. activeFile="formats/linkBlot.js"
  166. files={{
  167. 'index.html': scope.html,
  168. 'styles.css': `
  169. ${scope.basicCSS}
  170. `,
  171. 'formats/boldBlot.js': scope.boldBlot,
  172. 'formats/italicBlot.js': scope.italicBlot,
  173. 'formats/linkBlot.js': scope.linkBlot,
  174. 'index.js': `
  175. import './formats/boldBlot.js';
  176. import './formats/italicBlot.js';
  177. import './formats/linkBlot.js';
  178. const onClick = (selector, callback) => {
  179. document.querySelector(selector).addEventListener('click', callback);
  180. };
  181. onClick('#bold-button', () => {
  182. quill.format('bold', true);
  183. });
  184. onClick('#italic-button', () => {
  185. quill.format('italic', true);
  186. });
  187. onClick('#link-button', () => {
  188. const value = prompt('Enter link URL');
  189. quill.format('link', value);
  190. });
  191. const quill = new Quill('#editor');
  192. `
  193. }}
  194. />
  195. ### Blockquote and Headers
  196. Blockquotes are implemented the same way as Bold blots, except we will inherit from Block, the base block level Blot. While Inline blots can be nested, Block blots cannot. Instead of wrapping, Block blots replace one another when applied to the same text range.
  197. ```js
  198. const Block = Quill.import('blots/block');
  199. class BlockquoteBlot extends Block {
  200. static blotName = 'blockquote';
  201. static tagName = 'blockquote';
  202. }
  203. ```
  204. Headers are implemented exactly the same way, with only one difference: it can be represented by more than one DOM element. The value of the format by default becomes the tagName, instead of just `true`. We can customize this by extending `formats()`, similar to how we did so for [links](#links).
  205. ```js
  206. class HeaderBlot extends Block {
  207. static blotName = 'header';
  208. // Medium only supports two header sizes, so we will only demonstrate two,
  209. // but we could easily just add more tags into this array
  210. static tagName = ['H1', 'H2'];
  211. static formats(node) {
  212. return HeaderBlot.tagName.indexOf(node.tagName) + 1;
  213. }
  214. }
  215. ```
  216. Let's hook these new blots up to their respective buttons and add some CSS for the `<blockquote>` tag.
  217. <Sandpack
  218. externalResources={scope.externalResources}
  219. showFileTree
  220. defaultShowPreview
  221. activeFile="formats/blockquoteBlot.js"
  222. files={{
  223. 'index.html': scope.html,
  224. 'styles.css': `
  225. ${scope.cssWithBlockquoteAndHeader}
  226. `,
  227. 'formats/boldBlot.js': scope.boldBlot,
  228. 'formats/italicBlot.js': scope.italicBlot,
  229. 'formats/linkBlot.js': scope.linkBlot,
  230. 'formats/blockquoteBlot.js': scope.blockquoteBlot,
  231. 'formats/headerBlot.js': scope.headerBlot,
  232. 'index.js': `
  233. import './formats/boldBlot.js';
  234. import './formats/italicBlot.js';
  235. import './formats/linkBlot.js';
  236. import './formats/blockquoteBlot.js';
  237. import './formats/headerBlot.js';
  238. const onClick = (selector, callback) => {
  239. document.querySelector(selector).addEventListener('click', callback);
  240. };
  241. onClick('#bold-button', () => {
  242. quill.format('bold', true);
  243. });
  244. onClick('#italic-button', () => {
  245. quill.format('italic', true);
  246. });
  247. onClick('#link-button', () => {
  248. const value = prompt('Enter link URL');
  249. quill.format('link', value);
  250. });
  251. onClick('#blockquote-button', () => {
  252. quill.format('blockquote', true);
  253. });
  254. onClick('#header-1-button', () => {
  255. quill.format('header', 1);
  256. });
  257. onClick('#header-2-button', () => {
  258. quill.format('header', 2);
  259. });
  260. const quill = new Quill('#editor');
  261. `
  262. }}
  263. />
  264. Try setting some text to H1, and in your console, run `quill.getContents()`. You will see our custom static `formats()` function at work. Make sure to set the context to the correct CodePen iframe to be able to access the `quill` variable in the demo.
  265. ### Dividers
  266. Now let's implement our first leaf Blot. While our previous Blot examples contribute formatting and implement `format()`, leaf Blots contribute content and implement `value()`. Leaf Blots can either be Text or Embed Blots, so our section divider will be an Embed. Once created, Embed Blots' value is immutable, requiring deletion and reinsertion to change the content at that location.
  267. Our methodology is similar to before, except we inherit from a BlockEmbed. Embed also exists under `blots/embed`, but is meant for inline level blots. We want the block level implementation instead for dividers.
  268. ```js
  269. const BlockEmbed = Quill.import('blots/block/embed');
  270. class DividerBlot extends BlockEmbed {
  271. static blotName = 'divider';
  272. static tagName = 'hr';
  273. }
  274. ```
  275. Our click handler calls [`insertEmbed()`](/docs/api/#insertembed), which does not as conveniently determine, save, and restore the user selection for us like [`format()`](/docs/api/#format) does, so we have to do a little more work to preserve selection ourselves. In addition, when we try to insert a BlockEmbed in the middle of the Block, Quill splits the Block for us. To make this behavior more clear, we will explicitly split the block oursevles by inserting a newline before inserting the divider. Take a look at the Babel tab in the CodePen for specifics.
  276. <Sandpack
  277. externalResources={scope.externalResources}
  278. showFileTree
  279. defaultShowPreview
  280. activeFile="formats/dividerBlot.js"
  281. files={{
  282. 'index.html': scope.html,
  283. 'styles.css': scope.cssWithBlockquoteAndHeader,
  284. 'formats/boldBlot.js': scope.boldBlot,
  285. 'formats/italicBlot.js': scope.italicBlot,
  286. 'formats/linkBlot.js': scope.linkBlot,
  287. 'formats/blockquoteBlot.js': scope.blockquoteBlot,
  288. 'formats/headerBlot.js': scope.headerBlot,
  289. 'formats/dividerBlot.js': scope.dividerBlot,
  290. 'index.js': `
  291. import './formats/boldBlot.js';
  292. import './formats/italicBlot.js';
  293. import './formats/linkBlot.js';
  294. import './formats/blockquoteBlot.js';
  295. import './formats/headerBlot.js';
  296. import './formats/dividerBlot.js';
  297. const onClick = (selector, callback) => {
  298. document.querySelector(selector).addEventListener('click', callback);
  299. };
  300. onClick('#bold-button', () => {
  301. quill.format('bold', true);
  302. });
  303. onClick('#italic-button', () => {
  304. quill.format('italic', true);
  305. });
  306. onClick('#link-button', () => {
  307. const value = prompt('Enter link URL');
  308. quill.format('link', value);
  309. });
  310. onClick('#blockquote-button', () => {
  311. quill.format('blockquote', true);
  312. });
  313. onClick('#header-1-button', () => {
  314. quill.format('header', 1);
  315. });
  316. onClick('#header-2-button', () => {
  317. quill.format('header', 2);
  318. });
  319. onClick('#divider-button', () => {
  320. const range = quill.getSelection(true);
  321. quill.insertText(range.index, '\\n', Quill.sources.USER);
  322. quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);
  323. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  324. });
  325. const quill = new Quill('#editor');
  326. `
  327. }}
  328. />
  329. ### Images
  330. Images can be added with what we learned building the [Link](#links) and [Divider](#divider) blots. We will use an object for the value to show how this is supported. Our button handler to insert images will use a static value, so we are not distracted by tooltip UI code irrelevant to [Parchment](https://github.com/quilljs/parchment/), the focus of this guide.
  331. ```js
  332. const BlockEmbed = Quill.import('blots/block/embed');
  333. class ImageBlot extends BlockEmbed {
  334. static blotName = 'image';
  335. static tagName = 'img';
  336. static create(value) {
  337. const node = super.create();
  338. node.setAttribute('alt', value.alt);
  339. node.setAttribute('src', value.url);
  340. return node;
  341. }
  342. static value(node) {
  343. return {
  344. alt: node.getAttribute('alt'),
  345. url: node.getAttribute('src')
  346. };
  347. }
  348. }
  349. ```
  350. <Sandpack
  351. externalResources={scope.externalResources}
  352. showFileTree
  353. defaultShowPreview
  354. activeFile="formats/imageBlot.js"
  355. files={{
  356. 'index.html': scope.html,
  357. 'styles.css': scope.cssWithBlockquoteAndHeader,
  358. 'formats/boldBlot.js': scope.boldBlot,
  359. 'formats/italicBlot.js': scope.italicBlot,
  360. 'formats/linkBlot.js': scope.linkBlot,
  361. 'formats/blockquoteBlot.js': scope.blockquoteBlot,
  362. 'formats/headerBlot.js': scope.headerBlot,
  363. 'formats/dividerBlot.js': scope.dividerBlot,
  364. 'formats/imageBlot.js': scope.imageBlot,
  365. 'index.js': `
  366. import './formats/boldBlot.js';
  367. import './formats/italicBlot.js';
  368. import './formats/linkBlot.js';
  369. import './formats/blockquoteBlot.js';
  370. import './formats/headerBlot.js';
  371. import './formats/dividerBlot.js';
  372. import './formats/imageBlot.js';
  373. const onClick = (selector, callback) => {
  374. document.querySelector(selector).addEventListener('click', callback);
  375. };
  376. onClick('#bold-button', () => {
  377. quill.format('bold', true);
  378. });
  379. onClick('#italic-button', () => {
  380. quill.format('italic', true);
  381. });
  382. onClick('#link-button', () => {
  383. const value = prompt('Enter link URL');
  384. quill.format('link', value);
  385. });
  386. onClick('#blockquote-button', () => {
  387. quill.format('blockquote', true);
  388. });
  389. onClick('#header-1-button', () => {
  390. quill.format('header', 1);
  391. });
  392. onClick('#header-2-button', () => {
  393. quill.format('header', 2);
  394. });
  395. onClick('#divider-button', () => {
  396. const range = quill.getSelection(true);
  397. quill.insertText(range.index, '\\n', Quill.sources.USER);
  398. quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);
  399. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  400. });
  401. onClick('#image-button', () => {
  402. const range = quill.getSelection(true);
  403. quill.insertText(range.index, '\\n', Quill.sources.USER);
  404. quill.insertEmbed(range.index + 1, 'image', {
  405. alt: 'Quill Cloud',
  406. url: 'https://quilljs.com/0.20/assets/images/cloud.png'
  407. }, Quill.sources.USER);
  408. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  409. });
  410. const quill = new Quill('#editor');
  411. `
  412. }}
  413. />
  414. ### Videos
  415. We will implement videos in a similar way as we did [images](#images). We could use the HTML5 `<video>` tag but we cannot play YouTube videos this way, and since this is likely the more common and relevant use case, we will use an `<iframe>` to support this. We do not have to here, but if you want multiple Blots to use the same tag, you can use `className` in addition to `tagName`, demonstrated in the next [Tweet](#tweet) example.
  416. Additionally we will add support for widths and heights, as unregistered formats. Formats specific to Embeds do not have to be registered separately, as long as there is no namespace collision with registered formats. This works since Blots just pass unknown formats to its children, eventually reaching the leaves. This also allows different Embeds to handle unregistered formats differently. For example, our [image](#images) embed from earlier could have recognized and handled the `width` format differently than our video does here.
  417. ```js
  418. class VideoBlot extends BlockEmbed {
  419. static blotName = 'video';
  420. static tagName = 'iframe';
  421. static create(url) {
  422. const node = super.create();
  423. node.setAttribute('src', url);
  424. // Set non-format related attributes with static values
  425. node.setAttribute('frameborder', '0');
  426. node.setAttribute('allowfullscreen', true);
  427. return node;
  428. }
  429. static formats(node) {
  430. // We still need to report unregistered embed formats
  431. const format = {};
  432. if (node.hasAttribute('height')) {
  433. format.height = node.getAttribute('height');
  434. }
  435. if (node.hasAttribute('width')) {
  436. format.width = node.getAttribute('width');
  437. }
  438. return format;
  439. }
  440. static value(node) {
  441. return node.getAttribute('src');
  442. }
  443. format(name, value) {
  444. // Handle unregistered embed formats
  445. if (name === 'height' || name === 'width') {
  446. if (value) {
  447. this.domNode.setAttribute(name, value);
  448. } else {
  449. this.domNode.removeAttribute(name, value);
  450. }
  451. } else {
  452. super.format(name, value);
  453. }
  454. }
  455. }
  456. ```
  457. Note if you open your console and call [`getContents`](/docs/api/#getcontents), Quill will report the video as:
  458. ```js
  459. {
  460. ops: [{
  461. insert: {
  462. video: 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0'
  463. },
  464. attributes: {
  465. height: '170',
  466. width: '400'
  467. }
  468. }]
  469. }
  470. ```
  471. <Sandpack
  472. externalResources={scope.externalResources}
  473. showFileTree
  474. defaultShowPreview
  475. activeFile="formats/videoBlot.js"
  476. files={{
  477. 'index.html': scope.html,
  478. 'styles.css': scope.cssWithBlockquoteAndHeader,
  479. 'formats/boldBlot.js': scope.boldBlot,
  480. 'formats/italicBlot.js': scope.italicBlot,
  481. 'formats/linkBlot.js': scope.linkBlot,
  482. 'formats/blockquoteBlot.js': scope.blockquoteBlot,
  483. 'formats/headerBlot.js': scope.headerBlot,
  484. 'formats/dividerBlot.js': scope.dividerBlot,
  485. 'formats/imageBlot.js': scope.imageBlot,
  486. 'formats/videoBlot.js': scope.videoBlot,
  487. 'index.js': `
  488. import './formats/boldBlot.js';
  489. import './formats/italicBlot.js';
  490. import './formats/linkBlot.js';
  491. import './formats/blockquoteBlot.js';
  492. import './formats/headerBlot.js';
  493. import './formats/dividerBlot.js';
  494. import './formats/imageBlot.js';
  495. import './formats/videoBlot.js';
  496. const onClick = (selector, callback) => {
  497. document.querySelector(selector).addEventListener('click', callback);
  498. };
  499. onClick('#bold-button', () => {
  500. quill.format('bold', true);
  501. });
  502. onClick('#italic-button', () => {
  503. quill.format('italic', true);
  504. });
  505. onClick('#link-button', () => {
  506. const value = prompt('Enter link URL');
  507. quill.format('link', value);
  508. });
  509. onClick('#blockquote-button', () => {
  510. quill.format('blockquote', true);
  511. });
  512. onClick('#header-1-button', () => {
  513. quill.format('header', 1);
  514. });
  515. onClick('#header-2-button', () => {
  516. quill.format('header', 2);
  517. });
  518. onClick('#divider-button', () => {
  519. const range = quill.getSelection(true);
  520. quill.insertText(range.index, '\\n', Quill.sources.USER);
  521. quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);
  522. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  523. });
  524. onClick('#image-button', () => {
  525. const range = quill.getSelection(true);
  526. quill.insertText(range.index, '\\n', Quill.sources.USER);
  527. quill.insertEmbed(range.index + 1, 'image', {
  528. alt: 'Quill Cloud',
  529. url: 'https://quilljs.com/0.20/assets/images/cloud.png'
  530. }, Quill.sources.USER);
  531. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  532. });
  533. onClick('#video-button', () => {
  534. let range = quill.getSelection(true);
  535. quill.insertText(range.index, '\\n', Quill.sources.USER);
  536. let url = 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0';
  537. quill.insertEmbed(range.index + 1, 'video', url, Quill.sources.USER);
  538. quill.formatText(range.index + 1, 1, { height: '170', width: '400' });
  539. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  540. });
  541. const quill = new Quill('#editor');
  542. `
  543. }}
  544. />
  545. ### Tweets
  546. Medium supports many embed types, but we will just focus on Tweets for this guide. The Tweet blot is implemented almost exactly the same as [images](#images). We take advantage of the fact that Embed blots do not have to correspond to a void node. It can be any arbitrary node and Quill will treat it like a void node and not traverse its children or descendants. This allows us to use a `<div>` and the native Twitter JavaScript library to do what it pleases within the `<div>` container we specify.
  547. Since our root Scroll Blot also uses a `<div>`, we also specify a `className` to disambiguate. Note Inline blots use `<span>` and Block Blots use `<p>` by default, so if you would like to use these tags for your custom Blots, you will have to specify a `className` in addition to a `tagName`.
  548. We use the Tweet id as the value defining our Blot. Again our click handler uses a static value to avoid distraction from irrelevant UI code.
  549. ```js
  550. class TweetBlot extends BlockEmbed {
  551. static blotName = 'tweet';
  552. static tagName = 'div';
  553. static className = 'tweet';
  554. static create(id) {
  555. const node = super.create();
  556. node.dataset.id = id;
  557. // Allow twitter library to modify our contents
  558. twttr.widgets.createTweet(id, node);
  559. return node;
  560. }
  561. static value(domNode) {
  562. return domNode.dataset.id;
  563. }
  564. }
  565. ```
  566. <Sandpack
  567. externalResources={scope.externalResources}
  568. showFileTree
  569. defaultShowPreview
  570. activeFile="formats/tweetBlot.js"
  571. files={{
  572. 'index.html': scope.html,
  573. 'styles.css': scope.cssWithBlockquoteAndHeader,
  574. 'formats/boldBlot.js': scope.boldBlot,
  575. 'formats/italicBlot.js': scope.italicBlot,
  576. 'formats/linkBlot.js': scope.linkBlot,
  577. 'formats/blockquoteBlot.js': scope.blockquoteBlot,
  578. 'formats/headerBlot.js': scope.headerBlot,
  579. 'formats/dividerBlot.js': scope.dividerBlot,
  580. 'formats/imageBlot.js': scope.imageBlot,
  581. 'formats/videoBlot.js': scope.videoBlot,
  582. 'formats/tweetBlot.js': scope.tweetBlot,
  583. 'index.js': `
  584. import './formats/boldBlot.js';
  585. import './formats/italicBlot.js';
  586. import './formats/linkBlot.js';
  587. import './formats/blockquoteBlot.js';
  588. import './formats/headerBlot.js';
  589. import './formats/dividerBlot.js';
  590. import './formats/imageBlot.js';
  591. import './formats/videoBlot.js';
  592. import './formats/tweetBlot.js';
  593. const onClick = (selector, callback) => {
  594. document.querySelector(selector).addEventListener('click', callback);
  595. };
  596. onClick('#bold-button', () => {
  597. quill.format('bold', true);
  598. });
  599. onClick('#italic-button', () => {
  600. quill.format('italic', true);
  601. });
  602. onClick('#link-button', () => {
  603. const value = prompt('Enter link URL');
  604. quill.format('link', value);
  605. });
  606. onClick('#blockquote-button', () => {
  607. quill.format('blockquote', true);
  608. });
  609. onClick('#header-1-button', () => {
  610. quill.format('header', 1);
  611. });
  612. onClick('#header-2-button', () => {
  613. quill.format('header', 2);
  614. });
  615. onClick('#divider-button', () => {
  616. const range = quill.getSelection(true);
  617. quill.insertText(range.index, '\\n', Quill.sources.USER);
  618. quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);
  619. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  620. });
  621. onClick('#image-button', () => {
  622. const range = quill.getSelection(true);
  623. quill.insertText(range.index, '\\n', Quill.sources.USER);
  624. quill.insertEmbed(range.index + 1, 'image', {
  625. alt: 'Quill Cloud',
  626. url: 'https://quilljs.com/0.20/assets/images/cloud.png'
  627. }, Quill.sources.USER);
  628. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  629. });
  630. onClick('#video-button', () => {
  631. let range = quill.getSelection(true);
  632. quill.insertText(range.index, '\\n', Quill.sources.USER);
  633. let url = 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0';
  634. quill.insertEmbed(range.index + 1, 'video', url, Quill.sources.USER);
  635. quill.formatText(range.index + 1, 1, { height: '170', width: '400' });
  636. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  637. });
  638. onClick('#tweet-button', () => {
  639. const range = quill.getSelection(true);
  640. const id = '464454167226904576';
  641. quill.insertText(range.index, '\\n', Quill.sources.USER);
  642. quill.insertEmbed(range.index + 1, 'tweet', id, Quill.sources.USER);
  643. quill.setSelection(range.index + 2, Quill.sources.SILENT);
  644. });
  645. const quill = new Quill('#editor');
  646. `
  647. }}
  648. />
  649. {/*
  650. ### Final Polish
  651. We began with just a bunch of buttons and a Quill core that just understands plaintext. With Parchment, we are able to add bold, italic, links, blockquotes, headers, section dividers, images, videos, and even Tweets. All of this comes while maintaining a predictable and consistent document, allowing us to use Quill's powerful [APIs](/docs/api) with these new formats and content.
  652. Let's add some final polish to finish off our demo. It won't compare to Medium's UI, but we'll try to get close.
  653. <CodePen hash="qNJrYB" defaultTab="result" />
  654. */}