UNPKG

@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
"use strict"; 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