zkbind/src/main/resources/web/js/zkbind/Binder.ts
/* Binder.ts
Purpose:
Description:
History:
Wed, Jan 07, 2015 12:08:17 PM, Created by jumperchen
Copyright (C) Potix Corporation. All Rights Reserved.
*/
var _zkMatchMediaRegexPattern = /ZKMatchMedia=([^;]*)/,
_portrait: Record<string | number, boolean> = { '0': true, '180': true }, //default portrait definition
_initLandscape = jq.innerWidth() > jq.innerHeight(), // initial orientation is landscape or not
// eslint-disable-next-line zk/preferStrictBooleanType
_initDefault = _portrait[window.orientation] as boolean | undefined; //default orientation
declare module '@zk/widget' {
interface Widget {
/** @internal */
_$binder?: zkbind.Binder;
$ZKBINDER$?: boolean;
$ZKMATCHMEDIA$?: string[];
$binder(): zkbind.Binder | undefined;
}
}
const _WidgetX = zk.augment(zk.Widget.prototype, {
$binder(): zkbind.Binder | undefined {
// eslint-disable-next-line @typescript-eslint/no-this-alias
var w: zk.Widget | undefined = this;
for (; w; w = w.parent) {
if (w.$ZKBINDER$)
break;
}
if (w) {
if (!w._$binder)
w._$binder = new zkbind.Binder(w, this);
return w._$binder;
}
return;
},
$afterCommand(command: string, args?: unknown[]): void {
const binder = this.$binder();
if (binder)
binder.$doAfterCommand(command, args);
},
/** @internal */
unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
if (this._$binder) {
this._$binder.destroy();
this._$binder = undefined;
}
_WidgetX.unbind_.call(this, skipper, after, keepRod);
}
});
export interface BinderOptions {
exact?: boolean;
strict?: boolean;
child?: boolean;
}
/**
* Retrieves the binder if any.
* @param n - the object to look for. If it is a string,
* it is assumed to be UUID, unless it starts with '$'.
* For example, `zkbind.$('uuid')` is the same as `zkbind.$('#uuid')`,
* and both look for a widget whose ID is 'uuid'. On the other hand,
* `zkbind.$('$id')` looks for a widget whose ID is 'id'.<br/>
* and `zkbind.$('.className')` looks for a widget whose CSS selector is 'className'.<br/>
* If it is an DOM element ({@link DOMElement}), it will look up
* which widget it belongs to.<br/>
* If the object is not a DOM element and has a property called
* `target`, then `target` is assumed.
* Thus, you can pass an instance of {@link jq.Event} or {@link zk.Event},
* and the target widget will be returned.
* @param opts - the options. Allowed values:
* <ul>
* <li>exact - id must exactly match uuid (i.e., uuid-xx ignored).
* It also implies strict</li>
* <li>strict - whether not to look up the parent node.(since 5.0.2)
* If omitted, false is assumed (and it will look up parent).</li>
* <li>child - whether to ensure the given element is a child element
* of the widget's main element ({@link zk.Widget.$n}). In most cases, if ID
* of an element is xxx-yyy, the the element must be a child of
* the element whose ID is xxx. However, there is some exception
* such as the shadow of a window.</li>
* </ul>
* @since 8.0.0
*/
export function $(n: string | HTMLElement | zk.Event | JQuery.Event, opts?: BinderOptions): Binder | undefined {
var widget = zk.Widget.$(n, opts);
if (widget)
return widget.$binder();
zk.error('Not found ZK Binder with [' + String(n) + ']');
}
zkbind.$ = $;
function _fixCommandName(prefix: string, cmd: string, opts: zk.EventOptions, prop: string): void {
if (opts[prop]) {
var ignores = {};
ignores[prefix + cmd] = true;
opts[prop] = ignores;
}
}
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function encodeRFC5987ValueChars(str: string | string[]): string {
return encodeURIComponent(str).replace(/['()]/g, escape).replace(/\*/g, '%2A').replace(/%(?:7C|60|5E)/g, unescape);
}
//ZK-3133
function _matchMedia(event: MediaQueryListEvent | MediaQueryList, binder: Binder, value: string): void {
var cookies = binder._cookies!;
if (event.matches) {
var orient = '',
dpr = 1;
if (zk.mobile) {
if ((_initLandscape && _initDefault) || (!_initLandscape && !_initDefault))
_portrait = { '-90': true, '90': true };
orient = _portrait[window.orientation] ? 'portrait' : 'landscape';
} else {
orient = jq.innerWidth() > jq.innerHeight() ? 'landscape' : 'portrait';
}
if (window.devicePixelRatio)
dpr = window.devicePixelRatio;
// 16 is the length of string MATCHMEDIAVALUE_PREFIX in BinderCtrl.java
var ci = [new Date().getTimezoneOffset(), screen.width, screen.height, screen.colorDepth, jq.innerWidth(),
jq.innerHeight(), jq.innerX(), jq.innerY(), dpr.toFixed(1), orient, zk.mm.tz.guess(), event.matches, value.substring(16)];
// $ZKCLIENTINFO$ refers to CLIENT_INFO string in BinderCtrl.java
binder.command(value, { '$ZKCLIENTINFO$': ci });
if (!cookies.$contains(value)) cookies.push(value);
// eslint-disable-next-line @microsoft/sdl/no-cookies
document.cookie = 'ZKMatchMedia=' + encodeRFC5987ValueChars(cookies);
// eslint-disable-next-line @microsoft/sdl/no-cookies
document.cookie = 'ZKClientInfo=' + encodeRFC5987ValueChars(JSON.stringify(ci));
} else {
cookies.$remove(value);
// eslint-disable-next-line @microsoft/sdl/no-cookies
document.cookie = 'ZKMatchMedia=' + encodeRFC5987ValueChars(cookies);
}
}
export interface MediaQueryListWithHandler {
mql: MediaQueryList;
handler: (evt: MediaQueryListEvent) => void;
}
/**
* A data binder utile widget.
* @import _global_.File
* @since 8.0.0
*/
@zk.WrapClass('zkbind.Binder')
export class Binder extends zk.Object {
/** @internal */
_cookies?: string[];
/** @internal */
_lastcmd?: string;
/** @internal */
_aftercmd?: Record<string, CallableFunction[]>;
/** @internal */
_mediaQueryLists?: MediaQueryListWithHandler[];
/** @internal */
_processingAfterCommand?: boolean;
$view?: zk.Widget;
/** @internal */
_toDoUnAftercmd: Record<string, CallableFunction[]>;
$currentTarget?: object;
constructor(widget: zk.Widget, currentTarget: object) {
super(); // FIXME: params?
this.$view = widget;
this.$currentTarget = currentTarget;
this._aftercmd = {};
this._toDoUnAftercmd = {};
//ZK-3133
if (widget.$ZKMATCHMEDIA$) {
var cookies: string[] = [],
// eslint-disable-next-line @microsoft/sdl/no-cookies
matched = _zkMatchMediaRegexPattern.exec(document.cookie);
if (matched) {
var m = matched[1];
if (m) {
cookies = decodeURIComponent(m).trim().split(',');
}
}
this._cookies = cookies;
var binder = this,
mqls: MediaQueryListWithHandler[] = [];
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (var i = 0; i < widget.$ZKMATCHMEDIA$.length; i++) {
var media = widget.$ZKMATCHMEDIA$[i],
mql = window.matchMedia(media.substring(16)),
handler = (function (s) {
return function (event: MediaQueryListEvent | MediaQueryList) {
_matchMedia(event, binder, s);
};
})(media);
mql.addListener(handler);
handler(mql);
mqls.push({ mql: mql, handler: handler });
}
this._mediaQueryLists = mqls;
}
}
/**
* Registers a callback after some command executed.
* @param cmd - the name of the command
* @param fn - the function to execute
*/
after(cmd: string | CallableFunction, fn: CallableFunction): this {
if (!fn && jq.isFunction(cmd)) {
fn = cmd;
cmd = this._lastcmd!;
}
var ac = this._aftercmd![cmd as string];
if (!ac) this._aftercmd![cmd as string] = [fn];
else
ac.push(fn);
return this;
}
/**
* Unregisters a callback after some command executed.
* @param cmd - the name of the command
* @param fn - the function to execute
*/
unAfter(cmd: string, fn: CallableFunction): this {
var ac = this._aftercmd![cmd];
for (var j = ac ? ac.length : 0; j--;) {
if (ac[j] == fn) {
if (!this._processingAfterCommand)
ac.splice(j, 1);
else { // ZK-4482: queue unAfter if $doAfterCommand still processing
var tduac = this._toDoUnAftercmd[cmd];
if (!tduac) this._toDoUnAftercmd[cmd] = [fn];
else
tduac.push(fn);
}
}
}
return this;
}
/**
* Destroy this binder.
*/
destroy(): void {
this._aftercmd = undefined;
if (this._mediaQueryLists != null) {
var mqls = this._mediaQueryLists;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (var i = 0; i < mqls.length; i++) {
mqls[i].mql.removeListener(mqls[i].handler);
}
this._mediaQueryLists = undefined;
this._cookies = undefined;
}
this.$view = undefined;
this.$currentTarget = undefined;
}
/**
* Post a command to the binder
* @param cmd - the name of the command
* @param args - the arguments for this command. (the value should be json type)
* @param opts - a map of options to zk.Event, if any.
* @param timeout - the time (milliseconds) to wait before sending the request.
*/
command(cmd: string, args?: Record<string, unknown>, opts?: zk.EventOptions, timeout?: number): this {
var wgt = this.$view;
if (opts) {
if (opts.duplicateIgnore)
_fixCommandName('onBindCommand$', cmd, opts, 'duplicateIgnore');
if (opts.repeatIgnore)
_fixCommandName('onBindCommand$', cmd, opts, 'repeatIgnore');
}
zAu.send(new zk.Event(wgt, 'onBindCommand$' + cmd, { cmd: cmd, args: args }, zk.copy({ toServer: true }, opts)), timeout != undefined ? timeout : 38);
this._lastcmd = cmd;
return this;
}
/**
* Post a global command from the binder.
* @param cmd - the name of the command
* @param args - the arguments for this command. (the value should be json type)
* @param opts - a map of options to zk.Event, if any.
* @param timeout - the time (milliseconds) to wait before sending the request.
*/
globalCommand(cmd: string, args?: Record<string, unknown>, opts?: zk.EventOptions, timeout?: number): this {
var wgt = this.$view;
if (opts) {
if (opts.duplicateIgnore)
_fixCommandName('onBindGlobalCommand$', cmd, opts, 'duplicateIgnore');
if (opts.repeatIgnore)
_fixCommandName('onBindGlobalCommand$', cmd, opts, 'repeatIgnore');
}
zAu.send(new zk.Event(wgt, 'onBindGlobalCommand$' + cmd, { cmd: cmd, args: args }, zk.copy({ toServer: true }, opts)), timeout != undefined ? timeout : 38);
this._lastcmd = cmd;
return this;
}
$doAfterCommand(cmd: string, args?: unknown[]): void {
var ac = this._aftercmd![cmd],
tduac = this._toDoUnAftercmd[cmd];
this._processingAfterCommand = true; // ZK-4482
for (var i = 0, j = ac ? ac.length : 0; i < j; i++)
ac[i].bind(this)(args);
this._processingAfterCommand = false;
for (var i = 0, j = tduac ? tduac.length : 0; i < j; i++) { // ZK-4482: do unAfter
this.unAfter(cmd, tduac[i]);
}
this._toDoUnAftercmd[cmd] = [];
}
/**
* Post a upload command to the binder
* @param cmd - the name of the command
* @param file - the file to upload. (the value should be a file type)
* @since 9.0.1
*/
upload(cmd: string, file: File): void {
this.$view!.fire('onBindCommandUpload$' + cmd, { cmd: cmd }, { file: file });
}
/**
* Post a command to the binder from the give dom element.
* @param dom - the target of the dom element.
* @param command - the name of the command
* @param args - the arguments for this command. (the value should be json type)
* @param opt - a map of options to zk.Event, if any.
* @param timeout - the time (milliseconds) to wait before sending the request.
*/
static postCommand(dom: HTMLElement, command: string, args?: Record<string, unknown>, opt?: zk.EventOptions, timeout?: number): void {
var w = zk.Widget.$(dom);
if (w) {
var binder = w.$binder();
if (binder) {
binder.command(command, args, opt, timeout);
}
}
}
/**
* Post a global command from the binder of the give dom element.
* @param dom - the target of the dom element.
* @param command - the name of the command
* @param args - the arguments for this command. (the value should be json type)
* @param timeout - the time (milliseconds) to wait before sending the request.
*/
static postGlobalCommand(dom: HTMLElement, command: string, args?: Record<string, unknown>, opt?: zk.EventOptions, timeout?: number): void {
var w = zk.Widget.$(dom);
if (w) {
var binder = w.$binder();
if (binder) {
binder.globalCommand(command, args, opt, timeout);
}
}
}
}