From 19e5c483fab302585ac1c5b9a8c3be9f78427e13 Mon Sep 17 00:00:00 2001 From: Yurii Date: Sun, 8 Feb 2026 10:19:02 +0200 Subject: [PATCH] link field icon helper --- client/src/helpers/field/link-icon.js | 103 ++++++++++++++++++++++++++ frontend/less/espo/custom.less | 49 ++++++++---- 2 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 client/src/helpers/field/link-icon.js diff --git a/client/src/helpers/field/link-icon.js b/client/src/helpers/field/link-icon.js new file mode 100644 index 0000000000..564c019fc2 --- /dev/null +++ b/client/src/helpers/field/link-icon.js @@ -0,0 +1,103 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2026 EspoCRM, Inc. + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +/** + * @since 9.3.1 + */ +export default class LinkFieldIconHelper { + + /** + * @param {import('views/fields/link').default} view + * @param {{ + * iconClass: string, + * getColor: function(): string, + * }} options + */ + constructor(view, options) { + this.view = view; + this.options = options; + + view.listenTo(view, 'after:render', () => { + if (view.isEditMode()) { + this.control(); + } + }); + + view.addHandler('keydown', `input[data-name="${view.nameName}"]`, (/** KeyboardEvent */e, target) => { + if (e.code === 'Enter') { + return; + } + + target.classList.add('being-typed'); + }); + + view.addHandler('change', `input[data-name="${view.nameName}"]`, (e, target) => { + setTimeout(() => target.classList.remove('being-typed'), 200); + }); + + view.addHandler('blur', `input[data-name="${view.nameName}"]`, (e, target) => { + target.classList.remove('being-typed'); + }); + + view.on('change', () => { + if (!view.isEditMode()) { + return; + } + + const span = view.element.querySelector('span.icon-in-input'); + + if (span) { + span.parentNode.removeChild(span); + } + + setTimeout(() => this.control(), 0); + }); + } + + /** + * @private + */ + control() { + const view = this.view; + + const nameElement = view.element.querySelector(`input[data-name="${view.nameName}"]`); + nameElement.classList.remove('being-typed'); + + const icon = document.createElement('span'); + icon.className = 'icon-in-input ' + this.options.iconClass; + icon.style.color = this.options.getColor(); + + const input = view.element.querySelector('.input-group > input'); + + if (!input) { + return; + } + + input.after(icon); + } +} diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index b2f8a0c692..a9a1eee884 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -2364,24 +2364,41 @@ td.cell { } } -div:has(.avatar-in-input):not(:has(.being-typed)) { - > .input-group > input { - padding-left: var(--33px); +div:has(.avatar-in-input), +div:has(.icon-in-input) { + &:not(:has(.being-typed)) { + > .input-group > input { + padding-left: var(--33px); + } + + &:has(.color-icon) { + > .input-group > input { + padding-left: var(--21px); + } + } + + > .icon-in-input, + > .avatar-in-input { + display: inline-block; + position: absolute; + z-index: 3; + user-select: none; + pointer-events: none; + } + + > .avatar-in-input { + top: var(--9px); + left: var(--10px); + } + + > .icon-in-input { + top: var(--11px); + left: var(--10px); + } } - > .avatar-in-input { - display: inline-block; - position: absolute; - top: var(--9px); - left: var(--10px); - z-index: 3; - user-select: none; - pointer-events: none; - } -} - -div:has(.avatar-in-input) { - > .avatar-in-input { + > .avatar-in-input, + > .icon-in-input { display: none; } }