@logicflow/extension 2.1.6 → 2.1.7

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.
@@ -49,7 +49,7 @@ import { Component, observer } from '@logicflow/core';
49
49
  import { forEach, merge } from 'lodash-es';
50
50
  import Label from './Label';
51
51
  import LabelModel from './LabelModel';
52
- import { MediumEditor, defaultOptions, ColorPickerButton } from './mediumEditor';
52
+ import { MediumEditor, defaultOptions, createColorPickerButtonClass, } from './mediumEditor';
53
53
  // const { createUuid } = LogicFlowUtil
54
54
  // DONE: 解决问题,如果 LabelOverlay 设置为 Observer,拖拽 Label 时会导致 LabelOverlay 组件重新渲染,不知道为何
55
55
  // 目前解决了。流程是 拖动 -> 更新 label 的数据信息到 element model -> 重新渲染 LabelOverlay
@@ -74,7 +74,7 @@ var LabelOverlay = /** @class */ (function (_super) {
74
74
  this.editor = new MediumEditor('.lf-label-editor', merge(defaultOptions, {
75
75
  autoLink: true,
76
76
  extensions: {
77
- colorPicker: new ColorPickerButton(),
77
+ colorPicker: new (createColorPickerButtonClass(MediumEditor))(),
78
78
  },
79
79
  }));
80
80
  // TODO: 2. 在此处监听一些事件,当 node、edge 数据变化时,主动触发重新渲染,保证同步更新
@@ -99,7 +99,7 @@ var LabelOverlay = /** @class */ (function (_super) {
99
99
  this.editor = new MediumEditor('.lf-label-editor', merge(defaultOptions, {
100
100
  autoLink: true,
101
101
  extensions: {
102
- colorPicker: new ColorPickerButton(),
102
+ colorPicker: new (createColorPickerButtonClass(MediumEditor))(),
103
103
  },
104
104
  }));
105
105
  }
@@ -13,5 +13,5 @@ export declare const defaultOptions: {
13
13
  };
14
14
  disableEditing: boolean;
15
15
  };
16
- export declare const ColorPickerButton: any;
16
+ export declare function createColorPickerButtonClass(MediumEditor?: any): any;
17
17
  export { MediumEditor };
@@ -36,56 +36,93 @@ export var defaultOptions = {
36
36
  },
37
37
  disableEditing: true,
38
38
  };
39
- export var ColorPickerButton = MediumEditor.extensions.button.extend({
40
- name: 'colorpicker',
41
- tagNames: ['mark'],
42
- contentDefault: '<b>Color</b>',
43
- aria: 'Color Picker',
44
- action: 'colorPicker',
45
- init: function () {
46
- var _this = this;
47
- rangy.init();
48
- MediumEditor.extensions.button.prototype.init.call(this);
49
- this.colorPicker = new Picker({
50
- parent: this.button,
51
- color: '#000',
52
- onDone: function (res) {
53
- if (_this.coloredText && _this.coloredText.isAppliedToSelection()) {
54
- _this.coloredText.undoToSelection();
55
- }
56
- _this.coloredText = rangy.createClassApplier('colored', {
57
- elementTagName: 'span',
58
- elementProperties: {
59
- style: {
60
- color: res.hex,
61
- },
62
- },
63
- normalize: true,
64
- });
65
- _this.coloredText.toggleSelection();
66
- _this.base.checkContentChanged();
67
- _this.setInactive();
68
- },
69
- });
70
- },
71
- getButton: function () {
72
- return this.button;
73
- },
74
- handleClick: function () {
75
- this.setActive();
76
- this.colorPicker.show();
77
- },
78
- isAlreadyApplied: function (node) {
79
- return node.nodeName.toLowerCase() === 'mark';
80
- },
81
- isActive: function () {
82
- return this.button.classList.contains('medium-editor-button-active');
83
- },
84
- setInactive: function () {
85
- this.button.classList.remove('medium-editor-button-active');
86
- },
87
- setActive: function () {
88
- this.button.classList.add('medium-editor-button-active');
89
- },
90
- });
39
+ export function createColorPickerButtonClass(MediumEditor) {
40
+ var _a, _b;
41
+ var ButtonBase = ((_a = MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.extensions) === null || _a === void 0 ? void 0 : _a.button) || ((_b = MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.extensions) === null || _b === void 0 ? void 0 : _b.button);
42
+ var ExtensionBase = (MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.Extension) || (MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.Extension);
43
+ // Button 扩展基类不可用时,回退到 Extension 基类,避免在模块加载阶段抛错
44
+ var Base = ButtonBase || ExtensionBase;
45
+ if (!Base) {
46
+ console.warn('MediumEditor button/extension base not available; using noop extension');
47
+ return /** @class */ (function () {
48
+ function class_1() {
49
+ }
50
+ return class_1;
51
+ }());
52
+ }
53
+ return Base.extend({
54
+ name: 'colorpicker',
55
+ tagNames: ['mark'],
56
+ contentDefault: '<b>Color</b>',
57
+ aria: 'Color Picker',
58
+ action: 'colorPicker',
59
+ init: function () {
60
+ var _this = this;
61
+ var _a, _b;
62
+ try {
63
+ rangy.init();
64
+ }
65
+ catch (_c) {
66
+ console.error('rangy.init failed');
67
+ }
68
+ // 初始化按钮(ButtonBase 才有 prototype.init)
69
+ try {
70
+ ;
71
+ (_b = (_a = ButtonBase === null || ButtonBase === void 0 ? void 0 : ButtonBase.prototype) === null || _a === void 0 ? void 0 : _a.init) === null || _b === void 0 ? void 0 : _b.call(this);
72
+ }
73
+ catch (_d) {
74
+ console.error('ButtonBase.init failed');
75
+ }
76
+ this.colorPicker = new Picker({
77
+ parent: this.button || undefined,
78
+ color: '#000',
79
+ onDone: function (res) {
80
+ var _a, _b, _c, _d, _e;
81
+ try {
82
+ if (_this.coloredText && ((_b = (_a = _this.coloredText).isAppliedToSelection) === null || _b === void 0 ? void 0 : _b.call(_a))) {
83
+ _this.coloredText.undoToSelection();
84
+ }
85
+ _this.coloredText = rangy.createClassApplier('colored', {
86
+ elementTagName: 'span',
87
+ elementProperties: { style: { color: res.hex } },
88
+ normalize: true,
89
+ });
90
+ _this.coloredText.toggleSelection();
91
+ (_d = (_c = _this.base) === null || _c === void 0 ? void 0 : _c.checkContentChanged) === null || _d === void 0 ? void 0 : _d.call(_c);
92
+ (_e = _this.setInactive) === null || _e === void 0 ? void 0 : _e.call(_this);
93
+ }
94
+ catch (_f) {
95
+ console.error('Picker.onDone failed');
96
+ }
97
+ },
98
+ });
99
+ },
100
+ getButton: function () {
101
+ return this.button;
102
+ },
103
+ handleClick: function () {
104
+ var _a, _b, _c;
105
+ (_a = this.setActive) === null || _a === void 0 ? void 0 : _a.call(this);
106
+ (_c = (_b = this.colorPicker) === null || _b === void 0 ? void 0 : _b.show) === null || _c === void 0 ? void 0 : _c.call(_b);
107
+ },
108
+ isAlreadyApplied: function (node) {
109
+ var _a, _b;
110
+ return ((_b = (_a = node === null || node === void 0 ? void 0 : node.nodeName) === null || _a === void 0 ? void 0 : _a.toLowerCase) === null || _b === void 0 ? void 0 : _b.call(_a)) === 'mark';
111
+ },
112
+ isActive: function () {
113
+ var _a, _b;
114
+ return (_b = (_a = this.button) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.contains('medium-editor-button-active');
115
+ },
116
+ setInactive: function () {
117
+ var _a, _b;
118
+ ;
119
+ (_b = (_a = this.button) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.remove('medium-editor-button-active');
120
+ },
121
+ setActive: function () {
122
+ var _a, _b;
123
+ ;
124
+ (_b = (_a = this.button) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.add('medium-editor-button-active');
125
+ },
126
+ });
127
+ }
91
128
  export { MediumEditor };
@@ -80,7 +80,7 @@ var LabelOverlay = /** @class */ (function (_super) {
80
80
  this.editor = new mediumEditor_1.MediumEditor('.lf-label-editor', (0, lodash_es_1.merge)(mediumEditor_1.defaultOptions, {
81
81
  autoLink: true,
82
82
  extensions: {
83
- colorPicker: new mediumEditor_1.ColorPickerButton(),
83
+ colorPicker: new ((0, mediumEditor_1.createColorPickerButtonClass)(mediumEditor_1.MediumEditor))(),
84
84
  },
85
85
  }));
86
86
  // TODO: 2. 在此处监听一些事件,当 node、edge 数据变化时,主动触发重新渲染,保证同步更新
@@ -105,7 +105,7 @@ var LabelOverlay = /** @class */ (function (_super) {
105
105
  this.editor = new mediumEditor_1.MediumEditor('.lf-label-editor', (0, lodash_es_1.merge)(mediumEditor_1.defaultOptions, {
106
106
  autoLink: true,
107
107
  extensions: {
108
- colorPicker: new mediumEditor_1.ColorPickerButton(),
108
+ colorPicker: new ((0, mediumEditor_1.createColorPickerButtonClass)(mediumEditor_1.MediumEditor))(),
109
109
  },
110
110
  }));
111
111
  }
@@ -13,5 +13,5 @@ export declare const defaultOptions: {
13
13
  };
14
14
  disableEditing: boolean;
15
15
  };
16
- export declare const ColorPickerButton: any;
16
+ export declare function createColorPickerButtonClass(MediumEditor?: any): any;
17
17
  export { MediumEditor };
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MediumEditor = exports.ColorPickerButton = exports.defaultOptions = void 0;
6
+ exports.MediumEditor = exports.createColorPickerButtonClass = exports.defaultOptions = void 0;
7
7
  var medium_editor_1 = __importDefault(require("medium-editor"));
8
8
  exports.MediumEditor = medium_editor_1.default;
9
9
  var vanilla_picker_1 = __importDefault(require("vanilla-picker"));
@@ -43,55 +43,93 @@ exports.defaultOptions = {
43
43
  },
44
44
  disableEditing: true,
45
45
  };
46
- exports.ColorPickerButton = medium_editor_1.default.extensions.button.extend({
47
- name: 'colorpicker',
48
- tagNames: ['mark'],
49
- contentDefault: '<b>Color</b>',
50
- aria: 'Color Picker',
51
- action: 'colorPicker',
52
- init: function () {
53
- var _this = this;
54
- rangy_1.default.init();
55
- medium_editor_1.default.extensions.button.prototype.init.call(this);
56
- this.colorPicker = new vanilla_picker_1.default({
57
- parent: this.button,
58
- color: '#000',
59
- onDone: function (res) {
60
- if (_this.coloredText && _this.coloredText.isAppliedToSelection()) {
61
- _this.coloredText.undoToSelection();
62
- }
63
- _this.coloredText = rangy_1.default.createClassApplier('colored', {
64
- elementTagName: 'span',
65
- elementProperties: {
66
- style: {
67
- color: res.hex,
68
- },
69
- },
70
- normalize: true,
71
- });
72
- _this.coloredText.toggleSelection();
73
- _this.base.checkContentChanged();
74
- _this.setInactive();
75
- },
76
- });
77
- },
78
- getButton: function () {
79
- return this.button;
80
- },
81
- handleClick: function () {
82
- this.setActive();
83
- this.colorPicker.show();
84
- },
85
- isAlreadyApplied: function (node) {
86
- return node.nodeName.toLowerCase() === 'mark';
87
- },
88
- isActive: function () {
89
- return this.button.classList.contains('medium-editor-button-active');
90
- },
91
- setInactive: function () {
92
- this.button.classList.remove('medium-editor-button-active');
93
- },
94
- setActive: function () {
95
- this.button.classList.add('medium-editor-button-active');
96
- },
97
- });
46
+ function createColorPickerButtonClass(MediumEditor) {
47
+ var _a, _b;
48
+ var ButtonBase = ((_a = MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.extensions) === null || _a === void 0 ? void 0 : _a.button) || ((_b = MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.extensions) === null || _b === void 0 ? void 0 : _b.button);
49
+ var ExtensionBase = (MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.Extension) || (MediumEditor === null || MediumEditor === void 0 ? void 0 : MediumEditor.Extension);
50
+ // Button 扩展基类不可用时,回退到 Extension 基类,避免在模块加载阶段抛错
51
+ var Base = ButtonBase || ExtensionBase;
52
+ if (!Base) {
53
+ console.warn('MediumEditor button/extension base not available; using noop extension');
54
+ return /** @class */ (function () {
55
+ function class_1() {
56
+ }
57
+ return class_1;
58
+ }());
59
+ }
60
+ return Base.extend({
61
+ name: 'colorpicker',
62
+ tagNames: ['mark'],
63
+ contentDefault: '<b>Color</b>',
64
+ aria: 'Color Picker',
65
+ action: 'colorPicker',
66
+ init: function () {
67
+ var _this = this;
68
+ var _a, _b;
69
+ try {
70
+ rangy_1.default.init();
71
+ }
72
+ catch (_c) {
73
+ console.error('rangy.init failed');
74
+ }
75
+ // 初始化按钮(ButtonBase 才有 prototype.init)
76
+ try {
77
+ ;
78
+ (_b = (_a = ButtonBase === null || ButtonBase === void 0 ? void 0 : ButtonBase.prototype) === null || _a === void 0 ? void 0 : _a.init) === null || _b === void 0 ? void 0 : _b.call(this);
79
+ }
80
+ catch (_d) {
81
+ console.error('ButtonBase.init failed');
82
+ }
83
+ this.colorPicker = new vanilla_picker_1.default({
84
+ parent: this.button || undefined,
85
+ color: '#000',
86
+ onDone: function (res) {
87
+ var _a, _b, _c, _d, _e;
88
+ try {
89
+ if (_this.coloredText && ((_b = (_a = _this.coloredText).isAppliedToSelection) === null || _b === void 0 ? void 0 : _b.call(_a))) {
90
+ _this.coloredText.undoToSelection();
91
+ }
92
+ _this.coloredText = rangy_1.default.createClassApplier('colored', {
93
+ elementTagName: 'span',
94
+ elementProperties: { style: { color: res.hex } },
95
+ normalize: true,
96
+ });
97
+ _this.coloredText.toggleSelection();
98
+ (_d = (_c = _this.base) === null || _c === void 0 ? void 0 : _c.checkContentChanged) === null || _d === void 0 ? void 0 : _d.call(_c);
99
+ (_e = _this.setInactive) === null || _e === void 0 ? void 0 : _e.call(_this);
100
+ }
101
+ catch (_f) {
102
+ console.error('Picker.onDone failed');
103
+ }
104
+ },
105
+ });
106
+ },
107
+ getButton: function () {
108
+ return this.button;
109
+ },
110
+ handleClick: function () {
111
+ var _a, _b, _c;
112
+ (_a = this.setActive) === null || _a === void 0 ? void 0 : _a.call(this);
113
+ (_c = (_b = this.colorPicker) === null || _b === void 0 ? void 0 : _b.show) === null || _c === void 0 ? void 0 : _c.call(_b);
114
+ },
115
+ isAlreadyApplied: function (node) {
116
+ var _a, _b;
117
+ return ((_b = (_a = node === null || node === void 0 ? void 0 : node.nodeName) === null || _a === void 0 ? void 0 : _a.toLowerCase) === null || _b === void 0 ? void 0 : _b.call(_a)) === 'mark';
118
+ },
119
+ isActive: function () {
120
+ var _a, _b;
121
+ return (_b = (_a = this.button) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.contains('medium-editor-button-active');
122
+ },
123
+ setInactive: function () {
124
+ var _a, _b;
125
+ ;
126
+ (_b = (_a = this.button) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.remove('medium-editor-button-active');
127
+ },
128
+ setActive: function () {
129
+ var _a, _b;
130
+ ;
131
+ (_b = (_a = this.button) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.add('medium-editor-button-active');
132
+ },
133
+ });
134
+ }
135
+ exports.createColorPickerButtonClass = createColorPickerButtonClass;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logicflow/extension",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "LogicFlow Extensions",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -20,8 +20,8 @@
20
20
  "author": "Logicflow-Team",
21
21
  "license": "Apache-2.0",
22
22
  "peerDependencies": {
23
- "@logicflow/core": "2.1.4",
24
- "@logicflow/vue-node-registry": "1.1.5"
23
+ "@logicflow/core": "2.1.5",
24
+ "@logicflow/vue-node-registry": "1.1.6"
25
25
  },
26
26
  "dependencies": {
27
27
  "@antv/hierarchy": "^0.6.11",
@@ -32,8 +32,8 @@
32
32
  "preact": "^10.17.1",
33
33
  "rangy": "^1.3.1",
34
34
  "vanilla-picker": "^2.12.3",
35
- "@logicflow/core": "2.1.4",
36
- "@logicflow/vue-node-registry": "1.1.5"
35
+ "@logicflow/vue-node-registry": "1.1.6",
36
+ "@logicflow/core": "2.1.5"
37
37
  },
38
38
  "devDependencies": {
39
39
  "less": "^4.1.1",
@@ -4,7 +4,11 @@ import { forEach, merge } from 'lodash-es'
4
4
  import LabelPlugin from '.'
5
5
  import Label from './Label'
6
6
  import LabelModel from './LabelModel'
7
- import { MediumEditor, defaultOptions, ColorPickerButton } from './mediumEditor'
7
+ import {
8
+ MediumEditor,
9
+ defaultOptions,
10
+ createColorPickerButtonClass,
11
+ } from './mediumEditor'
8
12
 
9
13
  import LabelConfig = LogicFlow.LabelConfig
10
14
 
@@ -47,7 +51,7 @@ export class LabelOverlay extends Component<IToolProps, ILabelOverlayState> {
47
51
  merge(defaultOptions, {
48
52
  autoLink: true,
49
53
  extensions: {
50
- colorPicker: new ColorPickerButton(),
54
+ colorPicker: new (createColorPickerButtonClass(MediumEditor))(),
51
55
  },
52
56
  }),
53
57
  )
@@ -82,7 +86,7 @@ export class LabelOverlay extends Component<IToolProps, ILabelOverlayState> {
82
86
  merge(defaultOptions, {
83
87
  autoLink: true,
84
88
  extensions: {
85
- colorPicker: new ColorPickerButton(),
89
+ colorPicker: new (createColorPickerButtonClass(MediumEditor))(),
86
90
  },
87
91
  }),
88
92
  )
@@ -39,56 +39,83 @@ export const defaultOptions = {
39
39
  disableEditing: true,
40
40
  }
41
41
 
42
- export const ColorPickerButton = MediumEditor.extensions.button.extend({
43
- name: 'colorpicker',
44
- tagNames: ['mark'],
45
- contentDefault: '<b>Color</b>',
46
- aria: 'Color Picker',
47
- action: 'colorPicker',
48
- init: function () {
49
- rangy.init()
50
- MediumEditor.extensions.button.prototype.init.call(this)
51
- this.colorPicker = new Picker({
52
- parent: this.button,
53
- color: '#000',
54
- onDone: (res) => {
55
- if (this.coloredText && this.coloredText.isAppliedToSelection()) {
56
- this.coloredText.undoToSelection()
57
- }
58
- this.coloredText = rangy.createClassApplier('colored', {
59
- elementTagName: 'span',
60
- elementProperties: {
61
- style: {
62
- color: res.hex,
63
- },
64
- },
65
- normalize: true,
66
- })
67
- this.coloredText.toggleSelection()
68
- this.base.checkContentChanged()
69
- this.setInactive()
70
- },
71
- })
72
- },
73
- getButton: function () {
74
- return this.button
75
- },
76
- handleClick: function () {
77
- this.setActive()
78
- this.colorPicker.show()
79
- },
80
- isAlreadyApplied: function (node) {
81
- return node.nodeName.toLowerCase() === 'mark'
82
- },
83
- isActive: function () {
84
- return this.button.classList.contains('medium-editor-button-active')
85
- },
86
- setInactive: function () {
87
- this.button.classList.remove('medium-editor-button-active')
88
- },
89
- setActive: function () {
90
- this.button.classList.add('medium-editor-button-active')
91
- },
92
- })
42
+ export function createColorPickerButtonClass(MediumEditor?: any) {
43
+ const ButtonBase =
44
+ MediumEditor?.extensions?.button || MediumEditor?.extensions?.button
45
+ const ExtensionBase =
46
+ MediumEditor?.Extension || (MediumEditor as any)?.Extension
47
+
48
+ // Button 扩展基类不可用时,回退到 Extension 基类,避免在模块加载阶段抛错
49
+ const Base = ButtonBase || ExtensionBase
50
+ if (!Base) {
51
+ console.warn(
52
+ 'MediumEditor button/extension base not available; using noop extension',
53
+ )
54
+ return class {}
55
+ }
56
+
57
+ return Base.extend({
58
+ name: 'colorpicker',
59
+ tagNames: ['mark'],
60
+ contentDefault: '<b>Color</b>',
61
+ aria: 'Color Picker',
62
+ action: 'colorPicker',
63
+ init: function () {
64
+ try {
65
+ rangy.init()
66
+ } catch {
67
+ console.error('rangy.init failed')
68
+ }
69
+ // 初始化按钮(ButtonBase 才有 prototype.init)
70
+ try {
71
+ ;(ButtonBase as any)?.prototype?.init?.call(this)
72
+ } catch {
73
+ console.error('ButtonBase.init failed')
74
+ }
75
+ this.colorPicker = new Picker({
76
+ parent: (this as any).button || undefined,
77
+ color: '#000',
78
+ onDone: (res) => {
79
+ try {
80
+ if (this.coloredText && this.coloredText.isAppliedToSelection?.()) {
81
+ this.coloredText.undoToSelection()
82
+ }
83
+ this.coloredText = rangy.createClassApplier('colored', {
84
+ elementTagName: 'span',
85
+ elementProperties: { style: { color: res.hex } },
86
+ normalize: true,
87
+ })
88
+ this.coloredText.toggleSelection()
89
+ this.base?.checkContentChanged?.()
90
+ this.setInactive?.()
91
+ } catch {
92
+ console.error('Picker.onDone failed')
93
+ }
94
+ },
95
+ })
96
+ },
97
+ getButton: function () {
98
+ return (this as any).button
99
+ },
100
+ handleClick: function () {
101
+ this.setActive?.()
102
+ this.colorPicker?.show?.()
103
+ },
104
+ isAlreadyApplied: function (node) {
105
+ return node?.nodeName?.toLowerCase?.() === 'mark'
106
+ },
107
+ isActive: function () {
108
+ return (this as any).button?.classList?.contains(
109
+ 'medium-editor-button-active',
110
+ )
111
+ },
112
+ setInactive: function () {
113
+ ;(this as any).button?.classList?.remove('medium-editor-button-active')
114
+ },
115
+ setActive: function () {
116
+ ;(this as any).button?.classList?.add('medium-editor-button-active')
117
+ },
118
+ })
119
+ }
93
120
 
94
121
  export { MediumEditor }