@oscarpalmer/oui 0.27.0 → 0.28.0

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.
@@ -30,6 +30,15 @@
30
30
  transition-delay: 0.125s;
31
31
  }
32
32
 
33
+ :where([oui-dialog]) {
34
+ position: relative;
35
+ }
36
+
37
+ :where([oui-dialog-resizer]) {
38
+ position: absolute;
39
+ inset: auto 0 0 auto;
40
+ }
41
+
33
42
  :where([oui-popover], [oui-tooltip-content]) {
34
43
  --margin-bottom: 0.5em;
35
44
  --margin-left: 0.5em;
@@ -1,4 +1,5 @@
1
1
  import { h as OuiMovable } from "./draggable-zszaW1qS.mjs";
2
+ import { n as OuiResizable } from "./resizable.embedded-CXrUVPzR.mjs";
2
3
  import { RemovableEventListener } from "@oscarpalmer/toretto/models";
3
4
 
4
5
  //#region src/js/dialog.d.ts
@@ -32,6 +33,7 @@ type OuiDialogState = {
32
33
  listeners: RemovableEventListener[];
33
34
  modal: boolean;
34
35
  movable?: OuiMovable;
36
+ resizable?: OuiResizable;
35
37
  };
36
38
  /**
37
39
  * Get the _OuiDialog_ for an element
@@ -2,6 +2,7 @@ import { getOnBeforeToggleListener } from "./floatable/floatable.embedded.mjs";
2
2
  import { attributable } from "./internal/attributable.mjs";
3
3
  import { ATTRIBUTE_MOVABLE_HANDLE, createMovable } from "./movable/movable.embedded.mjs";
4
4
  import "./movable/movable.standalone.mjs";
5
+ import { RESIZABLE_ATTRIBUTE_HANDLE, createResizable } from "./resizable/resizable.embedded.mjs";
5
6
  import { on } from "@oscarpalmer/toretto/event";
6
7
  //#region src/js/dialog.ts
7
8
  var OuiDialog = class {
@@ -48,9 +49,13 @@ function destroyDialog(state) {
48
49
  state.element.setAttribute(ARIA_MODAL, String(state.modal));
49
50
  state.element.removeAttribute(ATTRIBUTE_OPEN);
50
51
  for (const listener of state.listeners) listener();
52
+ state.movable?.destroy();
53
+ state.resizable?.destroy();
51
54
  state.dialog = void 0;
52
55
  state.element = void 0;
53
56
  state.listeners = void 0;
57
+ state.movable = void 0;
58
+ state.resizable = void 0;
54
59
  }
55
60
  /**
56
61
  * Get the _OuiDialog_ for an element
@@ -67,6 +72,12 @@ function getMovable(element) {
67
72
  for (const handle of handles) handle.setAttribute(ATTRIBUTE_MOVABLE_HANDLE, "");
68
73
  return createMovable(element, { container: element.getAttribute(ATTRIBUTE_CONTAINER) ?? void 0 });
69
74
  }
75
+ function getResizable(element) {
76
+ const handle = element.querySelector(SELECTOR_RESIZER);
77
+ if (handle == null) return;
78
+ handle.setAttribute(RESIZABLE_ATTRIBUTE_HANDLE, "");
79
+ return createResizable(element);
80
+ }
70
81
  function getState(element) {
71
82
  const state = {
72
83
  element,
@@ -78,18 +89,23 @@ function getState(element) {
78
89
  element.setAttribute(ARIA_MODAL, String(modal));
79
90
  element.setAttribute(ATTRIBUTE_OPEN, "");
80
91
  element.focus();
92
+ setTimeout(() => {
93
+ state.resizable?.set();
94
+ }, 500);
81
95
  } else {
82
96
  element.removeAttribute(ATTRIBUTE_OPEN);
83
97
  element.setAttribute(ARIA_MODAL, String(state.modal));
84
98
  element.style.inset = "";
85
99
  element.style.position = "";
86
100
  element.style.transform = "";
101
+ state.resizable?.reset();
87
102
  }
88
103
  })],
89
104
  modal: element.getAttribute(ARIA_MODAL) === TRUE
90
105
  };
91
106
  state.dialog = new OuiDialog(state);
92
107
  state.movable = getMovable(element);
108
+ state.resizable = getResizable(element);
93
109
  return state;
94
110
  }
95
111
  function removeDialog(element) {
@@ -107,6 +123,7 @@ const EVENT_TOGGLE = "toggle";
107
123
  const MESSAGE = "The element must be an instance of HTMLDialogElement";
108
124
  const SELECTOR_MOVER = `[${ATTRIBUTE}-mover]`;
109
125
  const SELECTOR_MODAL = ":modal";
126
+ const SELECTOR_RESIZER = `[${ATTRIBUTE}-resizer]`;
110
127
  const TRUE = "true";
111
128
  const states = /* @__PURE__ */ new WeakMap();
112
129
  attributable(ATTRIBUTE, addDialog, removeDialog);
@@ -1,8 +1,9 @@
1
- import { g as createMovable, h as OuiMovable, t as CreateOuiDraggableOptions } from "./draggable-zszaW1qS.mjs";
1
+ import { d as OuiSortable, f as createSortable, g as createMovable, h as OuiMovable, t as CreateOuiDraggableOptions } from "./draggable-zszaW1qS.mjs";
2
+ import { a as createResizable, n as OuiResizable, t as CreateOuiResizableOptions } from "./resizable.embedded-CXrUVPzR.mjs";
2
3
  import { OuiDialog, getDialog } from "./dialog.mjs";
3
4
  import { c as createFloatable, i as OuiFloatable, n as CreateOuiFloatableOptions, o as OuiFloatablePosition } from "./floatable.embedded-YxGwzgbE.mjs";
4
5
  import { a as OuiFocusTrapOptions, i as OuiFocusTrap, o as createFocusTrap } from "./focus-trap.embedded-DR8NpZfr.mjs";
5
6
  import { OuiPopover, getPopover } from "./popover.mjs";
6
7
  import { OuiSplitterElement } from "./splitter.mjs";
7
8
  import { OuiTooltip, getTooltip, setTooltipDelay } from "./tooltip.mjs";
8
- export { type CreateOuiFloatableOptions, type CreateOuiDraggableOptions as CreateOuiMovableOptions, type OuiDialog, type OuiFloatable, type OuiFloatablePosition, type OuiFocusTrap, type OuiFocusTrapOptions, type OuiMovable, type OuiPopover, OuiSplitterElement, type OuiTooltip, createFloatable, createFocusTrap, createMovable, getDialog, getPopover, getTooltip, setTooltipDelay };
9
+ export { type CreateOuiFloatableOptions, type CreateOuiDraggableOptions as CreateOuiMovableOptions, type CreateOuiDraggableOptions as CreateOuiSortableOptions, type CreateOuiResizableOptions, type OuiDialog, type OuiFloatable, type OuiFloatablePosition, type OuiFocusTrap, type OuiFocusTrapOptions, type OuiMovable, type OuiPopover, type OuiResizable, type OuiSortable, OuiSplitterElement, type OuiTooltip, createFloatable, createFocusTrap, createMovable, createResizable as createOuiResizable, createSortable, getDialog, getPopover, getTooltip, setTooltipDelay };
package/dist/js/index.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  import { createFloatable } from "./floatable/floatable.embedded.mjs";
2
2
  import { createMovable } from "./movable/movable.embedded.mjs";
3
3
  import "./movable/movable.standalone.mjs";
4
+ import { createResizable } from "./resizable/resizable.embedded.mjs";
4
5
  import { getDialog } from "./dialog.mjs";
5
- import "./sortable/sortable.embedded.mjs";
6
6
  import "./toolbar.mjs";
7
7
  import "./floatable/floatable.standalone.mjs";
8
8
  import { createFocusTrap } from "./focus-trap/focus-trap.embedded.mjs";
9
9
  import "./focus-trap/focus-trap.standalone.mjs";
10
10
  import { getPopover } from "./popover.mjs";
11
+ import "./resizable/resizable.standalone.mjs";
12
+ import { createSortable } from "./sortable/sortable.embedded.mjs";
13
+ import "./sortable/sortable.standalone.mjs";
11
14
  import { OuiSplitterElement } from "./splitter.mjs";
12
15
  import { getTooltip, setTooltipDelay } from "./tooltip.mjs";
13
- export { OuiSplitterElement, createFloatable, createFocusTrap, createMovable, getDialog, getPopover, getTooltip, setTooltipDelay };
16
+ export { OuiSplitterElement, createFloatable, createFocusTrap, createMovable, createResizable as createOuiResizable, createSortable, getDialog, getPopover, getTooltip, setTooltipDelay };
@@ -261,7 +261,7 @@ on(document, "keydown", onKeydown);
261
261
  on(document, "mousedown", onPointerdown);
262
262
  on(document, "pointermove", onPointermove);
263
263
  on(document, "pointerup", onPointerup);
264
- on(document, "touchcancel", onPointerup, { passive: false });
264
+ on(document, "touchcancel", onPointerup);
265
265
  on(document, "touchstart", onPointerdown, { passive: false });
266
266
  //#endregion
267
267
  export { OuiDraggable, addDraggable, getDraggableStates, removeDraggable };
@@ -0,0 +1,2 @@
1
+ import { a as createResizable, i as RESIZABLE_ATTRIBUTE_HANDLE, n as OuiResizable, r as RESIZABLE_ATTRIBUTE, t as CreateOuiResizableOptions } from "../resizable.embedded-CXrUVPzR.mjs";
2
+ export { CreateOuiResizableOptions, OuiResizable, RESIZABLE_ATTRIBUTE, RESIZABLE_ATTRIBUTE_HANDLE, createResizable };
@@ -0,0 +1,186 @@
1
+ import { attributable } from "../internal/attributable.mjs";
2
+ import { getPosition, on } from "@oscarpalmer/toretto/event";
3
+ import { isPlainObject } from "@oscarpalmer/atoms/is";
4
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
5
+ import { getNumber } from "@oscarpalmer/atoms/number";
6
+ import { findAncestor } from "@oscarpalmer/toretto/find";
7
+ import { toggleStyles } from "@oscarpalmer/toretto/style";
8
+ import supportsTouch from "@oscarpalmer/toretto/touch";
9
+ //#region src/js/resizable/resizable.embedded.ts
10
+ var OuiResizable = class {
11
+ #state;
12
+ constructor(state) {
13
+ this.#state = state;
14
+ }
15
+ destroy() {
16
+ destroyResizable(this.#state);
17
+ }
18
+ set() {
19
+ const [width, height] = getAndSetDimensions(this.#state.element, this.#state.options);
20
+ console.log("set", width, height);
21
+ this.#state.width = width;
22
+ this.#state.height = height;
23
+ }
24
+ reset() {
25
+ this.#state.element.style.height = "";
26
+ this.#state.element.style.width = "";
27
+ }
28
+ };
29
+ function addResizable(element, options) {
30
+ let item = items.get(element);
31
+ if (item == null) {
32
+ const handle = element.querySelector(SELECTOR_HANDLE_SCOPED);
33
+ if (!isHTMLOrSVGElement(handle)) return;
34
+ const state = getState(element, handle, options);
35
+ item = {
36
+ instance: new OuiResizable(state),
37
+ state
38
+ };
39
+ items.set(element, item);
40
+ }
41
+ return item?.instance;
42
+ }
43
+ /**
44
+ * Creates _(or retrieves)_ a _OuiResizable_ instance for an element
45
+ *
46
+ * @param element Element to make resizable
47
+ * @returns _OuiResizable_ instance
48
+ */
49
+ function createResizable(element, options) {
50
+ if (!isHTMLOrSVGElement(element)) throw new TypeError(MESSAGE_ELEMENT);
51
+ if (!isHTMLOrSVGElement(element.querySelector(SELECTOR_HANDLE_SCOPED))) throw new Error(MESSAGE_HANDLE);
52
+ return addResizable(element, options ?? {});
53
+ }
54
+ function destroyResizable(state) {
55
+ state.element = void 0;
56
+ state.handle = void 0;
57
+ }
58
+ function getAndSetDimensions(element, options, input) {
59
+ let { height, width } = input ?? element.getBoundingClientRect();
60
+ if (options.maximumWidth > -1 && width > options.maximumWidth) width = options.maximumWidth;
61
+ else if (options.minimumWidth > -1 && width < options.minimumWidth) width = options.minimumWidth;
62
+ if (options.maximumHeight > -1 && height > options.maximumHeight) height = options.maximumHeight;
63
+ else if (options.minimumHeight > -1 && height < options.minimumHeight) height = options.minimumHeight;
64
+ if (element.checkVisibility()) {
65
+ element.style.height = `${height}px`;
66
+ element.style.width = `${width}px`;
67
+ }
68
+ return [width, height];
69
+ }
70
+ function getProperties(element, widthProperty, heightProperty) {
71
+ const computed = getComputedStyle(element);
72
+ return [computed[widthProperty], computed[heightProperty]];
73
+ }
74
+ function getRestrictions(maximum, minimum) {
75
+ const [maximumWidth, maximumHeight] = getXAndY.apply(null, typeof maximum === "string" ? [maximum] : maximum);
76
+ const [minimumWidth, minimumHeight] = getXAndY.apply(null, typeof minimum === "string" ? [minimum] : minimum);
77
+ if (maximumWidth > -1 && minimumWidth > -1 && maximumWidth <= minimumWidth) throw new Error(MESSAGE_WIDTH);
78
+ if (maximumHeight > -1 && minimumHeight > -1 && maximumHeight <= minimumHeight) throw new Error(MESSAGE_HEIGHT);
79
+ return {
80
+ maximumHeight,
81
+ maximumWidth,
82
+ minimumHeight,
83
+ minimumWidth
84
+ };
85
+ }
86
+ function getState(element, handle, input) {
87
+ element.setAttribute(RESIZABLE_ATTRIBUTE, "");
88
+ const options = getValidOptions(element, input);
89
+ const [width, height] = getAndSetDimensions(element, options);
90
+ return {
91
+ element,
92
+ handle,
93
+ height,
94
+ options,
95
+ width,
96
+ styles: toggleStyles(element, styles)
97
+ };
98
+ }
99
+ function getValidOptions(element, input) {
100
+ const original = isPlainObject(input) ? input : {};
101
+ return { ...getRestrictions(input == null ? element.getAttribute(ATTRIBUTE_MAXIMUM) ?? getProperties(element, "maxWidth", "maxHeight") : [original.maximumWidth, original.maximumHeight], input == null ? element.getAttribute(ATTRIBUTE_MINIMUM) ?? getProperties(element, "minWidth", "minHeight") : [original.minimumWidth, original.minimumHeight]) };
102
+ }
103
+ function getXAndY(first, second) {
104
+ const [x, y] = typeof first === "string" && second == null ? first.split(",") : [first, second];
105
+ let xAsNumber = typeof x === "string" ? Number.parseFloat(x) : getNumber(x);
106
+ let yAsNumber = typeof y === "string" ? Number.parseFloat(y) : getNumber(y);
107
+ if (Number.isNaN(xAsNumber) || xAsNumber <= 0) xAsNumber = -1;
108
+ if (Number.isNaN(yAsNumber) || yAsNumber <= 0) yAsNumber = -1;
109
+ return [xAsNumber, yAsNumber];
110
+ }
111
+ function onKeydown(event) {
112
+ if (current == null || event.key !== "Escape") return;
113
+ current.state.element.style.height = `${current.state.height}px`;
114
+ current.state.element.style.width = `${current.state.width}px`;
115
+ resetResizable();
116
+ }
117
+ function onPointerdown(event) {
118
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || (supportsTouch.value ? event.type === "mousedown" : event.type === "touchstart" || event.button !== 0)) return;
119
+ const handle = findAncestor(event, SELECTOR_HANDLE);
120
+ const element = handle && findAncestor(handle, SELECTOR);
121
+ const stored = element && items.get(element);
122
+ const position = getPosition(event);
123
+ if (stored == null || position == null) return;
124
+ if (supportsTouch.value) event.preventDefault();
125
+ current = stored;
126
+ start = position;
127
+ current.state.styles.set();
128
+ styleToggler.set();
129
+ }
130
+ function onPointermove(event) {
131
+ if (current == null) return;
132
+ const { x, y } = getPosition(event) ?? {};
133
+ if (x == null || y == null) return;
134
+ const width = current.state.width + (x - (start?.x ?? 0));
135
+ const height = current.state.height + (y - (start?.y ?? 0));
136
+ getAndSetDimensions(current.state.element, current.state.options, {
137
+ height,
138
+ width
139
+ });
140
+ }
141
+ function onPointerup() {
142
+ if (current == null) return;
143
+ const { height, width } = current.state.element.getBoundingClientRect();
144
+ current.state.height = height;
145
+ current.state.width = width;
146
+ resetResizable();
147
+ }
148
+ function removeResizable(element) {
149
+ items.get(element)?.instance.destroy();
150
+ items.delete(element);
151
+ }
152
+ function resetResizable() {
153
+ current?.state.styles.remove();
154
+ styleToggler.remove();
155
+ current = void 0;
156
+ start = void 0;
157
+ }
158
+ const RESIZABLE_ATTRIBUTE = "oui-resizable";
159
+ const RESIZABLE_ATTRIBUTE_HANDLE = `${RESIZABLE_ATTRIBUTE}-handle`;
160
+ const ATTRIBUTE_MAXIMUM = `${RESIZABLE_ATTRIBUTE}-maximum`;
161
+ const ATTRIBUTE_MINIMUM = `${RESIZABLE_ATTRIBUTE}-minimum`;
162
+ const MESSAGE_ELEMENT = "The element must be an instance of HTMLElement or SVGElement";
163
+ const MESSAGE_HEIGHT = "The maximum height must be greater than the minimum height";
164
+ const MESSAGE_HANDLE = `The element must contain a child with the attribute "${RESIZABLE_ATTRIBUTE_HANDLE}"`;
165
+ const MESSAGE_WIDTH = "The maximum width must be greater than the minimum width";
166
+ const SELECTOR = `[${RESIZABLE_ATTRIBUTE}]`;
167
+ const SELECTOR_HANDLE = `[${RESIZABLE_ATTRIBUTE_HANDLE}]`;
168
+ const SELECTOR_HANDLE_SCOPED = `:scope > ${SELECTOR_HANDLE}`;
169
+ const items = /* @__PURE__ */ new WeakMap();
170
+ const styles = {
171
+ touchAction: "none",
172
+ userSelect: "none",
173
+ webkitUserSelect: "none"
174
+ };
175
+ const styleToggler = toggleStyles(document.body, styles);
176
+ let current;
177
+ let start;
178
+ on(document, "keydown", onKeydown);
179
+ on(document, "mousedown", onPointerdown);
180
+ on(document, "pointermove", onPointermove);
181
+ on(document, "pointerup", onPointerup);
182
+ on(document, "touchcancel", onPointerup);
183
+ on(document, "touchstart", onPointerdown, { passive: false });
184
+ attributable(RESIZABLE_ATTRIBUTE, addResizable, removeResizable);
185
+ //#endregion
186
+ export { OuiResizable, RESIZABLE_ATTRIBUTE, RESIZABLE_ATTRIBUTE_HANDLE, createResizable };
@@ -0,0 +1,2 @@
1
+ import { a as createResizable, n as OuiResizable, t as CreateOuiResizableOptions } from "../resizable.embedded-CXrUVPzR.mjs";
2
+ export { type CreateOuiResizableOptions, type OuiResizable, createResizable as createOuiResizable };
@@ -0,0 +1,2 @@
1
+ import { createResizable } from "./resizable.embedded.mjs";
2
+ export { createResizable as createOuiResizable };
@@ -0,0 +1,35 @@
1
+ import { StyleToggler } from "@oscarpalmer/toretto/style";
2
+
3
+ //#region src/js/resizable/resizable.embedded.d.ts
4
+ type CreateOuiResizableOptions = {
5
+ maximumHeight?: number;
6
+ maximumWidth?: number;
7
+ minimumHeight?: number;
8
+ minimumWidth?: number;
9
+ };
10
+ declare class OuiResizable {
11
+ #private;
12
+ constructor(state: OuiResizableState);
13
+ destroy(): void;
14
+ set(): void;
15
+ reset(): void;
16
+ }
17
+ type OuiResizableState = {
18
+ element: HTMLElement;
19
+ handle: HTMLElement;
20
+ height: number;
21
+ options: Required<CreateOuiResizableOptions>;
22
+ styles: StyleToggler;
23
+ width: number;
24
+ };
25
+ /**
26
+ * Creates _(or retrieves)_ a _OuiResizable_ instance for an element
27
+ *
28
+ * @param element Element to make resizable
29
+ * @returns _OuiResizable_ instance
30
+ */
31
+ declare function createResizable(element: HTMLElement, options?: CreateOuiResizableOptions): OuiResizable;
32
+ declare const RESIZABLE_ATTRIBUTE = "oui-resizable";
33
+ declare const RESIZABLE_ATTRIBUTE_HANDLE = "oui-resizable-handle";
34
+ //#endregion
35
+ export { createResizable as a, RESIZABLE_ATTRIBUTE_HANDLE as i, OuiResizable as n, RESIZABLE_ATTRIBUTE as r, CreateOuiResizableOptions as t };
@@ -315,7 +315,7 @@ on(document, "keydown", onKeydown);
315
315
  on(document, "mousedown", onPointerdown);
316
316
  on(document, "pointermove", onPointermove);
317
317
  on(document, "pointerup", onPointerup);
318
- on(document, "touchcancel", onPointerup, { passive: false });
318
+ on(document, "touchcancel", onPointerup);
319
319
  on(document, "touchstart", onPointerdown, { passive: false });
320
320
  //#endregion
321
321
  export { OuiSplitterElement };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -53,6 +53,10 @@
53
53
  "types": "./dist/js/popover.d.mts",
54
54
  "default": "./dist/js/popover.mjs"
55
55
  },
56
+ "./resizable": {
57
+ "types": "./dist/js/resizable/resizable.standalone.d.mts",
58
+ "default": "./dist/js/resizable/resizable.standalone.mjs"
59
+ },
56
60
  "./sortable": {
57
61
  "types": "./dist/js/sortable/sortable.standalone.d.mts",
58
62
  "default": "./dist/js/sortable/sortable.standalone.mjs"
@@ -36,6 +36,15 @@
36
36
  transition-delay: 0.125s;
37
37
  }
38
38
 
39
+ :where([oui-dialog]) {
40
+ position: relative;
41
+ }
42
+
43
+ :where([oui-dialog-resizer]) {
44
+ position: absolute;
45
+ inset: auto 0 0 auto;
46
+ }
47
+
39
48
  :where([oui-popover], [oui-tooltip-content]) {
40
49
  @include mixins.floatable;
41
50
  }
package/src/css/form.scss CHANGED
@@ -173,9 +173,9 @@
173
173
 
174
174
  :where(
175
175
  &.oui-validate:not([disabled], [readonly], [aria-disabled='true' i]):is(
176
- :user-invalid:not([aria-invalid='false' i]),
177
- [aria-invalid='true' i]
178
- )
176
+ :user-invalid:not([aria-invalid='false' i]),
177
+ [aria-invalid='true' i]
178
+ )
179
179
  ) {
180
180
  @include invalid;
181
181
 
@@ -201,9 +201,9 @@
201
201
 
202
202
  :where(
203
203
  &.oui-validate:not([disabled], [readonly], [aria-disabled='true' i]):is(
204
- :user-valid:not([aria-invalid='true' i]),
205
- [aria-invalid='false' i]
206
- )
204
+ :user-valid:not([aria-invalid='true' i]),
205
+ [aria-invalid='false' i]
206
+ )
207
207
  ) {
208
208
  @include valid;
209
209
 
package/src/js/dialog.ts CHANGED
@@ -4,6 +4,8 @@ import {getOnBeforeToggleListener} from './floatable/floatable.embedded';
4
4
  import {attributable} from './internal/attributable';
5
5
  import {ATTRIBUTE_MOVABLE_HANDLE} from './movable/movable.embedded';
6
6
  import {createMovable, type OuiMovable} from './movable/movable.standalone';
7
+ import {createResizable, RESIZABLE_ATTRIBUTE_HANDLE} from './resizable/resizable.embedded';
8
+ import type {OuiResizable} from './resizable/resizable.standalone';
7
9
 
8
10
  // #region Types
9
11
 
@@ -82,6 +84,7 @@ type OuiDialogState = {
82
84
  listeners: RemovableEventListener[];
83
85
  modal: boolean;
84
86
  movable?: OuiMovable;
87
+ resizable?: OuiResizable;
85
88
  };
86
89
 
87
90
  // #endregion
@@ -108,9 +111,14 @@ function destroyDialog(state: OuiDialogState): void {
108
111
  listener();
109
112
  }
110
113
 
114
+ state.movable?.destroy();
115
+ state.resizable?.destroy();
116
+
111
117
  state.dialog = undefined as never;
112
118
  state.element = undefined as never;
113
119
  state.listeners = undefined as never;
120
+ state.movable = undefined;
121
+ state.resizable = undefined;
114
122
  }
115
123
 
116
124
  /**
@@ -139,6 +147,18 @@ function getMovable(element: HTMLDialogElement): OuiMovable | undefined {
139
147
  });
140
148
  }
141
149
 
150
+ function getResizable(element: HTMLDialogElement): OuiResizable | undefined {
151
+ const handle = element.querySelector(SELECTOR_RESIZER);
152
+
153
+ if (handle == null) {
154
+ return;
155
+ }
156
+
157
+ handle.setAttribute(RESIZABLE_ATTRIBUTE_HANDLE, '');
158
+
159
+ return createResizable(element);
160
+ }
161
+
142
162
  function getState(element: HTMLDialogElement): OuiDialogState {
143
163
  const state: OuiDialogState = {
144
164
  element,
@@ -157,6 +177,10 @@ function getState(element: HTMLDialogElement): OuiDialogState {
157
177
  element.setAttribute(ATTRIBUTE_OPEN, '');
158
178
 
159
179
  element.focus();
180
+
181
+ setTimeout(() => {
182
+ state.resizable?.set();
183
+ }, 500);
160
184
  } else {
161
185
  element.removeAttribute(ATTRIBUTE_OPEN);
162
186
  element.setAttribute(ARIA_MODAL, String(state.modal));
@@ -164,6 +188,8 @@ function getState(element: HTMLDialogElement): OuiDialogState {
164
188
  element.style.inset = '';
165
189
  element.style.position = '';
166
190
  element.style.transform = '';
191
+
192
+ state.resizable?.reset();
167
193
  }
168
194
  }),
169
195
  ],
@@ -171,7 +197,9 @@ function getState(element: HTMLDialogElement): OuiDialogState {
171
197
  };
172
198
 
173
199
  state.dialog = new OuiDialog(state);
200
+
174
201
  state.movable = getMovable(element);
202
+ state.resizable = getResizable(element);
175
203
 
176
204
  return state;
177
205
  }
@@ -210,6 +238,8 @@ const SELECTOR_MOVER = `[${ATTRIBUTE}-mover]`;
210
238
 
211
239
  const SELECTOR_MODAL = ':modal';
212
240
 
241
+ const SELECTOR_RESIZER = `[${ATTRIBUTE}-resizer]`;
242
+
213
243
  const TRUE = 'true';
214
244
 
215
245
  const states = new WeakMap<HTMLElement, OuiDialogState>();
package/src/js/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import './sortable/sortable.embedded';
2
1
  import './toolbar';
3
2
 
4
3
  export * from './dialog';
@@ -6,5 +5,7 @@ export * from './floatable/floatable.standalone';
6
5
  export * from './focus-trap/focus-trap.standalone';
7
6
  export * from './movable/movable.standalone';
8
7
  export * from './popover';
8
+ export * from './resizable/resizable.standalone';
9
+ export * from './sortable/sortable.standalone';
9
10
  export * from './splitter';
10
11
  export * from './tooltip';
@@ -650,7 +650,7 @@ on(document, 'keydown', onKeydown);
650
650
  on(document, 'mousedown', onPointerdown);
651
651
  on(document, 'pointermove', onPointermove);
652
652
  on(document, 'pointerup', onPointerup);
653
- on(document, 'touchcancel', onPointerup, {passive: false});
653
+ on(document, 'touchcancel', onPointerup);
654
654
  on(document, 'touchstart', onPointerdown, {passive: false});
655
655
 
656
656
  // #endregion
@@ -0,0 +1,383 @@
1
+ import {isPlainObject} from '@oscarpalmer/atoms/is';
2
+ import type {EventPosition} from '@oscarpalmer/atoms/models';
3
+ import {getNumber} from '@oscarpalmer/atoms/number';
4
+ import {getPosition, on} from '@oscarpalmer/toretto/event';
5
+ import {findAncestor} from '@oscarpalmer/toretto/find';
6
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
7
+ import {toggleStyles, type StyleToggler} from '@oscarpalmer/toretto/style';
8
+ import supportsTouch from '@oscarpalmer/toretto/touch';
9
+ import {attributable} from '../internal/attributable';
10
+
11
+ // #region Types
12
+
13
+ export type CreateOuiResizableOptions = {
14
+ maximumHeight?: number;
15
+ maximumWidth?: number;
16
+ minimumHeight?: number;
17
+ minimumWidth?: number;
18
+ };
19
+
20
+ export class OuiResizable {
21
+ readonly #state: OuiResizableState;
22
+
23
+ constructor(state: OuiResizableState) {
24
+ this.#state = state;
25
+ }
26
+
27
+ destroy(): void {
28
+ destroyResizable(this.#state);
29
+ }
30
+
31
+ set(): void {
32
+ const [width, height] = getAndSetDimensions(this.#state.element, this.#state.options);
33
+
34
+ console.log('set', width, height);
35
+
36
+ this.#state.width = width;
37
+ this.#state.height = height;
38
+ }
39
+
40
+ reset(): void {
41
+ this.#state.element.style.height = '';
42
+ this.#state.element.style.width = '';
43
+ }
44
+ }
45
+
46
+ type OuiResizableItem = {
47
+ instance: OuiResizable;
48
+ state: OuiResizableState;
49
+ };
50
+
51
+ type OuiResizableState = {
52
+ element: HTMLElement;
53
+ handle: HTMLElement;
54
+ height: number;
55
+ options: Required<CreateOuiResizableOptions>;
56
+ styles: StyleToggler;
57
+ width: number;
58
+ };
59
+
60
+ // #endregion
61
+
62
+ // #region Functions
63
+
64
+ function addResizable(
65
+ element: HTMLElement,
66
+ options?: CreateOuiResizableOptions,
67
+ ): OuiResizable | undefined {
68
+ let item = items.get(element);
69
+
70
+ if (item == null) {
71
+ const handle = element.querySelector(SELECTOR_HANDLE_SCOPED);
72
+
73
+ if (!isHTMLOrSVGElement(handle)) {
74
+ return;
75
+ }
76
+
77
+ const state = getState(element, handle, options);
78
+ const instance = new OuiResizable(state);
79
+
80
+ item = {
81
+ instance,
82
+ state,
83
+ };
84
+
85
+ items.set(element, item);
86
+ }
87
+
88
+ return item?.instance;
89
+ }
90
+
91
+ /**
92
+ * Creates _(or retrieves)_ a _OuiResizable_ instance for an element
93
+ *
94
+ * @param element Element to make resizable
95
+ * @returns _OuiResizable_ instance
96
+ */
97
+ export function createResizable(
98
+ element: HTMLElement,
99
+ options?: CreateOuiResizableOptions,
100
+ ): OuiResizable {
101
+ if (!isHTMLOrSVGElement(element)) {
102
+ throw new TypeError(MESSAGE_ELEMENT);
103
+ }
104
+
105
+ if (!isHTMLOrSVGElement(element.querySelector(SELECTOR_HANDLE_SCOPED))) {
106
+ throw new Error(MESSAGE_HANDLE);
107
+ }
108
+
109
+ return addResizable(element, options ?? {})!;
110
+ }
111
+
112
+ function destroyResizable(state: OuiResizableState): void {
113
+ state.element = undefined as never;
114
+ state.handle = undefined as never;
115
+ }
116
+
117
+ function getAndSetDimensions(
118
+ element: HTMLElement,
119
+ options: Required<CreateOuiResizableOptions>,
120
+ input?: Pick<DOMRect, 'height' | 'width'>,
121
+ ): [number, number] {
122
+ let {height, width} = input ?? element.getBoundingClientRect();
123
+
124
+ if (options.maximumWidth > -1 && width > options.maximumWidth) {
125
+ width = options.maximumWidth;
126
+ } else if (options.minimumWidth > -1 && width < options.minimumWidth) {
127
+ width = options.minimumWidth;
128
+ }
129
+
130
+ if (options.maximumHeight > -1 && height > options.maximumHeight) {
131
+ height = options.maximumHeight;
132
+ } else if (options.minimumHeight > -1 && height < options.minimumHeight) {
133
+ height = options.minimumHeight;
134
+ }
135
+
136
+ if (element.checkVisibility()) {
137
+ element.style.height = `${height}px`;
138
+ element.style.width = `${width}px`;
139
+ }
140
+
141
+ return [width, height];
142
+ }
143
+
144
+ function getProperties(
145
+ element: HTMLElement,
146
+ widthProperty: 'maxWidth' | 'minWidth',
147
+ heightProperty: 'maxHeight' | 'minHeight',
148
+ ): [string, string] {
149
+ const computed = getComputedStyle(element);
150
+
151
+ return [computed[widthProperty], computed[heightProperty]];
152
+ }
153
+
154
+ function getRestrictions(
155
+ maximum: string | [unknown, unknown],
156
+ minimum: string | [unknown, unknown],
157
+ ): Required<CreateOuiResizableOptions> {
158
+ const [maximumWidth, maximumHeight] = getXAndY.apply(
159
+ null,
160
+ typeof maximum === 'string' ? [maximum] : maximum,
161
+ );
162
+
163
+ const [minimumWidth, minimumHeight] = getXAndY.apply(
164
+ null,
165
+ typeof minimum === 'string' ? [minimum] : minimum,
166
+ );
167
+
168
+ if (maximumWidth > -1 && minimumWidth > -1 && maximumWidth <= minimumWidth) {
169
+ throw new Error(MESSAGE_WIDTH);
170
+ }
171
+
172
+ if (maximumHeight > -1 && minimumHeight > -1 && maximumHeight <= minimumHeight) {
173
+ throw new Error(MESSAGE_HEIGHT);
174
+ }
175
+
176
+ return {
177
+ maximumHeight,
178
+ maximumWidth,
179
+ minimumHeight,
180
+ minimumWidth,
181
+ };
182
+ }
183
+
184
+ function getState(
185
+ element: HTMLElement,
186
+ handle: HTMLElement,
187
+ input?: CreateOuiResizableOptions,
188
+ ): OuiResizableState {
189
+ element.setAttribute(RESIZABLE_ATTRIBUTE, '');
190
+
191
+ const options = getValidOptions(element, input);
192
+
193
+ const [width, height] = getAndSetDimensions(element, options);
194
+
195
+ return {
196
+ element,
197
+ handle,
198
+ height,
199
+ options,
200
+ width,
201
+ styles: toggleStyles(element, styles),
202
+ };
203
+ }
204
+
205
+ function getValidOptions(
206
+ element: HTMLElement,
207
+ input?: CreateOuiResizableOptions,
208
+ ): Required<CreateOuiResizableOptions> {
209
+ const original = isPlainObject(input) ? input : {};
210
+
211
+ return {
212
+ ...getRestrictions(
213
+ input == null
214
+ ? (element.getAttribute(ATTRIBUTE_MAXIMUM) ??
215
+ getProperties(element, 'maxWidth', 'maxHeight'))
216
+ : [original.maximumWidth, original.maximumHeight],
217
+ input == null
218
+ ? (element.getAttribute(ATTRIBUTE_MINIMUM) ??
219
+ getProperties(element, 'minWidth', 'minHeight'))
220
+ : [original.minimumWidth, original.minimumHeight],
221
+ ),
222
+ };
223
+ }
224
+
225
+ function getXAndY(first: unknown, second?: unknown): [number, number] {
226
+ const [x, y] = typeof first === 'string' && second == null ? first.split(',') : [first, second];
227
+
228
+ let xAsNumber = typeof x === 'string' ? Number.parseFloat(x) : getNumber(x);
229
+ let yAsNumber = typeof y === 'string' ? Number.parseFloat(y) : getNumber(y);
230
+
231
+ if (Number.isNaN(xAsNumber) || xAsNumber <= 0) {
232
+ xAsNumber = -1;
233
+ }
234
+
235
+ if (Number.isNaN(yAsNumber) || yAsNumber <= 0) {
236
+ yAsNumber = -1;
237
+ }
238
+
239
+ return [xAsNumber, yAsNumber];
240
+ }
241
+
242
+ function onKeydown(event: KeyboardEvent): void {
243
+ if (current == null || event.key !== 'Escape') {
244
+ return;
245
+ }
246
+
247
+ current.state.element.style.height = `${current.state.height}px`;
248
+ current.state.element.style.width = `${current.state.width}px`;
249
+
250
+ resetResizable();
251
+ }
252
+
253
+ function onPointerdown(event: MouseEvent | TouchEvent): void {
254
+ if (
255
+ event.altKey ||
256
+ event.ctrlKey ||
257
+ event.metaKey ||
258
+ event.shiftKey ||
259
+ (supportsTouch.value
260
+ ? event.type === 'mousedown'
261
+ : event.type === 'touchstart' || (event as MouseEvent).button !== 0)
262
+ ) {
263
+ return;
264
+ }
265
+
266
+ const handle = findAncestor(event, SELECTOR_HANDLE);
267
+ const element = handle && findAncestor(handle, SELECTOR);
268
+ const stored = element && items.get(element as HTMLElement);
269
+ const position = getPosition(event);
270
+
271
+ if (stored == null || position == null) {
272
+ return;
273
+ }
274
+
275
+ if (supportsTouch.value) {
276
+ event.preventDefault();
277
+ }
278
+
279
+ current = stored;
280
+ start = position;
281
+
282
+ current.state.styles.set();
283
+ styleToggler.set();
284
+ }
285
+
286
+ function onPointermove(event: MouseEvent | TouchEvent): void {
287
+ if (current == null) {
288
+ return;
289
+ }
290
+
291
+ const {x, y} = getPosition(event) ?? {};
292
+
293
+ if (x == null || y == null) {
294
+ return;
295
+ }
296
+
297
+ const width = current.state.width + (x - (start?.x ?? 0));
298
+ const height = current.state.height + (y - (start?.y ?? 0));
299
+
300
+ getAndSetDimensions(current.state.element, current.state.options, {height, width});
301
+ }
302
+
303
+ function onPointerup(): void {
304
+ if (current == null) {
305
+ return;
306
+ }
307
+
308
+ const {height, width} = current.state.element.getBoundingClientRect();
309
+
310
+ current.state.height = height;
311
+ current.state.width = width;
312
+
313
+ resetResizable();
314
+ }
315
+
316
+ function removeResizable(element: HTMLElement): void {
317
+ items.get(element)?.instance.destroy();
318
+
319
+ items.delete(element);
320
+ }
321
+
322
+ function resetResizable(): void {
323
+ current?.state.styles.remove();
324
+ styleToggler.remove();
325
+
326
+ current = undefined;
327
+ start = undefined;
328
+ }
329
+
330
+ // #endregion
331
+
332
+ // #region Variables
333
+
334
+ export const RESIZABLE_ATTRIBUTE = 'oui-resizable';
335
+
336
+ export const RESIZABLE_ATTRIBUTE_HANDLE = `${RESIZABLE_ATTRIBUTE}-handle`;
337
+
338
+ const ATTRIBUTE_MAXIMUM = `${RESIZABLE_ATTRIBUTE}-maximum`;
339
+
340
+ const ATTRIBUTE_MINIMUM = `${RESIZABLE_ATTRIBUTE}-minimum`;
341
+
342
+ const MESSAGE_ELEMENT = 'The element must be an instance of HTMLElement or SVGElement';
343
+
344
+ const MESSAGE_HEIGHT = 'The maximum height must be greater than the minimum height';
345
+
346
+ const MESSAGE_HANDLE = `The element must contain a child with the attribute "${RESIZABLE_ATTRIBUTE_HANDLE}"`;
347
+
348
+ const MESSAGE_WIDTH = 'The maximum width must be greater than the minimum width';
349
+
350
+ const SELECTOR = `[${RESIZABLE_ATTRIBUTE}]`;
351
+
352
+ const SELECTOR_HANDLE = `[${RESIZABLE_ATTRIBUTE_HANDLE}]`;
353
+
354
+ const SELECTOR_HANDLE_SCOPED = `:scope > ${SELECTOR_HANDLE}`;
355
+
356
+ const items = new WeakMap<HTMLElement, OuiResizableItem>();
357
+
358
+ const styles = {
359
+ touchAction: 'none',
360
+ userSelect: 'none',
361
+ webkitUserSelect: 'none',
362
+ };
363
+
364
+ const styleToggler = toggleStyles(document.body, styles);
365
+
366
+ let current: OuiResizableItem | undefined;
367
+
368
+ let start: EventPosition | undefined;
369
+
370
+ // #endregion
371
+
372
+ // #region Initialization
373
+
374
+ on(document, 'keydown', onKeydown);
375
+ on(document, 'mousedown', onPointerdown);
376
+ on(document, 'pointermove', onPointermove);
377
+ on(document, 'pointerup', onPointerup);
378
+ on(document, 'touchcancel', onPointerup);
379
+ on(document, 'touchstart', onPointerdown, {passive: false});
380
+
381
+ attributable(RESIZABLE_ATTRIBUTE, addResizable, removeResizable);
382
+
383
+ // #endregion
@@ -0,0 +1,5 @@
1
+ export {
2
+ createResizable as createOuiResizable,
3
+ type CreateOuiResizableOptions,
4
+ type OuiResizable,
5
+ } from './resizable.embedded';
@@ -557,5 +557,5 @@ on(document, 'keydown', onKeydown);
557
557
  on(document, 'mousedown', onPointerdown);
558
558
  on(document, 'pointermove', onPointermove);
559
559
  on(document, 'pointerup', onPointerup);
560
- on(document, 'touchcancel', onPointerup, {passive: false});
560
+ on(document, 'touchcancel', onPointerup);
561
561
  on(document, 'touchstart', onPointerdown, {passive: false});