@taiga-ui/cdk
Version:
Base library for creating Angular components and applications using Taiga UI principles regarding of actual visual appearance
398 lines • 18.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.migrateInput = migrateInput;
exports.buildTuiInputReplacement = buildTuiInputReplacement;
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/input';
const CONTROL_ATTR_NAMES = [
'formControlName',
'[formControl]',
'formControl',
'[(ngModel)]',
'[ngModel]',
'ngModel',
];
const CONTROL_ATTRS = new Set(CONTROL_ATTR_NAMES.map((name) => name.toLowerCase()));
const HINT_ATTRS = new Set([
'[tuiHintAppearance]'.toLowerCase(),
'[tuiHintContent]'.toLowerCase(),
'[tuiHintDirection]'.toLowerCase(),
'tuiHintAppearance'.toLowerCase(),
'tuiHintContent'.toLowerCase(),
'tuiHintDirection'.toLowerCase(),
]);
const TEXTFIELD_WRAPPER_ATTRS = new Set([
'[tuiTextfieldAppearance]'.toLowerCase(),
'[tuiTextfieldCleaner]'.toLowerCase(),
'[tuiTextfieldSize]'.toLowerCase(),
'tuiTextfieldAppearance'.toLowerCase(),
'tuiTextfieldCleaner'.toLowerCase(),
'tuiTextfieldSize'.toLowerCase(),
]);
const TEXTFIELD_WRAPPER_ATTR_RENAMES = new Map([
['[tuiTextfieldFiller]'.toLowerCase(), '[filler]'],
['[tuiTextfieldIcon]'.toLowerCase(), '[iconEnd]'],
['[tuiTextfieldIconLeft]'.toLowerCase(), '[iconStart]'],
['tuiTextfieldFiller'.toLowerCase(), 'filler'],
['tuiTextfieldIcon'.toLowerCase(), 'iconEnd'],
['tuiTextfieldIconLeft'.toLowerCase(), 'iconStart'],
]);
const ATTRS_WITH_NO_EQUIVALENT = new Set([
'[tuiTextfieldPostfix]'.toLowerCase(),
'[tuiTextfieldPrefix]'.toLowerCase(),
'tuiTextfieldPostfix'.toLowerCase(),
'tuiTextfieldPrefix'.toLowerCase(),
]);
const LABEL_OUTSIDE_ATTRS = new Set([
'[tuiTextfieldLabelOutside]'.toLowerCase(),
'tuiTextfieldLabelOutside'.toLowerCase(),
]);
const LEGACY_INPUT_ATTRS = new Set([
'tuiTextfield'.toLowerCase(),
'tuiTextfieldLegacy'.toLowerCase(),
]);
function isDropdownAttr(nameLower) {
const prefix = 'TuiDropdown'.toLowerCase();
const stripped = nameLower.replaceAll(/^\[|\]$|\(|\)/g, '');
return stripped.startsWith(prefix);
}
function isClassOrStyleAttr(nameLower) {
const stripped = nameLower.replaceAll(/^\[|\]$/g, '');
return (stripped === 'class' ||
stripped === 'style' ||
stripped === 'ngclass' ||
stripped === 'ngstyle' ||
stripped.startsWith('class.') ||
stripped.startsWith('style.'));
}
function hasHintContent(element) {
return element.attrs.some((attr) => {
const lower = attr.name.toLowerCase();
return (lower === 'tuiHintContent'.toLowerCase() ||
lower === '[tuiHintContent]'.toLowerCase());
});
}
function migrateInput({ 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-input');
const processable = elements.filter((element) => !hasHintContent(element));
(0, migrate_legacy_custom_content_1.registerCustomContentImports)(resource, processable);
const replacements = processable
.map((element) => buildTuiInputReplacement(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 buildTuiInputReplacement(template, element, hintIconStr = '') {
return buildReplacement(template, element, hintIconStr);
}
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, _c, _d, _e, _f;
const loc = element.sourceCodeLocation;
if (!(loc === null || loc === void 0 ? void 0 : loc.startTag) || !loc.endTag) {
return null;
}
const textfieldAttrs = [];
const inputAttrs = ['tuiInput'];
const controlStateAttrs = (0, control_state_attrs_1.getControlStateAttrs)(element);
const controlStateAttrsLower = new Set(controlStateAttrs.map((a) => a.name.toLowerCase()));
const ctx = {
placeholder: '',
labelOutsideValue: null,
labelOutsideIsBinding: false,
noEquivalentAttrs: [],
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 (LABEL_OUTSIDE_ATTRS.has(nameLower)) {
ctx.labelOutsideValue = attr.value || 'true';
ctx.labelOutsideIsBinding = nameLower.startsWith('[');
continue;
}
const renamedAttr = TEXTFIELD_WRAPPER_ATTR_RENAMES.get(nameLower);
if (renamedAttr !== undefined) {
textfieldAttrs.push(attr.value ? `${renamedAttr}="${attr.value}"` : renamedAttr);
continue;
}
if (ATTRS_WITH_NO_EQUIVALENT.has(nameLower)) {
const original = getOriginalAttrText(template, element, nameLower);
const originalName = (_b = (_a = original === null || original === void 0 ? void 0 : original.match(/^[\w[\]()]+/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : attr.name;
ctx.noEquivalentAttrs.push(originalName);
// Still place it on the wrapper so the code at least compiles with a warning
textfieldAttrs.push(original !== null && original !== void 0 ? original : (attr.value ? `${attr.name}="${attr.value}"` : attr.name));
continue;
}
if (isClassOrStyleAttr(nameLower)) {
const original = getOriginalAttrText(template, element, nameLower);
textfieldAttrs.push(original !== null && original !== void 0 ? original : (attr.value ? `${attr.name}="${attr.value}"` : attr.name));
continue;
}
if (TEXTFIELD_WRAPPER_ATTRS.has(nameLower) || isDropdownAttr(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 (HINT_ATTRS.has(nameLower)) {
continue;
}
if (CONTROL_ATTRS.has(nameLower)) {
const original = getOriginalAttrText(template, element, nameLower);
inputAttrs.push(original !== null && original !== void 0 ? original : (attr.value
? `${normalizeAttrName(attr.name)}="${attr.value}"`
: attr.name));
continue;
}
// Unrecognized attr — place on <tui-textfield> (the host replacement) with TODO
const original = getOriginalAttrText(template, element, nameLower);
const originalText = original !== null && original !== void 0 ? original : (attr.value ? `${attr.name}="${attr.value}"` : attr.name);
const originalName = (_d = (_c = original === null || original === void 0 ? void 0 : original.match(/^[\w[\]()]+/)) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : attr.name;
ctx.unknownAttrs.push(originalName);
textfieldAttrs.push(originalText);
}
const controlStateStr = (0, control_state_attrs_1.stringifyControlStateAttrs)(controlStateAttrs);
ctx.placeholder = getPlaceholderText(element);
const lineStart = template.lastIndexOf('\n', loc.startOffset) + 1;
const indent = (_f = (_e = /^[ \t]*/.exec(template.slice(lineStart, loc.startOffset))) === null || _e === void 0 ? void 0 : _e[0]) !== null && _f !== void 0 ? _f : '';
const isLabelOutsideTrue = ctx.labelOutsideValue === 'true' ||
(!ctx.labelOutsideIsBinding && ctx.labelOutsideValue === '');
const isLabelOutsideDynamic = ctx.labelOutsideValue !== null &&
!isLabelOutsideTrue &&
ctx.labelOutsideValue !== 'false';
const wrapperAttrsStr = textfieldAttrs.length > 0 ? ` ${textfieldAttrs.join(' ')}` : '';
const innerContent = buildInnerContent({
element,
template,
inputAttrs,
controlStateStr,
placeholder: ctx.placeholder,
indent,
labelOutsideIsTrue: isLabelOutsideTrue,
labelOutsideIsDynamic: isLabelOutsideDynamic,
hintIconStr,
customContentIconStr: (0, migrate_legacy_custom_content_1.buildCustomContentIconStr)(ctx.customContent, indent),
});
const todoComment = buildTodoComment(ctx);
// `indent` is added before <tui-textfield> only when there is a TODO — in that case
// todoComment ends with `\n` so the tag would otherwise start at column 0.
// Without a TODO the preserved whitespace before startOffset already provides the indent.
const tagIndent = todoComment ? indent : '';
const core = `${tagIndent}<tui-textfield${wrapperAttrsStr}>\n${innerContent}${indent}</tui-textfield>`;
const replacement = `${todoComment}${core}`;
return {
startOffset: loc.startOffset,
endOffset: loc.endOffset,
replacement,
};
}
function buildTodoComment(ctx) {
const notes = [];
const isLabelOutsideTrue = ctx.labelOutsideValue === 'true' ||
(!ctx.labelOutsideIsBinding && ctx.labelOutsideValue === '');
if (ctx.placeholder && !isLabelOutsideTrue) {
// labelOutside=false/absent: text → <label tuiLabel> inside — fully automatic, no note needed
}
if (ctx.noEquivalentAttrs.length > 0) {
notes.push(`${ctx.noEquivalentAttrs.join(', ')} has no direct equivalent in v5. Implement using custom content inside <tui-textfield>.`);
}
if (ctx.labelOutsideValue !== null &&
ctx.labelOutsideValue !== 'false' &&
!isLabelOutsideTrue) {
notes.push(`[tuiTextfieldLabelOutside]="${ctx.labelOutsideValue}" is dynamic and cannot be migrated automatically. Use <label tuiLabel> inside <tui-textfield> for floating label or outside for static label.`);
}
for (const name of ctx.unknownAttrs) {
notes.push(`"${name}" is an unrecognized attribute and was placed on <tui-textfield>. Move it to <input tuiInput> if it targets the native element.`);
}
if (notes.length === 0) {
return '';
}
const lines = [
`<!-- ${insert_todo_1.TODO_MARK} tui-input migration (see ${DOCS_LINK}):`,
...notes.map((n) => ` - ${n}`),
'-->',
];
return `${lines.join('\n')}\n`;
}
function buildInnerContent({ element, template, inputAttrs, controlStateStr, placeholder, indent, labelOutsideIsTrue, labelOutsideIsDynamic, hintIconStr, customContentIconStr, }) {
const childElements = element.childNodes.filter((node) => node.nodeName !== '#text' && node.nodeName !== '#comment');
const legacyInnerInput = childElements.find((node) => node.nodeName === 'input' &&
node.attrs.some((a) => LEGACY_INPUT_ATTRS.has(a.name.toLowerCase())));
const hintIconLine = hintIconStr ? `${hintIconStr}\n` : '';
const customContentLine = customContentIconStr ? `${customContentIconStr}\n` : '';
if (legacyInnerInput) {
return migrateInnerInput({
parent: element,
inner: legacyInnerInput,
template,
attrsToAdd: inputAttrs,
controlStateStr,
indent,
hintIconLine,
customContentLine,
});
}
const attrsStr = inputAttrs.length > 0 ? ` ${inputAttrs.join(' ')}` : '';
const otherChildren = childElements
.map((child) => {
const childLoc = child.sourceCodeLocation;
return childLoc
? template.slice(childLoc.startOffset, childLoc.endOffset)
: '';
})
.join('');
if (labelOutsideIsTrue) {
// labelOutside=true: text → placeholder on <input>, no label inside
const placeholderAttr = placeholder ? ` placeholder="${placeholder}"` : '';
return `${indent}<input${placeholderAttr}${attrsStr}${controlStateStr} />\n${otherChildren}${hintIconLine}${customContentLine}`;
}
// labelOutside=false/absent: text → <label tuiLabel> inside (floating label)
// dynamic: text left as-is, only TODO comment is added
let labelEl = '';
if (placeholder && labelOutsideIsDynamic) {
labelEl = `${indent}${placeholder}\n`;
}
else if (placeholder) {
labelEl = `${indent}<label tuiLabel>${placeholder}</label>\n`;
}
return `${labelEl}${indent}<input${attrsStr}${controlStateStr} />\n${otherChildren}${hintIconLine}${customContentLine}`;
}
/**
* Rewrites an existing <input tuiTextfieldLegacy> to <input tuiInput ...>
* by replacing the legacy directive attr and appending form control attrs.
*/
function migrateInnerInput({ parent, inner, template, attrsToAdd, controlStateStr, 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_INPUT_ATTRS.has(a.name.toLowerCase()));
const legacyAttrLoc = legacyAttr
? (_a = innerLoc.attrs) === null || _a === void 0 ? void 0 : _a[legacyAttr.name.toLowerCase()]
: undefined;
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)}tuiInput${startTag.slice(relEnd)}`;
}
// <input> is a void element — insert extra attrs before the closing > or />
// trimEnd() removes trailing whitespace/newlines before '>' to avoid a visual gap
const closePos = getVoidClosePos(startTag);
const extraAttrs = attrsToAdd.filter((a) => a !== 'tuiInput').join(' ');
const insertStr = `${extraAttrs ? ` ${extraAttrs}` : ''}${controlStateStr}`;
startTag = `${startTag.slice(0, closePos).trimEnd()}${insertStr}${startTag.slice(closePos)}`;
const labelContent = buildLabelContent(parent, inner, template);
const labelHtml = labelContent
? `${indent}<label tuiLabel>${labelContent}</label>\n`
: '';
const innerStart = innerLoc.startOffset;
const siblingsAfter = parent.childNodes
.filter((child) => {
if (child === inner ||
child.nodeName === '#text' ||
child.nodeName === '#comment') {
return false;
}
const loc = child.sourceCodeLocation;
return !!loc && loc.startOffset > innerStart;
})
.map((child) => {
const loc = child.sourceCodeLocation;
return loc ? template.slice(loc.startOffset, loc.endOffset) : '';
})
.join('');
return `${labelHtml}${indent}${startTag}\n${siblingsAfter}${hintIconLine}${customContentLine}`;
}
function buildLabelContent(parent, inner, template) {
var _a;
const innerStart = (_a = inner.sourceCodeLocation) === null || _a === void 0 ? void 0 : _a.startOffset;
return innerStart === undefined
? ''
: parent.childNodes
.filter((child) => {
if (child === inner || child.nodeName === '#comment') {
return false;
}
const loc = child.sourceCodeLocation;
return !!loc && loc.startOffset < innerStart;
})
.map((child) => {
if (child.nodeName === '#text') {
return child.value;
}
const loc = child.sourceCodeLocation;
return loc ? template.slice(loc.startOffset, loc.endOffset) : '';
})
.join('')
.replaceAll(/\s+/g, ' ')
.trim();
}
/**
* Returns the position just before the closing `>` or `/>` in a void element tag string.
*/
function getVoidClosePos(tag) {
return tag.endsWith('/>') ? tag.length - 2 : tag.lastIndexOf('>');
}
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 '[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-input.js.map