(extending_the_draftail_editor)= # Draftail 편집기 확장 Wagtail의 리치 텍스트 편집기는 다양한 유형의 확장을 지원하는 [Draftail](https://www.draftail.org/)로 구축되었습니다. ## 서식 확장 Draftail은 세 가지 유형의 서식을 지원합니다: - **인라인 스타일** – ` 굵게 `, ` 기울임꼴 ` 또는 ` 고정폭 ` 과 같이 줄의 일부를 서식 지정합니다. 텍스트는 필요한 만큼 많은 인라인 스타일을 가질 수 있습니다. 예를 들어, 동시에 굵게 _및_ 기울임꼴. - **블록** – ` 인용구 `, ` 순서 있는 목록 ` 과 같이 콘텐츠의 구조를 나타냅니다. 주어진 텍스트는 하나의 블록 유형만 가질 수 있습니다. - **엔티티** – ` 링크 `(URL 포함) 또는 ` 이미지 `(파일 포함)와 같이 추가 데이터/메타데이터를 입력합니다. 텍스트는 한 번에 하나의 엔티티만 적용할 수 있습니다. 이러한 모든 확장은 유사한 기준선으로 생성되며, 가장 간단한 예 중 하나인 `mark` 인라인 스타일의 사용자 지정 기능으로 시연할 수 있습니다. 다음을 설치된 앱의 `wagtail_hooks.py` 파일에 배치합니다: ```python import wagtail.admin.rich_text.editors.draftail.features as draftail_features from wagtail.admin.rich_text.converters.html_to_contentstate import InlineStyleElementHandler from wagtail import hooks # 1. register_rich_text_features 훅을 사용합니다. @hooks.register('register_rich_text_features') def register_mark_feature(features): """ `MARK` Draft.js 인라인 스타일 유형을 사용하고 `` 태그가 있는 HTML로 저장되는 `mark` 기능을 등록합니다. """ feature_name = 'mark' type_ = 'MARK' tag = 'mark' # 2. Draftail이 툴바에서 기능을 처리하는 방법을 구성합니다. control = { 'type': type_, 'label': '☆', 'description': 'Mark', # 이것은 필요하지도 않습니다 – Draftail에는 MARK에 대한 미리 정의된 스타일이 있습니다. # 'style': {'textDecoration': 'line-through'}, } # 3. register_editor_plugin을 호출하여 Draftail에 대한 구성을 등록합니다. features.register_editor_plugin( 'draftail', feature_name, draftail_features.InlineStyleFeature(control) ) # 4. DB에서 편집기로, 그리고 다시 편집기에서 DB로 콘텐츠 변환을 구성합니다. db_conversion = { 'from_database_format': {tag: InlineStyleElementHandler(type_)}, 'to_database_format': {'style_map': {type_: tag}}, } # 5. register_converter_rule을 호출하여 콘텐츠 변환 규칙을 등록합니다. features.register_converter_rule('contentstate', feature_name, db_conversion) # 6. (선택 사항) 명시적인 'features' 목록을 지정하지 않은 리치 텍스트 필드에서 # 사용할 수 있도록 기능을 기본 기능 목록에 추가합니다. features.default_features.append('mark') ``` 이러한 단계는 모든 Draftail 플러그인에 대해 항상 동일합니다. 중요한 부분은 다음과 같습니다: - 기능의 Draft.js 유형 또는 Wagtail 기능 이름을 적절하게 일관되게 사용합니다. - Draftail이 기능에 대한 버튼을 만들고 렌더링하는 방법을 알 수 있도록 충분한 정보를 제공합니다(자세한 내용은 나중에 설명). - 올바른 HTML 요소(DB에 저장된 대로)를 사용하도록 변환을 구성합니다. 자세한 구성 옵션은 [Draftail 문서](https://www.draftail.org/docs/formatting-options)에서 모든 세부 정보를 확인하십시오. 다음은 컨트롤에 대해 강조할 만한 몇 가지 부분입니다: - `type` 은 유일하게 필수적인 정보입니다. - 툴바에 컨트롤을 표시하려면 `icon`, `label`, `description` 을 결합합니다. - `icon` 은 [Wagtail 아이콘 라이브러리에 등록된](../../advanced_topics/icons) 아이콘 이름입니다. 예를 들어, `'icon': 'user',` 입니다. SVG 경로 또는 SVG 심볼 참조를 사용하기 위한 문자열 배열일 수도 있습니다. 예를 들어 `'icon': ['M100 100 H 900 V 900 H 100 Z'],` 입니다. 경로는 1024x1024 뷰박스에 대해 설정되어야 합니다. ### 새 인라인 스타일 만들기 초기 예제 외에도 인라인 스타일은 편집기에서 텍스트에 적용될 CSS 규칙을 정의하는 `style` 속성을 가집니다. 인라인 스타일에 대한 [Draftail 문서](https://www.draftail.org/docs/formatting-options)를 반드시 읽으십시오. 마지막으로, DB 간 변환은 `InlineStyleElementHandler` 를 사용하여 주어진 태그(위 예제의 ``)를 Draftail 유형에 매핑하고, 역 매핑은 `style_map` 의 [Draft.js exporter 구성](https://github.com/springload/draftjs_exporter)으로 수행됩니다. ### 새 블록 만들기 블록은 인라인 스타일만큼이나 간단합니다: ```python import wagtail.admin.rich_text.editors.draftail.features as draftail_features from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler from wagtail import hooks @hooks.register('register_rich_text_features') def register_help_text_feature(features): """ `help-text` Draft.js 블록 유형을 사용하고 `
` 태그가 있는 HTML로 저장되는 `help-text` 기능을 등록합니다. """ feature_name = 'help-text' type_ = 'help-text' control = { 'type': type_, 'label': '?', 'description': 'Help text', # 선택적으로, Draftail에 편집기에서 해당 블록을 표시할 때 사용할 요소를 알려줄 수 있습니다. 'element': 'div', } features.register_editor_plugin( 'draftail', feature_name, draftail_features.BlockFeature(control, css={'all': ['help-text.css']}) ) features.register_converter_rule('contentstate', feature_name, { 'from_database_format': {'div[class=help-text]': BlockElementHandler(type_)}, 'to_database_format': {'block_map': {type_: {'element': 'div', 'props': {'class': 'help-text'}}}}, }) ``` 주요 차이점은 다음과 같습니다: - `element` 를 구성하여 Draftail에 편집기에서 해당 블록을 렌더링하는 방법을 알려줄 수 있습니다. - `BlockFeature` 로 플러그인을 등록합니다. - `BlockElementHandler` 및 `block_map` 으로 변환을 설정합니다. 선택적으로, `Draftail-block--help-text` (`Draftail-block--`) CSS 클래스로 블록에 대한 스타일을 정의할 수도 있습니다. 그게 다입니다! 추가적인 복잡성은 편집기에서 블록 스타일을 지정하기 위해 CSS를 작성해야 할 수도 있다는 것입니다. (creating_new_draftail_editor_entities)= ### 새 엔티티 만들기 ```{warning} 이것은 고급 기능입니다. 정말 필요한지 신중하게 고려하십시오. ``` 엔티티는 단순히 툴바의 서식 지정 버튼이 아닙니다. 일반적으로 API와 통신하거나 추가 사용자 입력을 요청하는 등 훨씬 더 다재다능해야 합니다. 따라서, - 대부분의 경우 **상당량의 JavaScript**를 작성해야 하며, 일부는 React로 작성해야 합니다. - API는 매우 **저수준**입니다. 대부분의 경우 **Draft.js 지식**이 필요합니다. - 리치 텍스트의 사용자 지정 UI는 깨지기 쉽습니다. **여러 브라우저에서 테스트**하는 데 시간을 할애할 준비를 하십시오. 좋은 소식은 이러한 저수준 API를 통해 타사 Wagtail 플러그인이 리치 텍스트 기능에 혁신을 가져와 새로운 종류의 경험을 제공할 수 있다는 것입니다. 하지만 그동안에는 Django 개발자를 위한 검증된 API를 가진 [StreamField](../topics/streamfield)를 통해 UI를 구현하는 것을 고려하십시오. 새 엔티티 기능을 생성하기 위한 주요 요구 사항은 다음과 같습니다: - 인라인 스타일 및 블록과 마찬가지로 편집기 플러그인을 등록합니다. - 편집기 플러그인은 `source` 를 정의해야 합니다. 이는 Draft.js API를 사용하여 편집기에서 새 엔티티 인스턴스를 생성하는 React 컴포넌트입니다. - 편집기 플러그인에는 `decorator`(인라인 엔티티용) 또는 `block`(블록 엔티티용)도 필요합니다. 이는 편집기 내에서 엔티티 인스턴스를 표시하는 React 컴포넌트입니다. - 인라인 스타일 및 블록과 마찬가지로 DB 간 변환을 설정합니다. - 엔티티에는 HTML로 직렬화해야 하는 데이터가 포함되어 있으므로 변환이 더 복잡한 경우가 많습니다. React 컴포넌트를 작성하려면 Wagtail은 자체 React, Draft.js 및 Draftail 종속성을 전역 변수로 노출합니다. 이에 대한 자세한 내용은 [클라이언트 측 React 컴포넌트 확장](extending_client_side_react)에서 읽어보십시오. 더 나아가려면 [Draftail 문서](https://www.draftail.org/docs/formatting-options)와 [Draft.js exporter 문서](https://github.com/springload/draftjs_exporter)를 참조하십시오. 다음은 Wagtail 컨텍스트에서 이러한 도구가 어떻게 사용되는지 보여주는 자세한 예입니다. 예를 들어, 금융 신문에서 일하는 뉴스 팀을 상상해 볼 수 있습니다. 그들은 주식 시장에 대한 기사를 작성하고, 콘텐츠 내 어디에서든 특정 주식(예: 문장의 "$NEE" 토큰)을 참조한 다음, 기사에 주식 정보(링크, 숫자, 스파크라인)가 자동으로 풍부해지기를 원합니다. 편집기 툴바에는 사용 가능한 주식 목록을 표시한 다음 사용자의 선택을 텍스트 토큰으로 삽입하는 "주식 선택기"가 포함될 수 있습니다. 이 예에서는 임의의 주식을 선택합니다: ```{eval-rst} .. image:: ../_static/images/draftail_entity_stock_source.* ``` 이러한 토큰은 게시 시 리치 텍스트에 저장됩니다. 뉴스 기사가 사이트에 표시되면 각 토큰 옆에 API에서 가져온 실시간 시장 데이터를 삽입합니다: ![Draftail 엔티티 주식 렌더링](../_static/images/draftail_entity_stock_rendering.png) 이를 달성하기 위해 인라인 스타일 및 블록과 마찬가지로 리치 텍스트 기능을 등록하는 것으로 시작합니다: ```python @hooks.register('register_rich_text_features') def register_stock_feature(features): features.default_features.append('stock') """ `STOCK` Draft.js 엔티티 유형을 사용하고 `` 태그가 있는 HTML로 저장되는 `stock` 기능을 등록합니다. """ feature_name = 'stock' type_ = 'STOCK' control = { 'type': type_, 'label': '$', 'description': 'Stock', } features.register_editor_plugin( 'draftail', feature_name, draftail_features.EntityFeature( control, js=['stock.js'], css={'all': ['stock.css']} ) ) features.register_converter_rule('contentstate', feature_name, { # 여기에서 변환이 블록 및 인라인 스타일보다 더 복잡하다는 점에 유의하십시오. 'from_database_format': {'span[data-stock]': StockEntityElementHandler(type_)}, 'to_database_format': {'entity_decorators': {type_: stock_entity_decorator}}, }) ``` `EntityFeature` 의 `js` 및 `css` 키워드 인수는 이 기능이 활성화될 때 로드할 추가 JS 및 CSS 파일을 지정하는 데 사용할 수 있습니다. 둘 다 선택 사항입니다. 해당 값은 `Media` 객체에 추가되며, 이러한 객체에 대한 자세한 문서는 [Django 폼 자산 문서](inv:django#topics/forms/media)에서 확인할 수 있습니다. 엔티티는 데이터를 보유하므로 데이터베이스 형식으로의 변환이 더 복잡합니다. 두 개의 핸들러를 생성해야 합니다: ```python from draftjs_exporter.dom import DOM from wagtail.admin.rich_text.converters.html_to_contentstate import InlineEntityElementHandler def stock_entity_decorator(props): """ Draft.js ContentState를 데이터베이스 HTML로 변환합니다. STOCK 엔티티를 span 태그로 변환합니다. """ return DOM.create_element('span', { 'data-stock': props['stock'], }, props['children']) class StockEntityElementHandler(InlineEntityElementHandler): """ 데이터베이스 HTML을 Draft.js ContentState로 변환합니다. span 태그를 올바른 데이터가 있는 STOCK 엔티티로 변환합니다. """ mutability = 'IMMUTABLE' def get_attribute_data(self, attrs): """ `data-stock` HTML 속성에서 `stock` 값을 가져옵니다. """ return { 'stock': attrs['data-stock'] } ``` 두 핸들러가 유사한 변환을 수행하지만 다른 API를 사용한다는 점에 유의하십시오. `to_database_format` 은 [Draft.js exporter](https://github.com/springload/draftjs_exporter) 컴포넌트 API로 구축되었고, `from_database_format` 은 Wagtail API를 사용합니다. 다음 단계는 엔티티가 생성되는 방법(`source`)과 표시되는 방법(`decorator`)을 정의하는 JavaScript를 추가하는 것입니다. `stock.js` 내에서 소스 컴포넌트를 정의합니다: ```javascript // 실제 React 컴포넌트가 아닙니다. 렌더링되는 즉시 엔티티를 생성합니다. class StockSource extends window.React.Component { componentDidMount() { const { editorState, entityType, onComplete } = this.props; const content = editorState.getCurrentContent(); const selection = editorState.getSelection(); const demoStocks = ['AMD', 'AAPL', 'NEE', 'FSLR']; const randomStock = demoStocks[Math.floor(Math.random() * demoStocks.length)]; // 올바른 데이터로 새 엔티티를 생성하기 위해 Draft.js API를 사용합니다. const contentWithEntity = content.createEntity( entityType.type, 'IMMUTABLE', { stock: randomStock }, ); const entityKey = contentWithEntity.getLastCreatedEntityKey(); // 엔티티가 활성화될 텍스트도 추가합니다. const text = `$${randomStock}`; const newContent = window.DraftJS.Modifier.replaceText( content, selection, text, null, entityKey, ); const nextState = window.DraftJS.EditorState.push( editorState, newContent, 'insert-characters', ); onComplete(nextState); } render() { return null; } } ``` 이 소스 컴포넌트는 [Draftail](https://www.draftail.org/docs/api)에서 제공하는 데이터와 콜백을 사용합니다. 또한 전역 변수에서 종속성을 사용합니다. - [클라이언트 측 React 컴포넌트 확장](extending_client_side_react)을 참조하십시오. 그런 다음 데코레이터 컴포넌트를 생성합니다: ```javascript const Stock = (props) => { const { entityKey, contentState } = props; const data = contentState.getEntity(entityKey).getData(); return window.React.createElement( 'a', { role: 'button', onMouseUp: () => { window.open(`https://finance.yahoo.com/quote/${data.stock}`); }, }, props.children, ); }; ``` 이것은 간단한 React 컴포넌트입니다. JavaScript에 빌드 단계를 사용할 필요가 없으므로 JSX를 사용하지 않습니다. 마지막으로, 플러그인의 JS 컴포넌트를 등록합니다: ```javascript // 스크립트 실행 시 플러그인을 직접 등록하여 편집기가 초기화될 때 로드되도록 합니다. window.draftail.registerPlugin({ type: 'STOCK', source: StockSource, decorator: Stock, }, 'entityTypes'); ``` 그게 다입니다! 이 모든 설정은 마침내 사이트의 프런트엔드에 다음 HTML을 생성합니다: ```html

NextEra 기술 $NEE를 따르는 사람은 $FSLR도 살펴보아야 합니다.

``` 데모를 완전히 완료하려면, 링크와 작은 스파크라인으로 토큰을 장식하기 위해 프런트엔드에 약간의 JavaScript를 추가할 수 있습니다. ```javascript document.querySelectorAll('[data-stock]').forEach((elt) => { const link = document.createElement('a'); link.href = `https://finance.yahoo.com/quote/${elt.dataset.stock}`; link.innerHTML = `${elt.innerHTML}`; elt.innerHTML = ''; elt.appendChild(link); }); ``` 사용자 지정 블록 엔티티도 생성할 수 있지만(별도의 [Draftail 문서](https://www.draftail.org/docs/blocks) 참조), [StreamField](streamfield_topic)가 Wagtail에서 블록 수준 리치 텍스트를 생성하는 데 가장 적합한 방법이므로 여기서는 자세히 설명하지 않습니다. (extending_the_draftail_editor_advanced)= ## 기타 편집기 확장 Draftail은 더 복잡한 사용자 지정을 위한 추가 API를 제공합니다: - **컨트롤** – 편집기 툴바에 임의의 UI 요소를 추가합니다. - **데코레이터** – 임의의 텍스트 장식/강조 표시를 위한 것입니다. - **플러그인** – 모든 Draft.js API에 직접 액세스하기 위한 것입니다. ### 사용자 지정 툴바 컨트롤 편집기 툴바에 임의의 새 UI 요소를 추가하기 위해 Draftail은 [컨트롤 API](https://www.draftail.org/docs/arbitrary-controls)를 제공합니다. 컨트롤은 편집기 상태를 가져오고 설정할 수 있는 임의의 React 컴포넌트일 수 있습니다. 컨트롤은 편집기에서 _모든 키 입력_에 대해 업데이트되므로 빠르게 렌더링되는지 확인하십시오! 다음은 간단한 문장 카운터의 예입니다. 먼저 `wagtail_hooks.py` 에 편집기 기능을 등록합니다: ```python from wagtail.admin.rich_text.editors.draftail.features import ControlFeature from wagtail import hooks @hooks.register('register_rich_text_features') def register_sentences_counter(features): feature_name = 'sentences' features.default_features.append(feature_name) features.register_editor_plugin( 'draftail', feature_name, ControlFeature({ 'type': feature_name, }, js=['draftail_sentences.js'], ), ) ``` 그런 다음, `draftail_sentences.js` 는 편집기의 "메타" 하단 툴바에 렌더링될 React 컴포넌트를 선언합니다: ```javascript const countSentences = (str) => str ? (str.match(/[.?!…]+./g) || []).length + 1 : 0; const SentenceCounter = ({ getEditorState }) => { const editorState = getEditorState(); const content = editorState.getCurrentContent(); const text = content.getPlainText(); return window.React.createElement('div', { className: 'w-inline-block w-tabular-nums w-help-text w-mr-4', }, ` 문장: ${countSentences(text)}`); } window.draftail.registerPlugin({ type: 'sentences', meta: SentenceCounter, }, 'controls'); ``` ```{note} 이 새로운 'sentences' 기능을 사용할 수 있도록 `WAGTAILADMIN_RICH_TEXT_EDITORS` 설정에 설정된 사용자 지정 Draft 구성에 이 기능을 포함해야 합니다. ``` 예를 들어: ```python WAGTAILADMIN_RICH_TEXT_EDITORS = { 'default': { 'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea', 'OPTIONS': { 'features': ['bold', 'italic', 'link', 'sentences'], # 여기에 'sentences' 추가 }, }, } ``` ### 텍스트 데코레이터 [데코레이터 API](https://www.draftail.org/docs/decorators)는 Draftail / Draft.js가 편집기에서 특수 서식으로 텍스트를 강조 표시하는 방법을 지원하는 방법입니다. 이는 [CompositeDecorator](https://draftjs.org/docs/advanced-topics-decorators/#compositedecorator) API를 사용하며, 각 항목에는 어떤 텍스트를 대상으로 할지 결정하는 `strategy` 함수와 장식을 렌더링하는 `component` 함수가 있습니다. 이 API를 사용할 때 두 가지 중요한 고려 사항이 있습니다: - 순서가 중요합니다: 편집기에서 문자당 하나의 데코레이터만 렌더링할 수 있습니다. 여기에는 장식으로 렌더링되는 모든 엔티티가 포함됩니다. - 성능상의 이유로 Draft.js는 현재 포커스된 텍스트 줄에 있는 데코레이터만 다시 렌더링합니다. 다음은 문제가 있는 구두점 강조 표시의 예입니다. 먼저 `wagtail_hooks.py` 에 편집기 기능을 등록합니다: ```python from wagtail.admin.rich_text.editors.draftail.features import DecoratorFeature from wagtail import hooks @hooks.register('register_rich_text_features') def register_punctuation_highlighter(features): feature_name = 'punctuation' features.default_features.append(feature_name) features.register_editor_plugin( 'draftail', feature_name, DecoratorFeature({ 'type': feature_name, }, js=['draftail_punctuation.js'], ), ) ``` 그런 다음, `draftail_punctuation.js` 는 전략과 강조 표시 컴포넌트를 정의합니다: ```javascript const PUNCTUATION = /(\.\.\.|!!|\?!)/g; const punctuationStrategy = (block, callback) => { const text = block.getText(); let matches; while ((matches = PUNCTUATION.exec(text)) !== null) { callback(matches.index, matches.index + matches[0].length); } }; const errorHighlight = { color: 'var(--w-color-text-error)', outline: '1px solid currentColor', } const PunctuationHighlighter = ({ children }) => ( window.React.createElement('span', { style: errorHighlight, title: 'refer to our styleguide' }, children) ); window.draftail.registerPlugin({ type: 'punctuation', strategy: punctuationStrategy, component: PunctuationHighlighter, }, 'decorators'); ``` ### 임의의 플러그인 ```{warning} 이것은 고급 기능입니다. 정말 필요한지 신중하게 고려하십시오. ``` Draftail은 [Draft.js 플러그인](https://www.draft-js-plugins.com/) 아키텍처를 따르는 플러그인을 지원합니다. 이러한 플러그인은 편집기를 위한 가장 고급스럽고 강력한 유형의 확장으로, 사용자 지정 Draft.js 편집기로 가능한 것과 동일한 사용자 지정 기능을 제공합니다. 이 API가 도움이 될 수 있는 일반적인 시나리오는 맞춤형 복사-붙여넣기 처리를 추가하는 것입니다. 다음은 URL 앵커 해시 참조를 링크로 자동 변환하는 간단한 예입니다. 먼저 Python에서 확장을 등록합니다: ```python @hooks.register('register_rich_text_features') def register_anchorify(features): feature_name = 'anchorify' features.default_features.append(feature_name) features.register_editor_plugin( 'draftail', feature_name, PluginFeature({ 'type': feature_name, }, js=['draftail_anchorify.js'], ), ) ``` 그런 다음, `draftail_anchorify.js` 에서: ```javascript const anchorifyPlugin = { type: 'anchorify', handlePastedText(text, html, editorState, { setEditorState }) { let nextState = editorState; if (text.match(/^#[a-zA-Z0-9_-]+$/ig)) { const selection = nextState.getSelection(); let content = nextState.getCurrentContent(); content = content.createEntity("LINK", "MUTABLE", { url: text }); const entityKey = content.getLastCreatedEntityKey(); if (selection.isCollapsed()) { content = window.DraftJS.Modifier.insertText( content, selection, text, undefined, entityKey, ) nextState = window.DraftJS.EditorState.push( nextState, content, "insert-fragment", ); } else { nextState = window.DraftJS.RichUtils.toggleLink(nextState, selection, entityKey); } setEditorState(nextState); return "handled"; } return "not-handled"; }, }; window.draftail.registerPlugin(anchorifyPlugin, 'plugins'); ``` ## Draftail 위젯 통합 Draftail 위젯이 UI에 통합되는 방식을 추가로 사용자 지정하기 위해 CSS 및 JS에 대한 추가 확장 지점이 있습니다: - JavaScript에서 `[data-draftail-input]` 속성 선택기를 사용하여 데이터를 포함하는 입력을 대상으로 하고, `[data-draftail-editor-wrapper]` 를 사용하여 편집기를 감싸는 요소를 대상으로 합니다. - 편집기 인스턴스는 명령형 액세스를 위해 입력 필드에 바인딩됩니다. `document.querySelector('[data-draftail-input]').draftailEditor` 를 사용하십시오. - CSS에서 `Draftail-` 접두사가 붙은 클래스를 사용하십시오.