Draftail 편집기 확장¶
Wagtail의 리치 텍스트 편집기는 다양한 유형의 확장을 지원하는 Draftail로 구축되었습니다.
서식 확장¶
Draftail은 세 가지 유형의 서식을 지원합니다:
인라인 스타일 –
굵게,기울임꼴또는고정폭과 같이 줄의 일부를 서식 지정합니다. 텍스트는 필요한 만큼 많은 인라인 스타일을 가질 수 있습니다. 예를 들어, 동시에 굵게 및 기울임꼴.블록 –
인용구,순서 있는 목록과 같이 콘텐츠의 구조를 나타냅니다. 주어진 텍스트는 하나의 블록 유형만 가질 수 있습니다.엔티티 –
링크(URL 포함) 또는이미지(파일 포함)와 같이 추가 데이터/메타데이터를 입력합니다. 텍스트는 한 번에 하나의 엔티티만 적용할 수 있습니다.
이러한 모든 확장은 유사한 기준선으로 생성되며, 가장 간단한 예 중 하나인 mark 인라인 스타일의 사용자 지정 기능으로 시연할 수 있습니다. 다음을 설치된 앱의 wagtail_hooks.py 파일에 배치합니다:
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 인라인 스타일 유형을 사용하고
`<mark>` 태그가 있는 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 문서에서 모든 세부 정보를 확인하십시오. 다음은 컨트롤에 대해 강조할 만한 몇 가지 부분입니다:
type은 유일하게 필수적인 정보입니다.툴바에 컨트롤을 표시하려면
icon,label,description을 결합합니다.icon은 Wagtail 아이콘 라이브러리에 등록된 아이콘 이름입니다. 예를 들어,'icon': 'user',입니다. SVG 경로 또는 SVG 심볼 참조를 사용하기 위한 문자열 배열일 수도 있습니다. 예를 들어'icon': ['M100 100 H 900 V 900 H 100 Z'],입니다. 경로는 1024x1024 뷰박스에 대해 설정되어야 합니다.
새 인라인 스타일 만들기¶
초기 예제 외에도 인라인 스타일은 편집기에서 텍스트에 적용될 CSS 규칙을 정의하는 style 속성을 가집니다. 인라인 스타일에 대한 Draftail 문서를 반드시 읽으십시오.
마지막으로, DB 간 변환은 InlineStyleElementHandler 를 사용하여 주어진 태그(위 예제의 <mark>)를 Draftail 유형에 매핑하고, 역 매핑은 style_map 의 Draft.js exporter 구성으로 수행됩니다.
새 블록 만들기¶
블록은 인라인 스타일만큼이나 간단합니다:
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 블록 유형을 사용하고
`<div class="help-text">` 태그가 있는 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--<block type>) CSS 클래스로 블록에 대한 스타일을 정의할 수도 있습니다.
그게 다입니다! 추가적인 복잡성은 편집기에서 블록 스타일을 지정하기 위해 CSS를 작성해야 할 수도 있다는 것입니다.
새 엔티티 만들기¶
경고
이것은 고급 기능입니다. 정말 필요한지 신중하게 고려하십시오.
엔티티는 단순히 툴바의 서식 지정 버튼이 아닙니다. 일반적으로 API와 통신하거나 추가 사용자 입력을 요청하는 등 훨씬 더 다재다능해야 합니다. 따라서,
대부분의 경우 상당량의 JavaScript를 작성해야 하며, 일부는 React로 작성해야 합니다.
API는 매우 저수준입니다. 대부분의 경우 Draft.js 지식이 필요합니다.
리치 텍스트의 사용자 지정 UI는 깨지기 쉽습니다. 여러 브라우저에서 테스트하는 데 시간을 할애할 준비를 하십시오.
좋은 소식은 이러한 저수준 API를 통해 타사 Wagtail 플러그인이 리치 텍스트 기능에 혁신을 가져와 새로운 종류의 경험을 제공할 수 있다는 것입니다. 하지만 그동안에는 Django 개발자를 위한 검증된 API를 가진 StreamField를 통해 UI를 구현하는 것을 고려하십시오.
새 엔티티 기능을 생성하기 위한 주요 요구 사항은 다음과 같습니다:
인라인 스타일 및 블록과 마찬가지로 편집기 플러그인을 등록합니다.
편집기 플러그인은
source를 정의해야 합니다. 이는 Draft.js API를 사용하여 편집기에서 새 엔티티 인스턴스를 생성하는 React 컴포넌트입니다.편집기 플러그인에는
decorator(인라인 엔티티용) 또는block(블록 엔티티용)도 필요합니다. 이는 편집기 내에서 엔티티 인스턴스를 표시하는 React 컴포넌트입니다.인라인 스타일 및 블록과 마찬가지로 DB 간 변환을 설정합니다.
엔티티에는 HTML로 직렬화해야 하는 데이터가 포함되어 있으므로 변환이 더 복잡한 경우가 많습니다.
React 컴포넌트를 작성하려면 Wagtail은 자체 React, Draft.js 및 Draftail 종속성을 전역 변수로 노출합니다. 이에 대한 자세한 내용은 클라이언트 측 React 컴포넌트 확장에서 읽어보십시오. 더 나아가려면 Draftail 문서와 Draft.js exporter 문서를 참조하십시오.
다음은 Wagtail 컨텍스트에서 이러한 도구가 어떻게 사용되는지 보여주는 자세한 예입니다. 예를 들어, 금융 신문에서 일하는 뉴스 팀을 상상해 볼 수 있습니다. 그들은 주식 시장에 대한 기사를 작성하고, 콘텐츠 내 어디에서든 특정 주식(예: 문장의 “$NEE” 토큰)을 참조한 다음, 기사에 주식 정보(링크, 숫자, 스파크라인)가 자동으로 풍부해지기를 원합니다.
편집기 툴바에는 사용 가능한 주식 목록을 표시한 다음 사용자의 선택을 텍스트 토큰으로 삽입하는 “주식 선택기”가 포함될 수 있습니다. 이 예에서는 임의의 주식을 선택합니다:
이러한 토큰은 게시 시 리치 텍스트에 저장됩니다. 뉴스 기사가 사이트에 표시되면 각 토큰 옆에 API에서 가져온 실시간 시장 데이터를 삽입합니다:

이를 달성하기 위해 인라인 스타일 및 블록과 마찬가지로 리치 텍스트 기능을 등록하는 것으로 시작합니다:
@hooks.register('register_rich_text_features')
def register_stock_feature(features):
features.default_features.append('stock')
"""
`STOCK` Draft.js 엔티티 유형을 사용하고
`<span data-stock>` 태그가 있는 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 폼 자산 문서에서 확인할 수 있습니다.
엔티티는 데이터를 보유하므로 데이터베이스 형식으로의 변환이 더 복잡합니다. 두 개의 핸들러를 생성해야 합니다:
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 컴포넌트 API로 구축되었고, from_database_format 은 Wagtail API를 사용합니다.
다음 단계는 엔티티가 생성되는 방법(source)과 표시되는 방법(decorator)을 정의하는 JavaScript를 추가하는 것입니다. stock.js 내에서 소스 컴포넌트를 정의합니다:
// 실제 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에서 제공하는 데이터와 콜백을 사용합니다. 또한 전역 변수에서 종속성을 사용합니다. - 클라이언트 측 React 컴포넌트 확장을 참조하십시오.
그런 다음 데코레이터 컴포넌트를 생성합니다:
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 컴포넌트를 등록합니다:
// 스크립트 실행 시 플러그인을 직접 등록하여 편집기가 초기화될 때 로드되도록 합니다.
window.draftail.registerPlugin({
type: 'STOCK',
source: StockSource,
decorator: Stock,
}, 'entityTypes');
그게 다입니다! 이 모든 설정은 마침내 사이트의 프런트엔드에 다음 HTML을 생성합니다:
<p>
NextEra 기술 <span data-stock="NEE">$NEE</span>를 따르는 사람은
<span data-stock="FSLR">$FSLR</span>도 살펴보아야 합니다.
</p>
데모를 완전히 완료하려면, 링크와 작은 스파크라인으로 토큰을 장식하기 위해 프런트엔드에 약간의 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}<svg width="50" height="20" stroke-width="2" stroke="blue" fill="rgba(0, 0, 255, .2)"><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4" fill="none"></path><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4 V 20 L 4 20 Z" stroke="none"></path></svg>`;
elt.innerHTML = '';
elt.appendChild(link);
});
사용자 지정 블록 엔티티도 생성할 수 있지만(별도의 Draftail 문서 참조), StreamField가 Wagtail에서 블록 수준 리치 텍스트를 생성하는 데 가장 적합한 방법이므로 여기서는 자세히 설명하지 않습니다.
기타 편집기 확장¶
Draftail은 더 복잡한 사용자 지정을 위한 추가 API를 제공합니다:
컨트롤 – 편집기 툴바에 임의의 UI 요소를 추가합니다.
데코레이터 – 임의의 텍스트 장식/강조 표시를 위한 것입니다.
플러그인 – 모든 Draft.js API에 직접 액세스하기 위한 것입니다.
사용자 지정 툴바 컨트롤¶
편집기 툴바에 임의의 새 UI 요소를 추가하기 위해 Draftail은 컨트롤 API를 제공합니다. 컨트롤은 편집기 상태를 가져오고 설정할 수 있는 임의의 React 컴포넌트일 수 있습니다. 컨트롤은 편집기에서 _모든 키 입력_에 대해 업데이트되므로 빠르게 렌더링되는지 확인하십시오!
다음은 간단한 문장 카운터의 예입니다. 먼저 wagtail_hooks.py 에 편집기 기능을 등록합니다:
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 컴포넌트를 선언합니다:
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');
참고
이 새로운 ‘sentences’ 기능을 사용할 수 있도록 WAGTAILADMIN_RICH_TEXT_EDITORS 설정에 설정된 사용자 지정 Draft 구성에 이 기능을 포함해야 합니다.
예를 들어:
WAGTAILADMIN_RICH_TEXT_EDITORS = {
'default': {
'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea',
'OPTIONS': {
'features': ['bold', 'italic', 'link', 'sentences'], # 여기에 'sentences' 추가
},
},
}
텍스트 데코레이터¶
데코레이터 API는 Draftail / Draft.js가 편집기에서 특수 서식으로 텍스트를 강조 표시하는 방법을 지원하는 방법입니다. 이는 CompositeDecorator API를 사용하며, 각 항목에는 어떤 텍스트를 대상으로 할지 결정하는 strategy 함수와 장식을 렌더링하는 component 함수가 있습니다.
이 API를 사용할 때 두 가지 중요한 고려 사항이 있습니다:
순서가 중요합니다: 편집기에서 문자당 하나의 데코레이터만 렌더링할 수 있습니다. 여기에는 장식으로 렌더링되는 모든 엔티티가 포함됩니다.
성능상의 이유로 Draft.js는 현재 포커스된 텍스트 줄에 있는 데코레이터만 다시 렌더링합니다.
다음은 문제가 있는 구두점 강조 표시의 예입니다. 먼저 wagtail_hooks.py 에 편집기 기능을 등록합니다:
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 는 전략과 강조 표시 컴포넌트를 정의합니다:
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');
임의의 플러그인¶
경고
이것은 고급 기능입니다. 정말 필요한지 신중하게 고려하십시오.
Draftail은 Draft.js 플러그인 아키텍처를 따르는 플러그인을 지원합니다. 이러한 플러그인은 편집기를 위한 가장 고급스럽고 강력한 유형의 확장으로, 사용자 지정 Draft.js 편집기로 가능한 것과 동일한 사용자 지정 기능을 제공합니다.
이 API가 도움이 될 수 있는 일반적인 시나리오는 맞춤형 복사-붙여넣기 처리를 추가하는 것입니다. 다음은 URL 앵커 해시 참조를 링크로 자동 변환하는 간단한 예입니다. 먼저 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 에서:
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-접두사가 붙은 클래스를 사용하십시오.