Repository: nhn/tui.editor
Pull request #1700 (2021-07-27 02:29:11)

fix: smart task plugin emit script error
<!-- EDIT TITLE PLEASE -->
<!-- It should be one of them
  <ISSUE TYPE>: Short Description (<CLOSING TYPE> #<ISSUE NUMBERS>)
  ex)
  feat: add new feature (close #111)
  fix: wrong behavior (fix #111)
  chore: change build tool (ref #111)
-->

<!-- SPECIFY A ISSUE TYPE AT HEAD
  feat: A new feature
  fix: A bug fix
  docs: Documentation only changes
  style: Changes that do not affect the meaning of the code (white-space, formatting etc)
  refactor: A code change that neither fixes a bug or adds a feature
  perf: A code change that improves performance
  test: Adding missing tests
  chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

<!-- ADD CLOSING TYPE AND ISSUE NUMBER AT TAIL
  (<CLOSING TYPE> #<ISSUE NUMBERS>)
  close: resolve not a bug(feature, docs, etc) completely
  fix: resolve a bug completely
  ref: not fully resolved or related to
-->

### Please check if the PR fulfills these requirements
- [x] It's the right issue type on the title
- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] The commit message follows our guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [ ] Docs have been added/updated (for bug fixes/features)
- [ ] It does not introduce a breaking change or has a description of the breaking change

### Description
* fixed smart task plugin emit script error
* related issue(#1608)


---
Thank you for your contribution to TOAST UI product. 🎉 😘 ✨



Patch:
diff --git a/apps/editor/src/convertors/toWysiwyg/htmlToWwConvertors.ts b/apps/editor/src/convertors/toWysiwyg/htmlToWwConvertors.ts
index 025508c0..53d3ca8f 100644
--- a/apps/editor/src/convertors/toWysiwyg/htmlToWwConvertors.ts
+++ b/apps/editor/src/convertors/toWysiwyg/htmlToWwConvertors.ts
@@ -7,7 +7,7 @@ import {
   ToWwConvertorState,
 } from '@t/convertor';
 import { includes } from '@/utils/common';
-import { reHTMLTag } from '@/utils/constants';
+import { CLOSE_TAG, reHTMLTag } from '@/utils/constants';
 
 export function getTextWithoutTrailingNewline(text: string) {
   return text[text.length - 1] === '\n' ? text.slice(0, text.length - 1) : text;
@@ -159,21 +159,22 @@ const convertors: HTMLToWwConvertorMap = {
 
   br: (state, node) => {
     const { paragraph } = state.schema.nodes;
+    const { parent, prev, next } = node;
 
-    if (node.parent?.type === 'paragraph') {
-      if (node.prev) {
+    if (parent?.type === 'paragraph') {
+      if (prev && !(prev.type === 'htmlInline' && prev.literal!.match(CLOSE_TAG))) {
         state.openNode(paragraph);
       }
 
-      if (node.next) {
+      if (next) {
         state.closeNode();
       }
-    } else if (node.parent?.type === 'tableCell') {
-      if (node.prev && (isInlineNode(node.prev) || isCustomHTMLInlineNode(state, node.prev))) {
+    } else if (parent?.type === 'tableCell') {
+      if (prev && (isInlineNode(prev) || isCustomHTMLInlineNode(state, prev))) {
         state.closeNode();
       }
 
-      if (node.next && (isInlineNode(node.next) || isCustomHTMLInlineNode(state, node.next))) {
+      if (next && (isInlineNode(next) || isCustomHTMLInlineNode(state, next))) {
         state.openNode(paragraph);
       }
     }
diff --git a/apps/editor/src/css/editor.css b/apps/editor/src/css/editor.css
index aa02db62..3a6272da 100644
--- a/apps/editor/src/css/editor.css
+++ b/apps/editor/src/css/editor.css
@@ -125,6 +125,8 @@
 
 .toastui-editor-ww-container > .toastui-editor {
   height: inherit;
+  position: absolute;
+  width: 100%;
 }
 
 .toastui-editor-ww-container .toastui-editor-contents {
diff --git a/apps/editor/src/i18n/ru-ru.ts b/apps/editor/src/i18n/ru-ru.ts
index 6d7c591e..cbcddd71 100644
--- a/apps/editor/src/i18n/ru-ru.ts
+++ b/apps/editor/src/i18n/ru-ru.ts
@@ -1,39 +1,40 @@
 /**
  * @fileoverview I18N for Russian
  * @author Stepan Samko <stpnsamko@gmail.com>
+ * @author Veaceslav Grimalschi <grimalschi@yandex.ru>
  */
 import Editor from '../editorCore';
 
 Editor.setLanguage(['ru', 'ru-RU'], {
   Markdown: 'Markdown',
   WYSIWYG: 'WYSIWYG',
-  Write: 'Написать',
-  Preview: 'Предварительный просмотр',
+  Write: 'Редактор',
+  Preview: 'Просмотр',
   Headings: 'Заголовки',
   Paragraph: 'Абзац',
   Bold: 'Жирный',
   Italic: 'Курсив',
   Strike: 'Зачеркнутый',
-  Code: 'Встроенный код',
-  Line: 'Строка',
-  Blockquote: 'Блок цитирования',
+  Code: 'Код',
+  Line: 'Линия',
+  Blockquote: 'Цитата',
   'Unordered list': 'Неупорядоченный список',
   'Ordered list': 'Упорядоченный список',
-  Task: 'Задача',
-  Indent: 'отступ',
-  Outdent: 'Выступ',
+  Task: 'Галочка',
+  Indent: 'Увеличить отступ',
+  Outdent: 'Уменьшить отступ',
   'Insert link': 'Вставить ссылку',
-  'Insert CodeBlock': 'Вставить код',
+  'Insert CodeBlock': 'Вставить блок кода',
   'Insert table': 'Вставить таблицу',
   'Insert image': 'Вставить изображение',
   Heading: 'Заголовок',
   'Image URL': 'URL изображения',
   'Select image file': 'Выбрать файл изображения',
-  'Choose a file': 'Выберите файл',
+  'Choose a file': 'Выбрать',
   'No file': 'Нет файла',
   Description: 'Описание',
   OK: 'Хорошо',
-  More: 'еще',
+  More: 'Еще',
   Cancel: 'Отмена',
   File: 'Файл',
   URL: 'URL',
@@ -50,7 +51,7 @@ Editor.setLanguage(['ru', 'ru-RU'], {
   'Remove table': 'Удалить таблицу',
   'Would you like to paste as table?': 'Вы хотите вставить в виде таблицы?',
   'Text color': 'Цвет текста',
-  'Auto scroll enabled': 'Автоматическая прокрутка включена',
-  'Auto scroll disabled': 'Автоматическая прокрутка отключена',
+  'Auto scroll enabled': 'Автопрокрутка включена',
+  'Auto scroll disabled': 'Автопрокрутка отключена',
   'Choose language': 'Выбрать язык',
 });
diff --git a/apps/editor/src/markdown/plugins/smartTask.ts b/apps/editor/src/markdown/plugins/smartTask.ts
index 536646a7..2b21d0e3 100644
--- a/apps/editor/src/markdown/plugins/smartTask.ts
+++ b/apps/editor/src/markdown/plugins/smartTask.ts
@@ -26,7 +26,7 @@ export function smartTask({ schema, toastMark }: MdContext) {
               (node) => node!.type === 'paragraph' && node.parent?.type === 'item'
             );
 
-            if (paraNode && paraNode.firstChild) {
+            if (paraNode?.firstChild?.literal) {
               const { firstChild } = paraNode;
               const matched = firstChild.literal!.match(reTaskMarker);
 
diff --git a/apps/editor/src/sanitizer/htmlSanitizer.ts b/apps/editor/src/sanitizer/htmlSanitizer.ts
index f2471d94..d44a1ac5 100644
--- a/apps/editor/src/sanitizer/htmlSanitizer.ts
+++ b/apps/editor/src/sanitizer/htmlSanitizer.ts
@@ -68,7 +68,11 @@ const tagBlacklist = [...DEFAULT_TAG_BLACK_LIST];
 
 export function registerTagWhitelistIfPossible(tagName: string) {
   if (includes(CAN_BE_TAG_WHITE_LIST, tagName)) {
-    tagBlacklist.splice(tagBlacklist.indexOf(tagName), 1);
+    const index = tagBlacklist.indexOf(tagName);
+
+    if (index > -1) {
+      tagBlacklist.splice(index, 1);
+    }
   }
 }
 
diff --git a/apps/editor/src/ui/components/popup.ts b/apps/editor/src/ui/components/popup.ts
index 8da3d67e..e0b5b48d 100644
--- a/apps/editor/src/ui/components/popup.ts
+++ b/apps/editor/src/ui/components/popup.ts
@@ -1,6 +1,7 @@
 import { ExecCommand, HidePopup, PopupInfo, Pos } from '@t/ui';
 import { Emitter } from '@t/event';
 import { closest, cls } from '@/utils/dom';
+import { shallowEqual } from '@/utils/common';
 import html from '../vdom/template';
 import { Component } from '../vdom/component';
 
@@ -16,7 +17,13 @@ interface Props {
   execCommand: ExecCommand;
 }
 
-export class Popup extends Component<Props> {
+interface State {
+  popupPos: Pos | null;
+}
+
+const MARGIN_FROM_RIGHT_SIDE = 20;
+
+export class Popup extends Component<Props, State> {
   private handleMousedown = (ev: MouseEvent) => {
     if (
       !closest(ev.target as HTMLElement, `.${cls('popup')}`) &&
@@ -34,18 +41,39 @@ export class Popup extends Component<Props> {
     document.removeEventListener('mousedown', this.handleMousedown);
   }
 
-  render() {
-    const { info, show, hidePopup, eventEmitter, execCommand } = this.props;
-    const { className = '', style, render, pos, initialValues = {} } = info || {};
-    const popupStyle: PopupStyle = { display: show ? 'block' : 'none', ...style };
+  updated(prevProps: Props) {
+    const { show, info } = this.props;
 
-    if (pos) {
-      popupStyle.left = pos.left;
-      popupStyle.top = pos.top;
+    if (show && info.pos && prevProps.show !== show) {
+      const popupPos = { ...info.pos };
+      const { offsetWidth } = this.refs.el;
+      const toolbarEl = closest(this.refs.el, `.${cls('toolbar')}`) as HTMLElement;
+      const { offsetWidth: toolbarOffsetWidth } = toolbarEl;
+
+      if (popupPos.left + offsetWidth >= toolbarOffsetWidth) {
+        popupPos.left = toolbarOffsetWidth - offsetWidth - MARGIN_FROM_RIGHT_SIDE;
+      }
+      if (!shallowEqual(this.state.popupPos, popupPos)) {
+        this.setState({ popupPos });
+      }
     }
+  }
+
+  render() {
+    const { info, show, hidePopup, eventEmitter, execCommand } = this.props;
+    const { className = '', style, render, initialValues = {} } = info || {};
+    const popupStyle: PopupStyle = {
+      display: show ? 'block' : 'none',
+      ...style,
+      ...this.state.popupPos,
+    };
 
     return html`
-      <div class="${cls('popup')} ${className}" style=${popupStyle}>
+      <div
+        class="${cls('popup')} ${className}"
+        style=${popupStyle}
+        ref=${(el: HTMLElement) => (this.refs.el = el)}
+      >
         <div class="${cls('popup-body')}">
           ${render && render({ eventEmitter, show, hidePopup, execCommand, initialValues })}
         </div>
diff --git a/apps/editor/src/utils/common.ts b/apps/editor/src/utils/common.ts
index 9869973a..1022463f 100644
--- a/apps/editor/src/utils/common.ts
+++ b/apps/editor/src/utils/common.ts
@@ -68,7 +68,7 @@ function isNil(value: unknown): value is null | undefined {
   return isNull(value) || isUndefined(value);
 }
 
-export function shallowEqual(o1: Record<string, any>, o2: Record<string, any>) {
+export function shallowEqual(o1: Record<string, any> | null, o2: Record<string, any> | null) {
   if (o1 === null && o1 === o2) {
     return true;
   }


Test patch:
diff --git a/apps/editor/src/__test__/unit/convertor.spec.ts b/apps/editor/src/__test__/unit/convertor.spec.ts
index 0f929d34..471ed38a 100644
--- a/apps/editor/src/__test__/unit/convertor.spec.ts
+++ b/apps/editor/src/__test__/unit/convertor.spec.ts
@@ -340,7 +340,7 @@ describe('Convertor', () => {
       assertConverting(markdown, expected);
     });
 
-    it('<br> html string', () => {
+    it('<br>', () => {
       const markdown = source`
         foo
         <br>
@@ -369,7 +369,7 @@ describe('Convertor', () => {
       assertConverting(markdown, expected);
     });
 
-    it('<br> html string with soft break', () => {
+    it('<br> with soft break', () => {
       const markdown = source`
         foo
 
@@ -414,6 +414,21 @@ describe('Convertor', () => {
 
       assertConverting(markdown, expected);
     });
+
+    it('<br> with html inline node ', () => {
+      const markdown = source`
+        foo
+        bar
+        Para       <b>Word</b><br>
+      `;
+      const expected = source`
+        foo
+        bar
+        Para       <b>Word</b>
+      `;
+
+      assertConverting(markdown, expected);
+    });
   });
 
   describe('convert inline html', () => {
diff --git a/apps/editor/src/__test__/unit/markdown/smartTask.spec.ts b/apps/editor/src/__test__/unit/markdown/smartTask.spec.ts
index b319e1cd..12043c61 100644
--- a/apps/editor/src/__test__/unit/markdown/smartTask.spec.ts
+++ b/apps/editor/src/__test__/unit/markdown/smartTask.spec.ts
@@ -42,4 +42,13 @@ describe('smart task', () => {
 
     expect(getTextContent(mde)).toBe('* [x] aaa');
   });
+
+  it('should not emit script error and apply smart task when cursor position is not in the task list', () => {
+    mde.setMarkdown('*  *aaa*');
+    mde.setSelection([1, 4], [1, 4]);
+
+    dispatchKeyup();
+
+    expect(getTextContent(mde)).toBe('*  *aaa*');
+  });
 });