diff --git a/features/add-field-label/main.js b/features/add-field-label/main.js index da3a104..6138541 100644 --- a/features/add-field-label/main.js +++ b/features/add-field-label/main.js @@ -79,10 +79,11 @@ export const addFieldLabel = async () => { console.log(`Processed ${fieldsWithLabels.length} fields and ${spacerElements.length} spacer elements`); + labelProcessor.beforeProcess(formFields, layout, fieldsWithLabels, spacerElements); // 字段元素 - labelProcessor.processFieldLabels(fieldsWithLabels); + labelProcessor.processFieldLabels(fieldsWithLabels, layout); // 间距元素 - labelProcessor.processSpacerLabels(spacerElements); + labelProcessor.processSpacerLabels(spacerElements, layout); } catch (error) { console.error('Failed to add field information: ', error); diff --git a/features/add-field-label/settings.js b/features/add-field-label/settings.js index d616364..5a58f73 100644 --- a/features/add-field-label/settings.js +++ b/features/add-field-label/settings.js @@ -12,4 +12,27 @@ export const SPACING = { FIELD_CONTAINER_WIDTH: '100%', }; +// 样式配置常量 +export const STYLES = { + BASIC_LABEL_TOP: 'calc(5px + 1rem)', + SUBTABLE_LABEL_TOP: 'calc(12px + 1rem)', + GROUP_LABEL_TOP: 'calc(20px + 1rem)', +}; + +// LABEL 基本样式 +export const ADMIN_LABEL_STYLE = { + position: 'absolute', + zIndex: '1', + lineHeight: '1', + background: '#ffd9d9', + padding: '2px', + top: STYLES.BASIC_LABEL_TOP, + border: 'dotted red 1px', + fontSize: '0.75rem', +}; + +export const LABEL_STYLE = { + display: 'inline-block' +} + export let IS_FIELD_TYPE_DISPLAY = false; diff --git a/main.js b/main.js index 49d5b98..007762d 100644 --- a/main.js +++ b/main.js @@ -6,17 +6,17 @@ import { addFieldLabel } from './features/add-field-label/main.js'; /** - * 运行主要的 Kintone Helper 函数 + * 主入口函数:协调并运行所有扩展功能 * @returns {Promise} */ export const runKintoneHelper = async () => { try { await addFieldLabel(); } catch (error) { - console.error('Kintone Helper execution failed:', error); + console.error('❌ Kintone Helper Extension execution failed:', error); throw error; } }; -// 目前立即执行 +// 立即执行主函数 runKintoneHelper(); diff --git a/page/admin/form/admin-field-label-processor.js b/page/admin/form/admin-field-label-processor.js index aa9d015..e8f9e0f 100644 --- a/page/admin/form/admin-field-label-processor.js +++ b/page/admin/form/admin-field-label-processor.js @@ -3,6 +3,20 @@ * 负责处理 admin 表单页面上的字段和间距标签 */ +import { + getColumnWidths, + safelyInsertLabel, + safelyAppendLabel, + FieldTypeCheckerForAdminDom +} from '../../../utils/dom-utils.js'; +import { + createFieldWithLabels, + createFieldLabels +} from './dom.js'; +import { FieldTypeChecker } from '../../../utils/field-utils.js'; +import { DOM_CLASSES, FIELD_TYPES, LAYOUT_TYPES } from '../../../utils/constants.js'; +import { STYLES } from '../../../features/add-field-label/settings.js'; + /** * 用于处理 admin 表单页面上字段和间距标签的字段标签处理器类 */ @@ -16,7 +30,8 @@ export class AdminFieldLabelProcessor { constructor(options = {}) { this.appId = options.appId; this.pageType = options.pageType; - console.log(`AdminFieldLabelProcessor initialized with appId: ${this.appId}, pageType: ${this.pageType}`); + this.id = 0; + this.buildDomLayout(); } /** @@ -27,13 +42,142 @@ export class AdminFieldLabelProcessor { return true; } + buildDomLayout() { + this.domLayout = []; + this.fieldMap = []; + this.groupLayoutMap = {}; + this.spacerMap = {}; + const rows = document.querySelector(`.${DOM_CLASSES.CANVAS_ELEMENT} > .${DOM_CLASSES.CONTENT_ELEMENT}`).children; + + this._buildDomLayout(this.domLayout, rows); + } + + _buildDomLayout(layout, rowElements, parentGroupLayout) { + for (const row of rowElements) { + const rowLayout = { + id: ++this.id, + dom: row, + fields: [] + }; + row.setAttribute("data-kintone-helper-id", this.id) + layout.push(rowLayout); + this.fieldMap[rowLayout.id] = rowLayout + + if (!!parentGroupLayout) { + delete this.fieldMap[rowLayout.id] + } else if (FieldTypeCheckerForAdminDom.isGroup(row)) { + rowLayout.isGroup = true; + const groupField = { + id: this.id, + dom: row, + isGroup: true, + layout: [] + }; + const rows = row.querySelector(`.${DOM_CLASSES.GROUP} .${DOM_CLASSES.CONTENT_ELEMENT}`).children; + this._buildDomLayout(groupField.layout, rows, groupField); + this.groupLayoutMap[groupField.id] = groupField; + continue; + } else if (FieldTypeCheckerForAdminDom.isSubtable(row)) { + rowLayout.isSubtable = true; + } else if (FieldTypeCheckerForAdminDom.isReferenceTable(row)) { + rowLayout.isReferenceTable = true; + delete this.fieldMap[rowLayout.id] + } + + for (const field of row.children) { + if (!FieldTypeCheckerForAdminDom.isFieldElement(field)) { + continue; + } + const fieldLayout = { + id: ++this.id, + dom: field, + }; + field.setAttribute("data-kintone-helper-id", this.id) + + if (FieldTypeCheckerForAdminDom.isHr(field)) { + fieldLayout.isHr = true; + } else if (FieldTypeCheckerForAdminDom.isLabel(field)) { + fieldLayout.isLabel = true; + } else if (FieldTypeCheckerForAdminDom.isSpacer(field)) { + fieldLayout.isSpacer = true; + this.spacerMap[this.id] = fieldLayout; + } + + if (!rowLayout.isSubtable && !parentGroupLayout) { + this.fieldMap[fieldLayout.id] = fieldLayout + } + rowLayout.fields.push(fieldLayout); + } + } + } + + beforeProcess(rawFieldsMap, rawLayout, fields, spacers, targetLayout = this.domLayout) { + if (rawLayout.length !== targetLayout.length) { + // TODO throw error + return; + }; + for (let i = 0; i < rawLayout.length; i++) { + const layoutRow = rawLayout[i]; + const domLayoutRow = targetLayout[i]; + const { fields: rawFields, ...layoutRowWithoutFields } = layoutRow; + Object.assign(domLayoutRow, layoutRowWithoutFields); + + if (layoutRow.type === LAYOUT_TYPES.GROUP && domLayoutRow.isGroup) { + const domGroupLayoutRow = this.groupLayoutMap[domLayoutRow.id]; + const { layout, ...layoutRowWithoutLayout } = layoutRow; + Object.assign(domGroupLayoutRow, layoutRowWithoutLayout); + this.beforeProcess(rawFieldsMap, layoutRow.layout, fields, spacers, domGroupLayoutRow.layout); + continue; + } + + this.processFields(layoutRow.fields, domLayoutRow.fields, rawFieldsMap); + } + } + + processFields(rowFields, domFields, rawFieldsMap) { + if (!rowFields || !domFields || rowFields.length !== domFields.length) { + // TODO error + return + } + for (let i = 0; i < rowFields.length; i++) { + Object.assign(domFields[i], rowFields[i]); + + switch (rowFields[i].type) { + case FIELD_TYPES.REFERENCE_TABLE: + Object.assign(domFields[i], rawFieldsMap[rowFields[i].code]); + break; + } + } + } + /** * 为子表字段元素添加标签 * @param {Object} field - 字段配置信息 * @param {HTMLElement} fieldElement - 字段 DOM 元素 */ addSubtableLabel(field, fieldElement) { - console.log(`AdminFieldLabelProcessor.addSubtableLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + try { + // 在表上方添加字段代码标签 + const tableCodeLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + tableCodeLabel.style.display = 'block'; + safelyInsertLabel(fieldElement, tableCodeLabel); + + for (const col of field.fields) { + const fieldLabel = createFieldWithLabels({ + code: col.code, + type: col.type, + }); + fieldLabel.style.top = STYLES.SUBTABLE_LABEL_TOP; + safelyAppendLabel(col.dom.querySelector(`.${DOM_CLASSES.INSERT_LABEL_ELEMENT}`), fieldLabel); + } + + + } catch (error) { + console.error(`Failed to add label for subtable field ${field.code}:`, error); + } } /** @@ -42,7 +186,29 @@ export class AdminFieldLabelProcessor { * @param {HTMLElement} fieldElement - 字段 DOM 元素 */ addReferenceTableLabel(field, fieldElement) { - console.log(`AdminFieldLabelProcessor.addReferenceTableLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + try { + // 在引用表上方添加字段代码标签 + const tableCodeLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + tableCodeLabel.style.display = 'block'; + + safelyInsertLabel(fieldElement, tableCodeLabel); + + // 如果可用,添加显示字段标签 + // if (field.referenceTable?.displayFields) { + // const displayFields = field.referenceTable.displayFields; + // const columnWidths = getColumnWidths(fieldElement, field.type); + // const dataExist = !!fieldElement.querySelector('tbody > tr'); + // const displayLabels = createFieldLabels(displayFields, columnWidths, field.type, dataExist); + + // safelyInsertLabel(fieldElement, displayLabels); + // } + + } catch (error) { + console.error(`Failed to add label for reference table field ${field.code}:`, error); + } } /** @@ -51,7 +217,32 @@ export class AdminFieldLabelProcessor { * @param {HTMLElement} fieldElement - 字段 DOM 元素 */ addGroupLabel(field, fieldElement) { - console.log(`AdminFieldLabelProcessor.addGroupLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + try { + // 在组的父元素之前添加标签 + const parentElement = field.dom.querySelector(`.${DOM_CLASSES.INSERT_GROUP_LABEL_ELEMENT}`); + if (!parentElement) { + // TODO error + return; + } + const groupLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + + groupLabel.style.top = STYLES.GROUP_LABEL_TOP; + safelyAppendLabel(parentElement, groupLabel); + + const subLayout = this.groupLayoutMap[field.id]?.layout || []; + for (const subRow of subLayout) { + for (const subField of subRow.fields) { + const subFieldElement = subField.dom.querySelector(`.${DOM_CLASSES.INSERT_LABEL_ELEMENT}`); + this.addStandardFieldLabel(subField, subFieldElement); + } + } + + } catch (error) { + console.error(`Failed to add label for group field ${field.code}:`, error); + } } /** @@ -60,7 +251,21 @@ export class AdminFieldLabelProcessor { * @param {HTMLElement} fieldElement - 字段 DOM 元素 */ addStandardFieldLabel(field, fieldElement) { - console.log(`AdminFieldLabelProcessor.addStandardFieldLabel called with field: ${field.code}, fieldElement exists: ${!!fieldElement}`); + if (!fieldElement) { + return; + } + + try { + const fieldLabel = createFieldWithLabels({ + code: field.code, + type: field.type, + }); + + safelyAppendLabel(fieldElement, fieldLabel); + + } catch (error) { + console.error(`Failed to add label for standard field ${field.code}:`, error); + } } /** @@ -68,14 +273,53 @@ export class AdminFieldLabelProcessor { * @param {Array} fieldsWithLabels - 已处理的字段对象数组 */ processFieldLabels(fieldsWithLabels) { - console.log(`AdminFieldLabelProcessor.processFieldLabels called with ${fieldsWithLabels.length} fields`); + try { + for (const field of Object.values(this.fieldMap)) { + try { + // 获取字段元素 + const fieldElement = field.dom.querySelector(`.${DOM_CLASSES.INSERT_LABEL_ELEMENT}`); + + // 根据字段类型选择合适的标签添加方法 + if (FieldTypeChecker.isSubtable(field)) { + this.addSubtableLabel(field, fieldElement); + } else if (FieldTypeChecker.isReferenceTable(field)) { + this.addReferenceTableLabel(field, fieldElement); + } else if (FieldTypeChecker.isGroup(field)) { + this.addGroupLabel(field, fieldElement); + } else { + this.addStandardFieldLabel(field, fieldElement); + } + + } catch (fieldError) { + console.error(`Error occurred while processing label for field ${field.code}:`, fieldError); + } + } + } catch (error) { + console.error('Error occurred while processing field labels:', error); + throw error; + } } /** * 处理并为页面上的间距元素添加标签 * @param {Array} spacerElements - 间距元素配置数组 */ - processSpacerLabels(spacerElements) { - console.log(`AdminFieldLabelProcessor.processSpacerLabels called with ${spacerElements.length} spacer elements`); + processSpacerLabels() { + try { + for (const spacer of Object.values(this.spacerMap)) { + const spacerElement = spacer.dom.querySelector(`.${DOM_CLASSES.INSERT_SPACER_LABEL_ELEMENT}`); + + // 添加标签并设置边框 + const spacerLabel = createFieldWithLabels({ + code: spacer.elementId, + type: spacer.type, + }); + + safelyAppendLabel(spacerElement, spacerLabel); + } + } catch (error) { + console.error('Error occurred while processing spacer element labels:', error); + throw error; + } } } diff --git a/page/admin/form/dom.js b/page/admin/form/dom.js new file mode 100644 index 0000000..1d8ff9f --- /dev/null +++ b/page/admin/form/dom.js @@ -0,0 +1,101 @@ +/** + * DOM 字段标签操作工具模块 + * 提供用于创建和管理字段标签 DOM 元素的辅助函数 + */ + +import { FIELD_TYPES } from '../../../utils/constants.js'; +import { COLORS, SPACING, IS_FIELD_TYPE_DISPLAY, ADMIN_LABEL_STYLE } from '../../../features/add-field-label/settings.js'; +import { isInDetailPage } from '../../../utils/kintone-utils.js'; + +/** + * 创建带有适当样式的字段标签 span 元素 + * @param {Object} params - 标签参数配置 + * @param {string} params.code - 字段代码 + * @param {string} params.type - 字段类型(可选) + * @param {number} params.width - 字段宽度(可选) + * @returns {HTMLElement} 带有样式化 span 的容器 div 元素 + */ +const createFieldSpanElement = ({ code, type, width }) => { + const container = document.createElement('div'); + + // 处理宽度和边距设置 + if (width !== undefined) { + container.style.width = `${Number(width) - SPACING.TABLE_COLUMN_PADDING}px`; // 减去列填充 + container.style.marginLeft = `${SPACING.TABLE_COLUMN_PADDING}px`; // 添加左边距 + } else { + container.style.width = '100%'; // 默认全宽度 + } + + // 创建和样式化 span 元素 + const fieldSpan = document.createElement('span'); + fieldSpan.textContent = (IS_FIELD_TYPE_DISPLAY && type !== undefined) ? `${code} (${type})` : code; // 显示代码和类型 + fieldSpan.style.display = 'inline-block'; + fieldSpan.style.width = '100%'; + fieldSpan.style.color = COLORS.LABEL_TEXT; // 使用定义的工具提示文本颜色 + fieldSpan.style.overflowWrap = 'anywhere'; // 支持长文本换行 + fieldSpan.style.whiteSpace = 'pre-wrap'; // 保留空格和换行 + + // GROUP 类型字段的特殊样式 + if (type === FIELD_TYPES.GROUP && isInDetailPage()) { + fieldSpan.style.marginLeft = SPACING.GROUP_MARGIN_LEFT; + } + + container.appendChild(fieldSpan); + return container; +}; + +/** + * 创建字段标签容器元素 + * @param {Object} params - 标签参数配置 + * @param {string} params.code - 字段代码 + * @param {string} params.type - 字段类型(可选) + * @param {number} params.width - 容器宽度(可选) + * @returns {HTMLElement} 标签容器元素 + */ +export const createFieldWithLabels = ({ code, type, width }) => { + const container = document.createElement('div'); + + Object.assign(container.style, ADMIN_LABEL_STYLE) + + const fieldSpan = createFieldSpanElement({ code, type, width }); + container.appendChild(fieldSpan); + + return container; +}; + +/** + * 创建包含多个字段标签的容器 + * @param {Array} fieldNames - 要为其创建标签的字段名称数组 + * @param {Array} widths - 与字段名称对应的宽度 + * @param {string} fieldType - 字段类型(影响间距) + * @returns {HTMLElement} 带有所有标签的 span 容器元素 + */ +export const createFieldLabels = (fieldNames, widths, fieldType, specialSpacing = false) => { + const container = document.createElement('span'); + + // 为引用表字段添加特殊间距 + if (fieldType === FIELD_TYPES.REFERENCE_TABLE && specialSpacing) { + const spacerElement = document.createElement('span'); + spacerElement.style.width = SPACING.REFERENCE_TABLE_SPACER; + spacerElement.style.display = 'inline-block'; + container.appendChild(spacerElement); + } + + // 为每个字段创建标签,调整宽度 + const labelElements = fieldNames.map((fieldName, index) => { + // 稍稍减少最后一个元素的宽度以适应布局 + const adjustedWidth = index === fieldNames.length - 1 + ? widths[index] - 1 + : widths[index]; + + return createFieldWithLabels({ + code: fieldName, + width: adjustedWidth, + }); + }); + + // 将所有标签元素添加到容器中 + labelElements.forEach(labelElement => container.appendChild(labelElement)); + + return container; +}; diff --git a/page/detail/dom.js b/page/detail/dom.js index 814233d..1489c60 100644 --- a/page/detail/dom.js +++ b/page/detail/dom.js @@ -4,7 +4,7 @@ */ import { FIELD_TYPES } from '../../utils/constants.js'; -import { COLORS, SPACING, IS_FIELD_TYPE_DISPLAY } from '../../features/add-field-label/settings.js'; +import { COLORS, SPACING, IS_FIELD_TYPE_DISPLAY, LABEL_STYLE } from '../../features/add-field-label/settings.js'; /** * 创建带有适当样式的字段标签 span 元素 @@ -53,7 +53,8 @@ const createFieldSpanElement = ({ code, type, width }) => { */ export const createFieldWithLabels = ({ code, type, width }) => { const container = document.createElement('div'); - container.style.display = 'inline-block'; // 布局的内联块显示 + // container.style.display = 'inline-block'; // 布局的内联块显示 + Object.assign(container.style, LABEL_STYLE) const fieldSpan = createFieldSpanElement({ code, type, width }); container.appendChild(fieldSpan); diff --git a/page/detail/field-label-processor.js b/page/detail/field-label-processor.js index 9e7da3e..28e2c0d 100644 --- a/page/detail/field-label-processor.js +++ b/page/detail/field-label-processor.js @@ -39,6 +39,10 @@ export class FieldLabelProcessor { return false; } + beforeProcess(rawFieldsMap, rawLayout, fields, spacers) { + + } + /** * 为子表字段元素添加标签 * @param {Object} field - 字段配置信息 diff --git a/utils/constants.js b/utils/constants.js index bdbe7db..84132f7 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -94,6 +94,23 @@ export const LOOKUP_COPY_SUPPORTED_TYPES = [ FIELD_TYPES.RECORD_NUMBER, ]; +// DOM 类名常量定义(用于 admin 表单页面) +export const DOM_CLASSES = { + CANVAS_ELEMENT: 'fm-canvas-gaia', + CONTENT_ELEMENT: 'fm-canvas-contentElement-gaia', + INSERT_LABEL_ELEMENT: 'input-label-cybozu', + INSERT_GROUP_LABEL_ELEMENT: 'group-label-gaia', + INSERT_SPACER_LABEL_ELEMENT: 'fm-control-spacer-gaia', + ROW_ELEMENT: 'fm-row-gaia', + FIELD_ELEMENT: 'fm-control-gaia', + SUBTABLE: 'fm-subtable-gaia', + GROUP: 'fm-control-group-gaia', + REFERENCE_TABLE: 'fm-control-reference_table-field-gaia', + SPACER: 'fm-control-spacer-field-gaia', + HR: 'fm-control-hr-field-gaia', + LABEL: 'fm-control-label-field-gaia', +}; + // 页面类型常量定义 export const PAGE_TYPES = { DETAIL: 'detail', diff --git a/utils/dom-utils.js b/utils/dom-utils.js index 10d6e7a..0b8a1c4 100644 --- a/utils/dom-utils.js +++ b/utils/dom-utils.js @@ -3,7 +3,7 @@ * 提供用于创建和管理 DOM 元素的辅助函数 */ -import { FIELD_TYPES } from './constants.js'; +import { FIELD_TYPES, DOM_CLASSES } from './constants.js'; /** * 从表元素中提取列宽度信息 @@ -68,3 +68,40 @@ export const safelyAppendLabel = (targetElement, labelElement) => { return false; } }; + +const isRowElement = (element) => element?.classList?.contains(DOM_CLASSES.ROW_ELEMENT); +const isFieldElement = (element) => element?.classList?.contains(DOM_CLASSES.FIELD_ELEMENT); + +/** + * 基于 DOM 类名判断字段类型的工具对象 + * 用于 admin 表单页面通过 DOM 元素判断字段类型 + */ +export const FieldTypeCheckerForAdminDom = { + isRowElement, + isFieldElement, + + isSubtable: (element) => { + return !isRowElement(element) && !isFieldElement(element) && + element?.classList?.contains(DOM_CLASSES.SUBTABLE); + }, + isGroup: (element) => { + return isRowElement(element) && + !!element?.querySelector(`.${DOM_CLASSES.GROUP}`); + }, + isReferenceTable: (element) => { + return isRowElement(element) && + !!element?.querySelector(`.${DOM_CLASSES.REFERENCE_TABLE}`); + }, + isSpacer: (element) => { + return isFieldElement(element) && + element?.classList?.contains(DOM_CLASSES.SPACER); + }, + isLabel: (element) => { + return isFieldElement(element) && + element?.classList?.contains(DOM_CLASSES.LABEL); + }, + isHr: (element) => { + return isFieldElement(element) && + element?.classList?.contains(DOM_CLASSES.HR); + }, +}; diff --git a/utils/kintone-utils.js b/utils/kintone-utils.js index a04e473..5b3113c 100644 --- a/utils/kintone-utils.js +++ b/utils/kintone-utils.js @@ -34,7 +34,7 @@ const getAppIdFromUrl = () => { export const getAppId = () => { try { if (isInAdminPage()) { - return getAppIdFromUrl() || null; + return Number(cybozu?.data?.page?.APP_ID) || getAppIdFromUrl() || null; } if (!isGlobalKintoneExist()) {