1 import { Value } from 'slate'; |
1 import { Value } from 'slate'; |
2 import Plain from 'slate-plain-serializer'; |
2 import Plain from 'slate-plain-serializer'; |
3 import { Editor } from 'slate-react'; |
3 import { Editor } from 'slate-react'; |
4 import React from 'react'; |
4 import React from 'react'; |
5 import { Portal } from 'react-portal'; |
5 import { PortalWithState } from 'react-portal'; |
6 import { Trans, withNamespaces } from 'react-i18next'; |
6 import { Trans, withNamespaces } from 'react-i18next'; |
7 import * as R from 'ramda'; |
7 import * as R from 'ramda'; |
8 import HtmlSerializer from './HtmlSerializer'; |
8 import HtmlSerializer from './HtmlSerializer'; |
9 import AnnotationPlugin from './AnnotationPlugin'; |
9 import AnnotationPlugin from './AnnotationPlugin'; |
10 import CategoriesTooltip from './CategoriesTooltip'; |
10 import CategoriesTooltip from './CategoriesTooltip'; |
127 |
134 |
128 componentDidUpdate = () => { |
135 componentDidUpdate = () => { |
129 this.updateMenu(); |
136 this.updateMenu(); |
130 } |
137 } |
131 |
138 |
|
139 getDocumentLength = (document) => { |
|
140 return document.getBlocks().reduce((l, b) => l + b.text.length, 0) |
|
141 } |
|
142 |
132 /** |
143 /** |
133 * On change, save the new state. |
144 * On change, save the new state. |
134 * |
145 * |
135 * @param {Change} change |
146 * @param {Change} change |
136 */ |
147 */ |
137 |
148 |
138 onChange = ({value}) => { |
149 onChange = (change) => { |
|
150 |
|
151 const operationTypes = (change && change.operations) ? change.operations.map((o) => o.type).toArray() : []; |
|
152 console.log("CHANGE", change, operationTypes); |
|
153 const { value } = change; |
139 |
154 |
140 let newState = { |
155 let newState = { |
141 value: value, |
156 value: value, |
142 startedAt: this.state.startedAt |
157 startedAt: this.state.startedAt |
143 }; |
158 }; |
144 |
159 |
145 const isEmpty = value.document.length === 0; |
160 const isEmpty = this.getDocumentLength(value.document) === 0; |
146 |
161 |
147 // Reset timers when the text is empty |
162 // Reset timers when the text is empty |
148 if (isEmpty) { |
163 if (isEmpty) { |
149 Object.assign(newState, { |
164 Object.assign(newState, { |
150 startedAt: null, |
165 startedAt: null, |
158 if (!isEmpty && this.state.startedAt === null) { |
173 if (!isEmpty && this.state.startedAt === null) { |
159 Object.assign(newState, { startedAt: now() }); |
174 Object.assign(newState, { startedAt: now() }); |
160 } |
175 } |
161 |
176 |
162 const oldState = R.clone(this.state); |
177 const oldState = R.clone(this.state); |
163 this.setState(newState) |
178 |
164 |
179 const categories = value.marks.reduce((acc, mark) => { |
165 if (typeof this.props.onChange === 'function') { |
180 if(mark.type === 'category') { |
166 this.props.onChange(R.clone(this.state), oldState, newState); |
181 acc.push({ |
167 } |
182 key: mark.data.get('key'), |
|
183 name: mark.data.get('name'), |
|
184 color: mark.data.get('color'), |
|
185 text: mark.data.get('text'), |
|
186 selection: { |
|
187 start: mark.data.get('selection').start, |
|
188 end: mark.data.get('selection').end, |
|
189 }, |
|
190 comment: mark.data.get('comment') |
|
191 }) |
|
192 } |
|
193 return acc; |
|
194 }, |
|
195 []); |
|
196 |
|
197 console.log("ON CHANGE categorie", categories); |
|
198 |
|
199 newState['categories'] = categories; |
|
200 |
|
201 this.setState(newState, () => { |
|
202 if (typeof this.props.onChange === 'function') { |
|
203 this.props.onChange(R.clone(this.state), oldState, newState); |
|
204 } |
|
205 }) |
|
206 |
168 } |
207 } |
169 |
208 |
170 /** |
209 /** |
171 * Check if the current selection has a mark with `type` in it. |
210 * Check if the current selection has a mark with `type` in it. |
172 * |
211 * |
173 * @param {String} type |
212 * @param {String} type |
174 * @return {Boolean} |
213 * @return {Boolean} |
175 */ |
214 */ |
176 |
215 |
177 hasMark = type => { |
216 hasMark = type => { |
178 const { value } = this.state |
217 const { value } = this.state; |
179 return value.activeMarks.some(mark => mark.type === type) |
218 return value.activeMarks.some(mark => mark.type === type); |
180 } |
219 } |
181 |
220 |
182 /** |
221 /** |
183 * Check if the any of the currently selected blocks are of `type`. |
222 * Check if the any of the currently selected blocks are of `type`. |
184 * |
223 * |
212 return categories.delete(categoryIndex) |
251 return categories.delete(categoryIndex) |
213 } |
252 } |
214 |
253 |
215 clear = () => { |
254 clear = () => { |
216 const value = Plain.deserialize(''); |
255 const value = Plain.deserialize(''); |
217 this.onChange({value}); |
256 this.onChange({ |
|
257 value, |
|
258 }); |
218 } |
259 } |
219 |
260 |
220 focus = () => { |
261 focus = () => { |
221 if(this.editor.current) { |
262 if(this.editor) { |
222 this.editor.current.focus(); |
263 this.editor.focus(); |
223 } |
264 } |
|
265 } |
|
266 |
|
267 onClickCategoryButton = (openPortal, closePortal, isOpen, e) => { |
|
268 e.preventDefault(); |
|
269 const { categories, value } = this.state |
|
270 |
|
271 let newCategories = categories.slice(0); |
|
272 |
|
273 // Can't use toggleMark here, because it expects the same object |
|
274 // @see https://github.com/ianstormtaylor/slate/issues/873 |
|
275 if (this.hasMark('category')) { |
|
276 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
|
277 categoryMarks.forEach(mark => { |
|
278 const key = mark.data.get('key'); |
|
279 const text = mark.data.get('text'); |
|
280 |
|
281 newCategories = R.reject(category => category.key === key && category.text === text, newCategories); |
|
282 this.editor.removeMark(mark) |
|
283 }) |
|
284 this.setState({ |
|
285 value: this.editor.value, |
|
286 categories: newCategories |
|
287 }); |
|
288 closePortal(); |
|
289 } else { |
|
290 openPortal(); |
|
291 } |
|
292 // } else { |
|
293 // isOpen ? closePortal() : openPortal(); |
|
294 // } |
224 } |
295 } |
225 |
296 |
226 /** |
297 /** |
227 * When a mark button is clicked, toggle the current mark. |
298 * When a mark button is clicked, toggle the current mark. |
228 * |
299 * |
229 * @param {Event} e |
300 * @param {Event} e |
230 * @param {String} type |
301 * @param {String} type |
231 */ |
302 */ |
232 |
303 |
233 onClickMark = (e, type) => { |
304 onClickMark = (e, type) => { |
234 |
305 this.editor.toggleMark(type) |
235 e.preventDefault() |
|
236 const { value } = this.state |
|
237 let { categories } = this.state |
|
238 |
|
239 let isPortalOpen = false; |
|
240 |
|
241 if (type === 'category') { |
|
242 // Can't use toggleMark here, because it expects the same object |
|
243 // @see https://github.com/ianstormtaylor/slate/issues/873 |
|
244 if (this.hasMark('category')) { |
|
245 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
|
246 categoryMarks.forEach(mark => { |
|
247 const key = mark.data.key; |
|
248 const text = mark.data.text; |
|
249 |
|
250 categories = this.removeCategory(categories, key, text) |
|
251 const change = value.change().removeMark(mark) |
|
252 this.onChange(change) |
|
253 }) |
|
254 |
|
255 } else { |
|
256 isPortalOpen = !this.state.isPortalOpen; |
|
257 } |
|
258 } else { |
|
259 const change = value.change().toggleMark(type) |
|
260 this.onChange(change) |
|
261 } |
|
262 |
|
263 this.setState({ |
|
264 state: value.change, |
|
265 isPortalOpen: isPortalOpen, |
|
266 categories: categories |
|
267 }) |
|
268 } |
306 } |
269 |
307 |
270 /** |
308 /** |
271 * When a block button is clicked, toggle the block type. |
309 * When a block button is clicked, toggle the block type. |
272 * |
310 * |
274 * @param {String} type |
312 * @param {String} type |
275 */ |
313 */ |
276 |
314 |
277 onClickBlock = (e, type) => { |
315 onClickBlock = (e, type) => { |
278 e.preventDefault() |
316 e.preventDefault() |
279 const { value } = this.state |
317 |
280 const change = value.change() |
318 const { editor } = this; |
281 const { document } = value |
319 const { value } = editor; |
|
320 const { document } = value; |
282 |
321 |
283 // Handle everything but list buttons. |
322 // Handle everything but list buttons. |
284 if (type !== 'bulleted-list' && type !== 'numbered-list') { |
323 if (type !== 'bulleted-list' && type !== 'numbered-list') { |
285 const isActive = this.hasBlock(type) |
324 const isActive = this.hasBlock(type) |
286 const isList = this.hasBlock('list-item') |
325 const isList = this.hasBlock('list-item') |
287 |
326 |
288 if (isList) { |
327 if (isList) { |
289 change |
328 editor |
290 .setBlocks(isActive ? DEFAULT_NODE : type) |
329 .setBlocks(isActive ? DEFAULT_NODE : type) |
291 .unwrapBlock('bulleted-list') |
330 .unwrapBlock('bulleted-list') |
292 .unwrapBlock('numbered-list') |
331 .unwrapBlock('numbered-list') |
293 } |
332 } |
294 |
333 |
295 else { |
334 else { |
296 change |
335 editor |
297 .setBlocks(isActive ? DEFAULT_NODE : type) |
336 .setBlocks(isActive ? DEFAULT_NODE : type) |
298 } |
337 } |
299 } |
338 } |
300 |
339 |
301 // Handle the extra wrapping required for list buttons. |
340 // Handle the extra wrapping required for list buttons. |
304 const isType = value.blocks.some((block) => { |
343 const isType = value.blocks.some((block) => { |
305 return !!document.getClosest(block.key, parent => parent.type === type) |
344 return !!document.getClosest(block.key, parent => parent.type === type) |
306 }) |
345 }) |
307 |
346 |
308 if (isList && isType) { |
347 if (isList && isType) { |
309 change |
348 editor |
310 .setBlocks(DEFAULT_NODE) |
349 .setBlocks(DEFAULT_NODE) |
311 .unwrapBlock('bulleted-list') |
350 .unwrapBlock('bulleted-list') |
312 .unwrapBlock('numbered-list') |
351 .unwrapBlock('numbered-list') |
313 |
352 |
314 } else if (isList) { |
353 } else if (isList) { |
315 change |
354 editor |
316 .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') |
355 .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') |
317 .wrapBlock(type) |
356 .wrapBlock(type) |
318 |
357 |
319 } else { |
358 } else { |
320 change |
359 editor |
321 .setBlocks('list-item') |
360 .setBlocks('list-item') |
322 .wrapBlock(type) |
361 .wrapBlock(type) |
323 |
362 |
324 } |
363 } |
325 } |
364 } |
326 |
365 // this.onChange(change) |
327 |
366 } |
328 this.onChange(change) |
367 |
329 } |
368 onPortalOpen = () => { |
330 |
369 console.log("onPORTAL OPEN", this); |
331 onPortalOpen = (portal) => { |
370 this.updateMenu(); |
332 // When the portal opens, cache the menu element. |
371 // When the portal opens, cache the menu element. |
333 this.setState({ hoveringMenu: portal.firstChild }) |
372 // this.setState({ hoveringMenu: this.portal.firstChild }) |
334 } |
373 } |
335 |
374 |
336 onPortalClose = (portal) => { |
375 onPortalClose = (portal) => { |
337 let { value } = this.state |
376 console.log("onPORTAL CLOSE", this); |
338 |
377 // let { value } = this.state |
339 this.setState({ |
378 |
340 value: value.change, |
379 // this.setState({ |
341 isPortalOpen: false |
380 // value: value.change, |
342 }) |
381 // isPortalOpen: false |
343 } |
382 // }) |
344 |
383 } |
345 onCategoryClick = (category) => { |
384 |
346 |
385 getSelectionParams = () => { |
347 const { value, currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.state; |
386 |
348 const change = value.change() |
387 const { value } = this.editor |
|
388 const { selection } = value |
|
389 const { start, end} = selection |
|
390 |
|
391 if (selection.isCollapsed) { |
|
392 return {}; |
|
393 } |
|
394 |
|
395 const nodes = []; |
|
396 let hasStarted = false; |
|
397 let hasEnded = false; |
|
398 |
|
399 // Keep only the relevant nodes, |
|
400 // i.e. nodes which are contained within selection |
|
401 value.document.nodes.forEach((node) => { |
|
402 if (start.isInNode(node)) { |
|
403 hasStarted = true; |
|
404 } |
|
405 if (hasStarted && !hasEnded) { |
|
406 nodes.push(node); |
|
407 } |
|
408 if (end.isAtEndOfNode(node)) { |
|
409 hasEnded = true; |
|
410 } |
|
411 }); |
|
412 |
|
413 // Concatenate the nodes text |
|
414 const text = nodes.map((node) => { |
|
415 let textStart = start.isInNode(node) ? start.offset : 0; |
|
416 let textEnd = end.isInNode(node) ? end.offset : node.text.length; |
|
417 return node.text.substring(textStart,textEnd); |
|
418 }).join('\n'); |
|
419 |
|
420 return { |
|
421 currentSelectionText: text, |
|
422 currentSelectionStart: start.offset, |
|
423 currentSelectionEnd: end.offset |
|
424 }; |
|
425 } |
|
426 |
|
427 onCategoryClick = (closePortal, category) => { |
|
428 |
|
429 console.log("ON CATEGORY CLICK"); |
|
430 const { value } = this.state; |
349 let { categories } = this.state; |
431 let { categories } = this.state; |
350 |
432 |
|
433 const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(); |
|
434 |
|
435 if(!currentSelectionText) { |
|
436 closePortal(); |
|
437 return; |
|
438 } |
|
439 console.log("ACTIVE MARKS", category, currentSelectionText, currentSelectionStart, currentSelectionEnd) |
|
440 |
351 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
441 const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') |
352 categoryMarks.forEach(mark => change.removeMark(mark)); |
442 categoryMarks.forEach(mark => this.editor.removeMark(mark)); |
353 |
443 |
354 change.addMark({ |
444 this.editor.addMark({ |
355 type: 'category', |
445 type: 'category', |
356 data: { |
446 data: { |
357 text: currentSelectionText, |
447 text: currentSelectionText, |
358 selection: { |
448 selection: { |
359 start: currentSelectionStart, |
449 start: currentSelectionStart, |
360 end: currentSelectionEnd, |
450 end: currentSelectionEnd, |
361 }, |
451 }, |
362 color: category.color, |
452 color: category.color, |
363 key: category.key |
453 key: category.key, |
|
454 name: category.name, |
|
455 comment: category.comment |
364 } |
456 } |
365 }) |
457 }) |
366 |
458 |
367 Object.assign(category, { |
459 Object.assign(category, { |
368 text: currentSelectionText, |
460 text: currentSelectionText, |
369 selection: { |
461 selection: { |
370 start: currentSelectionStart, |
462 start: currentSelectionStart, |
371 end: currentSelectionEnd, |
463 end: currentSelectionEnd, |
372 }, |
464 }, |
373 }); |
465 }); |
374 categories = categories.push(category); |
466 categories.push(category); |
375 |
467 |
376 this.onChange(change) |
468 console.log("CATEGORIES", categories) |
377 |
469 |
378 this.setState({ |
470 this.setState({ |
379 value: value, |
471 categories: categories, |
380 isPortalOpen: false, |
472 value: this.editor.value |
381 categories: categories |
473 }, closePortal); |
382 }); |
|
383 } |
474 } |
384 |
475 |
385 onButtonClick = () => { |
476 onButtonClick = () => { |
386 if (typeof this.props.onButtonClick === 'function') { |
477 if (typeof this.props.onButtonClick === 'function') { |
387 this.props.onButtonClick(); |
478 this.props.onButtonClick(); |
561 <span className="material-icons">{icon}</span> |
651 <span className="material-icons">{icon}</span> |
562 </span> |
652 </span> |
563 ) |
653 ) |
564 } |
654 } |
565 |
655 |
566 // Add a `renderMark` method to render marks. |
656 /** |
567 |
657 * Render a mark-toggling toolbar button. |
568 renderMark = props => { |
|
569 const { children, mark, attributes } = props |
|
570 |
|
571 switch (mark.type) { |
|
572 case 'bold': |
|
573 return <strong {...attributes}>{children}</strong> |
|
574 case 'code': |
|
575 return <code {...attributes}>{children}</code> |
|
576 case 'italic': |
|
577 return <em {...attributes}>{children}</em> |
|
578 case 'underlined': |
|
579 return <ins {...attributes}>{children}</ins> |
|
580 default: |
|
581 return {children}; |
|
582 } |
|
583 } |
|
584 /** |
|
585 * Render a block-toggling toolbar button. |
|
586 * |
658 * |
587 * @param {String} type |
659 * @param {String} type |
588 * @param {String} icon |
660 * @param {String} icon |
589 * @return {Element} |
661 * @return {Element} |
590 */ |
662 */ |
591 |
663 |
|
664 renderCategoryButton = () => { |
|
665 const isActive = this.hasMark('category'); |
|
666 //const onMouseDown = e => this.onClickMark(e, type) |
|
667 const markActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); |
|
668 |
|
669 return ( |
|
670 <PortalWithState |
|
671 // closeOnOutsideClick |
|
672 closeOnEsc |
|
673 onOpen={this.onPortalOpen} |
|
674 onClose={this.onPortalClose} |
|
675 > |
|
676 {({ openPortal, closePortal, isOpen, portal }) => { |
|
677 console.log("PORTAL", isOpen); |
|
678 const onMouseDown = R.partial(this.onClickCategoryButton, [openPortal, closePortal, isOpen]); |
|
679 const onCategoryClick = R.partial(this.onCategoryClick, [closePortal,]); |
|
680 return ( |
|
681 <React.Fragment> |
|
682 <span className={markActivation} onMouseDown={onMouseDown} data-active={isActive}> |
|
683 <span className="material-icons">label</span> |
|
684 </span> |
|
685 {portal( |
|
686 <div className="hovering-menu" ref={this.hoveringMenuRef}> |
|
687 <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={onCategoryClick} /> |
|
688 </div> |
|
689 )} |
|
690 </React.Fragment> |
|
691 )} |
|
692 } |
|
693 </PortalWithState> |
|
694 ) |
|
695 } |
|
696 |
|
697 // Add a `renderMark` method to render marks. |
|
698 |
|
699 renderMark = (props, editor, next) => { |
|
700 const { children, mark, attributes } = props |
|
701 |
|
702 console.log("renderMark", mark, mark.type, mark.data.color); |
|
703 switch (mark.type) { |
|
704 case 'bold': |
|
705 return <strong {...attributes}>{children}</strong> |
|
706 case 'code': |
|
707 return <code {...attributes}>{children}</code> |
|
708 case 'italic': |
|
709 return <em {...attributes}>{children}</em> |
|
710 case 'underlined': |
|
711 return <ins {...attributes}>{children}</ins> |
|
712 case 'category': |
|
713 let spanStyle = { |
|
714 backgroundColor: mark.data.get('color') |
|
715 }; |
|
716 return <span {...attributes} style={ spanStyle } >{children}</span> |
|
717 default: |
|
718 return next(); |
|
719 } |
|
720 } |
|
721 /** |
|
722 * Render a block-toggling toolbar button. |
|
723 * |
|
724 * @param {String} type |
|
725 * @param {String} icon |
|
726 * @return {Element} |
|
727 */ |
|
728 |
592 renderBlockButton = (type, icon) => { |
729 renderBlockButton = (type, icon) => { |
593 let isActive = this.hasBlock(type) |
730 let isActive = this.hasBlock(type) |
594 |
731 |
595 if (['numbered-list', 'bulleted-list'].includes(type)) { |
732 if (['numbered-list', 'bulleted-list'].includes(type)) { |
596 const { value } = this.state |
733 const { value } = this.state; |
597 const parent = value.document.getParent(value.blocks.first().key) |
734 const firstBlock = value.blocks.first(); |
598 isActive = this.hasBlock('list-item') && parent && parent.type === type |
735 if(firstBlock) { |
|
736 const parent = value.document.getParent(firstBlock.key); |
|
737 isActive = this.hasBlock('list-item') && parent && parent.type === type; |
|
738 } |
599 } |
739 } |
600 const onMouseDown = e => this.onClickBlock(e, type) |
740 const onMouseDown = e => this.onClickBlock(e, type) |
601 const blockActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); |
741 const blockActivation = "button sticky-top" + ((!isActive)?" text-primary":" text-dark"); |
602 |
742 |
603 return ( |
743 return ( |
636 |
776 |
637 renderEditor = () => { |
777 renderEditor = () => { |
638 const t = this.props.t; |
778 const t = this.props.t; |
639 return ( |
779 return ( |
640 <div className="editor-slatejs p-2"> |
780 <div className="editor-slatejs p-2"> |
641 {this.renderHoveringMenu()} |
781 {/* {this.renderHoveringMenu()} */} |
642 <Editor |
782 <Editor |
643 ref={this.editor} |
783 ref={this.editorRef} |
644 spellCheck |
784 spellCheck |
645 placeholder={t('slate_editor.placeholder')} |
785 placeholder={t('slate_editor.placeholder')} |
646 schema={schema} |
786 // schema={schema} |
647 plugins={plugins} |
787 plugins={plugins} |
648 value={this.state.value} |
788 value={this.state.value} |
649 onChange={this.onChange} |
789 onChange={this.onChange} |
650 onKeyDown={this.onKeyDown} |
790 // onKeyDown={this.onKeyDown} |
651 renderMark={this.renderMark} |
791 renderMark={this.renderMark} |
652 renderNode = {this.renderNode} |
792 renderNode = {this.renderNode} |
653 /> |
793 /> |
654 </div> |
794 </div> |
655 ) |
795 ) |
656 } |
796 } |
657 |
797 |
658 renderHoveringMenu = () => { |
798 // renderHoveringMenu = () => { |
659 return ( |
799 // return ( |
660 <Portal ref="portal" |
800 // <Portal ref="portal" |
661 isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen} |
801 // isOpened={this.state.isPortalOpen} isOpen={this.state.isPortalOpen} |
662 onOpen={this.onPortalOpen} |
802 // onOpen={this.onPortalOpen} |
663 onClose={this.onPortalClose} |
803 // onClose={this.onPortalClose} |
664 closeOnOutsideClick={false} closeOnEsc={true}> |
804 // closeOnOutsideClick={false} closeOnEsc={true}> |
665 <div className="hovering-menu"> |
805 // <div className="hovering-menu"> |
666 <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={this.onCategoryClick} /> |
806 // <CategoriesTooltip categories={this.props.annotationCategories || defaultAnnotationsCategories} onCategoryClick={this.onCategoryClick} /> |
667 </div> |
807 // </div> |
668 </Portal> |
808 // </Portal> |
669 ) |
809 // ) |
670 } |
810 // } |
671 |
811 |
672 updateMenu = () => { |
812 updateMenu = () => { |
673 |
813 |
674 const { hoveringMenu } = this.state |
814 // const { hoveringMenu } = this.state |
|
815 const hoveringMenu = this.hoveringMenuRef.current; |
675 |
816 |
676 if (!hoveringMenu) return |
817 if (!hoveringMenu) return |
677 |
818 |
678 // if (state.isBlurred || state.isCollapsed) { |
819 // if (state.isBlurred || state.isCollapsed) { |
679 // hoveringMenu.removeAttribute('style') |
820 // hoveringMenu.removeAttribute('style') |