1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Namespace initialization
16if (typeof(RapidContext) == "undefined") {
17 RapidContext = {};
18}
19RapidContext.Widget = RapidContext.Widget || { Classes: {} };
20
21/**
22 * Creates a new dialog widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {string} [attrs.title] the dialog title, defaults to "Dialog"
27 * @param {boolean} [attrs.modal] the modal dialog flag, defaults to `false`
28 * @param {boolean} [attrs.system] the system dialog flag, implies modal,
29 * defaults to `false`
30 * @param {boolean} [attrs.center] the center dialog flag, defaults to `true`
31 * @param {boolean} [attrs.closeable] the closeable dialog flag, defaults to
32 * `true`
33 * @param {boolean} [attrs.resizeable] the resize dialog flag, defaults to
34 * `true`
35 * @param {boolean} [attrs.hidden] the hidden widget flag, defaults to `true`
36 * @param {...(Node|Widget|string)} [child] the child widgets or DOM nodes
37 *
38 * @return {Widget} the widget DOM node
39 *
40 * @class The dialog widget class. Used to provide a resizeable and
41 * moveable window within the current page. Internally it uses a
42 * number of `<div>` HTML elements.
43 * @extends RapidContext.Widget
44 *
45 * @example <caption>JavaScript</caption>
46 * let h1 = MochiKit.DOM.H1({}, "Hello, world!");
47 * let attrs = { title: "Hello", modal: true };
48 * let helloDialog = RapidContext.Widget.Dialog(attrs, h1);
49 * RapidContext.Util.registerSizeConstraints(helloDialog, "200", "75");
50 *
51 * @example <caption>User Interface XML</caption>
52 * <Dialog id="helloDialog" title="Hello" modal="true" w="200" h="75">
53 * <h1>Hello, world!</h1>
54 * </Dialog>
55 */
56RapidContext.Widget.Dialog = function (attrs/*, ... */) {
57 let DIV = MochiKit.DOM.DIV;
58 let title = DIV({ "class": "widgetDialogTitle", "data-dialog": "move" }, "Dialog");
59 let close = RapidContext.Widget.Icon({
60 "class": "widgetDialogClose fa fa-times",
61 "title": "Close",
62 "data-dialog": "close"
63 });
64 let resize = DIV({ "class": "widgetDialogResize", "data-dialog": "resize" });
65 let content = DIV({ "class": "widgetDialogContent" });
66 let o = DIV({}, title, close, resize, content);
67 RapidContext.Widget._widgetMixin(o, RapidContext.Widget.Dialog);
68 o.classList.add("widgetDialog");
69 o.resizeContent = o._resizeContent;
70 o._setHidden(true);
71 o.setAttrs(Object.assign({ modal: false, system: false, center: true }, attrs));
72 o.addAll(Array.from(arguments).slice(1));
73 o.on("click", "[data-dialog]", o._handleClick);
74 o.on("mousedown", "[data-dialog]", o._handleMouseDown);
75 return o;
76};
77
78// Register widget class
79RapidContext.Widget.Classes.Dialog = RapidContext.Widget.Dialog;
80
81/**
82 * Emitted when the dialog is shown.
83 *
84 * @name RapidContext.Widget.Dialog#onshow
85 * @event
86 */
87
88/**
89 * Emitted when the dialog is hidden.
90 *
91 * @name RapidContext.Widget.Dialog#onhide
92 * @event
93 */
94
95/**
96 * Emitted when the dialog is moved. The event will be sent
97 * repeatedly when moving with a mouse drag operation.
98 *
99 * @name RapidContext.Widget.Dialog#onmove
100 * @event
101 */
102
103/**
104 * Emitted when the dialog is resized. The event will be sent
105 * repeatedly when resizing with a mouse drag operation.
106 *
107 * @name RapidContext.Widget.Dialog#onresize
108 * @event
109 */
110
111/**
112 * Returns the widget container DOM node.
113 *
114 * @return {Node} the container DOM node
115 */
116RapidContext.Widget.Dialog.prototype._containerNode = function () {
117 return this.lastChild;
118};
119
120/**
121 * Returns the widget style DOM node.
122 *
123 * @return {Node} the style DOM node
124 */
125RapidContext.Widget.Dialog.prototype._styleNode = function () {
126 return this.lastChild;
127};
128
129/**
130 * Handles click events in the dialog. Will close the dialog if an element
131 * with `data-dialog="close"` attribute was clicked.
132 *
133 * @param {Event} evt the DOM Event object
134 */
135RapidContext.Widget.Dialog.prototype._handleClick = function (evt) {
136 if (evt.delegateTarget.dataset.dialog == "close") {
137 this.hide();
138 }
139};
140
141/**
142 * Handles mouse down events in the dialog. Will start move or resize actions
143 * if an element with a `data-dialog="move"` or `data-dialog="resize"`
144 * attribute was clicked.
145 *
146 * @param {Event} evt the DOM Event object
147 */
148RapidContext.Widget.Dialog.prototype._handleMouseDown = function (evt) {
149 let action = evt.delegateTarget.dataset.dialog;
150 if (action == "move" || action == "resize") {
151 evt.preventDefault();
152 let isDim = action == "resize";
153 let x = (isDim ? this.offsetWidth : this.offsetLeft) - evt.pageX;
154 let y = (isDim ? this.offsetHeight : this.offsetTop) - evt.pageY;
155 document._drag = { target: this, action: action, x: x, y: y };
156 document.addEventListener("mouseup", this._handleMouseUp);
157 document.addEventListener("mousemove", this._handleMouseMove);
158 }
159};
160
161/**
162 * Stops a dialog resize or move drag operation and removes event listeners.
163 * Note that this event handler is attached to the root `document`.
164 *
165 * @param {Event} evt the DOM Event object
166 */
167RapidContext.Widget.Dialog.prototype._handleMouseUp = function (evt) {
168 let o = document._drag;
169 if (o && o.target) {
170 // FIXME: Use AbortSignal instead to disconnect
171 document.removeEventListener("mouseup", o.target._handleMouseUp);
172 document.removeEventListener("mousemove", o.target._handleMouseMove);
173 }
174 delete document._drag;
175};
176
177/**
178 * Handles a dialog move drag operation.
179 *
180 * @param {Event} evt the DOM Event object
181 */
182RapidContext.Widget.Dialog.prototype._handleMouseMove = function (evt) {
183 let o = document._drag;
184 if (o && o.action == "move") {
185 o.target.moveTo(o.x + evt.pageX, o.y + evt.pageY);
186 } else if (o && o.action == "resize") {
187 o.target.resizeTo(o.x + evt.pageX, o.y + evt.pageY);
188 }
189};
190
191/**
192 * Updates the dialog or HTML DOM node attributes.
193 *
194 * @param {Object} attrs the widget and node attributes to set
195 * @param {string} [attrs.title] the dialog title
196 * @param {boolean} [attrs.modal] the modal dialog flag
197 * @param {boolean} [attrs.system] the system dialog flag, implies modal
198 * @param {boolean} [attrs.center] the center dialog flag
199 * @param {boolean} [attrs.closeable] the closeable dialog flag
200 * @param {boolean} [attrs.resizeable] the resize dialog flag
201 * @param {boolean} [attrs.hidden] the hidden widget flag
202 */
203RapidContext.Widget.Dialog.prototype.setAttrs = function (attrs) {
204 attrs = Object.assign({}, attrs);
205 if ("title" in attrs) {
206 this.firstChild.innerText = attrs.title || "";
207 delete attrs.title;
208 }
209 if ("modal" in attrs) {
210 attrs.modal = RapidContext.Data.bool(attrs.modal);
211 }
212 if ("system" in attrs) {
213 attrs.system = RapidContext.Data.bool(attrs.system);
214 this.classList.toggle("system", attrs.system);
215 }
216 if ("center" in attrs) {
217 attrs.center = RapidContext.Data.bool(attrs.center);
218 }
219 if ("resizeable" in attrs) {
220 attrs.resizeable = RapidContext.Data.bool(attrs.resizeable);
221 this.childNodes[2].classList.toggle("hidden", !attrs.resizeable);
222 }
223 if ("closeable" in attrs) {
224 attrs.closeable = RapidContext.Data.bool(attrs.closeable);
225 this.childNodes[1].setAttrs({ hidden: !attrs.closeable });
226 }
227 if ("hidden" in attrs) {
228 this._setHiddenDialog(RapidContext.Data.bool(attrs.hidden));
229 delete attrs.hidden;
230 }
231 this.__setAttrs(attrs);
232};
233
234/**
235 * Performs the changes corresponding to setting the `hidden`
236 * widget attribute for the Dialog widget.
237 *
238 * @param {boolean} value the new attribute value
239 */
240RapidContext.Widget.Dialog.prototype._setHiddenDialog = function (value) {
241 if (!!value === this.isHidden()) {
242 // Avoid repetitive show/hide calls
243 } else if (value) {
244 if (this._modalNode != null) {
245 RapidContext.Widget.destroyWidget(this._modalNode);
246 this._modalNode = null;
247 }
248 this.blurAll();
249 this._setHidden(true);
250 this.emit("hide");
251 } else {
252 if (this.parentNode == null) {
253 throw new Error("Cannot show Dialog widget without setting a parent DOM node");
254 }
255 if (this.modal || this.system) {
256 let attrs = { loading: false, message: "", style: { "z-index": "99" } };
257 if (this.system) {
258 attrs.dark = true;
259 }
260 this._modalNode = RapidContext.Widget.Overlay(attrs);
261 this.parentNode.append(this._modalNode);
262 }
263 this._setHidden(false);
264 this.resetScroll();
265 this._resizeContent();
266 this.emit("show");
267 }
268};
269
270/**
271 * Moves the dialog to the specified position (relative to the
272 * parent DOM node). The position will be restrained by the parent
273 * DOM node size.
274 *
275 * @param {number} x the horizontal position (in pixels)
276 * @param {number} y the vertical position (in pixels)
277 */
278RapidContext.Widget.Dialog.prototype.moveTo = function (x, y) {
279 let max = {
280 x: this.parentNode.offsetWidth - this.offsetWidth - 2,
281 y: this.parentNode.offsetHeight - this.offsetHeight - 2
282 };
283 let pos = {
284 x: Math.round(Math.max(0, Math.min(x, max.x))),
285 y: Math.round(Math.max(0, Math.min(y, max.y)))
286 };
287 this.style.left = pos.x + "px";
288 this.style.top = pos.y + "px";
289 let el = this.lastChild;
290 el.style.maxWidth = (this.parentNode.offsetWidth - pos.x - el.offsetLeft - 5) + "px";
291 el.style.maxHeight = (this.parentNode.offsetHeight - pos.y - el.offsetTop - 5) + "px";
292 this.center = false;
293 this.emit("move", { detail: pos });
294};
295
296/**
297 * Moves the dialog to the apparent center (relative to the parent DOM
298 * node). The vertical position actually uses the golden ratio instead
299 * of the geometric center for improved visual alignment.
300 */
301RapidContext.Widget.Dialog.prototype.moveToCenter = function () {
302 this.style.left = "0px";
303 this.style.top = "0px";
304 this.lastChild.style.maxWidth = "";
305 this.lastChild.style.maxHeight = "";
306 let x = (this.parentNode.offsetWidth - this.offsetWidth) / 2;
307 let y = (this.parentNode.offsetHeight - this.offsetHeight) / 2.618;
308 this.moveTo(x, y);
309 this.center = true;
310};
311
312/**
313 * Resizes the dialog to the specified size (in pixels). The size
314 * will be restrained by the parent DOM node size.
315 *
316 * @param {number} w the width (in pixels)
317 * @param {number} h the height (in pixels)
318 *
319 * @return {Dimensions} an object with "w" and "h" properties for the
320 * actual size used
321 */
322RapidContext.Widget.Dialog.prototype.resizeTo = function (w, h) {
323 let max = {
324 w: this.parentNode.offsetWidth - this.offsetLeft - 2,
325 h: this.parentNode.offsetHeight - this.offsetTop - 2
326 };
327 let dim = {
328 w: Math.round(Math.max(150, Math.min(w, max.w))),
329 h: Math.round(Math.max(100, Math.min(h, max.h)))
330 };
331 this.style.width = dim.w + "px";
332 this.style.height = dim.h + "px";
333 delete this.sizeConstraints; // FIXME: Remove with RapidContext.Util.registerSizeConstraints
334 this.center = false;
335 this._resizeContent();
336 this.emit("resize", { detail: dim });
337 return dim;
338};
339
340/**
341 * Resizes the dialog to the optimal size for the current content.
342 * Note that the size reported by the content may vary depending on
343 * if it has already been displayed, is absolutely positioned, etc.
344 * The size will be restrained by the parent DOM node size.
345 *
346 * @return {Dimensions} an object with "w" and "h" properties for the
347 * actual size used
348 */
349RapidContext.Widget.Dialog.prototype.resizeToContent = function () {
350 let el = this.lastChild;
351 let w = Math.max(el.scrollWidth, el.offsetWidth) + 2;
352 let h = Math.max(el.scrollHeight, el.offsetHeight) + el.offsetTop + 2;
353 return this.resizeTo(w, h);
354};
355
356/**
357 * Called when dialog content should be resized.
358 */
359RapidContext.Widget.Dialog.prototype._resizeContent = function () {
360 if (!this.isHidden()) {
361 RapidContext.Util.resizeElements(this.lastChild);
362 if (this.center) {
363 this.moveToCenter();
364 }
365 }
366};
367
368/**
369 * Resets the scroll offsets for all child elements in the dialog.
370 */
371RapidContext.Widget.Dialog.prototype.resetScroll = function () {
372 function scrollReset(el) {
373 el.scrollTop = 0;
374 el.scrollLeft = 0;
375 }
376 Array.from(this.querySelectorAll("*")).forEach(scrollReset);
377};
378
RapidContext
Access · Discovery · Insight
www.rapidcontext.com