@taiga-ui/cdk
Version:
Base library for creating Angular components and applications using Taiga UI principles regarding of actual visual appearance
348 lines • 16.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildTuiTextareaReplacement = buildTuiTextareaReplacement;
exports.migrateTextarea = migrateTextarea;
const insert_todo_1 = require("../../../../utils/insert-todo");
const elements_1 = require("../../../../utils/templates/elements");
const migrate_attr_value_1 = require("../../../../utils/templates/migrate-attr-value");
const template_resource_1 = require("../../../../utils/templates/template-resource");
const control_state_attrs_1 = require("../../../utils/templates/control-state-attrs");
const migrate_legacy_custom_content_1 = require("./migrate-legacy-custom-content");
const DOCS_LINK = 'https://taiga-ui.dev/components/textarea';
const CONTROL_ATTR_NAMES = [
'formControlName',
'[formControl]',
'formControl',
'[(ngModel)]',
'[ngModel]',
'ngModel',
'(ngModelChange)',
];
const CONTROL_ATTRS = new Set(CONTROL_ATTR_NAMES.map((name) => name.toLowerCase()));
const TEXTFIELD_WRAPPER_ATTRS = new Set([
'[tuiTextfieldAppearance]'.toLowerCase(),
'[tuiTextfieldCleaner]'.toLowerCase(),
'[tuiTextfieldSize]'.toLowerCase(),
'tuiTextfieldAppearance'.toLowerCase(),
'tuiTextfieldCleaner'.toLowerCase(),
'tuiTextfieldSize'.toLowerCase(),
]);
const HINT_ATTRS = new Set([
'[tuiHintAppearance]'.toLowerCase(),
'[tuiHintContent]'.toLowerCase(),
'[tuiHintDirection]'.toLowerCase(),
'tuiHintAppearance'.toLowerCase(),
'tuiHintContent'.toLowerCase(),
'tuiHintDirection'.toLowerCase(),
]);
const ATTRS_TO_DROP = new Set([
'[tuiTextfieldLabelOutside]'.toLowerCase(),
'tuiTextfieldLabelOutside'.toLowerCase(),
]);
function hasHintContent(element) {
return element.attrs.some((attr) => {
const lower = attr.name.toLowerCase();
return (lower === 'tuiHintContent'.toLowerCase() ||
lower === '[tuiHintContent]'.toLowerCase());
});
}
function buildTuiTextareaReplacement(template, element, hintIconStr = '') {
return buildReplacement(template, element, hintIconStr);
}
function migrateTextarea({ resource, recorder, fileSystem, }) {
const template = (0, template_resource_1.getTemplateFromTemplateResource)(resource, fileSystem);
const templateOffset = (0, template_resource_1.getTemplateOffset)(resource);
const elements = (0, elements_1.findElementsByTagName)(template, 'tui-textarea');
const processable = elements.filter((element) => !hasHintContent(element));
(0, migrate_legacy_custom_content_1.registerCustomContentImports)(resource, processable);
const replacements = processable
.map((element) => buildReplacement(template, element))
.filter((x) => Boolean(x))
.sort((a, b) => b.startOffset - a.startOffset);
replacements.forEach(({ startOffset, endOffset, replacement }) => {
recorder.remove(templateOffset + startOffset, endOffset - startOffset);
recorder.insertRight(templateOffset + startOffset, replacement);
});
}
function getOriginalAttrText(template, element, attrNameLower) {
var _a, _b;
const attrLoc = (_b = (_a = element.sourceCodeLocation) === null || _a === void 0 ? void 0 : _a.attrs) === null || _b === void 0 ? void 0 : _b[attrNameLower];
return attrLoc ? template.slice(attrLoc.startOffset, attrLoc.endOffset) : null;
}
function buildReplacement(template, element, hintIconStr = '') {
var _a, _b;
const loc = element.sourceCodeLocation;
if (!(loc === null || loc === void 0 ? void 0 : loc.startTag)) {
return null;
}
const isSelfClosing = !loc.endTag;
const endOffset = isSelfClosing ? loc.startTag.endOffset : loc.endOffset;
const textfieldAttrs = [];
const textareaAttrs = ['tuiTextarea'];
const controlStateAttrs = (0, control_state_attrs_1.getControlStateAttrs)(element);
const controlStateAttrsLower = new Set(controlStateAttrs.map((a) => a.name.toLowerCase()));
let maxLengthAttrText = null;
let maxLengthIsBinding = false;
const ctx = {
expandableValue: null,
rowsMigratedTo: null,
placeholder: '',
labelOutside: false,
unknownAttrs: [],
customContent: null,
};
for (const attr of element.attrs) {
const nameLower = attr.name.toLowerCase();
if (controlStateAttrsLower.has(nameLower)) {
continue;
}
if (migrate_legacy_custom_content_1.CUSTOM_CONTENT_ATTRS.has(nameLower)) {
ctx.customContent = {
value: attr.value,
isBinding: nameLower.startsWith('['),
};
continue;
}
if (nameLower === 'expandable' || nameLower === '[expandable]') {
ctx.expandableValue = attr.value || 'true';
continue;
}
if (ATTRS_TO_DROP.has(nameLower)) {
// All attrs in ATTRS_TO_DROP are labelOutside variants — capture value before dropping
const val = attr.value.trim();
if (val === 'true' || val === '') {
ctx.labelOutside = true;
}
else if (val === 'false') {
ctx.labelOutside = false;
}
else {
ctx.labelOutside = 'dynamic';
}
continue;
}
if (HINT_ATTRS.has(nameLower)) {
continue;
}
if (TEXTFIELD_WRAPPER_ATTRS.has(nameLower)) {
const original = getOriginalAttrText(template, element, nameLower);
const migratedValue = (0, migrate_attr_value_1.migrateAttrValue)(nameLower, attr.value);
let attrText;
if (original) {
attrText = original.replace(`="${attr.value}"`, `="${migratedValue}"`);
}
else if (attr.value) {
attrText = `${attr.name}="${migratedValue}"`;
}
else {
attrText = attr.name;
}
textfieldAttrs.push(attrText);
continue;
}
if (CONTROL_ATTRS.has(nameLower)) {
const original = getOriginalAttrText(template, element, nameLower);
textareaAttrs.push(original !== null && original !== void 0 ? original : (attr.value
? `${normalizeAttrName(attr.name)}="${attr.value}"`
: attr.name));
continue;
}
if (nameLower === '[maxlength]' || nameLower === 'maxlength') {
maxLengthAttrText = attr.value;
maxLengthIsBinding = nameLower.startsWith('[');
continue;
}
if (nameLower === '[rows]' || nameLower === 'rows') {
const isBinding = nameLower.startsWith('[');
const attrName = isBinding ? '[max]' : 'max';
const attrText = `${attrName}="${attr.value}"`;
ctx.rowsMigratedTo = attrText;
textareaAttrs.push(attrText);
continue;
}
const original = getOriginalAttrText(template, element, nameLower);
const attrText = original !== null && original !== void 0 ? original : (attr.value ? `${attr.name}="${attr.value}"` : attr.name);
// Unknown attrs go on <tui-textfield> (direct host replacement) with a TODO
textfieldAttrs.push(attrText);
ctx.unknownAttrs.push(attrText);
}
if (maxLengthAttrText !== null) {
const attrName = maxLengthIsBinding ? '[limit]' : 'limit';
textareaAttrs.push(`${attrName}="${maxLengthAttrText}"`);
}
const controlStateStr = (0, control_state_attrs_1.stringifyControlStateAttrs)(controlStateAttrs);
ctx.placeholder = isSelfClosing ? '' : getPlaceholderText(element);
const lineStart = template.lastIndexOf('\n', loc.startOffset) + 1;
const indent = (_b = (_a = /^[ \t]*/.exec(template.slice(lineStart, loc.startOffset))) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : '';
const wrapperAttrsStr = textfieldAttrs.length > 0 ? ` ${textfieldAttrs.join(' ')}` : '';
const innerContent = buildInnerContent({
element,
template,
textareaAttrs,
controlStateStr,
ctx,
indent,
hintIconStr,
customContentIconStr: (0, migrate_legacy_custom_content_1.buildCustomContentIconStr)(ctx.customContent, indent),
isSelfClosing,
});
const todoComment = buildTodoComment(ctx);
const replacement = `${todoComment}${indent}<tui-textfield${wrapperAttrsStr}>\n${innerContent}${indent}</tui-textfield>`;
return {
startOffset: loc.startOffset,
endOffset,
replacement,
};
}
function buildTodoComment(ctx) {
const notes = [];
if (ctx.placeholder && ctx.labelOutside === 'dynamic') {
notes.push('[tuiTextfieldLabelOutside] was dynamic and cannot be migrated automatically. Use <label tuiLabel> inside <tui-textfield> for floating label or outside for static label.');
}
// labelOutside=true: text → placeholder — fully automatic, no note needed
// labelOutside=false/absent: text → <label tuiLabel> inside — fully automatic, no note needed
if (ctx.expandableValue !== null) {
const wasFixed = ctx.expandableValue === 'false' || ctx.expandableValue === '';
if (wasFixed) {
if (ctx.rowsMigratedTo) {
notes.push(`expandable="false" was removed. New component always auto-resizes. To restore fixed height, also add the same value for [min] on <textarea tuiTextarea> (${ctx.rowsMigratedTo} was already migrated).`);
}
else {
notes.push('expandable="false" was removed. New component always auto-resizes. To restore fixed height, set [min] and [max] to the same value on <textarea tuiTextarea> (legacy default was 20 rows).');
}
}
else {
notes.push(`[expandable] was removed. New component always auto-resizes between [min] (default: 1) and [max] (default: 3) rows.${ctx.rowsMigratedTo ? ` [rows] was migrated to ${ctx.rowsMigratedTo}.` : ''}`);
}
}
else if (ctx.rowsMigratedTo) {
// rows present but expandable not specified — legacy default was fixed height
notes.push(`[rows] was migrated to ${ctx.rowsMigratedTo}. Legacy tui-textarea had fixed height by default (expandable=false). To restore fixed height, also add the same value for [min] on <textarea tuiTextarea>.`);
}
else {
// Neither expandable nor rows — legacy default was 20 rows fixed height
notes.push('Legacy tui-textarea had a fixed height of 20 rows by default. New component auto-resizes between [min] (default: 1) and [max] (default: 3) rows. Set min and max explicitly if the previous layout needs to be preserved.');
}
for (const attr of ctx.unknownAttrs) {
notes.push(`Unrecognized attribute "${attr}" was placed on <tui-textfield>. Move it to <textarea tuiTextarea> if it targets the native element.`);
}
const lines = [
`<!-- ${insert_todo_1.TODO_MARK} tui-textarea migration (see ${DOCS_LINK}):`,
...notes.map((n) => ` - ${n}`),
'-->',
];
return `${lines.join('\n')}\n`;
}
const LEGACY_TEXTAREA_ATTRS = new Set([
'tuiTextfield'.toLowerCase(),
'tuiTextfieldLegacy'.toLowerCase(),
]);
function buildInnerContent({ element, template, textareaAttrs, controlStateStr, ctx, indent, hintIconStr = '', customContentIconStr = '', isSelfClosing = false, }) {
const { placeholder, labelOutside } = ctx;
const childElements = isSelfClosing
? []
: element.childNodes.filter((node) => node.nodeName !== '#text' && node.nodeName !== '#comment');
// Auto-add <label tuiLabel> inside <tui-textfield> when text content is present
// and labelOutside is false/absent (dynamic: left as-is with only TODO comment)
const labelEl = placeholder && labelOutside === false
? `${indent}<label tuiLabel>${placeholder}</label>\n`
: '';
const hintIconLine = hintIconStr ? `${hintIconStr}\n` : '';
const customContentLine = customContentIconStr ? `${customContentIconStr}\n` : '';
// If user already put an explicit <textarea tuiTextfieldLegacy> inside,
// reuse it instead of generating a new one.
const legacyInnerTextarea = childElements.find((node) => node.nodeName === 'textarea' &&
node.attrs.some((a) => LEGACY_TEXTAREA_ATTRS.has(a.name.toLowerCase())));
if (legacyInnerTextarea) {
return `${labelEl}${migrateInnerTextarea({
inner: legacyInnerTextarea,
template,
attrsToAdd: textareaAttrs,
controlStateStr,
allChildren: childElements,
indent,
hintIconLine,
customContentLine,
})}`;
}
const attrsStr = textareaAttrs.length > 0 ? ` ${textareaAttrs.join(' ')}` : '';
// placeholder is added only when labelOutside=true (text → hint inside field)
// when labelOutside=false/absent the text becomes <label tuiLabel> (floating label) instead
const placeholderAttr = placeholder && labelOutside === true ? ` placeholder="${placeholder}"` : '';
const otherChildren = childElements
.map((child) => {
const childLoc = child.sourceCodeLocation;
return childLoc
? template.slice(childLoc.startOffset, childLoc.endOffset)
: '';
})
.join('');
return `${labelEl}${indent}<textarea${placeholderAttr}${attrsStr}${controlStateStr}></textarea>\n${otherChildren}${hintIconLine}${customContentLine}`;
}
/**
* Rewrites an existing <textarea tuiTextfieldLegacy> to <textarea tuiTextarea ...>
* by replacing the legacy directive attr and appending form control attrs.
*/
function migrateInnerTextarea({ inner, template, attrsToAdd, controlStateStr, allChildren, indent, hintIconLine = '', customContentLine = '', }) {
var _a;
const innerLoc = inner.sourceCodeLocation;
if (!(innerLoc === null || innerLoc === void 0 ? void 0 : innerLoc.startTag)) {
return '';
}
const legacyAttr = inner.attrs.find((a) => LEGACY_TEXTAREA_ATTRS.has(a.name.toLowerCase()));
const legacyAttrLoc = legacyAttr
? (_a = innerLoc.attrs) === null || _a === void 0 ? void 0 : _a[legacyAttr.name.toLowerCase()]
: undefined;
// Rebuild the inner textarea: replace legacy attr with tuiTextarea, append extra attrs
let startTag = template.slice(innerLoc.startTag.startOffset, innerLoc.startTag.endOffset);
if (legacyAttrLoc) {
const relStart = legacyAttrLoc.startOffset - innerLoc.startTag.startOffset;
const relEnd = legacyAttrLoc.endOffset - innerLoc.startTag.startOffset;
startTag = `${startTag.slice(0, relStart)}tuiTextarea${startTag.slice(relEnd)}`;
}
// Append form control / other attrs from the outer <tui-textarea> before the closing >
const closeAngle = startTag.lastIndexOf('>');
const extraAttrs = attrsToAdd.filter((a) => a !== 'tuiTextarea').join(' ');
const insertStr = `${extraAttrs ? ` ${extraAttrs}` : ''}${controlStateStr}`;
// trimEnd() removes trailing whitespace/newlines before '>' to avoid a visual gap
startTag = `${startTag.slice(0, closeAngle).trimEnd()}${insertStr}${startTag.slice(closeAngle)}`;
// Reconstruct full inner element (self-closing or with end tag)
const innerContent = innerLoc.endTag
? `${startTag}${template.slice(innerLoc.startTag.endOffset, innerLoc.endOffset)}`
: startTag;
// Include other sibling elements (e.g. tui-data-list-wrapper, tui-error)
const siblings = allChildren
.filter((c) => c !== inner)
.map((c) => {
const loc = c.sourceCodeLocation;
return loc ? template.slice(loc.startOffset, loc.endOffset) : '';
})
.join('');
return `${indent}${innerContent}\n${siblings}${hintIconLine}${customContentLine}`;
}
function getPlaceholderText(element) {
var _a;
const textNode = element.childNodes.find((node) => node.nodeName === '#text' ? !!node.value.trim() : false);
return (_a = textNode === null || textNode === void 0 ? void 0 : textNode.value.trim()) !== null && _a !== void 0 ? _a : '';
}
function normalizeAttrName(name) {
switch (name.toLowerCase()) {
case '(ngModelChange)'.toLowerCase():
return '(ngModelChange)';
case '[formControl]'.toLowerCase():
return '[formControl]';
case '[ngModel]'.toLowerCase():
return '[ngModel]';
case 'formControl'.toLowerCase():
return 'formControl';
case 'formControlName'.toLowerCase():
return 'formControlName';
case 'ngModel'.toLowerCase():
return 'ngModel';
case '[(ngmodel)]':
return '[(ngModel)]';
default:
return name;
}
}
//# sourceMappingURL=migrate-textarea.js.map