95 |
119 |
96 componentDidUpdate = () => { |
120 componentDidUpdate = () => { |
97 this.updateMenu(); |
121 this.updateMenu(); |
98 } |
122 } |
99 |
123 |
100 /** |
124 /** |
101 * Check if the current selection has a mark with `type` in it. |
|
102 * |
|
103 * @param {String} type |
|
104 * @return {Boolean} |
|
105 */ |
|
106 |
|
107 hasMark = (type) => { |
|
108 const { state } = this.state |
|
109 return state.marks.some(mark => mark.type === type) |
|
110 } |
|
111 |
|
112 /** |
|
113 * Check if the any of the currently selected blocks are of `type`. |
|
114 * |
|
115 * @param {String} type |
|
116 * @return {Boolean} |
|
117 */ |
|
118 |
|
119 hasBlock = (type) => { |
|
120 const { state } = this.state |
|
121 return state.blocks.some(node => node.type === type) |
|
122 } |
|
123 |
|
124 /** |
|
125 * On change, save the new state. |
125 * On change, save the new state. |
126 * |
126 * |
127 * @param {State} state |
127 * @param {Change} change |
128 */ |
128 */ |
129 |
129 |
130 onChange = (state) => { |
130 onChange = ({value}) => { |
131 |
131 |
132 let newState = { |
132 let newState = { |
133 state: state, |
133 value: value, |
134 startedAt: this.state.startedAt |
134 startedAt: this.state.startedAt |
135 }; |
135 }; |
136 |
136 |
137 const isEmpty = state.document.length === 0; |
137 const isEmpty = value.document.length === 0; |
138 |
138 |
139 // Reset timers when the text is empty |
139 // Reset timers when the text is empty |
140 if (isEmpty) { |
140 if (isEmpty) { |
141 Object.assign(newState, { |
141 Object.assign(newState, { |
142 startedAt: null, |
142 startedAt: null, |
156 if (typeof this.props.onChange === 'function') { |
156 if (typeof this.props.onChange === 'function') { |
157 this.props.onChange(newState); |
157 this.props.onChange(newState); |
158 } |
158 } |
159 } |
159 } |
160 |
160 |
|
161 /** |
|
162 * Check if the current selection has a mark with `type` in it. |
|
163 * |
|
164 * @param {String} type |
|
165 * @return {Boolean} |
|
166 */ |
|
167 |
|
168 hasMark = type => { |
|
169 const { value } = this.state |
|
170 return value.activeMarks.some(mark => mark.type === type) |
|
171 } |
|
172 |
|
173 /** |
|
174 * Check if the any of the currently selected blocks are of `type`. |
|
175 * |
|
176 * @param {String} type |
|
177 * @return {Boolean} |
|
178 */ |
|
179 |
|
180 hasBlock = type => { |
|
181 const { value } = this.state |
|
182 return value.blocks.some(node => node.type === type) |
|
183 } |
|
184 |
161 asPlain = () => { |
185 asPlain = () => { |
162 return Plain.serialize(this.state.state); |
186 return Plain.serialize(this.state.value); |
163 } |
187 } |
164 |
188 |
165 asRaw = () => { |
189 asRaw = () => { |
166 return Raw.serialize(this.state.state); |
190 return JSON.stringify(this.state.value.toJSON()); |
167 } |
191 } |
168 |
192 |
169 asHtml = () => { |
193 asHtml = () => { |
170 return HtmlSerializer.serialize(this.state.state); |
194 return HtmlSerializer.serialize(this.state.value); |
171 } |
195 } |
172 |
196 |
173 asCategories = () => { |
197 asCategories = () => { |
174 return this.state.categories |
198 return this.state.categories |
175 } |
199 } |
191 /** |
215 /** |
192 * On key down, if it's a formatting command toggle a mark. |
216 * On key down, if it's a formatting command toggle a mark. |
193 * |
217 * |
194 * @param {Event} e |
218 * @param {Event} e |
195 * @param {Object} data |
219 * @param {Object} data |
196 * @param {State} state |
220 * @param {Change} change |
197 * @return {State} |
221 * @return {Change} |
198 */ |
222 */ |
199 |
223 |
200 onKeyDown = (e, data, state) => { |
224 onKeyDown = (e, change) => { |
201 |
225 // if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') { |
202 if (data.key === 'enter' && this.props.isChecked && typeof this.props.onEnterKeyDown === 'function') { |
226 |
203 e.preventDefault(); |
227 // e.preventDefault(); |
204 this.props.onEnterKeyDown(); |
228 // this.props.onEnterKeyDown(); |
205 |
229 |
206 return state; |
230 // return change; |
207 } |
231 // } |
208 |
232 |
209 if (!data.isMod) return |
233 // if (!data.isMod) return |
210 let mark |
234 if (!e.ctrlKey) return |
211 |
235 // Decide what to do based on the key code... |
212 switch (data.key) { |
236 switch (e.key) { |
213 case 'b': |
237 // When "B" is pressed, add a "bold" mark to the text. |
214 mark = 'bold' |
238 case 'b': { |
215 break |
239 e.preventDefault() |
216 case 'i': |
240 change.toggleMark('bold') |
217 mark = 'italic' |
241 |
218 break |
242 return true |
219 case 'u': |
243 } |
220 mark = 'underlined' |
244 case 'i': { |
221 break |
245 // When "U" is pressed, add an "italic" mark to the text. |
222 default: |
246 e.preventDefault() |
223 return |
247 change.toggleMark('italic') |
224 } |
248 |
225 |
249 return true |
226 state = state |
250 } |
227 .transform() |
251 case 'u': { |
228 .toggleMark(mark) |
252 // When "U" is pressed, add an "underline" mark to the text. |
229 .apply() |
253 e.preventDefault() |
230 |
254 change.toggleMark('underlined') |
231 e.preventDefault() |
255 |
232 return state |
256 return true |
233 } |
257 } |
234 |
258 } |
235 /** |
259 } |
|
260 |
|
261 /** |
236 * When a mark button is clicked, toggle the current mark. |
262 * When a mark button is clicked, toggle the current mark. |
237 * |
263 * |
238 * @param {Event} e |
264 * @param {Event} e |
239 * @param {String} type |
265 * @param {String} type |
240 */ |
266 */ |
241 |
267 |
242 onClickMark = (e, type) => { |
268 onClickMark = (e, type) => { |
243 e.preventDefault() |
269 |
244 const { state } = this.state |
270 e.preventDefault() |
|
271 const { value } = this.state |
245 let { categories } = this.state |
272 let { categories } = this.state |
246 const transform = state.transform() |
|
247 |
273 |
248 let isPortalOpen = false; |
274 let isPortalOpen = false; |
249 |
275 |
250 if (type === 'category') { |
276 if (type === 'category') { |
251 // Can't use toggleMark here, because it expects the same object |
277 // Can't use toggleMark here, because it expects the same object |
252 // @see https://github.com/ianstormtaylor/slate/issues/873 |
278 // @see https://github.com/ianstormtaylor/slate/issues/873 |
253 if (this.hasMark('category')) { |
279 if (this.hasMark('category')) { |
254 const categoryMarks = state.marks.filter(mark => mark.type === 'category') |
280 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
255 categoryMarks.forEach(mark => { |
281 categoryMarks.forEach(mark => { |
256 const key = mark.data.get('key'); |
282 const key = mark.data.get('key'); |
257 const text = mark.data.get('text'); |
283 const text = mark.data.get('text'); |
258 |
284 |
259 categories = this.removeCategory(categories, key, text) |
285 categories = this.removeCategory(categories, key, text) |
260 transform.removeMark(mark) |
286 const change = value.change().removeMark(mark) |
|
287 this.onChange(change) |
261 }) |
288 }) |
262 |
289 |
263 } else { |
290 } else { |
264 isPortalOpen = !this.state.isPortalOpen; |
291 isPortalOpen = !this.state.isPortalOpen; |
265 } |
292 } |
266 } else { |
293 } else { |
267 transform.toggleMark(type) |
294 const change = value.change().toggleMark(type) |
|
295 this.onChange(change) |
268 } |
296 } |
269 |
297 |
270 this.setState({ |
298 this.setState({ |
271 state: transform.apply(), |
299 state: value.change, |
272 isPortalOpen: isPortalOpen, |
300 isPortalOpen: isPortalOpen, |
273 categories: categories |
301 categories: categories |
274 }) |
302 }) |
275 } |
303 } |
276 |
304 |
281 * @param {String} type |
309 * @param {String} type |
282 */ |
310 */ |
283 |
311 |
284 onClickBlock = (e, type) => { |
312 onClickBlock = (e, type) => { |
285 e.preventDefault() |
313 e.preventDefault() |
286 let { state } = this.state |
314 const { value } = this.state |
287 const transform = state.transform() |
315 const change = value.change() |
288 const { document } = state |
316 const { document } = value |
289 |
317 |
290 // Handle everything but list buttons. |
318 // Handle everything but list buttons. |
291 if (type !== 'bulleted-list' && type !== 'numbered-list') { |
319 if (type !== 'bulleted-list' && type !== 'numbered-list') { |
292 const isActive = this.hasBlock(type) |
320 const isActive = this.hasBlock(type) |
293 const isList = this.hasBlock('list-item') |
321 const isList = this.hasBlock('list-item') |
294 |
322 |
295 if (isList) { |
323 if (isList) { |
296 transform |
324 change |
297 .setBlock(isActive ? DEFAULT_NODE : type) |
325 .setBlocks(isActive ? DEFAULT_NODE : type) |
298 .unwrapBlock('bulleted-list') |
326 .unwrapBlock('bulleted-list') |
299 .unwrapBlock('numbered-list') |
327 .unwrapBlock('numbered-list') |
300 } |
328 } |
301 |
329 |
302 else { |
330 else { |
303 transform |
331 change |
304 .setBlock(isActive ? DEFAULT_NODE : type) |
332 .setBlocks(isActive ? DEFAULT_NODE : type) |
305 } |
333 } |
306 } |
334 } |
307 |
335 |
308 // Handle the extra wrapping required for list buttons. |
336 // Handle the extra wrapping required for list buttons. |
309 else { |
337 else { |
310 const isList = this.hasBlock('list-item') |
338 const isList = this.hasBlock('list-item') |
311 const isType = state.blocks.some((block) => { |
339 const isType = value.blocks.some((block) => { |
312 return !!document.getClosest(block.key, parent => parent.type === type) |
340 return !!document.getClosest(block.key, parent => parent.type === type) |
313 }) |
341 }) |
314 |
342 |
315 if (isList && isType) { |
343 if (isList && isType) { |
316 transform |
344 change |
317 .setBlock(DEFAULT_NODE) |
345 .setBlocks(DEFAULT_NODE) |
318 .unwrapBlock('bulleted-list') |
346 .unwrapBlock('bulleted-list') |
319 .unwrapBlock('numbered-list') |
347 .unwrapBlock('numbered-list') |
|
348 |
320 } else if (isList) { |
349 } else if (isList) { |
321 transform |
350 change |
322 .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') |
351 .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') |
323 .wrapBlock(type) |
352 .wrapBlock(type) |
|
353 |
324 } else { |
354 } else { |
325 transform |
355 change |
326 .setBlock('list-item') |
356 .setBlocks('list-item') |
327 .wrapBlock(type) |
357 .wrapBlock(type) |
|
358 |
328 } |
359 } |
329 } |
360 } |
330 |
361 |
331 state = transform.apply() |
362 |
332 this.setState({ state }) |
363 this.onChange(change) |
333 } |
364 } |
334 |
365 |
335 onPortalOpen = (portal) => { |
366 onPortalOpen = (portal) => { |
336 // When the portal opens, cache the menu element. |
367 // When the portal opens, cache the menu element. |
337 this.setState({ hoveringMenu: portal.firstChild }) |
368 this.setState({ hoveringMenu: portal.firstChild }) |
338 } |
369 } |
339 |
370 |
340 onPortalClose = (portal) => { |
371 onPortalClose = (portal) => { |
341 let { state } = this.state |
372 let { value } = this.state |
342 const transform = state.transform(); |
|
343 |
373 |
344 this.setState({ |
374 this.setState({ |
345 state: transform.apply(), |
375 value: value.change, |
346 isPortalOpen: false |
376 isPortalOpen: false |
347 }) |
377 }) |
348 } |
378 } |
349 |
379 |
350 onCategoryClick = (category) => { |
380 onCategoryClick = (category) => { |
351 |
381 |
352 const { state, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state; |
382 const { value, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state; |
|
383 const change = value.change() |
353 let { categories } = this.state; |
384 let { categories } = this.state; |
354 const transform = state.transform(); |
385 |
355 |
386 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
356 const categoryMarks = state.marks.filter(mark => mark.type === 'category') |
387 categoryMarks.forEach(mark => change.removeMark(mark)); |
357 categoryMarks.forEach(mark => transform.removeMark(mark)); |
388 |
358 |
389 change.addMark({ |
359 transform.addMark({ |
|
360 type: 'category', |
390 type: 'category', |
361 data: { |
391 data: { |
362 text: currentSelectionText, |
392 text: currentSelectionText, |
363 selection: { |
393 selection: { |
364 start: currentSelectionStart, |
394 start: currentSelectionStart, |
403 * @return {Element} |
434 * @return {Element} |
404 */ |
435 */ |
405 |
436 |
406 render = () => { |
437 render = () => { |
407 return ( |
438 return ( |
408 <div> |
439 <div className="bg-secondary mb-5"> |
|
440 <div className="sticky-top"> |
409 {this.renderToolbar()} |
441 {this.renderToolbar()} |
|
442 </div> |
410 {this.renderEditor()} |
443 {this.renderEditor()} |
|
444 </div> |
|
445 ) |
|
446 } |
|
447 |
|
448 /** |
|
449 * Render the toolbar. |
|
450 * |
|
451 * @return {Element} |
|
452 */ |
|
453 |
|
454 renderToolbar = () => { |
|
455 return ( |
|
456 <div className="menu toolbar-menu d-flex bg-secondary"> |
|
457 {this.renderMarkButton('bold', 'format_bold')} |
|
458 {this.renderMarkButton('italic', 'format_italic')} |
|
459 {this.renderMarkButton('underlined', 'format_underlined')} |
|
460 {this.renderMarkButton('category', 'label')} |
|
461 |
|
462 |
|
463 {this.renderBlockButton('numbered-list', 'format_list_numbered')} |
|
464 {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} |
|
465 |
|
466 {this.renderToolbarButtons()} |
411 </div> |
467 </div> |
412 ) |
468 ) |
413 } |
469 } |
414 |
470 |
415 /** |
|
416 * Render the toolbar. |
|
417 * |
|
418 * @return {Element} |
|
419 */ |
|
420 |
|
421 renderToolbar = () => { |
|
422 return ( |
|
423 <div className="menu toolbar-menu"> |
|
424 {this.renderMarkButton('bold', 'format_bold')} |
|
425 {this.renderMarkButton('italic', 'format_italic')} |
|
426 {this.renderMarkButton('underlined', 'format_underlined')} |
|
427 {this.renderMarkButton('category', 'label')} |
|
428 |
|
429 {this.renderBlockButton('numbered-list', 'format_list_numbered')} |
|
430 {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} |
|
431 |
|
432 {this.renderToolbarButtons()} |
|
433 </div> |
|
434 ) |
|
435 } |
|
436 |
|
437 renderToolbarCheckbox = () => { |
471 renderToolbarCheckbox = () => { |
438 return ( |
472 return ( |
439 <div className="checkbox"> |
473 <div className="checkbox float-right"> |
440 <label> |
474 <label className="mr-2"> |
441 <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /> <kbd>Entrée</kbd>= Ajouter une note |
475 <input type="checkbox" checked={this.props.isChecked} onChange={this.onCheckboxChange} /><small className="text-muted ml-1"> Appuyer sur <kbd className="bg-danger text-muted ml-1">Entrée</kbd> pour ajouter une note</small> |
442 </label> |
476 </label> |
443 </div> |
477 </div> |
444 ) |
478 ) |
445 } |
479 } |
446 |
480 |
447 renderToolbarButtons = () => { |
481 renderToolbarButtons = () => { |
448 return ( |
482 return ( |
449 <div> |
483 <div> |
450 { !this.props.note && this.renderToolbarCheckbox() } |
484 <button type="button" className="btn btn-primary btn-sm text-secondary float-right mr-5" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}> |
451 <button type="button" className="btn btn-primary btn-lg" disabled={this.props.isButtonDisabled} onClick={this.onButtonClick}> |
|
452 { this.props.note ? 'Save note' : 'Ajouter' } |
485 { this.props.note ? 'Save note' : 'Ajouter' } |
453 </button> |
486 </button> |
|
487 { !this.props.note && this.renderToolbarCheckbox() } |
454 </div> |
488 </div> |
455 ); |
489 ); |
456 } |
490 } |
457 |
491 |
458 /** |
492 /** |
465 |
499 |
466 renderMarkButton = (type, icon) => { |
500 renderMarkButton = (type, icon) => { |
467 const isActive = this.hasMark(type) |
501 const isActive = this.hasMark(type) |
468 const onMouseDown = e => this.onClickMark(e, type) |
502 const onMouseDown = e => this.onClickMark(e, type) |
469 |
503 |
470 return ( |
504 |
471 <span className="button" onMouseDown={onMouseDown} data-active={isActive}> |
505 return ( |
|
506 <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}> |
472 <span className="material-icons">{icon}</span> |
507 <span className="material-icons">{icon}</span> |
473 </span> |
508 </span> |
474 ) |
509 ) |
475 } |
510 } |
476 |
511 |
|
512 // Add a `renderMark` method to render marks. |
|
513 |
|
514 renderMark = props => { |
|
515 const { children, mark, attributes } = props |
|
516 |
|
517 switch (mark.type) { |
|
518 case 'bold': |
|
519 return <strong {...attributes}>{children}</strong> |
|
520 case 'code': |
|
521 return <code {...attributes}>{children}</code> |
|
522 case 'italic': |
|
523 return <em {...attributes}>{children}</em> |
|
524 case 'underlined': |
|
525 return <ins {...attributes}>{children}</ins> |
|
526 } |
|
527 } |
477 /** |
528 /** |
478 * Render a block-toggling toolbar button. |
529 * Render a block-toggling toolbar button. |
479 * |
530 * |
480 * @param {String} type |
531 * @param {String} type |
481 * @param {String} icon |
532 * @param {String} icon |
482 * @return {Element} |
533 * @return {Element} |
483 */ |
534 */ |
484 |
535 |
485 renderBlockButton = (type, icon) => { |
536 renderBlockButton = (type, icon) => { |
486 const isActive = this.hasBlock(type) |
537 let isActive = this.hasBlock(type) |
|
538 |
|
539 if (['numbered-list', 'bulleted-list'].includes(type)) { |
|
540 const { value } = this.state |
|
541 const parent = value.document.getParent(value.blocks.first().key) |
|
542 isActive = this.hasBlock('list-item') && parent && parent.type === type |
|
543 } |
487 const onMouseDown = e => this.onClickBlock(e, type) |
544 const onMouseDown = e => this.onClickBlock(e, type) |
488 |
545 |
489 return ( |
546 return ( |
490 <span className="button" onMouseDown={onMouseDown} data-active={isActive}> |
547 <span className="button text-primary" onMouseDown={onMouseDown} data-active={isActive}> |
491 <span className="material-icons">{icon}</span> |
548 <span className="material-icons">{icon}</span> |
492 </span> |
549 </span> |
493 ) |
550 ) |
494 } |
551 } |
495 |
552 |
|
553 renderNode = props => { |
|
554 const { attributes, children, node } = props |
|
555 |
|
556 switch (node.type) { |
|
557 case 'block-quote': |
|
558 return <blockquote {...attributes}>{children}</blockquote> |
|
559 case 'bulleted-list': |
|
560 return <ul {...attributes}>{children}</ul> |
|
561 case 'heading-one': |
|
562 return <h1 {...attributes}>{children}</h1> |
|
563 case 'heading-two': |
|
564 return <h2 {...attributes}>{children}</h2> |
|
565 case 'list-item': |
|
566 return <li {...attributes}>{children}</li> |
|
567 case 'numbered-list': |
|
568 return <ol {...attributes}>{children}</ol> |
|
569 } |
|
570 } |
|
571 |
496 /** |
572 /** |
497 * Render the Slate editor. |
573 * Render the Slate editor. |
498 * |
574 * |
499 * @return {Element} |
575 * @return {Element} |
500 */ |
576 */ |
501 |
577 |
502 renderEditor = () => { |
578 renderEditor = () => { |
503 return ( |
579 return ( |
|
580 <div className="editor-wrapper sticky-bottom p-2"> |
504 <div className="editor-slatejs"> |
581 <div className="editor-slatejs"> |
505 {this.renderHoveringMenu()} |
582 {this.renderHoveringMenu()} |
506 <Editor |
583 <Editor |
507 ref="editor" |
584 ref="editor" |
508 spellCheck |
585 spellCheck |
509 placeholder={'Enter some rich text...'} |
586 autoFocus |
|
587 placeholder={'Votre espace de prise de note...'} |
510 schema={schema} |
588 schema={schema} |
511 plugins={plugins} |
589 plugins={plugins} |
512 state={this.state.state} |
590 value={this.state.value} |
513 onChange={this.onChange} |
591 onChange={this.onChange} |
514 onKeyDown={this.onKeyDown} |
592 onKeyDown={this.onKeyDown} |
|
593 renderMark={this.renderMark} |
|
594 renderNode = {this.renderNode} |
515 /> |
595 /> |
|
596 </div> |
516 </div> |
597 </div> |
517 ) |
598 ) |
518 } |
599 } |
519 |
600 |
520 renderHoveringMenu = () => { |
601 renderHoveringMenu = () => { |