modules/frontend/app/services/FormUtils.service.js (273 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import _ from 'lodash'; /** * @param {ng.IWindowService} $window * @param {ReturnType<typeof import('./Focus.service').default>} Focus * @param {ng.IRootScopeService} $rootScope */ export default function service($window, Focus, $rootScope) { function ensureActivePanel(ui, pnl, focusId) { if (ui && ui.loadPanel) { const collapses = $('[bs-collapse-target]'); ui.loadPanel(pnl); const idx = _.findIndex(collapses, function(collapse) { return collapse.id === pnl; }); if (idx >= 0) { const activePanels = ui.activePanels; if (!_.includes(ui.topPanels, idx)) { ui.expanded = true; const customExpanded = ui[pnl]; if (customExpanded) ui[customExpanded] = true; } if (!activePanels || activePanels.length < 1) ui.activePanels = [idx]; else if (!_.includes(activePanels, idx)) { const newActivePanels = _.cloneDeep(activePanels); newActivePanels.push(idx); ui.activePanels = newActivePanels; } } if (!_.isNil(focusId)) Focus.move(focusId); } } /** @type {CanvasRenderingContext2D} */ let context = null; /** * Calculate width of specified text in body's font. * * @param {string} text Text to calculate width. * @returns {Number} Width of text in pixels. */ function measureText(text) { if (!context) { const canvas = document.createElement('canvas'); context = canvas.getContext('2d'); const style = window.getComputedStyle(document.getElementsByTagName('body')[0]); context.font = style.fontSize + ' ' + style.fontFamily; } return context.measureText(text).width; } /** * Compact java full class name by max number of characters. * * @param names Array of class names to compact. * @param nameLength Max available width in characters for simple name. * @returns {*} Array of compacted class names. */ function compactByMaxCharts(names, nameLength) { for (let nameIx = 0; nameIx < names.length; nameIx++) { const s = names[nameIx]; if (s.length > nameLength) { let totalLength = s.length; const packages = s.split('.'); const packageCnt = packages.length - 1; for (let i = 0; i < packageCnt && totalLength > nameLength; i++) { if (packages[i].length > 0) { totalLength -= packages[i].length - 1; packages[i] = packages[i][0]; } } if (totalLength > nameLength) { const className = packages[packageCnt]; const classNameLen = className.length; let remains = Math.min(nameLength - totalLength + classNameLen, classNameLen); if (remains < 3) remains = Math.min(3, classNameLen); packages[packageCnt] = className.substring(0, remains) + '...'; } let result = packages[0]; for (let i = 1; i < packages.length; i++) result += '.' + packages[i]; names[nameIx] = result; } } return names; } /** * Compact java full class name by max number of pixels. * * @param names Array of class names to compact. * @param nameLength Max available width in characters for simple name. Used for calculation optimization. * @param nameWidth Maximum available width in pixels for simple name. * @returns {*} Array of compacted class names. */ function compactByMaxPixels(names, nameLength, nameWidth) { if (nameWidth <= 0) return names; const fitted = []; const widthByName = []; const len = names.length; let divideTo = len; for (let nameIx = 0; nameIx < len; nameIx++) { fitted[nameIx] = false; widthByName[nameIx] = nameWidth; } // Try to distribute space from short class names to long class names. let remains = 0; do { for (let nameIx = 0; nameIx < len; nameIx++) { if (!fitted[nameIx]) { const curNameWidth = measureText(names[nameIx]); if (widthByName[nameIx] > curNameWidth) { fitted[nameIx] = true; remains += widthByName[nameIx] - curNameWidth; divideTo -= 1; widthByName[nameIx] = curNameWidth; } } } const remainsByName = remains / divideTo; for (let nameIx = 0; nameIx < len; nameIx++) { if (!fitted[nameIx]) widthByName[nameIx] += remainsByName; } } while (remains > 0); // Compact class names to available for each space. for (let nameIx = 0; nameIx < len; nameIx++) { const s = names[nameIx]; if (s.length > (nameLength / 2 | 0)) { let totalWidth = measureText(s); if (totalWidth > widthByName[nameIx]) { const packages = s.split('.'); const packageCnt = packages.length - 1; for (let i = 0; i < packageCnt && totalWidth > widthByName[nameIx]; i++) { if (packages[i].length > 1) { totalWidth -= measureText(packages[i].substring(1, packages[i].length)); packages[i] = packages[i][0]; } } let shortPackage = ''; for (let i = 0; i < packageCnt; i++) shortPackage += packages[i] + '.'; const className = packages[packageCnt]; const classLen = className.length; let minLen = Math.min(classLen, 3); totalWidth = measureText(shortPackage + className); // Compact class name if shorten package path is very long. if (totalWidth > widthByName[nameIx]) { let maxLen = classLen; let middleLen = (minLen + (maxLen - minLen) / 2 ) | 0; while (middleLen !== minLen && middleLen !== maxLen) { const middleLenPx = measureText(shortPackage + className.substr(0, middleLen) + '...'); if (middleLenPx > widthByName[nameIx]) maxLen = middleLen; else minLen = middleLen; middleLen = (minLen + (maxLen - minLen) / 2 ) | 0; } names[nameIx] = shortPackage + className.substring(0, middleLen) + '...'; } else names[nameIx] = shortPackage + className; } } } return names; } /** * Compact any string by max number of pixels. * * @param label String to compact. * @param nameWidth Maximum available width in pixels for simple name. * @returns {*} Compacted string. */ function compactLabelByPixels(label, nameWidth) { if (nameWidth <= 0) return label; const totalWidth = measureText(label); if (totalWidth > nameWidth) { let maxLen = label.length; let minLen = Math.min(maxLen, 3); let middleLen = (minLen + (maxLen - minLen) / 2 ) | 0; while (middleLen !== minLen && middleLen !== maxLen) { const middleLenPx = measureText(label.substr(0, middleLen) + '...'); if (middleLenPx > nameWidth) maxLen = middleLen; else minLen = middleLen; middleLen = (minLen + (maxLen - minLen) / 2 ) | 0; } return label.substring(0, middleLen) + '...'; } return label; } /** * Calculate available width for text in link to edit element. * * @param index Showed index of element for calculation of maximum width in pixels. * @param id Id of contains link table. * @returns {*[]} First element is length of class for single value, second element is length for pair vlaue. */ function availableWidth(index, id) { const idElem = $('#' + id); let width = 0; switch (idElem.prop('tagName')) { // Detection of available width in presentation table row. case 'TABLE': const cont = $(idElem.find('tr')[index - 1]).find('td')[0]; width = cont.clientWidth; if (width > 0) { const children = $(cont).children(':not("a")'); _.forEach(children, function(child) { if ('offsetWidth' in child) width -= $(child).outerWidth(true); }); } break; // Detection of available width in dropdown row. case 'A': width = idElem.width(); $(idElem).children(':not("span")').each(function(ix, child) { if ('offsetWidth' in child) width -= child.offsetWidth; }); break; default: } return width | 0; } // TODO: move somewhere else function triggerValidation(form) { const fe = (m) => Object.keys(m.$error)[0]; const em = (e) => (m) => { if (!e) return; const walk = (m) => { if (!m || !m.$error[e]) return; if (m.$error[e] === true) return m; return walk(m.$error[e][0]); }; return walk(m); }; $rootScope.$broadcast('$showValidationError', em(fe(form))(form)); } return { /** * Cut class name by width in pixel or width in symbol count. * * @param id Id of parent table. * @param index Row number in table. * @param maxLength Maximum length in symbols for all names. * @param names Array of class names to compact. * @param divider String to visualy divide items. * @returns {*} Array of compacted class names. */ compactJavaName(id, index, maxLength, names, divider) { divider = ' ' + divider + ' '; const prefix = index + ') '; const nameCnt = names.length; const nameLength = ((maxLength - 3 * (nameCnt - 1)) / nameCnt) | 0; try { const nameWidth = (availableWidth(index, id) - measureText(prefix) - (nameCnt - 1) * measureText(divider)) / nameCnt | 0; // HTML5 calculation of showed message width. names = compactByMaxPixels(names, nameLength, nameWidth); } catch (err) { names = compactByMaxCharts(names, nameLength); } let result = prefix + names[0]; for (let nameIx = 1; nameIx < names.length; nameIx++) result += divider + names[nameIx]; return result; }, /** * Compact text by width in pixels or symbols count. * * @param id Id of parent table. * @param index Row number in table. * @param maxLength Maximum length in symbols for all names. * @param label Text to compact. * @returns Compacted label text. */ compactTableLabel(id, index, maxLength, label) { label = index + ') ' + label; try { const nameWidth = availableWidth(index, id) | 0; // HTML5 calculation of showed message width. label = compactLabelByPixels(label, nameWidth); } catch (err) { const nameLength = maxLength - 3 | 0; label = label.length > maxLength ? label.substr(0, nameLength) + '...' : label; } return label; }, widthIsSufficient(id, index, text) { try { const available = availableWidth(index, id); const required = measureText(text); return !available || available >= Math.floor(required); } catch (err) { return true; } }, ensureActivePanel(panels, id, focusId) { ensureActivePanel(panels, id, focusId); }, saveBtnTipText(dirty, objectName) { if (dirty) return 'Save ' + objectName; return 'Nothing to save'; }, formUI() { return { ready: false, expanded: false, loadedPanels: [], loadPanel(pnl) { if (!_.includes(this.loadedPanels, pnl)) this.loadedPanels.push(pnl); }, isPanelLoaded(pnl) { return _.includes(this.loadedPanels, pnl); } }; }, markPristineInvalidAsDirty(ngModelCtrl) { if (ngModelCtrl && ngModelCtrl.$invalid && ngModelCtrl.$pristine) ngModelCtrl.$setDirty(); }, triggerValidation }; } service.$inject = ['$window', 'IgniteFocus', '$rootScope'];