suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
919 lines (796 loc) • 32 kB
JavaScript
import { dom, env, converter, markdown } from '../../../helper';
const { _w, _d } = env;
/**
* @description Viewer (`codeView`, `fullScreen`, `showBlocks`) class
*/
class Viewer {
#kernel;
#$;
#store;
#icons;
#lang;
#frameRoots;
#context;
#frameContext;
#options;
#frameOptions;
#eventManager;
#disallowedTagNameRegExp;
#bodyOverflow = '';
#editorAreaOriginCssText = '';
#wysiwygOriginCssText = '';
#codeWrapperOriginCssText = '';
#codeOriginCssText = '';
#codeNumberOriginCssText = '';
#markdownWrapperOriginCssText = '';
#markdownOriginCssText = '';
#markdownNumberOriginCssText = '';
#toolbarOriginCssText = '';
#arrowOriginCssText = '';
#fullScreenInnerHeight = 0;
#fullScreenSticky = false;
#fullScreenBalloon = false;
#fullScreenInline = false;
#toolbarParent = null;
#originCssText = '';
/**
* @constructor
* @param {SunEditor.Kernel} kernel
*/
constructor(kernel) {
this.#kernel = kernel;
this.#$ = kernel.$;
this.#store = kernel.store;
this.#icons = this.#$.icons;
this.#lang = this.#$.lang;
this.#frameRoots = this.#$.frameRoots;
this.#context = this.#$.context;
this.#frameContext = this.#$.frameContext;
this.#options = this.#$.options;
this.#frameOptions = this.#$.frameOptions;
this.#eventManager = this.#$.eventManager;
// members
this.#disallowedTagNameRegExp = new RegExp(`^(${this.#options.get('_disallowedExtraTag')})$`, 'i');
}
/**
* @description Changes to code view or wysiwyg view
* @param {boolean} [value] `true`/`false`, If `undefined` toggle the `codeView` mode.
*/
codeView(value) {
const fc = this.#frameContext;
if (!fc.get('codeWrapper')) return;
if (value === undefined) value = !fc.get('isCodeView');
if (value === fc.get('isCodeView')) return;
// Mutual exclusivity with markdown view
if (value && fc.get('isMarkdownView')) {
this.markdownView(false);
}
fc.set('isCodeView', value);
this.#$.ui.offCurrentController();
this.#$.ui.offCurrentModal();
const codeWrapper = fc.get('codeWrapper');
const codeFrame = fc.get('code');
const wysiwygFrame = fc.get('wysiwygFrame');
const wrapper = fc.get('wrapper');
if (value) {
this.#$.finder.close();
this.#setEditorDataToCodeView();
codeWrapper.style.setProperty('display', 'flex', 'important');
wysiwygFrame.style.display = 'none';
if (fc.get('isFullScreen')) {
codeFrame.style.height = '100%';
} else if (this.#frameOptions.get('height') === 'auto' && !this.#options.get('hasCodeMirror')) {
codeFrame.style.height = codeFrame.scrollHeight > 0 ? codeFrame.scrollHeight + 'px' : 'auto';
}
if (this.#options.get('hasCodeMirror')) {
this._codeMirrorEditor('refresh', null, null);
}
if (!fc.get('isFullScreen')) {
this.#$.ui.preventToolbarHide(true);
if (this.#store.mode.isBalloon) {
this.#context.get('toolbar_arrow').style.display = 'none';
this.#context.get('toolbar_main').style.left = '';
this.#store.mode.isInline = this.#$.toolbar.isInlineMode = true;
this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = false;
this.#$.toolbar._showInline();
}
}
if (this.#store.mode.isBalloon) {
this.#$.subToolbar.hide();
}
CreateLineNumbers(fc);
this.#store.set('_range', null);
codeFrame.focus();
dom.utils.addClass(this.#$.commandDispatcher.targets.get('codeView'), 'active');
dom.utils.addClass(wrapper, 'se-source-view-status');
} else {
if (!dom.check.isNonEditable(wysiwygFrame)) this.#setCodeDataToEditor();
wysiwygFrame.scrollTop = 0;
codeWrapper.style.setProperty('display', 'none', 'important');
wysiwygFrame.style.display = 'block';
if (this.#frameOptions.get('height') === 'auto' && !this.#options.get('hasCodeMirror')) fc.get('code').style.height = '0px';
if (!fc.get('isFullScreen')) {
this.#$.ui.preventToolbarHide(false);
if (/balloon/.test(this.#options.get('mode'))) {
this.#context.get('toolbar_arrow').style.display = '';
this.#store.mode.isInline = this.#$.toolbar.isInlineMode = false;
this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = true;
this.#kernel._eventOrchestrator._hideToolbar();
}
}
this.#$.focusManager.nativeFocus();
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('codeView'), 'active');
if (!dom.check.isNonEditable(wysiwygFrame)) {
this.#$.history.push(false);
this.#$.history.resetButtons(fc.get('key'), null);
}
dom.utils.removeClass(wrapper, 'se-source-view-status');
}
this.#$.ui._updatePlaceholder(fc);
this.#$.ui._toggleCodeViewButtons(value);
// document type
if (fc.has('documentType_use_header')) {
if (value) {
fc.get('documentTypeInner').style.display = 'none';
} else {
fc.get('documentTypeInner').style.display = '';
fc.get('documentType').reHeader();
}
}
// user event
this.#eventManager.triggerEvent('onToggleCodeView', { frameContext: fc, is: fc.get('isCodeView') });
}
/**
* @description Changes to markdown view or wysiwyg view
* @param {boolean} [value] `true`/`false`, If `undefined` toggle the `markdownView` mode.
*/
markdownView(value) {
const fc = this.#frameContext;
if (!fc.get('markdownWrapper')) return;
if (value === undefined) value = !fc.get('isMarkdownView');
if (value === fc.get('isMarkdownView')) return;
// Mutual exclusivity with code view
if (value && fc.get('isCodeView')) {
this.codeView(false);
}
fc.set('isMarkdownView', value);
this.#$.ui.offCurrentController();
this.#$.ui.offCurrentModal();
const markdownWrapper = fc.get('markdownWrapper');
const markdownFrame = fc.get('markdown');
const wysiwygFrame = fc.get('wysiwygFrame');
const wrapper = fc.get('wrapper');
if (value) {
this.#$.finder.close();
this.#setEditorDataToMarkdownView();
markdownWrapper.style.setProperty('display', 'flex', 'important');
wysiwygFrame.style.display = 'none';
if (fc.get('isFullScreen')) {
markdownFrame.style.height = '100%';
} else if (this.#frameOptions.get('height') === 'auto') {
markdownFrame.style.height = markdownFrame.scrollHeight > 0 ? markdownFrame.scrollHeight + 'px' : 'auto';
}
if (!fc.get('isFullScreen')) {
this.#$.ui.preventToolbarHide(true);
if (this.#store.mode.isBalloon) {
this.#context.get('toolbar_arrow').style.display = 'none';
this.#context.get('toolbar_main').style.left = '';
this.#store.mode.isInline = this.#$.toolbar.isInlineMode = true;
this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = false;
this.#$.toolbar._showInline();
}
}
if (this.#store.mode.isBalloon) {
this.#$.subToolbar.hide();
}
CreateLineNumbers(fc, 'markdown');
this.#store.set('_range', null);
markdownFrame.focus();
dom.utils.addClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
dom.utils.addClass(wrapper, 'se-source-view-status');
} else {
if (!dom.check.isNonEditable(wysiwygFrame)) this.#setMarkdownDataToEditor();
wysiwygFrame.scrollTop = 0;
markdownWrapper.style.setProperty('display', 'none', 'important');
wysiwygFrame.style.display = 'block';
if (this.#frameOptions.get('height') === 'auto') markdownFrame.style.height = '0px';
if (!fc.get('isFullScreen')) {
this.#$.ui.preventToolbarHide(false);
if (/balloon/.test(this.#options.get('mode'))) {
this.#context.get('toolbar_arrow').style.display = '';
this.#store.mode.isInline = this.#$.toolbar.isInlineMode = false;
this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = true;
this.#kernel._eventOrchestrator._hideToolbar();
}
}
this.#$.focusManager.nativeFocus();
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
if (!dom.check.isNonEditable(wysiwygFrame)) {
this.#$.history.push(false);
this.#$.history.resetButtons(fc.get('key'), null);
}
dom.utils.removeClass(wrapper, 'se-source-view-status');
}
this.#$.ui._updatePlaceholder(fc);
this.#$.ui._toggleCodeViewButtons(value);
// document type
if (fc.has('documentType_use_header')) {
if (value) {
fc.get('documentTypeInner').style.display = 'none';
} else {
fc.get('documentTypeInner').style.display = '';
fc.get('documentType').reHeader();
}
}
// user event
this.#eventManager.triggerEvent('onToggleMarkdownView', { frameContext: fc, is: fc.get('isMarkdownView') });
}
/**
* @description Changes to full screen or default screen
* @param {boolean} [value] `true`/`false`, If `undefined` toggle the `fullScreen` mode.
*/
fullScreen(value) {
const fc = this.#frameContext;
if (value === undefined) value = !fc.get('isFullScreen');
if (value === fc.get('isFullScreen')) return;
fc.set('isFullScreen', value);
const topArea = fc.get('topArea');
const toolbar = this.#context.get('toolbar_main');
const editorArea = fc.get('wrapper');
const wysiwygFrame = fc.get('wysiwygFrame');
const codeWrapper = fc.get('codeWrapper');
const code = fc.get('code');
const codeNumbers = fc.get('codeNumbers');
const markdownWrapper = fc.get('markdownWrapper');
const markdownFrame = fc.get('markdown');
const markdownNumbers = fc.get('markdownNumbers');
const isCodeView = this.#frameContext.get('isCodeView');
const isMarkdownView = this.#frameContext.get('isMarkdownView');
const arrow = this.#context.get('toolbar_arrow');
this.#$.ui.offCurrentController();
const wasToolbarHidden = toolbar.style.display === 'none' || (this.#store.mode.isInline && !this.#$.toolbar.inlineToolbarAttr.isShow);
if (value) {
this.#originCssText = topArea.style.cssText;
this.#editorAreaOriginCssText = editorArea.style.cssText;
this.#wysiwygOriginCssText = wysiwygFrame.style.cssText;
this.#codeWrapperOriginCssText = codeWrapper?.style.cssText;
this.#codeOriginCssText = code.style.cssText;
this.#codeNumberOriginCssText = codeNumbers?.style.cssText;
this.#markdownWrapperOriginCssText = markdownWrapper?.style.cssText;
this.#markdownOriginCssText = markdownFrame?.style.cssText;
this.#markdownNumberOriginCssText = markdownNumbers?.style.cssText;
this.#toolbarOriginCssText = toolbar.style.cssText;
if (arrow) this.#arrowOriginCssText = arrow.style.cssText;
if (this.#store.mode.isBalloon || this.#store.mode.isInline) {
if (arrow) arrow.style.display = 'none';
this.#fullScreenInline = this.#store.mode.isInline;
this.#fullScreenBalloon = this.#store.mode.isBalloon;
this.#store.mode.isInline = this.#$.toolbar.isInlineMode = false;
this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = false;
}
if (this.#options.get('toolbar_container')) {
this.#toolbarParent = toolbar.parentElement;
fc.get('container').insertBefore(toolbar, editorArea);
}
topArea.style.position = 'fixed';
topArea.style.top = '0';
topArea.style.left = '0';
topArea.style.width = '100%';
topArea.style.maxWidth = '100%';
topArea.style.height = '100%';
topArea.style.zIndex = '2147483639';
if (fc.get('_stickyDummy').style.display !== 'none' && fc.get('_stickyDummy').style.display !== '') {
this.#fullScreenSticky = true;
fc.get('_stickyDummy').style.display = 'none';
dom.utils.removeClass(toolbar, 'se-toolbar-sticky');
}
this.#bodyOverflow = _d.body.style.overflow;
_d.body.style.overflow = 'hidden';
// frame
editorArea.style.cssText = toolbar.style.cssText = '';
wysiwygFrame.style.cssText = (wysiwygFrame.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + this.#frameOptions.get('_defaultStyles').editor + (isCodeView || isMarkdownView ? 'display: none;' : '');
// code wrapper
if (codeWrapper) {
codeWrapper.style.cssText = (codeWrapper.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
codeWrapper.style.overflow = 'auto';
codeWrapper.style.height = '100%';
}
// markdown wrapper
if (markdownWrapper) {
markdownWrapper.style.cssText = (markdownWrapper.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + `display: ${!isMarkdownView ? 'none' : 'flex'} !important;`;
markdownWrapper.style.overflow = 'auto';
markdownWrapper.style.height = '100%';
}
// code
code.style.height = '';
if (markdownFrame) markdownFrame.style.height = '';
// toolbar, editor area
toolbar.style.width = wysiwygFrame.style.height = '100%';
toolbar.style.position = 'relative';
toolbar.style.display = 'block';
this.#fullScreenInnerHeight = _w.innerHeight - toolbar.offsetHeight;
editorArea.style.height = this.#fullScreenInnerHeight - (fc.has('statusbar') ? fc.get('statusbar').offsetHeight : 0) - this.#options.get('fullScreenOffset') + 'px';
if (this.#frameOptions.get('iframe') && this.#frameOptions.get('height') === 'auto') {
editorArea.style.overflow = 'auto';
this.#$.ui._iframeAutoHeight(fc);
}
fc.get('topArea').style.marginTop = this.#options.get('fullScreenOffset') + 'px';
const reductionIcon = this.#icons.reduction;
this.#$.commandDispatcher.applyTargets('fullScreen', (e) => {
dom.utils.changeElement(e.firstElementChild, reductionIcon);
dom.utils.addClass(e, 'active');
});
} else {
// frame
wysiwygFrame.style.cssText = this.#wysiwygOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + (isCodeView || isMarkdownView ? 'display: none;' : '');
// code wrapper
if (codeWrapper) {
codeWrapper.style.cssText = this.#codeWrapperOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
}
// code
code.style.cssText = this.#codeOriginCssText;
if (codeNumbers) codeNumbers.style.cssText = this.#codeNumberOriginCssText;
// markdown wrapper
if (markdownWrapper) {
markdownWrapper.style.cssText = this.#markdownWrapperOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + `display: ${!isMarkdownView ? 'none' : 'flex'} !important;`;
}
if (markdownFrame) markdownFrame.style.cssText = this.#markdownOriginCssText;
if (markdownNumbers) markdownNumbers.style.cssText = this.#markdownNumberOriginCssText;
// toolbar, editor area
toolbar.style.cssText = this.#toolbarOriginCssText;
editorArea.style.cssText = this.#editorAreaOriginCssText;
topArea.style.cssText = this.#originCssText;
if (arrow) arrow.style.cssText = this.#arrowOriginCssText;
_d.body.style.overflow = this.#bodyOverflow;
if (this.#frameOptions.get('height') === 'auto' && !this.#options.get('hasCodeMirror')) this._codeViewAutoHeight(fc.get('code'), fc.get('codeNumbers'), true);
if (this.#toolbarParent) {
this.#toolbarParent.appendChild(toolbar);
this.#toolbarParent = null;
}
if (this.#options.get('_toolbar_sticky') > -1) {
dom.utils.removeClass(toolbar, 'se-toolbar-sticky');
}
if (this.#fullScreenSticky && !this.#options.get('toolbar_container')) {
this.#fullScreenSticky = false;
fc.get('_stickyDummy').style.display = 'block';
dom.utils.addClass(toolbar, 'se-toolbar-sticky');
}
this.#store.mode.isInline = this.#$.toolbar.isInlineMode = this.#fullScreenInline;
this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = this.#fullScreenBalloon;
if (!fc.get('isCodeView')) {
if (this.#store.mode.isInline) this.#$.toolbar._showInline();
else if (this.#store.mode.isBalloon) this.#$.toolbar._showBalloon();
}
this.#$.toolbar._resetSticky();
fc.get('topArea').style.marginTop = '';
const expansionIcon = this.#icons.expansion;
this.#$.commandDispatcher.applyTargets('fullScreen', (e) => {
dom.utils.changeElement(e.firstElementChild, expansionIcon);
dom.utils.removeClass(e, 'active');
});
}
if (wasToolbarHidden && !fc.get('isCodeView') && !fc.get('isMarkdownView')) this.#$.toolbar.hide();
// user event
this.#eventManager.triggerEvent('onToggleFullScreen', { frameContext: fc, is: fc.get('isFullScreen') });
}
/**
* @description Add or remove the class name of `body` so that the code block is visible
* @param {boolean} [value] `true`/`false`, If `undefined` toggle the `showBlocks` mode.
*/
showBlocks(value) {
const fc = this.#frameContext;
if (value === undefined) value = !fc.get('isShowBlocks');
fc.set('isShowBlocks', !!value);
if (value) {
dom.utils.addClass(fc.get('wysiwyg'), 'se-show-block');
dom.utils.addClass(this.#$.commandDispatcher.targets.get('showBlocks'), 'active');
} else {
dom.utils.removeClass(fc.get('wysiwyg'), 'se-show-block');
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('showBlocks'), 'active');
}
this.#$.ui._syncFrameState(fc);
}
/**
* @internal
* @description Set the `active` class to the button of the toolbar
*/
_setButtonsActive() {
const fc = this.#frameContext;
// codeView
if (fc.get('isCodeView')) {
dom.utils.addClass(this.#$.commandDispatcher.targets.get('codeView'), 'active');
} else {
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('codeView'), 'active');
}
// fullScreen
if (fc.get('isFullScreen')) {
const reductionIcon = this.#icons.reduction;
this.#$.commandDispatcher.applyTargets('fullScreen', (e) => {
dom.utils.changeElement(e.firstElementChild, reductionIcon);
dom.utils.addClass(e, 'active');
});
} else {
const expansionIcon = this.#icons.expansion;
this.#$.commandDispatcher.applyTargets('fullScreen', (e) => {
dom.utils.changeElement(e.firstElementChild, expansionIcon);
dom.utils.removeClass(e, 'active');
});
}
// markdownView
if (fc.get('isMarkdownView')) {
dom.utils.addClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
} else {
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
}
// showBlocks
if (fc.get('isShowBlocks')) {
dom.utils.addClass(this.#$.commandDispatcher.targets.get('showBlocks'), 'active');
} else {
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('showBlocks'), 'active');
}
}
/**
* @description Prints the current content of the editor.
* @throws {Error} Throws error if print operation fails.
*/
print() {
/** @type {HTMLIFrameElement} */
const iframe = dom.utils.createElement('IFRAME', { style: 'display: none;' });
_d.body.appendChild(iframe);
const innerPadding = _w.getComputedStyle(this.#frameContext.get('wysiwyg')).padding;
const contentHTML = this.#options.get('printTemplate') ? this.#options.get('printTemplate').replace(/\{\{\s*contents\s*\}\}/i, this.#$.html.get()) : this.#$.html.get();
const printDocument = dom.query.getIframeDocument(iframe);
const wDoc = this.#frameContext.get('_wd');
const rtlClass = this.#options.get('_rtl') ? ' se-rtl' : '';
const pageCSS = /*html*/ `
<style>
{
size: A4;
margin: ${innerPadding};
}
</style>`;
if (this.#frameOptions.get('iframe')) {
const arrts = this.#options.get('printClass')
? 'class="' + this.#options.get('printClass') + rtlClass + '"'
: this.#frameOptions.get('iframe_fullPage')
? dom.utils.getAttributesToString(wDoc.body, ['contenteditable'])
: 'class="' + this.#options.get('_editableClass') + rtlClass + '"';
printDocument.write(/*html*/ `
<!DOCTYPE html>
<html>
<head>
${wDoc.head.innerHTML}
${pageCSS}
</head>
<body ${arrts} style="padding: 0; padding-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0;">
${contentHTML}
</body>
</html>`);
} else {
const links = _d.head.getElementsByTagName('link');
const styles = _d.head.getElementsByTagName('style');
let linkHTML = '';
for (let i = 0, len = links.length; i < len; i++) {
linkHTML += links[i].outerHTML;
}
for (let i = 0, len = styles.length; i < len; i++) {
linkHTML += styles[i].outerHTML;
}
printDocument.write(/*html*/ `
<!DOCTYPE html>
<html>
<head>
${linkHTML}
${pageCSS}
</head>
<body class="${(this.#options.get('printClass') || this.#options.get('_editableClass')) + rtlClass}" style="padding: 0; padding-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0;">
${contentHTML}
</body>
</html>`);
}
this.#$.ui.showLoading();
// Defer print — allow loading overlay to render before blocking the main thread with print dialog
_w.setTimeout(() => {
try {
iframe.focus();
// Edge, Chromium
if (env.isEdge || env.isChromium || 'StyleMedia' in env._w) {
try {
iframe.contentWindow.document.execCommand('print', false, null);
} catch (e) {
console.warn('[SUNEDITOR.print.warn]', e);
iframe.contentWindow.print();
}
} else {
// Other browsers
iframe.contentWindow.print();
}
} catch (error) {
throw Error(`[SUNEDITOR.print.fail] error: ${error.message}`);
} finally {
this.#$.ui.hideLoading();
dom.utils.removeItem(iframe);
}
}, 1000);
}
/**
* @description Open the preview window.
*/
preview() {
this.#$.menu.dropdownOff();
this.#$.menu.containerOff();
this.#$.ui.offCurrentController();
this.#$.ui.offCurrentModal();
const contentHTML = this.#options.get('previewTemplate') ? this.#options.get('previewTemplate').replace(/\{\{\s*contents\s*\}\}/i, this.#$.html.get({ withFrame: true })) : this.#$.html.get({ withFrame: true });
const windowObject = _w.open('', '_blank');
const wDoc = this.#frameContext.get('_wd');
const rtlClass = this.#options.get('_rtl') ? ' se-rtl' : '';
if (this.#frameOptions.get('iframe')) {
const arrts = this.#options.get('printClass')
? 'class="' + this.#options.get('printClass') + rtlClass + '"'
: this.#frameOptions.get('iframe_fullPage')
? dom.utils.getAttributesToString(wDoc.body, ['contenteditable'])
: 'class="' + this.#options.get('_editableClass') + rtlClass + '"';
windowObject.document.write(/*html*/ `<!DOCTYPE html>
<html>
<head>
${wDoc.head.innerHTML}
<style>
body {overflow:auto !important; height:auto !important;}
</style>
</head>
<body ${arrts}>
${contentHTML}
</body>
</html>`);
} else {
const links = _d.head.getElementsByTagName('link');
const styles = _d.head.getElementsByTagName('style');
let linkHTML = '';
for (let i = 0, len = links.length; i < len; i++) {
linkHTML += links[i].outerHTML;
}
for (let i = 0, len = styles.length; i < len; i++) {
linkHTML += styles[i].outerHTML;
}
windowObject.document.write(/*html*/ `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>${this.#lang.preview}</title>
${linkHTML}
</head>
<body class="${(this.#options.get('printClass') ? this.#options.get('printClass') : this.#options.get('_editableClass')) + rtlClass}" style="height:auto">
${contentHTML}
</body>
</html>`);
}
}
/**
* @internal
* @description Resets the full-screen height of the editor.
* - Updates the editor's height dynamically when in full-screen mode.
*/
_resetFullScreenHeight() {
if (this.#frameContext.get('isFullScreen')) {
this.#fullScreenInnerHeight += _w.innerHeight - this.#context.get('toolbar_main').offsetHeight - (this.#frameContext.has('statusbar') ? this.#frameContext.get('statusbar').offsetHeight : 0) - this.#fullScreenInnerHeight;
this.#frameContext.get('wrapper').style.height = this.#fullScreenInnerHeight + 'px';
return true;
}
}
/**
* @internal
* @description Run `CodeMirror` Editor
* @param {"set"|"get"|"readonly"|"refresh"} key Method key
* @param {*} value `CodeMirror` params
* @param {string} [rootKey] Root key
*/
_codeMirrorEditor(key, value, rootKey) {
const fo = rootKey ? this.#frameRoots.get(rootKey).get('options') : this.#frameOptions;
switch (key) {
case 'set':
fo.get('codeMirrorEditor').getDoc().setValue(value);
break;
case 'get':
return fo.get('codeMirrorEditor').getDoc().getValue();
case 'readonly':
fo.get('codeMirrorEditor').setOption('readOnly', value);
break;
case 'refresh':
fo.get('codeMirrorEditor').refresh();
break;
}
}
/**
* @internal
* @description Set method in the code view area
* @param {string} value HTML string
*/
_setCodeView(value) {
if (this.#options.get('hasCodeMirror')) {
this._codeMirrorEditor('set', value, null);
} else {
this.#frameContext.get('code').value = value;
}
}
/**
* @internal
* @description Get method in the code view area
*/
_getCodeView() {
if (this.#options.get('hasCodeMirror')) {
return this._codeMirrorEditor('get', null, null);
} else {
return this.#frameContext.get('code').value;
}
}
/**
* @internal
* @description Adjusts the height of the code view area.
* - Ensures the code block `auto`-resizes based on its content.
* @param {HTMLTextAreaElement} code - Code area
* @param {HTMLTextAreaElement} codeNumbers - Code numbers area
* @param {boolean} isAuto - `auto` height option
*/
_codeViewAutoHeight(code, codeNumbers, isAuto) {
if (isAuto) code.style.height = code.scrollHeight + 'px';
this.#updateLineNumbers(codeNumbers, code);
}
/**
* @internal
* @this {HTMLElement} Code numbers area
* @description Synchronizes scrolling of line numbers with the code editor.
* - Keeps the line numbers aligned with the text.
* @param {HTMLTextAreaElement} codeNumbers - Code numbers textarea
*/
_scrollLineNumbers(codeNumbers) {
codeNumbers.scrollTop = this.scrollTop;
codeNumbers.scrollLeft = this.scrollLeft;
}
/**
* @internal
* @description Adjusts the height of the markdown view area.
* @param {HTMLTextAreaElement} md - Markdown area
* @param {HTMLTextAreaElement} mdNumbers - Markdown numbers area
* @param {boolean} isAuto - `auto` height option
*/
_markdownViewAutoHeight(md, mdNumbers, isAuto) {
if (isAuto) md.style.height = md.scrollHeight + 'px';
this.#updateLineNumbers(mdNumbers, md);
}
/**
* @internal
* @this {HTMLElement} Markdown numbers area
* @description Synchronizes scrolling of line numbers with the markdown editor.
* @param {HTMLTextAreaElement} mdNumbers - Markdown numbers textarea
*/
_scrollMarkdownLineNumbers(mdNumbers) {
mdNumbers.scrollTop = this.scrollTop;
mdNumbers.scrollLeft = this.scrollLeft;
}
/**
* @description Convert the data of the code view and put it in the `WYSIWYG` area.
*/
#setCodeDataToEditor() {
const code_html = this._getCodeView();
if (this.#frameOptions.get('iframe_fullPage')) {
const wDoc = this.#frameContext.get('_wd');
const parseDocument = new DOMParser().parseFromString(code_html, 'text/html');
if (!this.#disallowedTagNameRegExp.test('script')) {
const headChildren = parseDocument.head.children;
for (let i = 0, len = headChildren.length; i < len; i++) {
if (/^script$/i.test(headChildren[i].tagName)) {
parseDocument.head.removeChild(headChildren[i]);
i--;
len--;
}
}
}
let headers = parseDocument.head.innerHTML;
if (!parseDocument.head.querySelector('link[rel="stylesheet"]') || (this.#frameOptions.get('height') === 'auto' && !parseDocument.head.querySelector('style'))) {
headers += converter._setIframeStyleLinks(this.#frameOptions.get('iframe_cssFileName')) + converter._setAutoHeightStyle(this.#frameOptions.get('height'));
}
wDoc.head.innerHTML = headers;
wDoc.body.innerHTML = this.#$.html.clean(parseDocument.body.innerHTML, { forceFormat: true, whitelist: null, blacklist: null, _freeCodeViewMode: this.#options.get('freeCodeViewMode') });
const attrs = parseDocument.body.attributes;
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].name === 'contenteditable') continue;
wDoc.body.setAttribute(attrs[i].name, attrs[i].value);
}
if (!dom.utils.hasClass(wDoc.body, 'sun-editor-editable')) {
const editableClasses = this.#options.get('_editableClass').split(' ');
for (let i = 0; i < editableClasses.length; i++) {
dom.utils.addClass(wDoc.body, editableClasses[i]);
}
}
} else {
this.#frameContext.get('wysiwyg').innerHTML =
code_html.length > 0
? this.#$.html.clean(code_html, { forceFormat: true, whitelist: null, blacklist: null, _freeCodeViewMode: this.#options.get('freeCodeViewMode') })
: '<' + this.#options.get('defaultLine') + '><br></' + this.#options.get('defaultLine') + '>';
}
}
/**
* @description Convert the data of the `WYSIWYG` area and put it in the code view area.
*/
#setEditorDataToCodeView() {
const codeContent = this.#$.html._convertToCode(this.#frameContext.get('wysiwyg'), false);
let codeValue = '';
if (this.#frameOptions.get('iframe_fullPage')) {
const attrs = dom.utils.getAttributesToString(this.#frameContext.get('_wd').body, null);
codeValue = '<!DOCTYPE html>\n<html>\n' + this.#frameContext.get('_wd').head.outerHTML.replace(/>(?!\n)/g, '>\n') + '<body ' + attrs + '>\n' + codeContent + '</body>\n</html>';
} else {
codeValue = codeContent;
}
this._setCodeView(codeValue);
}
/**
* @description Convert the data of the `WYSIWYG` area and put it in the markdown view area.
*/
#setEditorDataToMarkdownView() {
const json = converter.htmlToJson(this.#frameContext.get('wysiwyg').innerHTML);
const md = markdown.jsonToMarkdown(json);
this.#frameContext.get('markdown').value = md;
}
/**
* @description Convert the data of the markdown view and put it in the `WYSIWYG` area.
*/
#setMarkdownDataToEditor() {
const md = this.#frameContext.get('markdown').value;
const html = markdown.markdownToHtml(md, this.#options.get('defaultLine'));
this.#frameContext.get('wysiwyg').innerHTML =
html.length > 0 ? this.#$.html.clean(html, { forceFormat: true, whitelist: null, blacklist: null }) : '<' + this.#options.get('defaultLine') + '><br></' + this.#options.get('defaultLine') + '>';
}
/**
* @description Updates the line numbers for the code editor.
* - Dynamically adjusts line numbers as content grows.
* @param {HTMLTextAreaElement} lineNumbers - Code numbers area
* @param {HTMLTextAreaElement} code - Code area
*/
#updateLineNumbers(lineNumbers, code) {
if (!lineNumbers) return;
const numberOfLinesNeeded = (code.value.match(/\n/g) || []).length + 1;
const currentLineCount = (lineNumbers.value.match(/\n/g) || []).length;
if (numberOfLinesNeeded !== currentLineCount) {
let n = '';
for (let i = 1; i <= numberOfLinesNeeded; i++) {
n += `${i}\n`;
}
lineNumbers.value = n;
}
}
/**
* @internal
* @description Destroy the Viewer instance and release memory
*/
_destroy() {
// No internal state to clean up
}
}
/**
* @description Create line numbers for the code/markdown view area
* @param {SunEditor.FrameContext} fc - Frame context
* @param {"code"|"markdown"} [type="code"] - View type
*/
function CreateLineNumbers(fc, type) {
const numbersKey = type === 'markdown' ? 'markdownNumbers' : 'codeNumbers';
const contentKey = type === 'markdown' ? 'markdown' : 'code';
const lineNumbers = fc.get(numbersKey);
if (!lineNumbers) return;
const content = fc.get(contentKey);
const numberOfLines = (content.value.match(/\n/g) || []).length + 1;
let n = '';
for (let i = 1; i <= numberOfLines; i++) {
n += `${i}\n`;
}
// Sync font and line-height for accurate scroll alignment
const contentStyle = _w.getComputedStyle(content);
lineNumbers.style.lineHeight = contentStyle.lineHeight;
lineNumbers.style.fontSize = contentStyle.fontSize;
lineNumbers.style.fontFamily = contentStyle.fontFamily;
lineNumbers.style.paddingTop = contentStyle.paddingTop;
lineNumbers.style.paddingBottom = contentStyle.paddingBottom;
lineNumbers.value = n;
}
export default Viewer;