Skip to content

Instantly share code, notes, and snippets.

@deadmann
Last active June 30, 2025 08:24
Show Gist options
  • Save deadmann/015cf0a4511ec1cbdb01a6daf86b4337 to your computer and use it in GitHub Desktop.
Save deadmann/015cf0a4511ec1cbdb01a6daf86b4337 to your computer and use it in GitHub Desktop.
This template-editor is razor based component that allow user to define template for sending SMS, email, with fixed template and dynamic data.
@*
Model Description:
// Expected Models:
1. string // The initial value to be passed to editor
2. dynamic object {
CurrentValue: string, // The initial value to be passed to editor
InputName: string // The custom name of form input field
}
USAGE:
// Top of Page:
@{ // For Typed Model
public class TemplateEditorModel
{
public string InputName { get; set; } = null!;
public string? CurrentValue { get; set; };
}
}
@{
var scripts = new List<IHtmlContent>();
var stringModel = "value";
var typedModel = new TemplateEditorModel
{
CurrentValue = "CustomValue",
InputName = "CustomInputName"
};
}
// typed model
// We Need to manually pass `Script` as a reference, to be able to retrive it later, otherwise object created on partial (smaller object) will not return
// Data flows from: layout → view → partial
@await Html.PartialAsync("_Editor", stringModel/typedModel, new ViewDataDictionary(ViewData)
{
["Scripts"] = scripts
})
@section Scripts {
...
// Add _Editor.cshtml scripts
@foreach (var script in scripts)
{
@script
}
...
}
EXPOSED API:
window.templateEditor {
getTemplate():template,
setTemplate(template)
}
*@
@using Microsoft.AspNetCore.Html
@model dynamic
@functions
{
public class TagDefinition
{
public string Tag { get; set; } = null!;
public string Sample { get; set; } = null!;
public int MaxExpectedLength { get; set; }
public string ContentType { get; set; } = null!; // UTF, ASCII, GSM-7, etc.
public string Title { get; set; } = null!;
public string? Description { get; set; }
}
private static List<TagDefinition> Tags => new()
{
new TagDefinition { Tag = "Title", Title = "عنوان", Sample = "آقا/خانم", Description = "افزودن عنوان آقا، خانم، یا آقا/خانم در صورت عدم تعیین جنسیت", MaxExpectedLength = 8, ContentType = "UTF" },
new TagDefinition { Tag = "Respectful", Title = "محترم", Sample = "محترم", Description = "افزودن محترم", MaxExpectedLength = 5, ContentType = "UTF" },
new TagDefinition { Tag = "FullName", Title = "نام کامل", Sample = "نام و نام خانوادگی", Description = "افزودن نام و نام خانوادگی مشتری", MaxExpectedLength = 25, ContentType = "UTF" },
new TagDefinition { Tag = "FirstName", Title = "نام", Sample = "حسن", Description = "افزودن نام مشتری", MaxExpectedLength = 10, ContentType = "UTF" },
new TagDefinition { Tag = "LastName", Title = "نام خانوادگی", Sample = "فقیهی", Description = "افزودن نام خانوادگی مشتری", MaxExpectedLength = 12, ContentType = "UTF" },
new TagDefinition { Tag = "PhoneNumber", Title = "شماره تلفن", Sample = "09123456789", Description = "افزودن شماره تلفن ایرانی کار", MaxExpectedLength = 11, ContentType = "ASCII" },
new TagDefinition { Tag = "CompanyName", Title = "نام شرکت", Sample = "ایرانی‌کار", Description = "افزودن عنوان ایرانی کار", MaxExpectedLength = 20, ContentType = "UTF" },
new TagDefinition { Tag = "Link", Title = "لینک", Sample = "https://iranecar.com", Description = "افزودن لینک ایرانی کار", MaxExpectedLength = 21, ContentType = "ASCII" },
new TagDefinition { Tag = "Date", Title = "تاریخ", Sample = "1404/04/07", Description = "افزودن تاریخ روز", MaxExpectedLength = 10, ContentType = "ASCII" },
new TagDefinition { Tag = "Time", Title = "زمان", Sample = "14:30", Description = "افزودن ساعت روز", MaxExpectedLength = 5, ContentType = "ASCII" },
};
}
@{
Layout = null;
}
<style>
.editor-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.toolbar {
background: #2c3e50;
padding: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.tag-button {
background: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
transition: background 0.3s;
}
.tag-button:hover {
background: #2980b9;
}
.editor {
position: relative;
min-height: 300px;
padding: 30px 55px 20px 20px;
font-size: 16px;
line-height: 30px;
outline: none;
background: white;
cursor: text;
word-wrap: break-word;
white-space: pre-wrap;
direction: rtl;
text-align: right;
/* Notebook styling */
background-image:
linear-gradient(-90deg, transparent 49px, #ffe0c4 0, #ffe0c4 51px, transparent 0),
linear-gradient(#eee 1px, transparent 1px);
background-size: 100% 30px, 100% 30px;
background-position: 0 0;
}
.editor:empty::before {
content: 'متن پیام خود را اینجا تایپ کنید...';
color: #999;
font-style: italic;
}
.visual-tag {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
margin: 0 1px;
position: relative;
cursor: pointer;
user-select: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
vertical-align: baseline;
}
.visual-tag:hover {
transform: translateY(-1px);
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
}
.visual-tag::after {
content: attr(data-description);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #2c3e50;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: 1000;
}
.visual-tag:hover::after {
opacity: 1;
}
.output-section {
margin-top: 20px;
padding: 20px;
background: #ecf0f1;
}
.output-box {
background: white;
border: 1px solid #bdc3c7;
border-radius: 5px;
padding: 15px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
margin-bottom: 10px;
}
/*.char-count {*/
/* text-align: left;*/
/* color: #7f8c8d;*/
/* font-size: 12px;*/
/* margin-top: 10px;*/
/*}*/
/*.explanationBox {*/
/* color: #7d9091;*/
/* font-size: 12px;*/
/* margin-top: 10px;*/
/*}*/
.preview-section {
background: #e8f5e8;
padding: 15px;
border-radius: 5px;
margin-top: 10px;
}
.preview-title {
font-weight: bold;
margin-bottom: 10px;
}
.preview-title.sample {
color: #27ae60;
}
.preview-title.template {
color: #476e77;
}
.stats-row {
display: flex;
gap: 15px;
margin-bottom: 20px; /* space before the template output */
}
.stat-item {
flex: 1;
background: white;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 10px;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #2d3748;
}
.stat-label {
font-size: 12px;
color: #718096;
margin-top: 5px;
}
/* explanation tooltip */
.explanation-trigger {
position: relative;
}
.explanationBox {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
width: 190px;
max-width: 200px; /* limit width */
max-height: 150px; /* limit height */
white-space: normal; /* allow wrapping */
/*overflow-wrap: anywhere; !* break long words *!*/
hyphens: auto; /* add hyphens */
overflow-y: auto; /* scroll if too tall */
background: rgba(45,55,72,0.95);
color: white;
padding: 6px 10px;
border-radius: 6px;
font-size: 11px;
z-index: 1000;
}
.explanation-trigger:hover .explanationBox {
display: block;
}
</style>
<div class="editor-container">
<div class="toolbar">
@foreach (var tag in Tags)
{
@* !!! IMPORTANT NOTE: this piece of code get overwritten by JS code in LoadTagScript() at runtime, trying to keep toolbar in sync for runtime changes *@
@* The runtime behaviour mentioned is not yet available or implemented *@
<button type="button" class="tag-button" data-tag="{{@tag.Tag}}" data-title="@tag.Title" data-description="@tag.Description" title="@tag.Description">
@tag.Title
</button>
}
</div>
<input type="hidden"
name="@(Model switch
{
null => "SmsTemplate",
string => "SmsTemplate",
_ => Model.InputName
})"
id="smsTemplateInput"
value="@(Model switch
{
null => "",
string => Model,
_ => Model.CurrentValue
})" />
<div class="editor" contenteditable="true" id="smsEditor">
@Html.Raw(Model switch
{
null => "",
string => Model,
_ => Model.CurrentValue
})
</div>
</div>
<div class="output-section">
@* <div class="char-count" id="sampleCharCount">تعداد کاراکتر نمونه: 0</div> *@
@* <div class="explanationBox" id="explanationBox"></div> *@
<div class="stats-row">
<div class="stat-item explanation-trigger">
<div class="stat-number" id="maxExpectedCharCount">0</div>
<div class="stat-label">تعداد کاراکتر احتمالی</div>
<div class="explanationBox" id="maxExpectedCharCountExplanationBox">
این عدد از جمع «حداکثر طول در نظر گرفته شده» همه تگ‌ها، به‌علاوه کاراکتر های متن عادی محاسبه می‌شود. این مقدار صرفاً پیشنهادی است و انتظار ما از یک پیام معمولی می باشد، ولی همیشه استثنا وجود دارد و قانونی برای حداکثر کاراکتر تعیین نمی کند.
</div>
</div>
<div class="stat-item explanation-trigger">
<div class="stat-number" id="expectedSmsCount">0</div>
<div class="stat-label">تعداد پیام احتمالی</div>
<div class="explanationBox" id="expectedSmsCountExplanationBox"></div>
</div>
<div class="stat-item explanation-trigger">
<div class="stat-number" id="sampleCharCount">0</div>
<div class="stat-label">تعداد کاراکتر نمونه</div>
<div class="explanationBox" id="sampleCharCountExplanationBox">
تعداد کاراکتر محاسبه شده بر اساس استفاده از الگو با داده های نمونه
</div>
</div>
<div class="stat-item explanation-trigger">
<div class="stat-number" id="sampleSmsCount">0</div>
<div class="stat-label">تعداد پیام نمونه</div>
<div class="explanationBox" id="sampleSmsCountExplanationBox"></div>
</div>
<div class="stat-item">
<div class="stat-number" id="tagCount">0</div>
<div class="stat-label">تعداد تگ</div>
</div>
<div class="stat-item">
<div class="stat-number" id="sampleWordCount">0</div>
<div class="stat-label">تعداد کلمه</div>
</div>
<div class="stat-item" style="display:none">
<div class="stat-number" id="templateLength">0</div>
<div class="stat-label">طول تمپلیت</div>
</div>
</div>
<div class="preview-section">
<div class="preview-title sample">پیش‌نمایش با داده‌های نمونه:</div>
<div class="output-box" id="previewOutput"></div>
</div>
<div class="preview-section">
<div class="preview-title template">خروجی قابل ذخیره (Template):</div>
<div class="output-box" id="templateOutput"></div>
</div>
</div>
@functions
{
private IHtmlContent TagJsonScript() => new HtmlString($@"
<script id='tag-data' type='application/json'>
{Json.Serialize(Tags)}
</script>
");
private IHtmlContent LoadTagScript() => new HtmlString($@"
<script>
$(function () {{
const tagScript = document.getElementById('tag-data');
const tagData = JSON.parse(tagScript.textContent);
window.tags = tagData;
const toolbar = document.querySelector('.toolbar');
if (toolbar) {{
toolbar.innerHTML = '';
tagData.forEach(tag => {{
const button = document.createElement('button');
button.type = 'button';
button.className = 'tag-button';
button.setAttribute('data-tag', `{{{{${{tag.tag}}}}}}`);
button.setAttribute('data-title', tag.title);
button.setAttribute('data-desc', tag.description);
button.setAttribute('title', tag.description);
button.innerText = tag.title ?? tag.title;
toolbar.appendChild(button);
}});
}}
}});
</script>
");
private IHtmlContent LoadEditorCoreScript() => new HtmlString($@"
<script>
/*
* Editor Core Functionality
* */
$(document).ready(function() {{
const editor = $('#smsEditor');
const templateOutput = $('#templateOutput');
const previewOutput = $('#previewOutput');
const pasteInput = $('#pasteInput');
const tags = window.tags;
// || [
// {{ tag: 'Title', title: 'عنوان' , sample: 'آقا/خانم', maxExpectedLength: 8, contentType: 'UTF' }},
// {{ tag: 'Respectful', title: 'محترم', sample: 'محترم', maxExpectedLength: 5, contentType: 'UTF' }},
// {{ tag: 'FullName', title: 'نام کامل', sample: 'حسن فقیهی', maxExpectedLength: 25, contentType: 'UTF' }},
// {{ tag: 'FirstName', title: 'نام', sample: 'حسن', maxExpectedLength: 10, contentType: 'UTF' }},
// {{ tag: 'LastName', title: 'نام خانوادگی', sample: 'فقیهی', maxExpectedLength: 12, contentType: 'UTF' }},
// {{ tag: 'PhoneNumber', title: 'شماره تلفن', sample: '09130000000', maxExpectedLength: 11, contentType: 'ASCII' }},
// {{ tag: 'CompanyName', title: 'نام شرکت', sample: 'ایرانی‌کار', maxExpectedLength: 12, contentType: 'UTF' }},
// {{ tag: 'Date', title: 'تاریخ', sample: '1404/04/07', maxExpectedLength: 10, contentType: 'ASCII' }},
// {{ tag: 'Time', title: 'زمان', sample: '14:30', maxExpectedLength: 5, contentType: 'ASCII' }},
// {{ tag: 'Link', title: 'لینک', sample: 'https://example.com', maxExpectedLength: 35, contentType: 'ASCII' }},
// {{ tag: 'PlateNumber', title: 'شماره پلاک', sample: '12ج345-67', maxExpectedLength: 9, contentType: 'UTF' }}
// ];
const sampleData = Object.fromEntries(tags.map(t => [`{{{{${{t.tag}}}}}}`, t.sample]));
const validTags = Object.fromEntries(tags.map(t => [`{{{{${{t.tag}}}}}}`, t]));
let isProcessing = false;
$('.tag-button').click(function() {{
const tag = $(this).data('tag');
const title = $(this).data('title');
const description = $(this).data('desc');
insertTag(tag, title, description);
}});
function insertTag(tag, title, description) {{
editor.focus();
const selection = window.getSelection();
let range;
if (selection.rangeCount > 0) {{
range = selection.getRangeAt(0);
}} else {{
range = document.createRange();
range.selectNodeContents(editor[0]);
range.collapse(false);
}}
const tagElement = document.createElement('span');
tagElement.className = 'visual-tag';
tagElement.setAttribute('data-tag', tag);
tagElement.setAttribute('data-description', description);
tagElement.setAttribute('contenteditable', 'false');
tagElement.textContent = title;
range.deleteContents();
range.insertNode(tagElement);
range.setStartAfter(tagElement);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
updateOutputs();
}}
editor.on('input', function(e) {{
if (isProcessing) return;
clearTimeout(window.inputTimeout);
window.inputTimeout = setTimeout(function() {{
processContent();
}}, 100);
}});
// ENFORCE UNFORMATTED TEXT, PASTE WITH FORMATTING, CAN CAUSE TAG TO BEHAVE INCORRECTLY
editor.on('paste', function(e) {{
e.preventDefault();
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
// Insert as unformatted text at the cursor
document.execCommand('insertText', false, text);
}});
function processContent() {{
if (isProcessing) return;
isProcessing = true;
try {{
const walker = document.createTreeWalker(editor[0], NodeFilter.SHOW_TEXT, null, false);
const textNodes = [];
let node;
while (node = walker.nextNode()) {{
textNodes.push(node);
}}
textNodes.forEach(textNode => {{
let content = textNode.textContent;
let newContent = content;
Object.keys(validTags).forEach(tag => {{
const regex = new RegExp(tag.replace(/[{{}}]/g, '\\$&'), 'g');
if (regex.test(newContent)) {{
const placeholder = `__TAG_${{tag.replace(/[{{}}]/g, '')}}_PLACEHOLDER__`;
newContent = newContent.replace(regex, placeholder);
}}
}});
if (newContent !== content) {{
let processedHTML = newContent;
debugger
Object.keys(validTags).forEach(tag => {{
const placeholder = `__TAG_${{tag.replace(/[{{}}]/g, '')}}_PLACEHOLDER__`;
const tagHTML = `<span class=""visual-tag"" data-tag=""${{tag}}"" data-description=""${{validTags[tag].description}}"" contenteditable=""false"">${{validTags[tag].title}}</span>`;
processedHTML = processedHTML.replace(new RegExp(placeholder, 'g'), tagHTML);
}});
const fragment = document.createRange().createContextualFragment(processedHTML);
textNode.parentNode.replaceChild(fragment, textNode);
}}
}});
}} catch (error) {{
console.error('Error processing content:', error);
}} finally {{
isProcessing = false;
updateOutputs();
}}
}}
editor.on('keydown', function(e) {{
const selection = window.getSelection();
if ((e.key === 'Backspace' || e.key === 'Delete') && selection.rangeCount > 0) {{
const range = selection.getRangeAt(0);
if (range.collapsed) {{
let targetElement = null;
if (e.key === 'Backspace') {{
let currentNode = range.startContainer;
if (currentNode.nodeType === Node.TEXT_NODE && range.startOffset === 0) {{
let prevSibling = currentNode.previousSibling;
if (prevSibling && $(prevSibling).hasClass('visual-tag')) targetElement = prevSibling;
}} else if (currentNode.nodeType === Node.ELEMENT_NODE) {{
let child = currentNode.childNodes[range.startOffset - 1];
if (child && $(child).hasClass('visual-tag')) targetElement = child;
}}
}} else if (e.key === 'Delete') {{
let currentNode = range.startContainer;
if (currentNode.nodeType === Node.TEXT_NODE && range.startOffset === currentNode.textContent.length) {{
let nextSibling = currentNode.nextSibling;
if (nextSibling && $(nextSibling).hasClass('visual-tag')) targetElement = nextSibling;
}} else if (currentNode.nodeType === Node.ELEMENT_NODE) {{
let child = currentNode.childNodes[range.startOffset];
if (child && $(child).hasClass('visual-tag')) targetElement = child;
}}
}}
if (targetElement) {{
e.preventDefault();
targetElement.remove();
updateOutputs();
return false;
}}
}}
}}
setTimeout(updateOutputs, 50);
}});
function updateOutputs() {{
if (isProcessing) return;
let template = '';
let preview = '';
let tagCount = 0;
editor[0].childNodes.forEach(function(node) {{
if (node.nodeType === Node.TEXT_NODE) {{
template += node.textContent;
preview += node.textContent;
}} else if (node.nodeType === Node.ELEMENT_NODE && $(node).hasClass('visual-tag')) {{
const tag = $(node).attr('data-tag');
template += tag;
preview += sampleData[tag] || tag;
tagCount++;
}} else if (node.nodeType === Node.ELEMENT_NODE) {{
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ALL, null, false);
let childNode;
while (childNode = walker.nextNode()) {{
if (childNode.nodeType === Node.TEXT_NODE) {{
template += childNode.textContent;
preview += childNode.textContent;
}} else if (childNode.nodeType === Node.ELEMENT_NODE && $(childNode).hasClass('visual-tag')) {{
const tag = $(childNode).attr('data-tag');
template += tag;
preview += sampleData[tag] || tag;
tagCount++;
}}
}}
}}
}});
templateOutput.text(template);
previewOutput.text(preview);
// Based on SAMPLE data
const sampleSmsStats = calculateSMS(preview);
// Based On MAX expected length
const expectedMessage = generateExpectedMessage(template)
const expectedStats = calculateSMS(expectedMessage);
$('.stat-item #maxExpectedCharCount').text(expectedStats.charCount);
$('.stat-item #expectedSmsCount').text(expectedStats.segments);
$('.stat-item #sampleCharCount').text(sampleSmsStats.charCount);
$('.stat-item #sampleSmsCount').text(sampleSmsStats.segments);
$('.stat-item #tagCount').text(tagCount);
$('.stat-item #sampleWordCount').text(preview.trim().split(/\s+/).filter(w => w.length > 0).length);
$('.stat-item #templateLength').text(template.length);
$('.explanation-trigger .explanationBox#expectedSmsCountExplanationBox').text(expectedStats.explanation);
$('.explanation-trigger .explanationBox#sampleSmsCountExplanationBox').text(sampleSmsStats.explanation);
changeColor('#maxExpectedCharCount', expectedStats);
changeColor('#sampleCharCount', sampleSmsStats);
function changeColor(selector, calcData){{
const charElement = $(selector);
if (calcData.charCount <= calcData.perSegmentLimit) {{
charElement.css('color', '#38a169');
}} else if (calcData.charCount <= calcData.perSegmentLimit * 2) {{
charElement.css('color', '#d69e2e');
}} else {{
charElement.css('color', '#e53e3e');
}}
}}
}}
window.pasteTemplate = function() {{
const templateText = pasteInput.val().trim();
if (!templateText) return;
editor.empty();
let processedHTML = templateText;
Object.keys(validTags).forEach(tag => {{
const regex = new RegExp(tag.replace(/[{{}}]/g, '\\$&'), 'g');
const tagHTML = `<span class=""visual-tag"" data-tag=""${{tag}}"" data-description=""${{validTags[tag].description}}"" contenteditable=""false"">${{validTags[tag].title}}</span>`;
processedHTML = processedHTML.replace(regex, tagHTML);
}});
editor.html(processedHTML);
pasteInput.val('');
updateOutputs();
editor.focus();
showNotification('تمپلیت با موفقیت اعمال شد!');
}};
pasteInput.on('keydown', function(e) {{
if (e.key === 'Enter') {{
e.preventDefault();
pasteTemplate();
}}
}});
window.copyToClipboard = function(elementId) {{
const element = document.getElementById(elementId);
const text = element.textContent;
if (navigator.clipboard && window.isSecureContext) {{
navigator.clipboard.writeText(text).then(function() {{
showNotification('متن کپی شد!');
}});
}} else {{
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('متن کپی شد!');
}}
}};
function showNotification(message) {{
const notification = $(`<div style=""position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #48bb78; color: white; padding: 10px 20px; border-radius: 5px; z-index: 10000;"">${{message}}</div>`);
$('body').append(notification);
setTimeout(function() {{
notification.fadeOut(function() {{
notification.remove();
}});
}}, 2000);
}}
editor.on('click', function(e) {{
if (!$(e.target).hasClass('visual-tag')) {{
editor.focus();
}}
}});
editor.on('selectstart', function(e) {{
if ($(e.target).hasClass('visual-tag')) {{
e.preventDefault();
return false;
}}
}});
// ✂️ Strip out empty text‑nodes (just spaces/newlines) so they don’t count (node added due to source tag placement)
editor.contents().filter(function() {{
return this.nodeType === Node.TEXT_NODE && !/\S/.test(this.nodeValue);
}}).remove();
editor.focus();
updateOutputs();
function detectEncoding(text) {{
const gsmChars = /^[\x00-\x7F]*$/;
return gsmChars.test(text) ? 'ASCII' : 'UTF';
}}
function calculateSMS(message) {{
const encoding = detectEncoding(message);
const charCount = message.length;
const singleLimit = encoding === 'UTF' ? 70 : 160;
const multiLimit = encoding === 'UTF' ? 67 : 153;
const segments = charCount <= singleLimit ? 1 : Math.ceil(charCount / multiLimit);
const explanation = segments > 1
? `📄 پیام شما شامل ${{charCount}} کاراکتر ${{encoding === 'UTF' ? 'یونیکد' : 'لاتین'}} است، به همین دلیل طول آن ${{segments}} پیام است. (هر پیام ${{encoding === 'UTF' ? 'یونیکد' : 'لاتین'}} تا ${{singleLimit}} کاراکتر، و پیام‌های ادامه‌دار فقط ${{multiLimit}} کاراکتر را در خود جای می‌دهند.)`
: `📄 پیام شما شامل ${{charCount}} کاراکتر ${{encoding === 'UTF' ? 'یونیکد' : 'لاتین'}} است.`;
return {{ charCount, encoding, segments, perSegmentLimit: singleLimit, continuationLimit: multiLimit, explanation }};
}}
function generateExpectedMessage(template) {{
let result = '';
let cursor = 0;
const regex = /{{{{[^{{}}]+}}}}/g;
let match;
while ((match = regex.exec(template)) !== null) {{
// append text before tag
result += template.slice(cursor, match.index);
const tagToken = match[0];
const def = tags.find(t => `{{{{${{t.tag}}}}}}` === tagToken);
if (def) {{
// use ASCII filler for ASCII tags, UTF filler for UTF tags
const fillerChar = def.contentType === 'UTF' ? 'ا' : 'A';
result += fillerChar.repeat(def.maxExpectedLength);
}} else {{
// unknown tags treated as literal text
result += tagToken;
}}
cursor = match.index + tagToken.length;
}}
// append remaining text
result += template.slice(cursor);
return result;
}}
// Expose API for external control
window.templateEditor = {{
/**
* Get current template string (with tags)
*/
getTemplate: function() {{
return templateOutput.text();
}},
/**
* Set template string (with tags) into editor
* @param {{string}} tpl - template text with tags
*/
setTemplate: function(tpl) {{
// Clear editor and render tags
editor.empty();
// Use same logic as pasteTemplate
let processedHTML = tpl;
Object.keys(sampleData).forEach(tag => {{
const regex = new RegExp(tag.replace(/[{{}}]/g, '\$&'), 'g');
const tagHTML = `<span class=""visual-tag"" data-tag=""${{tag}}"" data-description=""${{validTags[tag].description}}"" contenteditable=""false"">${{validTags[tag].title}}</span>`;
processedHTML = processedHTML.replace(regex, tagHTML);
}});
editor.html(processedHTML);
updateOutputs();
editor.focus();
}}
}};
}});
</script>
");
private IHtmlContent UpdateInputScript() => new HtmlString($@"
<script>
/*
* Keep the Input updated
* */
const $hiddenInput = $('#smsTemplateInput');
const $templateOutput = $('#templateOutput');
const observer = new MutationObserver(function () {{
if ($templateOutput.length && $hiddenInput.length) {{
$hiddenInput.val($templateOutput.text() || """");
}}
}});
if ($templateOutput.length) {{
observer.observe($templateOutput[0], {{ childList: true, subtree: true, characterData: true }});
}}
</script>
");
}
@{
// Pass it to the caller via ViewData or another mechanism
ViewData["Scripts"] ??= new List<IHtmlContent>();
var scriptList = (List<IHtmlContent>)ViewData["Scripts"]!;
scriptList.Add(TagJsonScript());
scriptList.Add(LoadTagScript());
scriptList.Add(LoadEditorCoreScript());
scriptList.Add(UpdateInputScript());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment