UNPKG

igniteui-angular

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

362 lines (361 loc) • 20.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const UpdateChanges_1 = require("../common/UpdateChanges"); const util_1 = require("../common/util"); // use bare specifier to escape the schematics encapsulation for the dynamic import: const import_helper_js_1 = require("igniteui-angular/migrations/common/import-helper.js"); const version = '12.0.0'; exports.default = (options) => (host, context) => __awaiter(void 0, void 0, void 0, function* () { context.logger.info(`Applying migration for Ignite UI for Angular to version ${version}`); const { HtmlParser } = yield (0, import_helper_js_1.nativeImport)('@angular/compiler'); // eslint-disable-next-line max-len const UPDATE_NOTE = `<!--NOTE: This component has been updated by Infragistics migration: v${version}\nPlease check your template whether all bindings/event handlers are correct.-->\n`; const COMPONENTS = [ { component: 'igx-bottom-nav', tags: ['igx-bottom-nav-item', 'igx-tab-panel', 'igx-tab'], tabItem: 'igx-bottom-nav-item', headerItem: 'igx-bottom-nav-header', panelItem: 'igx-bottom-nav-content', iconDirective: 'igxBottomNavHeaderIcon', labelDirective: 'igxBottomNavHeaderLabel' }, { component: 'igx-tabs', tags: ['igx-tabs-group', 'igx-tab-item'], tabItem: 'igx-tab-item', headerItem: 'igx-tab-header', panelItem: 'igx-tab-content', iconDirective: 'igxTabHeaderIcon', labelDirective: 'igxTabHeaderLabel' } ]; const EDITOR_COMPONENTS = [{ COMPONENT: 'igx-date-picker', TEMPLATE_DIRECTIVE: 'igxDatePickerTemplate', TEMPLATE_WARN_MSG: `\n<!-- igxDatePickerTemplate has been removed. Label, prefix, suffix and hint can now be projected directly. See https://www.infragistics.com/products/ignite-ui-angular/angular/components/date-picker -->\n` }, { COMPONENT: 'igx-time-picker', TEMPLATE_DIRECTIVE: 'igxTimePickerTemplate', TEMPLATE_WARN_MSG: `\n<!-- igxTimePickerTemplate has been removed. Label, prefix, suffix and hint can now be projected directly. See https://www.infragistics.com/products/ignite-ui-angular/angular/components/time-picker -->\n` }]; const EDITORS_MODE = ['[mode]', 'mode']; const EDITORS_LABEL = ['[label]', 'label']; const EDITORS_LABEL_VISIBILITY = ['[labelVisibility]', 'labelVisibility']; const update = new UpdateChanges_1.UpdateChanges(__dirname, host, context); const changes = new Map(); const htmlFiles = update.templateFiles; const sassFiles = update.sassFiles; const _tsFiles = update.tsFiles; let applyComment = false; const applyChanges = () => { for (const [path, change] of changes.entries()) { let buffer = host.read(path).toString(); change.sort((c, c1) => c.position - c1.position) .reverse() .forEach(c => buffer = c.apply(buffer)); host.overwrite(path, buffer); applyComment = true; } }; const addChange = (path, change) => { if (changes.has(path)) { changes.get(path).push(change); } else { changes.set(path, [change]); } }; const isEmptyOrSpaces = (str) => str === null || str === '' || str === '\n' || str === '\r\n' || str.match(/^[\r\n\t]* *$/) !== null; // Replace the tabsType input with tabsAligment for (const path of htmlFiles) { (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), 'igx-tabs') .forEach(node => { if ((0, util_1.hasAttribute)(node, 'type')) { const { startTag, file } = (0, util_1.getSourceOffset)(node); const tabsType = (0, util_1.getAttribute)(node, 'type')[0]; let alignment; if (tabsType.value.toLowerCase() === 'fixed') { alignment = 'justify'; } else if (tabsType.value.toLowerCase() === 'contentfit') { alignment = 'start'; } const tabAlignment = alignment ? ` tabAlignment="${alignment}"` : ''; addChange(file.url, new util_1.FileChange(startTag.end - 1, tabAlignment)); } }); } applyChanges(); changes.clear(); for (const comp of COMPONENTS) { for (const path of htmlFiles) { // Replace the <ng-template igxTab> if any with <igx-tab-item> (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.tags) .map(tab => (0, util_1.findElementNodes)([tab], 'ng-template')) .reduce((prev, curr) => prev.concat(curr), []) .filter(template => (0, util_1.hasAttribute)(template, 'igxTab')) .forEach(node => { const { startTag, endTag, file } = (0, util_1.getSourceOffset)(node); const content = file.content.substring(startTag.end, endTag.start); const textToReplace = file.content.substring(startTag.start, endTag.end); const tabPanel = `<${comp.headerItem}>${content}</${comp.headerItem}>`; addChange(file.url, new util_1.FileChange(startTag.start, tabPanel, textToReplace, 'replace')); }); applyChanges(); changes.clear(); // Convert label and icon to igx-tab-header children -> // <igx-icon igxTabHeaderIcon> and <span igxTabHeaderLabel> (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.tags). map(node => (0, util_1.getSourceOffset)(node)). forEach(offset => { const { startTag, file, node } = offset; // Label content let labelText = ''; if ((0, util_1.hasAttribute)(node, 'label')) { const labelAttr = (0, util_1.getAttribute)(node, 'label')[0]; labelText = `\n<span ${comp.labelDirective}>${labelAttr.value}</span>`; } // Icon content let iconText = ''; if ((0, util_1.hasAttribute)(node, 'icon')) { const iconAttr = (0, util_1.getAttribute)(node, 'icon')[0]; iconText = `\n<igx-icon ${comp.iconDirective}>${iconAttr.value}</igx-icon>`; } // RouterLink let routerLinkText = ''; if ((0, util_1.hasAttribute)(node, 'routerLink')) { const routerLink = (0, util_1.getAttribute)(node, 'routerLink')[0]; routerLinkText = ` ${routerLink.name}="${routerLink.value}"`; } let classText = ''; if ((node.name === 'igx-tab-item' || node.name === 'igx-tab') && (0, util_1.hasAttribute)(node, 'class')) { const classAttr = (0, util_1.getAttribute)(node, 'class')[0].value; classText = !isEmptyOrSpaces(classAttr) ? ` class="${classAttr}"` : ''; } if (iconText || labelText || routerLinkText) { // Get igx-tab-header if it's present const headerNode = (0, util_1.findElementNodes)([offset.node], comp.headerItem) .map(hNode => (0, util_1.getSourceOffset)(hNode)); if (headerNode && headerNode.length > 0) { addChange(file.url, new util_1.FileChange(headerNode[0].startTag.end - 1, `${routerLinkText}${classText}`)); } else { // eslint-disable-next-line max-len const tabHeader = `\n<${comp.headerItem}${routerLinkText}${classText}>${iconText}${labelText}\n</${comp.headerItem}>`; addChange(file.url, new util_1.FileChange(startTag.end, tabHeader)); } } }); applyChanges(); changes.clear(); // Grab the content between <igx-tabs-group> and create a <igx-tab-content> // Also migrate class from igx-tabs-group to igx-tab-content, if any (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.tags) .map(node => (0, util_1.getSourceOffset)(node)) .forEach(offset => { const tabHeader = offset.node.children.find(c => c.name === comp.headerItem); let classAttrText = ''; if (offset.node.name === 'igx-tab-panel' || offset.node.name === 'igx-tabs-group') { const classAttr = (0, util_1.hasAttribute)(offset.node, 'class') ? (0, util_1.getAttribute)(offset.node, 'class')[0].value : ''; classAttrText = !isEmptyOrSpaces(classAttr) ? ` class="${classAttr}"` : ''; } if (tabHeader) { const content = offset.file.content.substring(tabHeader.sourceSpan.end.offset, offset.endTag.start); // Since igx-tab-item tag is common for old and new igx-tabs // Check whether igx-tab-content is already present! const tabContentTag = new RegExp(String.raw `${comp.panelItem}`); const hasTabContent = content.match(tabContentTag); if (!hasTabContent && !isEmptyOrSpaces(content)) { const tabPanel = `\n<${comp.panelItem}${classAttrText}>${content}</${comp.panelItem}>\n`; addChange(offset.file.url, new util_1.FileChange(tabHeader.sourceSpan.end.offset, tabPanel, content, 'replace')); } } }); applyChanges(); changes.clear(); // Insert a comment indicating the change/replacement if (applyComment) { (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.component). map(node => (0, util_1.getSourceOffset)(node)). forEach(offset => { const { startTag, file } = offset; const commentText = UPDATE_NOTE; addChange(file.url, new util_1.FileChange(startTag.start, commentText)); }); applyChanges(); changes.clear(); } } for (const sassPath of sassFiles) { const origContent = host.read(sassPath).toString(); let newContent = origContent; let changed = false; for (const item of comp.tags) { const searchText = new RegExp(String.raw `${item}(?=[\s])`); let replaceText = ''; if (comp.component === 'igx-tabs') { if (item === 'igx-tab-item') { replaceText = comp.headerItem; } else if (item === 'igx-tabs-group') { replaceText = comp.panelItem; } } if (comp.component === 'igx-bottom-nav') { if (item === 'igx-tab') { replaceText = comp.headerItem; } else if (item === 'igx-tab-panel') { replaceText = comp.panelItem; } } if (searchText.test(origContent)) { changed = true; newContent = newContent.replace(searchText, replaceText); } } if (changed) { host.overwrite(sassPath, newContent); } } } // igxDatePicker & igxTimePicker migrations for (const comp of EDITOR_COMPONENTS) { for (const path of htmlFiles) { // DatePicker and TimePicker don't support templates anymore. // That is why migrations inserts a comment to notify the developer to remove the templates. (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.COMPONENT) .map(editor => (0, util_1.findElementNodes)([editor], 'ng-template')) .reduce((prev, curr) => prev.concat(curr), []) .filter(template => (0, util_1.hasAttribute)(template, comp.TEMPLATE_DIRECTIVE)) .map(node => (0, util_1.getSourceOffset)(node)) .forEach(offset => { const { startTag, file } = offset; addChange(file.url, new util_1.FileChange(startTag.start, comp.TEMPLATE_WARN_MSG)); }); // DatePicker and TimePicker default mode is changed to dropdown. // 1. That is why any occurrence of drop down mode is removed and // 2. dialog mode is added for those that didn't explicitly set the mode prop. // 1. Remove dropdown mode (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.COMPONENT) .filter(template => (0, util_1.hasAttribute)(template, EDITORS_MODE)) .map(node => (0, util_1.getSourceOffset)(node)) .forEach(offset => { const { file } = offset; (0, util_1.getAttribute)(offset.node, EDITORS_MODE).forEach(attr => { const { sourceSpan, value } = attr; if (value.replace(/'/g, '').replace(/"/g, '') === 'dropdown') { const attrKeyValue = file.content.substring(sourceSpan.start.offset, sourceSpan.end.offset); addChange(file.url, new util_1.FileChange(sourceSpan.start.offset, '', attrKeyValue, 'replace')); } }); }); // 2. Insert dialog mode (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.COMPONENT) .filter(template => !(0, util_1.hasAttribute)(template, EDITORS_MODE)) .map(node => (0, util_1.getSourceOffset)(node)) .forEach(offset => { const { startTag, file } = offset; addChange(file.url, new util_1.FileChange(startTag.end - 1, ' mode="dialog"')); }); // Remove label property and project it as <label igxLabel></label> // Check also labelVisibility value. (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.COMPONENT) .filter(template => (0, util_1.hasAttribute)(template, EDITORS_LABEL)) .map(node => (0, util_1.getSourceOffset)(node)) .forEach(offset => { const { startTag, file } = offset; let visibilityValue = true; if ((0, util_1.hasAttribute)(offset.node, EDITORS_LABEL_VISIBILITY)) { const visibility = (0, util_1.getAttribute)(offset.node, EDITORS_LABEL_VISIBILITY); visibilityValue = visibility[0].value; } (0, util_1.getAttribute)(offset.node, EDITORS_LABEL).forEach(attr => { const { sourceSpan, name, value } = attr; const attrKeyValue = file.content.substring(sourceSpan.start.offset, sourceSpan.end.offset); let label; const ngIF = (typeof visibilityValue === 'boolean') ? `` : ` *ngIf="${visibilityValue}"`; if (name.startsWith('[')) { label = `\n<label igxLabel${ngIF}>{{${value}}}</label>`; } else { label = `\n<label igxLabel${ngIF}>${value}</label>`; } addChange(file.url, new util_1.FileChange(sourceSpan.start.offset, '', attrKeyValue, 'replace')); addChange(file.url, new util_1.FileChange(startTag.end, label)); }); }); // If label and labelVisibility are not set this means that we should project default labels: "Date" & "Time" (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), comp.COMPONENT) .filter(template => !(0, util_1.hasAttribute)(template, EDITORS_LABEL) && !(0, util_1.hasAttribute)(template, EDITORS_LABEL_VISIBILITY)) .map(node => (0, util_1.getSourceOffset)(node)) .forEach(offset => { if ((0, util_1.findElementNodes)([offset.node], 'label').length === 0) { const { startTag, file } = offset; addChange(file.url, new util_1.FileChange(startTag.end, `\n<label igxLabel>${comp.COMPONENT === 'igx-date-picker' ? 'Date' : 'Time'}</label>`)); } }); applyChanges(); changes.clear(); } } // input group disabled property const INPUT_GROUP_CHANGES = { GROUP_TAG: 'igx-input-group', ATTRIBUTES: ['[disabled]', 'disabled'], INPUT_TAG: ['input', 'textarea'], DIRECTIVE: 'igxInput' }; for (const path of htmlFiles) { const inputGroups = (0, util_1.findElementNodes)((0, util_1.parseFile)(new HtmlParser(), host, path), INPUT_GROUP_CHANGES.GROUP_TAG); inputGroups.forEach((el) => { const parentEl = el; if ((0, util_1.hasAttribute)(parentEl, INPUT_GROUP_CHANGES.ATTRIBUTES)) { const inputChildren = (0, util_1.findElementNodes)([el], INPUT_GROUP_CHANGES.INPUT_TAG) .reduce((prev, curr) => prev.concat(curr), []) .filter(template => (0, util_1.hasAttribute)(template, INPUT_GROUP_CHANGES.DIRECTIVE)); INPUT_GROUP_CHANGES.ATTRIBUTES.forEach((a) => { const attr = (0, util_1.getAttribute)(parentEl, a)[0]; if (attr) { inputChildren.forEach((node) => { if (!(0, util_1.hasAttribute)(node, INPUT_GROUP_CHANGES.ATTRIBUTES)) { const { startTag, file } = (0, util_1.getSourceOffset)(node); // input is self-closing, so the element === startTag const repTxt = file.content.substring(startTag.start, startTag.end); const matches = repTxt.match(/\/?>/g); // should always be only 1 match const lastIndex = repTxt.indexOf(matches[0]); let property = `${attr.name}`; if (attr.name === INPUT_GROUP_CHANGES.ATTRIBUTES[0] || attr.value) { property += `="${attr.value}"`; } const addedAttr = `${repTxt.substring(0, lastIndex)} ${property}${repTxt.substring(lastIndex)}`; addChange(file.url, new util_1.FileChange(startTag.start, addedAttr, repTxt, 'replace')); } }); } }); } }); applyChanges(); changes.clear(); } // Apply all selector and input changes update.shouldInvokeLS = options['shouldInvokeLS']; update.applyChanges(); });