import { async_each, async_getValueOf, async_next, async_waterfall, async_map, async_aggr, async_traverse, async_filter, async_toThenable } from '../utils/async' import { class_Dfr, is_ArrayLike } from 'atma-utils'; import { dfr_run } from '../utils/dfr'; import { each } from '../utils/arr'; import { Deferred } from '../types/Deferred'; import { SelectorsEx } from './SelectorsEx'; export class IQueryCtx { owner: IQuery self: IQuery source: string url: string status: number headers: { [key: string]: string} breadcrumbs: string[] = []; thener: (resolve, reject) => IQuery Ctor: new (mix?) => IQuery newSync(arr?: any, parent?: IQuery) { let query = new this.Ctor(arr); query.ctx.owner = parent ?? this.self; IQueryCtx.copyFrom(query.ctx, parent?.ctx); return query; } newAsync(arr?: any, parent?: IQuery) { let query = new this.Ctor(arr); query.ctx.owner = parent ?? this.self; (query as any).then = query.ctx.thener; IQueryCtx.copyFrom(query.ctx, parent?.ctx); return query; } static copyFrom (targetCtx: IQueryCtx, parentCtx: IQueryCtx) { if (parentCtx != null) { targetCtx.url = parentCtx.url; targetCtx.source = parentCtx.source; targetCtx.status = parentCtx.status; targetCtx.headers = parentCtx.headers; targetCtx.breadcrumbs = [ ... (parentCtx?.breadcrumbs ?? []) ]; } return targetCtx; } } export type IQuerySync = Omit, 'then' | 'resolve' | 'reject' | 'done' | 'fail'>; export type TSync> = Omit; export abstract class IQuery & { then: never } = any> extends class_Dfr implements PromiseLike { [index: number]: TElement; length: number = 0 ctx: IQueryCtx constructor(mix?) { super(); this.ctx = new IQueryCtx; this.ctx.self = this; this.ctx.thener = this.then; this.ctx.Ctor = this.constructor; this.ensureSync(); this.add(mix); if (mix != null) { this.resolve(this); } } get [Symbol.toStringTag]() { return 'IQuery'; } ensureSync(): IQuery { this.then = null; return this; } ensureAsync(): IQuery { if (this.then != null) { return this; } return this.ctx.newAsync(this, this); } resolve(...args) { if (args.length !== 0) { let x = args[0]; if (x != null && x.then != null && x instanceof IQuery) { let q = new x.ctx.Ctor(x); args[0] = q; } } return super.resolve(...args); } wait (ms: number) { return async_next(this, ($, source) => { return new Promise(resolve => { setTimeout(() => { resolve(this); }, ms); }); }); } require (opts?: { count?: number }) { const $ = this.ctx.newAsync(null, this); async_toThenable(this).then((ctx: IQuery) => { let expect = opts?.count ?? 1; if (ctx.length !== expect) { throw new Error(`Elements count missmatch. ${ this.ctx.breadcrumbs.join(', ') }. Expected: ${expect}. Got ${ctx.length}`); } $.resolve(ctx); }); return $; } //#region CssClass hasClass(name: string): PromiseLike { return Arr.mapFirst(this, node => this.hasClassFn(node, name)); } protected abstract hasClassFn(node: TElement, name: string): Deferred; addClass(name: string) { return Arr.mutate(this, node => this.addClassFn(node, name)); } protected abstract addClassFn(node: TElement, name: string): Deferred; removeClass(name: string): IQuery { return Arr.mutate(this, node => this.removeClassFn(node, name)); } protected abstract removeClassFn(node: TElement, name: string): Deferred; toggleClass(name: string): IQuery { return Arr.mutate(this, node => this.toggleClassFn(node, name)); } protected abstract toggleClassFn(node: TElement, name: string): Deferred; //#endregion CssClass //#region Collection add(mix) { if (mix == null) { return this; } if (is_ArrayLike(mix) === true) { return each(mix, this.add, this); } this[this.length++] = mix; return this; } eq(index: number) { return async_next(this, ($, source) => { if (index < source.length) { $.add(source[index]); } }); } get(index: number) { return this[index]; } slice(start: number = 0, end?: number) { return async_next(this, ($, source) => { var i = start; if (end > source.length) { end = source.length; } if (i < end) { for (; i < end; i++) { $.add(source[i]); } } }); } each(fn: (node: TElement, i?: number) => void | TElement | any) { return async_next(this, ($, source) => { return async_waterfall(source, (node, i) => { $.add(node); return fn(node, i); }) }); } map (fn: (node: IQuery, i?: number) => TResult) { return async_map(this, fn); } toArray(): Promise { return dfr_run(resolve => { this.ensureAsync().done($ => { var arr = Array.prototype.slice.call($); resolve(arr); }); }) } as (): T { let t: any; t = this; return t as T; } use IQuery> (Ctor: TCtor) { let proto = Ctor.prototype; while (proto != null && proto !== Object.prototype) { Object .getOwnPropertyNames(proto) .forEach(key => { if (key in IQuery.prototype) { return; } IQuery.prototype[key] = Ctor.prototype[key]; }); proto = Object.getPrototypeOf(proto); } return this as InstanceType; } //#endregion Collection //#region Content text(): PromiseLike; text(str: string): IQuery; text(str?: string) { if (typeof str === 'undefined') { return async_aggr('', this, (accum, node) => { return this.textGetFn(node).then(val => accum + val) }) as PromiseLike; } return >async_each(this, ($, node) => { return this .textSetFn(node, str) .then(() => { $.add(node) }); }); } protected abstract textGetFn(node: TElement): Deferred; protected abstract textSetFn(node: TElement, text: string): Deferred; html(): PromiseLike; html(str: string): IQuery; html(str?: string) { if (typeof str === 'undefined') { return async_aggr('', this, (accum, node) => { return this.htmlGetFn(node).then(val => accum + val) }) as PromiseLike; } return >async_each(this, ($, node) => { return this .htmlSetFn(node, str) .then(() => { $.add(node); }); }); } outerHtml(): PromiseLike { return async_aggr('', this, (accum, node) => { return this.htmlOuterGetFn(node).then(val => accum + val) }) as PromiseLike; } protected abstract htmlGetFn(node: TElement): Deferred; protected abstract htmlOuterGetFn(node: TElement): Deferred; protected abstract htmlSetFn(node: TElement, text: string): Deferred; append(html: string): IQuery { return Arr.mutate(this, node => { return this.appendFn(node, html) }); } protected abstract appendFn(node: TElement, html: string): Deferred; // not possible to manipulate IElements in node, which are not already attached to the Live DOM. // appendTo(selector: string): IQuery { // return Arr.mutate(this, async node => { // await this.appendToFn(selector, node); // }); // } // protected abstract appendToFn(selector: string, node: TElement): PromiseLike; prepend(html: string) { return Arr.mutate(this, node => this.prependFn(node, html)); } protected abstract prependFn(node: TElement, html: string): Deferred; before(html: string) { return Arr.mutate(this, node => this.beforeFn(node, html)); } protected abstract beforeFn(node: TElement, html: string): Deferred; after(html: string) { return Arr.mutate(this, node => this.afterFn(node, html)); } protected abstract afterFn(node: TElement, html: string): Deferred; //#endregion Content //#region Css css(cssObj: { [key: string]: any }): IQuery; css(key: string, val: any): IQuery; css(key: string): Promise; css(mix, val?) { if (arguments.length === 1 && typeof mix === 'string') { return Arr.mapFirst(this, node => this.cssGet(node, mix)); } let css = arguments.length === 1 ? mix : { [mix]: val }; return Arr.mutate(this, node => this.cssSet(node, css)); } protected abstract cssGet(node: TElement, prop: string): Promise protected abstract cssSet(node: TElement, css: { [key: string]: any }): Deferred height(): Promise; height(val): IQuery; height(val?) { if (val == null) { return Arr.mapFirst(this, node => this.heightGetFn(node)); } return this.css('height', val); } protected abstract heightGetFn(node: TElement): Promise innerHeight(): PromiseLike { return Arr.mapFirst(this, node => this.innerHeightFn(node)); } protected abstract innerHeightFn(node: TElement): Promise width(): Promise; width(val): IQuery width(val = null) { if (val == null) { return Arr.mapFirst(this, node => this.widthGetFn(node)); } return this.css('width', val); } protected abstract widthGetFn(node: TElement): Promise innerWidth(): PromiseLike { return Arr.mapFirst(this, node => this.innerWidthFn(node)); } protected abstract innerWidthFn(node: TElement): Promise offset(): Promise<{ top: number, left: number }> { return Arr.mapFirst(this, node => this.getBoundingClientRect(node)); } protected abstract getBoundingClientRect(node: TElement): Promise<{ top: number, left: number, width: number, height: number }> position(): Promise<{ top: number, left: number }> { return Arr.mapFirst(this, node => this.getPosition(node)); } protected abstract getPosition(node: TElement): Promise<{ top: number, left: number }>; scrollTop(): Promise; scrollTop(scroll: number): IQuery; scrollTop(mix?: number) { if (arguments.length === 0) { return Arr.mapFirst(this, node => this.scrollTopGetFn(node)); } return Arr.mutate(this, node => this.scrollTopSetFn(node, mix)); } protected abstract scrollTopGetFn(node: TElement): Promise; protected abstract scrollTopSetFn(node: TElement, scroll: number): Deferred; scrollLeft(): Promise; scrollLeft(scroll: number): IQuery; scrollLeft(mix?: number) { if (arguments.length === 0) { return Arr.mapFirst(this, node => this.scrollLeftGetFn(node)); } return Arr.mutate(this, node => this.scrollLeftSetFn(node, mix)); } protected abstract scrollLeftGetFn(node: TElement): Promise; protected abstract scrollLeftSetFn(node: TElement, scroll: number): Deferred; //#endregion //#region driver eval(mix: Function | string, ...args): Promise { return Arr.mapFirst(this, node => this.evalFn(node, mix, ...args)); } protected abstract evalFn(node: TElement, mix: Function | string, ...args): Promise //#endregion //#region Events click(): IQuery { return Arr.mutate(this, node => this.clickFn(node)); } protected abstract clickFn(node: TElement): Promise trigger(type, ...args): IQuery { return Arr.mutate(this, node => this.triggerFn(node, type, ...args)); } protected abstract triggerFn(node: TElement, type: string, ...args): Promise select(...args): IQuery { return Arr.mutate(this, node => this.selectFn(node, ...args)); } protected abstract selectFn(node: TElement, ...args): Promise focus(): IQuery { return Arr.mutate(this, node => this.focusFn(node)); } protected abstract focusFn(node: TElement): Promise blur(): IQuery { return Arr.mutate(this, node => this.blurFn(node)); } protected abstract blurFn(node: TElement): Promise sendKeys(mix: string): IQuery { return Arr.mutate(this, node => this.sendKeysFn(node, mix)); } protected abstract sendKeysFn(node: TElement, mix): Promise type(str: string): IQuery { return Arr.mutate(this, node => this.typeFn(node, str)); } protected abstract typeFn(node: TElement, str: string): Promise press(str: string): IQuery { return Arr.mutate(this, node => this.pressFn(node, str)); } protected abstract pressFn(node: TElement, str: string): Promise //#endregion //#region Manipulate remove(): IQuery { return Arr.mutate(this, node => this.removeFn(node)); } protected abstract removeFn(node: TElement): Promise //#endregion //#region Properties attr(name: string): Promise attr(vals: { [key: string]: any }): IQuery attr(key: string, val: any): IQuery attr(mix, val?) { if (arguments.length === 1 && typeof mix === 'string') { return Arr.mapFirst(this, node => this.attrGetFn(node, mix)); } let attr = arguments.length === 2 ? { [mix]: val } : mix; return Arr.mutate(this, node => this.attrSetFn(node, attr)) } protected abstract attrGetFn(node: TElement, prop: string): Promise protected abstract attrSetFn(node: TElement, attr: { [key: string]: any }): Deferred val(): PromiseLike val(val: any): IQuery val(val?) { if (arguments.length === 0) { return Arr.mapFirst(this, node => this.valGetFn(node)) } return Arr.mutate(this, node => this.valSetFn(node, val)); } protected abstract valGetFn(node: TElement): Promise protected abstract valSetFn(node: TElement, value: any): Deferred data(key: string): PromiseLike; data(key: string, val: any): IQuery data(dataObj: { [key: string]: any }): IQuery data(mix, val?) { if (arguments.length === 1 && typeof mix === 'string') { return Arr.mapFirst(this, node => this.dataGetFn(node, mix)); } let data = arguments.length === 2 ? { [mix]: val } : mix; return Arr.mutate(this, node => this.dataSetFn(node, data)); } protected abstract dataGetFn(node: TElement, key: string): Promise protected abstract dataSetFn(node: TElement, data: object): Deferred prop(key: string): PromiseLike; prop(key: string, val: any): IQuery prop(obj: { [key: string]: any }): IQuery prop(mix, val?) { if (arguments.length === 1 && typeof mix === 'string') { return Arr.mapFirst(this, node => this.getField(node, mix)); } let obj = arguments.length === 2 ? { [mix]: val } : mix; return Arr.mutate(this, node => this.setField(node, obj)); } protected abstract propGetFn(node: TElement, key: string): Promise protected abstract propSetFn(node: TElement, data: object): Deferred //#endregion //#region Traverse find(sel: string): IQuery { return SelectorsEx.find(this, sel, (el, sel) => { return async_traverse(el, (node: TElement) => this.findFn(node, sel)); }); } protected abstract findFn(node: TElement, selector: string): Deferred filter(fn: (node: TElement) => boolean | Promise): IQuery; filter(sel: string): IQuery; filter(mix): IQuery { if (typeof mix === 'string') { let selector = mix; return async_filter(this, $single => this.matchesFn($single[0], selector)); } let fn = mix; return async_filter(this, fn); } protected abstract matchesFn(node: TElement, selector: string): Promise; parent(): IQuery { return async_traverse(this, node => { return this.parentFn(node); }); } protected abstract parentFn(node: TElement): Promise; closest(sel: string): IQuery { return async_traverse(this, node => { return this.closestFn(node, sel); }); } protected abstract closestFn(node: TElement, sel: string): Promise; children(sel?: string): IQuery { return async_traverse(this, node => { return this.childrenFn(node, sel); }); } protected abstract childrenFn(node: TElement, sel?: string): Promise; next(sel?: string): IQuery { return async_traverse(this, node => { return this.nextFn(node, sel); }); } protected abstract nextFn(node: TElement, sel?: string): Promise; //#endregion protected abstract getField(node: TElement, field: string): Deferred; protected abstract setField(node: TElement, obj: any): Deferred; protected abstract setField(node: TElement, field: string, val: any): Deferred; protected abstract callField(node: TElement, field: string, ...args): Deferred; on(type: string, cb: (el: TElement) => void) { return async_each(this, (ctx, node) => this._onFn(node, type, cb)); } off(type: string, cb: Function) { return async_each(this, (ctx, node) => this._offFn(node, type, cb)); } once(type: string, cb: (event: any) => void = null) { return async_each(this, (ctx, node) => this._onOnceFn(node, type, cb)); } protected abstract _onFn(node: TElement, type: string, cb: Function): Promise protected abstract _onOnceFn(node: TElement, type: string, cb: Function): Promise protected abstract _offFn(node: TElement, type: string, cb: Function): Promise abstract waitForElement (selector: string, mix?: IQueryWaitOptions | IQueryConditionFn): IQuery } namespace Arr { export function mutate(self: IQuery, mutator: (node: TElement) => Promise): IQuery { return >async_each(self, ($, node) => { $.add(node); return mutator(node); }); } export function mapFirst(self: IQuery, map: (node: TElement) => Promise): Promise { return async_getValueOf(0, self, map); } } export interface IQueryConditionFn { ($: IQuery): Promise } export interface IQueryWaitOptions { hidden?: boolean visible?: boolean check?: IQueryConditionFn interval?: number timeout?: number /** execute any other code while waiting. */ tick?: () => Promise }