diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8933dad..1b23d55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,9 +11,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install + - name: Corepack run: | + npm install --global corepack@latest corepack enable + + - name: Install + run: | make nodejs - name: Run tests diff --git a/CHANGES.rst b/CHANGES.rst index e3bb961..00cf004 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,34 @@ Changes ======= +2.0.0 (unreleased) +------------------ + +- Split ``ajax.js`` into ``ssr`` js module. + [lenadax] + +- Rework ``Spinner`` class to prevent persisting detached elements. + [lenadax] + +- Unbind events on Motion ``reset_state`` method. + [lenadax] + +- Unbind events on ``destroy`` method in ``create_listener`` subclass factory. + [lenadax] + +- Move ``AjaxDestroy`` to ``ajaxdestroy`` module to prevent circular import dependency. + [lenadax] + +- Fix widgets not being fully destroyed if rendered within an ``Overlay`` inside body. + [lenadax] + +- Modify ``AjaxDestroy`` parse method to prevent DOM memory leaks. + [lenadax] + +- Replace svg spinner with Bootstrap5 spinner. + [lenadax] + + 1.0.0 (unreleased) ------------------ diff --git a/pyproject.toml b/pyproject.toml index 76381c1..de15537 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "treibstoff" -version = "1.0.0.dev0" +version = "2.0.0.dev0" description = "" dynamic = ["readme"] requires-python = ">=3.10" diff --git a/src/ajax.js b/src/ajax.js deleted file mode 100644 index 40f4b59..0000000 --- a/src/ajax.js +++ /dev/null @@ -1,1145 +0,0 @@ -import $ from 'jquery'; -import { - compile_template, - Parser -} from './parser.js'; -import { - get_overlay, - Overlay, - show_dialog, - show_error, - show_info, - show_message, - show_warning -} from './overlay.js'; -import { - deprecate, - parse_path, - parse_query, - parse_url, - set_default, - uuid4 -} from './utils.js'; -import {Events} from './events.js'; -import { - HTTPRequest, - http_request -} from './request.js'; -import {spinner} from './spinner.js'; - -/** - * Ajax utility mixin. - */ -export class AjaxUtil extends Events { - - /** - * Parse URL, query and path from URL string:: - * - * >> ts.ajax.parse_target('http://tld.com/some/path?param=value'); - * -> { - * url: 'http://tld.com/some/path', - * params: { param: 'value' }, - * path: '/some/path', - * query: '?param=value' - * } - * - * @param {string} target - URL string to parse. - * @returns {Object} Containing ``url``, ``params``, ``path`` and ``query``. - */ - parse_target(target) { - return { - url: target ? parse_url(target) : undefined, - params: target ? parse_query(target) : {}, - path: target ? parse_path(target) : undefined, - query: target ? parse_query(target, true) : undefined - }; - } - - /** - * Parse ajax operation definition from string into array. - * - * XXX: Fails if spaces in selector. Fix. - * - * @param {string} val - Definition string to parse. - * @returns {Array} Containing operation definitions. - */ - parse_definition(val) { - return val.replace(/\s+/g, ' ').split(' '); - } - - /** - * Get ajax target for action. - * - * Lookup ``ajaxtarget`` on event, fall back to ``ajax:target`` attribute - * on elem. - * - * @param {$} elem - jQuery wrapped DOM element. - * @param {$.Event} evt - jQuery event. - * @returns {Object} Target for event. - */ - action_target(elem, evt) { - if (evt.ajaxtarget) { - return evt.ajaxtarget; - } - return this.parse_target(elem.attr('ajax:target')); - } -} - -/** - * Abstract Ajax operation. - */ -export class AjaxOperation extends AjaxUtil { - - /** - * Create Ajax operation. - * - * Binds this operation to given dispatcher event. - * - * @param {Object} opts - Ajax operation options. - * @param {AjaxDispatcher} opts.dispatcher - Dispatcher instance. - * @param {string} opts.event - AjaxDispatecher event to bind. - */ - constructor(opts) { - super(); - this.event = opts.event; - this.dispatcher = opts.dispatcher; - this.dispatcher.on(this.event, this.handle.bind(this)); - } - - /** - * Execute operation as JavaScript API. - * - * @abstract - * @param {Object} opts - Options needed for operation execution. - */ - execute(opts) { - throw 'Abstract AjaxOperation does not implement execute'; - } - - /** - * Handle operation from dispatcher. - * - * @abstract - * @param {AjaxDispatcher} inst - Dispatcher instance. - * @param {Object} opts - Options needed for operation execution. - */ - handle(inst, opts) { - throw 'Abstract AjaxOperation does not implement handle'; - } -} - -/** - * Handle for path operation. - */ -export class AjaxPath extends AjaxOperation { - - constructor(opts) { - opts.event = 'on_path'; - super(opts); - this.win = opts.win; - $(this.win).on('popstate', this.state_handle.bind(this)); - } - - execute(opts) { - let history = this.win.history; - if (history.pushState === undefined) { - return; - } - let path = opts.path.charAt(0) !== '/' ? `/${opts.path}` : opts.path; - set_default(opts, 'target', this.win.location.origin + path); - set_default(opts, 'replace', false); - let replace = opts.replace; - // delete options which should not end up in state - delete opts.path; - delete opts.replace; - opts._t_ajax = true; - if (replace) { - history.replaceState(opts, '', path); - } else { - history.pushState(opts, '', path); - } - } - - state_handle(evt) { - let state = evt.originalEvent.state; - if (!state) { - return; - } - if (!state._t_ajax) { - return; - } - evt.preventDefault(); - let target; - if (state.target.url) { - target = state.target; - } else { - target = this.parse_target(state.target); - } - target.params.popstate = '1'; - if (state.action) { - this.dispatcher.trigger('on_action', { - target: target, - action: state.action - }); - } - if (state.event) { - this.dispatcher.trigger('on_event', { - target: target, - event: state.event - }); - } - if (state.overlay) { - this.dispatcher.trigger('on_overlay', { - target: target, - overlay: state.overlay, - css: state.overlay_css, - uid: state.overlay_uid, - title: state.overlay_title - }); - } - if (!state.action && !state.event && !state.overlay) { - this.win.location = target.url; - } - } - - handle(inst, opts) { - let elem = opts.elem, - evt = opts.event, - path = elem.attr('ajax:path'); - if (path === 'href') { - let href = elem.attr('href'); - path = parse_path(href, true); - } else if (path === 'target') { - let tgt = this.action_target(elem, evt); - path = tgt.path + tgt.query; - } - let target; - if (this.has_attr(elem, 'ajax:path-target')) { - let path_target = elem.attr('ajax:path-target'); - if (path_target) { - target = this.parse_target(path_target); - } - } else { - target = this.action_target(elem, evt); - } - let p_opts = { - path: path, - target: target - } - p_opts.action = this.attr_val(elem, 'ajax:path-action', 'ajax:action'); - p_opts.event = this.attr_val(elem, 'ajax:path-event', 'ajax:event'); - p_opts.overlay = this.attr_val(elem, 'ajax:path-overlay', 'ajax:overlay'); - if (p_opts.overlay) { - p_opts.overlay_css = this.attr_val( - elem, - 'ajax:path-overlay-css', - 'ajax:overlay-css' - ); - p_opts.overlay_uid = this.attr_val( - elem, - 'ajax:path-overlay-uid', - 'ajax:overlay-uid' - ); - p_opts.overlay_title = this.attr_val( - elem, - 'ajax:path-overlay-title', - 'ajax:overlay-title' - ); - } - this.execute(p_opts); - } - - has_attr(elem, name) { - let val = elem.attr(name); - // In some browsers val is undefined, in others it's false. - return val !== undefined && val !== false; - } - - attr_val(elem, name, fallback) { - if (this.has_attr(elem, name)) { - return elem.attr(name); - } else { - return elem.attr(fallback); - } - } -} - -/** - * Handle for action operation. - */ -export class AjaxAction extends AjaxOperation { - - constructor(opts) { - set_default(opts, 'event', 'on_action'); - super(opts); - this.spinner = opts.spinner; - this._handle = opts.handle; - this._request = opts.request; - } - - execute(opts) { - opts.success = this.complete.bind(this); - this.request(opts); - } - - request(opts) { - opts.params['ajax.action'] = opts.name; - opts.params['ajax.mode'] = opts.mode; - opts.params['ajax.selector'] = opts.selector; - this._request.execute({ - url: parse_url(opts.url) + '/ajaxaction', - type: 'json', - params: opts.params, - success: opts.success - }); - } - - complete(data) { - if (!data) { - show_error('Empty Response'); - this.spinner.hide(); - } else { - this._handle.update(data); - this._handle.next(data.continuation); - } - } - - handle(inst, opts) { - let target = opts.target, - action = opts.action; - for (let action_ of this.parse_definition(action)) { - let defs = action_.split(':'); - this.execute({ - name: defs[0], - selector: defs[1], - mode: defs[2], - url: target.url, - params: target.params - }); - } - } -} - -/** - * Handle for event operation. - */ -export class AjaxEvent extends AjaxOperation { - - constructor(opts) { - opts.event = 'on_event'; - super(opts); - } - - execute(opts) { - let create_event = this.create_event.bind(this); - $(opts.selector).each(function() { - $(this).trigger(create_event(opts.name, opts.target, opts.data)); - }); - } - - create_event(name, target, data) { - let evt = $.Event(name); - if (target.url) { - evt.ajaxtarget = target; - } else { - evt.ajaxtarget = this.parse_target(target); - } - evt.ajaxdata = data; - return evt; - } - - handle(inst, opts) { - let target = opts.target, - event = opts.event; - for (let event_ of this.parse_definition(event)) { - let def = event_.split(':'); - this.execute({ - name: def[0], - selector: def[1], - target: target - }); - } - } -} - -/** - * Handle for overlay operation. - */ -export class AjaxOverlay extends AjaxAction { - - constructor(opts) { - opts.event = 'on_overlay'; - super(opts); - this.overlay_content_sel = '.modal-body'; - } - - execute(opts) { - let ol; - if (opts.close) { - ol = get_overlay(opts.uid); - if (ol) { - ol.close(); - } - return ol; - } - let url, params; - if (opts.target) { - let target = opts.target; - if (!target.url) { - target = this.parse_target(target); - } - url = target.url; - params = target.params; - } else { - url = opts.url; - params = opts.params; - } - let uid = opts.uid ? opts.uid : uuid4(); - params['ajax.overlay-uid'] = uid; - ol = new Overlay({ - uid: uid, - css: opts.css, - title: opts.title, - on_close: opts.on_close - }) - this.request({ - name: opts.action, - selector: `#${uid} ${this.overlay_content_sel}`, - mode: 'inner', - url: url, - params: params, - success: function(data) { - // overlays are not displayed if no payload is received. - if (!data.payload) { - // ensure continuation gets performed anyway. - this.complete(data); - return; - } - ol.open(); - this.complete(data); - }.bind(this) - }); - return ol; - } - - handle(inst, opts) { - let target = opts.target, - overlay = opts.overlay; - if (overlay.indexOf('CLOSE') > -1) { - this.execute({ - close: true, - uid: overlay.indexOf(':') > -1 ? overlay.split(':')[1] : opts.uid - }); - return; - } - this.execute({ - action: overlay, - url: target.url, - params: target.params, - css: opts.css, - uid: opts.uid, - title: opts.title - }); - } -} - -/** - * Handle for Ajax form. - */ -export class AjaxForm { - - constructor(opts) { - this.handle = opts.handle; - this.spinner = opts.spinner; - this.afr = null; - } - - bind(form) { - if (!this.afr) { - compile_template(this, ` - - `, $('body')); - } - $(form) - .append('') - .attr('target', 'ajaxformresponse') - .off() - .on('submit', function(event) { - this.spinner.show(); - }.bind(this)); - } - - render(opts) { - this.spinner.hide(); - if (!opts.error) { - this.afr.remove(); - this.afr = null; - } - if (opts.payload) { - this.handle.update(opts); - } - this.handle.next(opts.next); - } -} - -/** - * DOM event handle for elements defining Ajax operations. - */ -export class AjaxDispatcher extends AjaxUtil { - - bind(node, evts) { - $(node).off(evts).on(evts, this.dispatch_handle.bind(this)); - } - - dispatch_handle(evt) { - evt.preventDefault(); - evt.stopPropagation(); - let elem = $(evt.currentTarget), - opts = { - elem: elem, - event: evt - }; - if (elem.attr('ajax:confirm')) { - show_dialog({ - message: elem.attr('ajax:confirm'), - on_confirm: function(inst) { - this.dispatch(opts); - }.bind(this) - }); - } else { - this.dispatch(opts); - } - } - - dispatch(opts) { - let elem = opts.elem, - event = opts.event; - if (elem.attr('ajax:action')) { - this.trigger('on_action', { - target: this.action_target(elem, event), - action: elem.attr('ajax:action') - }); - } - if (elem.attr('ajax:event')) { - this.trigger('on_event', { - target: elem.attr('ajax:target'), - event: elem.attr('ajax:event') - }); - } - if (elem.attr('ajax:overlay')) { - this.trigger('on_overlay', { - target: this.action_target(elem, event), - overlay: elem.attr('ajax:overlay'), - css: elem.attr('ajax:overlay-css'), - uid: elem.attr('ajax:overlay-uid'), - title: elem.attr('ajax:overlay-title') - }); - } - if (elem.attr('ajax:path')) { - this.trigger('on_path', { - elem: elem, - event: event - }); - } - } -} - -/** - * DOM parser for destroying JavaScript instances attached to DOM elements - * going to be removed. - */ -export class AjaxDestroy extends Parser { - - parse(node) { - let instances = node._ajax_attached; - if (instances !== undefined) { - for (let instance of instances) { - if (instance.destroy !== undefined) { - instance.destroy(); - } - } - } - } -} - -/** - * Handle for DOM manipulation and Ajax continuation operations. - */ -export class AjaxHandle extends AjaxUtil { - - constructor(ajax) { - super(); - this.ajax = ajax; - this.spinner = ajax.spinner; - } - - destroy(context) { - let parser = new AjaxDestroy(); - context.each(function() { - parser.walk(this); - }); - } - - update(opts) { - let payload = opts.payload, - selector = opts.selector, - mode = opts.mode, - context; - if (mode === 'replace') { - let old_context = $(selector); - this.destroy(old_context); - old_context.replaceWith(payload); - context = $(selector); - if (context.length) { - this.ajax.bind(context.parent()); - } else { - this.ajax.bind($(document)); - } - } else if (mode === 'inner') { - context = $(selector); - this.destroy(context.children()); - context.html(payload); - this.ajax.bind(context); - } - } - - next(operations) { - if (!operations || !operations.length) { - return; - } - this.spinner.hide(); - for (let op of operations) { - let type = op.type; - delete op.type; - if (type === 'path') { - this.ajax.path(op); - } else if (type === 'action') { - let target = this.parse_target(op.target); - op.url = target.url; - op.params = target.params; - this.ajax.action(op); - } else if (type === 'event') { - this.ajax.trigger(op); - } else if (type === 'overlay') { - let target = this.parse_target(op.target); - op.url = target.url; - op.params = target.params; - this.ajax.overlay(op); - } else if (type === 'message') { - // if flavor given, message rendered in overlay - if (op.flavor) { - show_message({ - message: op.payload, - flavor: op.flavor - }); - // no overlay message, set message payload at selector - } else { - $(op.selector).html(op.payload); - } - } - } - } -} - -/** - * DOM Parser for binding Ajax operations. - */ -export class AjaxParser extends Parser { - - constructor(opts) { - super(); - this.dispatcher = opts.dispatcher; - this.form = opts.form; - } - - parse(node) { - let attrs = this.node_attrs(node); - if (attrs['ajax:bind'] && ( - attrs['ajax:action'] || - attrs['ajax:event'] || - attrs['ajax:overlay'])) { - let evts = attrs['ajax:bind']; - this.dispatcher.bind(node, evts); - } - if (attrs['ajax:form']) { - this.form.bind(node); - } - if (node.tagName.toLowerCase() === 'form') { - if (node.className.split(' ').includes('ajax')) { - this.form.bind(node); - } - } - } -} - -/** - * Ajax singleton. - */ -export class Ajax extends AjaxUtil { - - constructor(win=window) { - super(); - this.win = win; - this.binders = {}; - let spinner_ = this.spinner = spinner; - let dispatcher = this.dispatcher = new AjaxDispatcher(); - let request = this._request = new HTTPRequest({win: win}); - this._path = new AjaxPath({dispatcher: dispatcher, win: win}); - this._event = new AjaxEvent({dispatcher: dispatcher}); - let handle = new AjaxHandle(this); - let action_opts = { - dispatcher: dispatcher, - win: win, - handle: handle, - spinner: spinner_, - request: request - } - this._action = new AjaxAction(action_opts); - this._overlay = new AjaxOverlay(action_opts); - this._form = new AjaxForm({handle: handle, spinner: spinner_}); - this._is_bound = false; - } - - /** - * Register binder callback function. - * - * Integration of custom JavaScript to the binding mechanism is done via - * this function. The register function takes a callback function and a - * boolean flag whether to immediately execute the callback as arguments. - * - * The passed binder callback gets called every time when markup is changed - * by this object and gets passed the changed DOM part as ``context``:: - * - * $(function() { - * ts.ajax.register(function(context) { - * $('.sel', context).on('click', function() {}); - * }, true); - * }); - * - * @param {function} func - Binder callback. - * @param {boolean} instant - Flag whether to execute binder callback - * immediately at registration time. - */ - register(func, instant) { - let func_name = 'binder_' + uuid4(); - while (true) { - if (this.binders[func_name] === undefined) { - break; - } - func_name = 'binder_' + uuid4(); - } - this.binders[func_name] = func; - // Only execute instant if ajax.bind() already has been initially - // called via document ready event. Otherwise the binder functions - // would be called twice on page load. This can happen if - // ``ajax.register`` gets called in a document ready event handler - // which is registered before treibstoff's document ready handler. - if (instant && this._is_bound) { - func(); - } - } - - /** - * Bind Ajax operations. - * - * Parses given piece of DOM for Ajax operation related attributes and binds - * the Ajax dispatcher. - * - * Additionally calls all registered binder functions for given context. - * - * @param {$} context - jQuery wrapped piece of DOM. - * @returns {$} The given jQuery wrapped context. - */ - bind(context) { - this._is_bound = true; - let parser = new AjaxParser({ - dispatcher: this.dispatcher, - form: this._form - }); - context.each(function() { - parser.walk(this); - }); - for (let func_name in this.binders) { - try { - this.binders[func_name](context) - } catch (err) { - console.log(err); - } - } - return context; - } - - /** - * Attach JavaScript instance to DOM element. - * - * ``destroy`` function of attached instance gets called when DOM element - * is removed. - * - * This is the supposed mechanism if a user needs to gracefully destruct - * things when DOM parts get removed. - * - * Attaching of instances is normally done inside a binder function or a - * subsequent operation of it. - * - * A best practice pattern look like so:: - * - * class Widget { - * - * static initialize(context) { - * $('.sel', context).each(function() { - * new Widget($(this)); - * }); - * } - * - * constructor(elem) { - * ts.ajax.attach(this, elem); - * } - * - * destroy() { - * // graceful destruction goes here - * } - * } - * - * $(function() { - * ts.ajax.register(Widget.initialize, true); - * }); - * - * @param {Object} instance - Arbitrary JavaScript object instance. - * @param {HTMLElement|$} elem - DOM element to attach instance to. - */ - attach(instance, elem) { - if (elem instanceof $) { - if (elem.length != 1) { - throw 'Instance can be attached to exactly one DOM element'; - } - elem = elem[0]; - } - if (elem._ajax_attached === undefined) { - elem._ajax_attached = []; - } - elem._ajax_attached.push(instance); - } - - /** - * Execute path operation. - * - * Wites browser session history stack. Executes Ajax operations on - * window popstate event. - * - * Add an entry to the browser history:: - * - * ts.ajax.path({ - * path: '/some/path', - * target: 'http://tld.com/some/path', - * action: 'layout:#layout:replace', - * event: 'contextchanged:#layout', - * overlay: 'actionname', - * overlay_css: 'additional-overlay-css-class' - * }); - * - * If ``replace`` option is given, browser history gets reset:: - * - * ts.ajax.path({ - * path: '/some/path', - * target: 'http://example.com/some/path', - * action: 'layout:#layout:replace', - * replace: true - * }); - * - * @param {Object} opts - Path options. - * @param {string} opts.path - The path to write to the address bar. - * @param {string} opts.target - Related target URL. - * @param {string} opts.action - Ajax action to perform. - * @param {string} opts.event - Ajax event to trigger. - * @param {string} opts.overlay - Ajax overlay to display. - * @param {string} opts.overlay_css - CSS class to add to ajax overlay. - * @param {boolean} opts.replace - Flag whether to reset browser history. - */ - path(opts) { - this._path.execute(opts); - } - - /** - * Execute action operation. - * - * Requests ``ajaxaction`` on server and modifies DOM with response - * according to mode and selector:: - * - * let target = ts.ajax.parse_target('http://tld.com/some/path?param=value'); - * ts.ajax.action({ - * name: 'content', - * selector: '#content', - * mode: 'inner', - * url: target.url, - * params: target.params - * }); - * - * @param {Object} opts - Ajax options. - * @param {string} opts.name - Action name. - * @param {string} opts.selector - CSS selector of DOM element to modify - * with response payload. - * @param {string} opts.mode - Mode for manipulation. Either ``inner`` or - * ``replace``. - * @param {string} opts.url - URL on which ``ajaxaction`` gets requested. - * @param {Object} opts.params - Query parameters. - */ - action(opts) { - this._action.execute(opts); - } - - /** - * Execute event operation. - * - * Creates an event providing ``ajaxtarget`` and ``ajaxdata`` properties - * and trigger it on DOM elements by selector. - * - * The ``ajaxtarget`` property on the event instance is an object containing - * ``url`` and ``params`` properties, as returned by ``Ajax.parse_target``:: - * - * let url = 'http://tls.com?param=value'; - * ts.ajax.trigger({ - * name: 'contextchanged', - * selector: '.contextsensitiv', - * target: ts.ajax.parse_target(url); - * }); - * - * If given target is a URL string, it gets automatically parsed by the - * trigger function:: - * - * ts.ajax.trigger({ - * name: 'contextchanged', - * selector: '.contextsensitiv', - * target: 'http://tls.com?param=value' - * }); - * - * Optionally a ``data`` option can be passed, which gets set at the - * ``ajaxdata`` attribute of the event:: - * - * ts.ajax.trigger({ - * name: 'contextchanged', - * selector: '.contextsensitiv', - * target: 'http://tld.com?param=value', - * data: {key: 'val'} - * }); - * - * **Note** - For B/C reasons, ``trigger`` can be called with positional - * arguments (name, selector, target, data). This behavior is deprecated - * and will be removed in future versions. - * - * @param {Object} opts - Event options. - * @param {string} opts.name - Event name. - * @param {string} opts.selector - CSS selector of DOM elements on which to - * trigger events on. - * @param {string|Object} opts.target - Event target. Gets set as - * ``ajaxtarget`` property on event instance. - * @param {*} opts.data - Optional event data. Gets set as - * ``ajaxdata`` property on event instance. - */ - trigger(opts) { - if (arguments.length > 1) { - deprecate('Calling Ajax.event with positional arguments', 'opts', '1.0'); - opts = { - name: arguments[0], - selector: arguments[1], - target: arguments[2], - data: arguments[3] - } - } - this._event.execute(opts); - } - - /** - * Execute overlay operation. - * - * Load action result into an overlay. - * - * Display action operation in overlay. Contents of the ``title`` option - * gets displayed in the overlay header:: - * - * ts.ajax.overlay({ - * action: 'actionname', - * url: 'https://tld.com', - * params: {param: 'value'}, - * title: 'Overlay Title' - * }); - * - * Optional to ``url`` and ``params``, ``target`` can be passed as option. - * If both ``target`` and ``url``/``params`` given, ``target`` takes - * precedence:: - * - * ts.ajax.overlay({ - * action: 'actionname', - * target: 'https://tld.com?param=value' - * }); - * - * If ``css`` option is given, it gets set on overlay DOM element. This - * way it's possible to add custom styles for a specific overlay:: - * - * ts.ajax.overlay({ - * action: 'actionname', - * target: 'https://tld.com?param=value', - * css: 'some-class' - * }); - * - * Overlays get a generated UID by default for later reference which gets - * passed as ``ajax:overlay-uid`` request parameter to the server. - * ``Ajax.overlay`` returns the overlay instance, from which this uid - * can be read:: - * - * let overlay = ts.ajax.overlay({ - * action: 'actionname', - * target: 'https://tld.com?param=value' - * }); - * let uid = overlay.uid; - * - * Already open ajax overlays can be closed by passing the ``close`` option - * and the overlay ``uid``:: - * - * ts.ajax.overlay({ - * close: true, - * uid: uid - * }); - * - * A callback can be provided when overlay gets closed by passing it as - * ``on_close`` option:: - * - * ts.ajax.overlay({ - * action: 'actionname', - * target: 'http://foobar.org?param=value', - * on_close: function(inst) { - * // inst is the overlay instance. - * } - * }); - * - * @param {Object} opts - Overlay options. - * @param {string} opts.action - Ajax action name. - * @param {string} opts.url - URL on which ``ajaxaction`` gets requested. - * @param {Object} opts.params - Query parameters. - * @param {string|Object} opts.target - Optional action target. Takes - * precedence over ``url`` and ``params``. - * @param {string} opts.title - Title to display in overlay header. - * @param {string} opts.css - CSS class to add to overlay DOM element. - * @param {string} opts.uid - The overlay UID. - * @param {boolean} opts.close - Flag whether to close an open overlay. - * @returns {Overlay} Overlay instance. - */ - overlay(opts) { - return this._overlay.execute(opts); - } - - /** - * Render ajax form after form processing. - * - * Gets called from hidden form iframe when response returns. See server - * integration documentation for details. - * - * @param {Object} opts - Form options. - * @param {HTMLElement} opts.payload - The rendered form. - * @param {string} opts.selector - CSS selector of the form. - * @param {string} opts.mode - DOM manipulation mode. - * @param {Array} opts.next - Continuation operation definitions. - * @param {boolean} opts.error - A flag whether an error occured while - * processing the form. The error flag not means a validation error but - * an exception happened and is needed for proper application - * state handling. - */ - form(opts) { - this._form.render(opts); - } - - /** - * This function is deprecated. Use ``ts.parse_url`` instead. - */ - parseurl(url) { - deprecate('ts.ajax.parseurl', 'ts.parse_url', '1.0'); - return parse_url(url); - } - - /** - * This function is deprecated. Use ``ts.parse_query`` instead. - */ - parsequery(url, as_string) { - deprecate('ts.ajax.parsequery', 'ts.parse_query', '1.0'); - return parse_query(url, as_string); - } - - /** - * This function is deprecated. Use ``ts.parse_path`` instead. - */ - parsepath(url, include_query) { - deprecate('ts.ajax.parsepath', 'ts.parse_path', '1.0'); - return parse_path(url, include_query); - } - - /** - * This function is deprecated. Use ``ts.ajax.parse_target`` instead. - */ - parsetarget(target) { - deprecate('ts.ajax.parsetarget', 'ts.ajax.parse_target', '1.0'); - return this.parse_target(target); - } - - /** - * This function is deprecated. Use ``ts.show_message`` instead. - */ - message(message, flavor='') { - deprecate('ts.ajax.message', 'ts.show_message', '1.0'); - show_message({message: message, flavor: flavor}); - } - - /** - * This function is deprecated. Use ``ts.show_info`` instead. - */ - info(message) { - deprecate('ts.ajax.info', 'ts.show_info', '1.0'); - show_info(message); - } - - /** - * This function is deprecated. Use ``ts.show_warning`` instead. - */ - warning(message) { - deprecate('ts.ajax.warning', 'ts.show_warning', '1.0'); - show_warning(message); - } - - /** - * This function is deprecated. Use ``ts.show_error`` instead. - */ - error(message) { - deprecate('ts.ajax.error', 'ts.show_error', '1.0'); - show_error(message); - } - - /** - * This function is deprecated. Use ``ts.show_dialog`` instead. - */ - dialog(opts, callback) { - deprecate('ts.ajax.dialog', 'ts.show_dialog', '1.0'); - show_dialog({ - message: opts.message, - on_confirm: function() { - callback(opts); - } - }); - } - - /** - * This function is deprecated. Use ``ts.http_request`` instead. - */ - request(opts) { - deprecate('ts.ajax.request', 'ts.http_request', '1.0'); - http_request(opts); - } -} - -let ajax = new Ajax(); -export {ajax}; - -$.fn.tsajax = function() { - ajax.bind(this); - return this; -} diff --git a/src/bootstrap.js b/src/bootstrap.js new file mode 100644 index 0000000..1459537 --- /dev/null +++ b/src/bootstrap.js @@ -0,0 +1,21 @@ +import $ from 'jquery'; +import {register_ajax_destroy_handle} from "./ssr/destroy.js"; + +function destroy_bootstrap(node) { + let dd = window.bootstrap.Dropdown.getInstance(node); + let tt = window.bootstrap.Tooltip.getInstance(node); + if (dd) { + dd.dispose(); + } + if (tt) { + tt.dispose(); + } + dd = null; + tt = null; +} + +$(function() { + if (window.bootstrap !== undefined) { + register_ajax_destroy_handle(destroy_bootstrap); + } +}); diff --git a/src/bundle.js b/src/bundle.js index 9ceebba..30ea307 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -1,7 +1,19 @@ import $ from 'jquery'; -import {ajax} from './ajax.js'; +import {ajax} from './ssr/ajax.js'; -export * from './ajax.js'; +export * from './ssr/ajax.js'; +export * from './ssr/action.js'; +export * from './ssr/destroy.js'; +export * from './ssr/dispatcher.js'; +export * from './ssr/event.js'; +export * from './ssr/form.js'; +export * from './ssr/handle.js'; +export * from './ssr/overlay.js'; +export * from './ssr/parser.js'; +export * from './ssr/path.js'; +export * from './ssr/util.js'; + +export * from './bootstrap.js'; export * from './clock.js'; export * from './events.js'; export * from './form.js'; diff --git a/src/events.js b/src/events.js index 2bdd37a..7af8350 100644 --- a/src/events.js +++ b/src/events.js @@ -45,7 +45,7 @@ export class Events { } let idx = subscribers.indexOf(subscriber); if (idx > -1) { - subscribers = subscribers.splice(idx, 1); + subscribers.splice(idx, 1); } this._subscribers[event] = subscribers; return this; diff --git a/src/form.js b/src/form.js index 4dec540..cecd8a1 100644 --- a/src/form.js +++ b/src/form.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -import {http_request} from '../src/request.js'; -import {Events} from '../src/events.js'; +import {http_request} from './request.js'; +import {Events} from './events.js'; import {changeListener} from './listener.js'; import { get_elem, diff --git a/src/listener.js b/src/listener.js index c5ffe38..b6133c1 100644 --- a/src/listener.js +++ b/src/listener.js @@ -1,4 +1,5 @@ -import {Events} from '../src/events.js'; +import {ajax} from './ssr/ajax.js'; +import {Events} from './events.js'; /** * Create listener base or mixin class handling given DOM event. @@ -67,16 +68,26 @@ export function create_listener(event, base=null) { } else { super(opts); } - let elem = this.elem + let elem = this.elem; if (!elem && opts !== undefined) { elem = this.elem = opts.elem; } if (!elem) { throw 'No element found'; } - elem.on(event, evt => { - this.trigger(`on_${event}`, evt); - }); + this.event = event; + this.trigger_event = this.trigger_event.bind(this); + this.elem.on(this.event, this.trigger_event); + + ajax.attach(this, this.elem); + } + + trigger_event(evt) { + this.trigger(`on_${event}`, evt); + } + + destroy() { + this.elem.off(this.event, this.trigger_event); } }; } diff --git a/src/overlay.js b/src/overlay.js index 69a21a5..d36c841 100644 --- a/src/overlay.js +++ b/src/overlay.js @@ -5,12 +5,14 @@ import { set_default, uuid4 } from './utils.js'; +import {ajax_destroy} from './ssr/destroy.js'; export class Overlay extends Events { constructor(opts) { super(); this.uid = opts.uid ? opts.uid : uuid4(); + this.flavor = opts.flavor ? opts.flavor : ''; this.css = opts.css ? opts.css : ''; this.title = opts.title ? opts.title : ' '; this.content = opts.content ? opts.content : ''; @@ -22,19 +24,23 @@ export class Overlay extends Events { } compile() { + let z_index = 1055; // default bootstrap modal z-index + z_index += $('.modal:visible').length; // increase zindex based on currently open modals compile_template(this, ` -