@liferay-editor-custom-fields/framework 7.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/eslint.config.mjs +11 -0
- package/package.json +27 -0
- package/src/index.ts +34 -0
- package/src/messagingEvents/fireFragmentConfigOnLoad.ts +2 -0
- package/src/messagingEvents/fireImageOnChange.ts +4 -0
- package/src/messagingEvents/fireWebContentFieldsOnLoad.ts +2 -0
- package/src/observers/index.ts +4 -0
- package/src/observers/observeFragmentConfig.ts +27 -0
- package/src/observers/observePreviewImage.ts +22 -0
- package/src/observers/observeWebContentFields.ts +24 -0
- package/src/types/ContentType.type.ts +1 -0
- package/src/util/debounce.ts +9 -0
- package/src/util/getContentImageInput.ts +5 -0
- package/src/util/getFieldByLabel.ts +25 -0
- package/src/util/getPreviewImage.ts +10 -0
- package/src/util/index.ts +7 -0
- package/src/util/setReactDomInputValue.ts +22 -0
- package/tsconfig.json +21 -0
- package/webpack.config.js +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Liferay Custom Fields Framework
|
|
2
|
+
|
|
3
|
+
The engine behind Liferay Custom Fields. You only need this if you are creating a Liferay Editor Custom Fields package. Otherwise you can just install the package.
|
|
4
|
+
|
|
5
|
+
You can read more about how it works [Creating a Liferay global JS client extension with TypeScript and Webpack](https://xtivia.com/blog/creating-a-liferay-global-js-client-extension-with-typescript-and-webpack).
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Events and Helpers
|
|
13
|
+
|
|
14
|
+
The framework profides the Liferay events and exposed functions below to use in your implementation.
|
|
15
|
+
|
|
16
|
+
### Events
|
|
17
|
+
|
|
18
|
+
Use Liferay.on to subscribe to these events:
|
|
19
|
+
- `EditorCustomFields_WebContentFields_OnLoad` - Fires when web content field DOM is loaded.
|
|
20
|
+
- `EditorCustomFields_FragmenConfig_OnLoad` - Fires when a new Fragment config is loaded on the right pane of a page editor.
|
|
21
|
+
- `EditorCustomFields_Image_OnChange` - Fires when an lfr-editable image or Web Content image is changed.
|
|
22
|
+
|
|
23
|
+
### Helper functions
|
|
24
|
+
|
|
25
|
+
The helper functions below are children of Liferay.editorCustomFields
|
|
26
|
+
- `debounce(callback:function, wait:number)` - A simple callback function
|
|
27
|
+
- `getContentImageInput(label:string)` - Gets the metadata input field associated with the web content image.
|
|
28
|
+
- `getFieldByLabel(label:string)` - Uses Xpath to get an input field by its label. Usually used to append a GUI element.
|
|
29
|
+
- `getPreviewImage()` - Gets the preview Image
|
|
30
|
+
- `setReactDomInputValue({fieldEl:element,value:string})` - Sets React DOM input value and triggers an autosave on fragment config
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT Licensed. Copyright (c) Xtivia 2026.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import eslint from "@eslint/js";
|
|
4
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
|
|
7
|
+
export default defineConfig(
|
|
8
|
+
eslint.configs.recommended,
|
|
9
|
+
tseslint.configs.recommended,
|
|
10
|
+
[globalIgnores(["**/*.js", "**/*.cjs", "**/*.mjs"])],
|
|
11
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liferay-editor-custom-fields/framework",
|
|
3
|
+
"repository": "https://github.com/lbeharxtivia/liferay-editor-custom-fields-framework",
|
|
4
|
+
"author": "lbehar@xtivia.com",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "An framework to create custom fields for Liferay fragments and structured web content",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@eslint/js": "^9.0.0",
|
|
9
|
+
"eslint": "^9.0.0",
|
|
10
|
+
"eslint-webpack-plugin": "^5.0.2",
|
|
11
|
+
"ts-loader": "9.5.1",
|
|
12
|
+
"typescript": "^5.9.3",
|
|
13
|
+
"typescript-eslint": "^8.55.0",
|
|
14
|
+
"webpack": "5.90.1",
|
|
15
|
+
"webpack-cli": "5.1.4",
|
|
16
|
+
"webpack-dev-server": "4.15.1"
|
|
17
|
+
},
|
|
18
|
+
"main": "./src/index.ts",
|
|
19
|
+
"module": "./src/index.ts",
|
|
20
|
+
"types": "./src/index.ts",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "webpack",
|
|
23
|
+
"start": "webpack serve",
|
|
24
|
+
"watch": "tsc --watch"
|
|
25
|
+
},
|
|
26
|
+
"version": "7.4.1"
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare const Liferay;
|
|
2
|
+
|
|
3
|
+
import { observeFragmentConfig, observeWebContentFields } from "./observers";
|
|
4
|
+
import {
|
|
5
|
+
debounce,
|
|
6
|
+
getContentImageInput,
|
|
7
|
+
getFieldByLabel,
|
|
8
|
+
getPreviewImage,
|
|
9
|
+
setReactDomInputValue,
|
|
10
|
+
} from "./util";
|
|
11
|
+
|
|
12
|
+
const initFramework:()=>void = () => {
|
|
13
|
+
const startObservers = () => {
|
|
14
|
+
observeWebContentFields();
|
|
15
|
+
// Fragment element we are watching is loaded client-side, and AFAIK there is no event fired when client-side is loaded.
|
|
16
|
+
setTimeout(observeFragmentConfig, 500);
|
|
17
|
+
};
|
|
18
|
+
if (Liferay && !Liferay?.editorCustomFields) {
|
|
19
|
+
Liferay.editorCustomFields = {
|
|
20
|
+
debounce,
|
|
21
|
+
getFieldByLabel,
|
|
22
|
+
setReactDomInputValue,
|
|
23
|
+
getContentImageInput,
|
|
24
|
+
getPreviewImage,
|
|
25
|
+
};
|
|
26
|
+
Liferay.on("allPortletsReady", () => {
|
|
27
|
+
startObservers();
|
|
28
|
+
});
|
|
29
|
+
Liferay.on("endNavigate", () => {
|
|
30
|
+
startObservers();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export default initFramework;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
declare const Liferay;
|
|
2
|
+
|
|
3
|
+
import { fireFragmentConfigOnLoad } from "../messagingEvents/fireFragmentConfigOnLoad";
|
|
4
|
+
import { debounce } from "../util";
|
|
5
|
+
|
|
6
|
+
import { observePreviewImage } from "./observePreviewImage";
|
|
7
|
+
|
|
8
|
+
export const observeFragmentConfig = () => {
|
|
9
|
+
const sidePanelQuery = ".page-editor__item-configuration-sidebar";
|
|
10
|
+
const sidePanelEl: HTMLDivElement = document.querySelector(
|
|
11
|
+
sidePanelQuery
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (sidePanelEl) {
|
|
15
|
+
if (Liferay.editorCustomFields.fragmentObserver) {
|
|
16
|
+
Liferay.editorCustomFields.fragmentObserver.disconnect?.();
|
|
17
|
+
}
|
|
18
|
+
const debouncedAddButton = debounce(() => {
|
|
19
|
+
observePreviewImage('fragment');
|
|
20
|
+
fireFragmentConfigOnLoad();
|
|
21
|
+
}, 500);
|
|
22
|
+
Liferay.editorCustomFields.fragmentObserver = new MutationObserver(debouncedAddButton);
|
|
23
|
+
const observer = Liferay.editorCustomFields.fragmentObserver;
|
|
24
|
+
const config = { attributes: false, childList: true, subtree: false };
|
|
25
|
+
observer.observe(sidePanelEl, config);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare const Liferay;
|
|
2
|
+
|
|
3
|
+
import { fireImageOnChange } from "../messagingEvents/fireImageOnChange";
|
|
4
|
+
import { ContentType } from "../types/ContentType.type";
|
|
5
|
+
import { debounce, getContentImageInput, getPreviewImage } from "../util";
|
|
6
|
+
|
|
7
|
+
export const observePreviewImage = (contentType: ContentType) => {
|
|
8
|
+
if(Liferay.editorCustomFields.imageObserver){
|
|
9
|
+
Liferay.editorCustomFields.imageObserver.disconnect?.();
|
|
10
|
+
}
|
|
11
|
+
// For when image changes
|
|
12
|
+
const elToObserve = contentType === 'fragment' ? getPreviewImage() : getContentImageInput();
|
|
13
|
+
if (contentType && elToObserve && !Liferay.editorCustomFields.imageObserver) {
|
|
14
|
+
const debouncedAddContentBlurhash = debounce(() => fireImageOnChange(), 500);
|
|
15
|
+
Liferay.editorCustomFields.imageObserver = new MutationObserver(debouncedAddContentBlurhash);
|
|
16
|
+
const observer = Liferay.editorCustomFields.imageObserver
|
|
17
|
+
const config = { attributes: true, childList: false, subtree: false };
|
|
18
|
+
observer.observe(elToObserve, config);
|
|
19
|
+
}
|
|
20
|
+
// For when image is already present on load
|
|
21
|
+
fireImageOnChange();
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare const Liferay;
|
|
2
|
+
|
|
3
|
+
import { fireWebContentFieldsOnLoad } from "../messagingEvents/fireWebContentFieldsOnLoad";
|
|
4
|
+
import { debounce } from "../util";
|
|
5
|
+
import { observePreviewImage } from "./observePreviewImage";
|
|
6
|
+
|
|
7
|
+
export const observeWebContentFields = () => {
|
|
8
|
+
const fieldsContainerEl: HTMLDivElement = document.querySelector(
|
|
9
|
+
"#_com_liferay_journal_web_portlet_JournalPortlet_fieldsContent",
|
|
10
|
+
);
|
|
11
|
+
if (fieldsContainerEl) {
|
|
12
|
+
if (Liferay.editorCustomFields.webContentObserver) {
|
|
13
|
+
Liferay.editorCustomFields.webContentObserver.disconnect?.();
|
|
14
|
+
}
|
|
15
|
+
const debouncedFieldEvent = debounce(() => {
|
|
16
|
+
observePreviewImage('content');
|
|
17
|
+
fireWebContentFieldsOnLoad();
|
|
18
|
+
}, 500);
|
|
19
|
+
Liferay.editorCustomFields.webContentObserver = new MutationObserver(debouncedFieldEvent);
|
|
20
|
+
const observer = Liferay.editorCustomFields.webContentObserver
|
|
21
|
+
const config = { attributes: false, childList: true, subtree: true };
|
|
22
|
+
observer.observe(fieldsContainerEl, config);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ContentType = 'fragment' | 'content';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const getContentImageInput = () => {
|
|
2
|
+
const contentImageInputQuery = 'input[name^="_com_liferay_journal_web_portlet_JournalPortlet_ddm$$Image"]';
|
|
3
|
+
const contentImageInput: HTMLInputElement = document.querySelector(contentImageInputQuery);
|
|
4
|
+
return contentImageInput;
|
|
5
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uses xpath to get a LR field by label
|
|
3
|
+
* since there is no attribute that corresponds with field name or key
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
type GetFieldByLabel = (label:string) => HTMLInputElement;
|
|
7
|
+
export const getFieldByLabel:GetFieldByLabel = (label) => {
|
|
8
|
+
const parentEl = document.querySelector(
|
|
9
|
+
"#portlet_com_liferay_journal_web_portlet_JournalPortlet, .page-editor__item-configuration-sidebar",
|
|
10
|
+
);
|
|
11
|
+
const match = document.evaluate(
|
|
12
|
+
`//label[text()='${label}']`,
|
|
13
|
+
parentEl,
|
|
14
|
+
null,
|
|
15
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
16
|
+
null,
|
|
17
|
+
).singleNodeValue as HTMLElement;
|
|
18
|
+
|
|
19
|
+
const inputEl =
|
|
20
|
+
match?.closest(".form-group, .field-wrapper")?.querySelector("input") ||
|
|
21
|
+
match?.nextElementSibling?.querySelector("input") ||
|
|
22
|
+
match?.nextSibling;
|
|
23
|
+
|
|
24
|
+
return inputEl as HTMLInputElement;
|
|
25
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const getPreviewImage = () => {
|
|
2
|
+
const parentEl:HTMLDivElement = document?.querySelector(
|
|
3
|
+
"#portlet_com_liferay_journal_web_portlet_JournalPortlet, .page-editor__topper.active",
|
|
4
|
+
);
|
|
5
|
+
const previewImage:HTMLImageElement = parentEl?.querySelector(
|
|
6
|
+
".page-editor__topper.active img.page-editor__editable, .image-picker-preview>img",
|
|
7
|
+
) as HTMLImageElement;
|
|
8
|
+
|
|
9
|
+
return previewImage;
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { debounce } from "./debounce";
|
|
2
|
+
import { getContentImageInput } from "./getContentImageInput";
|
|
3
|
+
import { getFieldByLabel } from "./getFieldByLabel";
|
|
4
|
+
import { getPreviewImage } from "./getPreviewImage";
|
|
5
|
+
import { setReactDomInputValue } from "./setReactDomInputValue";
|
|
6
|
+
|
|
7
|
+
export { debounce, getContentImageInput, getFieldByLabel, getPreviewImage, setReactDomInputValue };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fragment editor config inputs use react DOM
|
|
3
|
+
* So setting input element value doesn't work
|
|
4
|
+
*
|
|
5
|
+
* @param {element} inputEl
|
|
6
|
+
* @param {string} value
|
|
7
|
+
*/
|
|
8
|
+
export const setReactDomInputValue = (inputEl:HTMLInputElement, value:string) => {
|
|
9
|
+
inputEl.focus();
|
|
10
|
+
const nativeValueSetter = Object.getOwnPropertyDescriptor(
|
|
11
|
+
window.HTMLInputElement.prototype,
|
|
12
|
+
"value",
|
|
13
|
+
).set;
|
|
14
|
+
nativeValueSetter.call(inputEl, value);
|
|
15
|
+
|
|
16
|
+
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
17
|
+
inputEl.dispatchEvent(new Event("change", { bubbles: true }));
|
|
18
|
+
|
|
19
|
+
document.body.focus();
|
|
20
|
+
|
|
21
|
+
inputEl.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
22
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"lib": [
|
|
6
|
+
"DOM",
|
|
7
|
+
"ES2020"
|
|
8
|
+
],
|
|
9
|
+
"module": "ES2020",
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"outDir": "./build/static",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"sourceMap": false,
|
|
14
|
+
"target": "ES2020",
|
|
15
|
+
"types": [
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"./src/**/*"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
|
|
3
|
+
* SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const webpack = require("webpack");
|
|
8
|
+
const ESLintPlugin = require("eslint-webpack-plugin");
|
|
9
|
+
|
|
10
|
+
const DEVELOPMENT = process.env.NODE_ENV === "development";
|
|
11
|
+
const WEBPACK_SERVE = !!process.env.WEBPACK_SERVE;
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
devServer: {
|
|
15
|
+
headers: {
|
|
16
|
+
"Access-Control-Allow-Origin": "*",
|
|
17
|
+
},
|
|
18
|
+
port: 3000,
|
|
19
|
+
},
|
|
20
|
+
devtool: DEVELOPMENT ? "source-map" : false,
|
|
21
|
+
entry: {
|
|
22
|
+
index: "./src/index.ts",
|
|
23
|
+
},
|
|
24
|
+
experiments: {
|
|
25
|
+
outputModule: true,
|
|
26
|
+
},
|
|
27
|
+
mode: DEVELOPMENT ? "development" : "production",
|
|
28
|
+
module: {
|
|
29
|
+
rules: [
|
|
30
|
+
{
|
|
31
|
+
test: /\.ts$/i,
|
|
32
|
+
use: ["ts-loader"],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
optimization: {
|
|
37
|
+
minimize: !DEVELOPMENT,
|
|
38
|
+
},
|
|
39
|
+
output: {
|
|
40
|
+
clean: true,
|
|
41
|
+
environment: {
|
|
42
|
+
dynamicImport: true,
|
|
43
|
+
},
|
|
44
|
+
filename: WEBPACK_SERVE ? "[name].js" : "[name].js",
|
|
45
|
+
library: {
|
|
46
|
+
type: "module",
|
|
47
|
+
},
|
|
48
|
+
path: path.resolve("build", "static"),
|
|
49
|
+
},
|
|
50
|
+
plugins: [
|
|
51
|
+
new ESLintPlugin({
|
|
52
|
+
files: "src/**/*.ts",
|
|
53
|
+
overrideConfigFile: `eslint.config.mjs`,
|
|
54
|
+
}),
|
|
55
|
+
new webpack.optimize.LimitChunkCountPlugin({
|
|
56
|
+
maxChunks: 1,
|
|
57
|
+
}),
|
|
58
|
+
],
|
|
59
|
+
resolve: {
|
|
60
|
+
extensions: [".js", ".ts"],
|
|
61
|
+
},
|
|
62
|
+
};
|