UNPKG

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