diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-02-15 11:29:58 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-02-15 11:30:06 +0100 |
commit | 526e88695f6c8a2b3dea4b9befeb2a85ba00b66b (patch) | |
tree | db2a0422d322ad45842cd12e1e107f1f08718f99 /extension | |
parent | c34a6612ec079a78e67325dac8d9a18775abe022 (diff) | |
download | wallet-core-526e88695f6c8a2b3dea4b9befeb2a85ba00b66b.tar.xz |
suggest mint based on currency (stub)
Diffstat (limited to 'extension')
-rw-r--r-- | extension/content_scripts/notify.js | 2 | ||||
-rw-r--r-- | extension/content_scripts/notify.ts | 2 | ||||
-rw-r--r-- | extension/lib/decl/mithril.d.ts | 1805 | ||||
-rw-r--r-- | extension/lib/module-trampoline.js | 10 | ||||
-rw-r--r-- | extension/lib/vendor/mithril.js | 3538 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.js | 1 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.ts | 2 | ||||
-rw-r--r-- | extension/pages/confirm-create-reserve.html | 2 | ||||
-rw-r--r-- | extension/pages/confirm-create-reserve.js | 143 | ||||
-rw-r--r-- | extension/pages/confirm-create-reserve.tsx | 164 | ||||
-rw-r--r-- | extension/style/wallet.css | 16 | ||||
-rw-r--r-- | extension/tsconfig.json | 2 |
12 files changed, 3287 insertions, 2400 deletions
diff --git a/extension/content_scripts/notify.js b/extension/content_scripts/notify.js index e99dabc32..4749fe9bf 100644 --- a/extension/content_scripts/notify.js +++ b/extension/content_scripts/notify.js @@ -44,6 +44,8 @@ var TalerNotify; var params = { amount: JSON.stringify(e.detail.amount), callback_url: URI(e.detail.callback_url).absoluteTo(document.location.href), + bank_url: document.location.href, + suggested_mint: e.detail.suggested_mint, }; var uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html")); document.location.href = uri.query(params).href(); diff --git a/extension/content_scripts/notify.ts b/extension/content_scripts/notify.ts index 47f57b6a0..9a64bcf0c 100644 --- a/extension/content_scripts/notify.ts +++ b/extension/content_scripts/notify.ts @@ -53,6 +53,8 @@ namespace TalerNotify { let params = { amount: JSON.stringify(e.detail.amount), callback_url: URI(e.detail.callback_url).absoluteTo(document.location.href), + bank_url: document.location.href, + suggested_mint: e.detail.suggested_mint, }; let uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html")); document.location.href = uri.query(params).href(); diff --git a/extension/lib/decl/mithril.d.ts b/extension/lib/decl/mithril.d.ts index d2646cbef..c87102260 100644 --- a/extension/lib/decl/mithril.d.ts +++ b/extension/lib/decl/mithril.d.ts @@ -1,879 +1,926 @@ -// Mithril type definitions for Typescript
-
-/**
- * This is the module containing all the types/declarations/etc. for Mithril
- */
-declare interface MithrilStatic {
- /**
- * Creates a virtual element for use with m.render, m.mount, etc.
- *
- * @param selector A simple CSS selector. May include SVG tags. Nested
- * selectors are not supported.
- * @param attributes Attributes to add. Any DOM attribute may be used
- * as an attribute, although innerHTML and the like may be overwritten
- * silently.
- * @param children Child elements, components, and text to add.
- * @return A virtual element.
- *
- * @see m.render
- * @see m.mount
- * @see m.component
- */
- <T extends MithrilController>(selector: string,
- attributes: MithrilAttributes,
- ...children: Array<string |
- MithrilVirtualElement<T> |
- MithrilComponent<T>>): MithrilVirtualElement<T>;
-
- /**
- * Initializes a component for use with m.render, m.mount, etc.
- *
- * @param component A component.
- * @param args Arguments to optionally pass to the component.
- * @return A component.
- *
- * @see m.render
- * @see m.mount
- * @see m
- */
- <T extends MithrilController>(component: MithrilComponent<T>,
- ...args: any[]): MithrilComponent<T>;
-
- /**
- * Creates a virtual element for use with m.render, m.mount, etc.
- *
- * @param selector A simple CSS selector. Nested selectors are not
- * supported.
- * @param children Child elements, components, and text to add.
- * @return A virtual element.
- *
- * @see m.render
- * @see m.mount
- * @see m.component
- */
- <T extends MithrilController>(selector: string,
- ...children: Array<string |
- MithrilVirtualElement<T> |
- MithrilComponent<T>>): MithrilVirtualElement<T>;
-
- /**
- * Initializes a component for use with m.render, m.mount, etc.
- * Shorthand for m.component.
- *
- * @param selector A component.
- * @param args Arguments to optionally pass to the component.
- * @return A component.
- *
- * @see m.render
- * @see m.mount
- * @see m.component
- */
- <T extends MithrilController>(component: MithrilComponent<T>,
- ...args: any[]): MithrilComponent<T>;
-
- /**
- * Creates a getter-setter function that wraps a Mithril promise. Useful
- * for uniform data access, m.withAttr, etc.
- *
- * @param promise A thennable to initialize the property with. It may
- * optionally be a Mithril promise.
- * @return A getter-setter function wrapping the promise.
- *
- * @see m.withAttr
- */
- prop<T>(promise: Thennable<T>) : MithrilPromiseProperty<T>;
-
- /**
- * Creates a getter-setter function that wraps a simple value. Useful
- * for uniform data access, m.withAttr, etc.
- *
- * @param value A value to initialize the property with
- * @return A getter-setter function wrapping the value.
- *
- * @see m.withAttr
- */
- prop<T>(value: T): MithrilBasicProperty<T>;
-
- /**
- * Creates a getter-setter function that wraps a simple value. Useful
- * for uniform data access, m.withAttr, etc.
- *
- * @return A getter-setter function wrapping the value.
- *
- * @see m.withAttr
- */
- prop<T>(): MithrilBasicProperty<T>;
-
- /**
- * Returns a event handler that can be bound to an element, firing with
- * the specified property.
- *
- * @param property The property to get from the event.
- * @param callback The handler to use the value from the event.
- * @return A function suitable for listening to an event.
- */
- withAttr(property: string,
- callback: (value: any) => void,
- callbackThis: any): (e: Event) => any;
-
- /**
- * @deprecated Use m.mount instead
- */
- module<T extends MithrilController>(rootElement: Node,
- component: MithrilComponent<T>): T;
-
- /**
- * Mounts a component to a base DOM node.
- *
- * @param rootElement The base node.
- * @param component The component to mount.
- * @return An instance of the top-level component's controller
- */
- mount<T extends MithrilController>(rootElement: Node,
- component: MithrilComponent<T>): T;
-
- /**
- * Initializes a component for use with m.render, m.mount, etc.
- *
- * @param selector A component.
- * @param args Arguments to optionally pass to the component.
- * @return A component.
- *
- * @see m.render
- * @see m.mount
- * @see m
- */
- component<T extends MithrilController>(component: MithrilComponent<T>,
- ...args: any[]): MithrilComponent<T>;
-
- /**
- * Trust this string of HTML.
- *
- * @param html The HTML to trust
- * @return A String object instance with an added internal flag to mark
- * it as trusted.
- */
- trust(html: string): MithrilTrustedString;
-
- /**
- * Render a virtual DOM tree.
- *
- * @param rootElement The base element/node to render the tree from.
- * @param children One or more child nodes to add to the tree.
- * @param forceRecreation If true, overwrite the entire tree without
- * diffing against it.
- */
- render<T extends MithrilController>(rootElement: Element,
- children: MithrilVirtualElement<T>|MithrilVirtualElement<T>[],
- forceRecreation?: boolean): void;
-
- redraw: {
- /**
- * Force a redraw the active component. It redraws asynchronously by
- * default to allow for simultaneous events to run before redrawing,
- * such as the event combination keypress + input frequently used for
- * input.
- *
- * @param force If true, redraw synchronously.
- */
- (force?: boolean): void;
-
- strategy: {
- /**
- * Gets the current redraw strategy, which returns one of the
- * following:
- *
- * "all" - recreates the DOM tree from scratch
- * "diff" - recreates the DOM tree from scratch
- * "none" - leaves the DOM tree intact
- *
- * This is useful for event handlers, which may want to cancel
- * the next redraw if the event doesn't update the UI.
- *
- * @return The current strategy
- */
- (): string;
-
- /**
- * Sets the current redraw strategy. The parameter must be one of
- * the following values:
- *
- * "all" - recreates the DOM tree from scratch
- * "diff" - recreates the DOM tree from scratch
- * "none" - leaves the DOM tree intact
- *
- * This is useful for event handlers, which may want to cancel
- * the next redraw if the event doesn't update the UI.
- *
- * @param value The value to set
- * @return The new strategy
- */
- (value: string): string;
-
- /**
- * @private
- * Implementation detail - it's a MithrilBasicProperty instance
- */
- toJSON(): string;
- }
- }
-
- route: {
- /**
- * Enable routing, mounting a controller based on the route. It
- * automatically mounts the components for you, starting with the one
- * specified by the default route.
- *
- * @param rootElement The element to mount the active controller to.
- * @param defaultRoute The route to start with.
- * @param routes A key-value mapping of pathname to controller.
- */
- <T extends MithrilController>(rootElement: Element,
- defaultRoute: string,
- routes: MithrilRoutes): void;
-
- /**
- * This allows m.route to be used as the `config` attribute for a
- * virtual element, particularly useful for cases like this:
- *
- * ```ts
- * // Note that the '#' is not required in `href`, thanks to the
- * `config` setting.
- * m("a[href='/dashboard/alicesmith']", {config: m.route});
- * ```
- */
- <T extends MithrilController>(element: Element,
- isInitialized: boolean,
- context?: MithrilContext,
- vdom?: MithrilVirtualElement<T>): void;
-
- /**
- * Programmatically redirect to another route.
- *
- * @param path The route to go to.
- * @param params Parameters to pass as a query string.
- * @param shouldReplaceHistory Whether to replace the current history
- * instead of adding a new one.
- */
- (path: string, params?: any, shouldReplaceHistory?: boolean): void;
-
- /**
- * Gets the current route.
- *
- * @return The current route.
- */
- (): string;
-
- /**
- * Gets a route parameter.
- *
- * @param key The key to get.
- * @return The value associated with the parameter key.
- */
- param(key: string): string;
-
- /**
- * The current routing mode. This may be changed before calling
- * m.route to change the part of the URL used to perform the routing.
- *
- * The value can be set to one of the following, defaulting to
- * "hash":
- *
- * "search" - Uses the query string. This allows for named anchors to
- * work on the page, but changes cause IE8 and lower to refresh the
- * page.
- *
- * "hash" - Uses the hash. This is the only routing mode that does
- * not cause page refreshes on any browser, but it does not support
- * named anchors.
- *
- * "pathname" - Uses the URL pathname. This requires server-side
- * setup to support bookmarking and page refreshes. It always causes
- * page refreshes on IE8 and lower. Note that this requires that the
- * application to be run from the root of the URL.
- */
- mode: string;
-
- /**
- * Serialize an object into a query string.
- *
- * @param data The data to serialize.
- * @return The serialized string.
- */
- buildQueryString(data: Object): String
-
- /**
- * Parse a query string into an object.
- *
- * @param data The data to parse.
- * @return The parsed object data.
- */
- parseQueryString(data: String): Object
- }
-
- /**
- * Send a request to a server to server. Note that the `url` option is
- * required.
- *
- * @param options The options to use
- * @return A promise to the returned data for "GET" requests, or a void
- * promise for any other request type.
- *
- * @see MithrilXHROptions for the available options.
- */
- request<T>(options: MithrilXHROptions<T>): MithrilPromise<T>;
-
- deferred: {
- /**
- * Create a Mithril deferred object. It behaves synchronously if
- * possible, an intentional deviation from Promises/A+. Note that
- * deferreds are completely separate from the redrawing system, and
- * never trigger a redraw on their own.
- *
- * @return A new Mithril deferred instance.
- *
- * @see m.deferred.onerror for the error callback called for Error
- * subclasses
- */
- <T>(): MithrilDeferred<T>;
-
- /**
- * A callback for all uncaught native Error subclasses in deferreds.
- * This defaults to synchronously rethrowing all errors, a deviation
- * from Promises/A+, but the behavior is configurable. To restore
- * Promises/A+-compatible behavior. simply set this to a no-op.
- */
- onerror(e: Error): void;
- }
-
- /**
- * Takes a list of promises or thennables and returns a Mithril promise
- * that resolves once all in the list are resolved, or rejects if any of
- * them reject.
- *
- * @param promises A list of promises to try to resolve.
- * @return A promise that resolves to all the promises if all resolve, or
- * rejects with the error contained in the first rejection.
- */
- sync<T>(promises: Thennable<T>[]): MithrilPromise<T[]>;
-
- /**
- * Use this and endComputation if your views aren't redrawing after
- * calls to third-party libraries. For integrating asynchronous code,
- * this should be called before any asynchronous work is done. For
- * synchronous code, this should be called at the beginning of the
- * problematic segment. Note that these calls must be balanced, much like
- * braces and parentheses. This is mostly used internally. Prefer
- * m.redraw where possible, especially when making repeated calls.
- *
- * @see endComputation
- * @see m.render
- */
- startComputation(): void;
-
- /**
- * Use startComputation and this if your views aren't redrawing after
- * calls to third-party libraries. For integrating asynchronous code,
- * this should be called after all asynchronous work completes. For
- * synchronous code, this should be called at the end of the problematic
- * segment. Note that these calls must be balanced, much like braces and
- * parentheses. This is mostly used internally. Prefer m.redraw where
- * possible, especially when making repeated calls.
- *
- * @see startComputation
- * @see m.render
- */
- endComputation(): void;
-
- /**
- * This overwrites the internal version of window used by Mithril.
- * It's mostly useful for testing, and is also used internally by
- * Mithril to test itself. By default Mithril uses `window` for the
- * dependency.
- *
- * @param mockWindow The mock to use for the window.
- * @return The mock that was passed in.
- */
- deps(mockWindow: Window): Window;
-}
-
-interface MithrilTrustedString extends String {
- /** @private Implementation detail. Don't depend on it. */
- $trusted: boolean;
-}
-
-/**
- * The interface for a virtual element. It's best to consider this immutable
- * for most use cases.
- *
- * @see m
- */
-interface MithrilVirtualElement<T extends MithrilController> {
- /**
- * A key to optionally associate with this element.
- */
- key?: number;
-
- /**
- * The tag name of this element.
- */
- tag?: string;
-
- /**
- * The attributes of this element.
- */
- attrs?: MithrilAttributes;
-
- /**
- * The children of this element.
- */
- children?: Array<string|MithrilVirtualElement<T>|MithrilComponent<T>>;
-}
-
-/**
- * An event passed by Mithril to unload event handlers.
- */
-interface MithrilEvent {
- /**
- * Prevent the default behavior of scrolling the page and updating the
- * URL on next route change.
- */
- preventDefault(): void;
-}
-
-/**
- * A context object for configuration functions.
- *
- * @see MithrilElementConfig
- */
-interface MithrilContext {
- /**
- * A function to call when the node is unloaded. Useful for cleanup.
- */
- onunload?(): any;
-
- /**
- * Set true if the backing DOM node needs to be retained between route
- * changes if possible. Set false if this node needs to be recreated
- * every single time, regardless of how "different" it is.
- */
- retain?: boolean;
-}
-
-/**
- * This represents a callback function for a virtual element's config
- * attribute. It's a low-level function useful for extra cleanup after
- * removal from the tree, storing instances of third-party classes that
- * need to be associated with the DOM, etc.
- *
- * @see MithrilAttributes
- * @see MithrilContext
- */
-interface MithrilElementConfig {
- /**
- * A callback function for a virtual element's config attribute.
- *
- * @param element The associated DOM element.
- * @param isInitialized Whether this is the first call for the virtual
- * element or not.
- * @param context The associated context for this element.
- * @param vdom The associated virtual element.
- */
- <T extends MithrilController>(element: Element,
- isInitialized: boolean,
- context: MithrilContext,
- vdom: MithrilVirtualElement<T>): void;
-}
-
-/**
- * This represents the attributes available for configuring virtual elements,
- * beyond the applicable DOM attributes.
- *
- * @see m
- */
-interface MithrilAttributes {
- /**
- * The class name(s) for this virtual element, as a space-separated list.
- */
- className?: string;
-
- /**
- * The class name(s) for this virtual element, as a space-separated list.
- */
- class?: string;
-
- /**
- * A custom, low-level configuration in case this element needs special
- * cleanup after removal from the tree.
- *
- * @see MithrilElementConfig
- */
- config?: MithrilElementConfig;
-
- /**
- * Any other virtual element properties including attributes and
- * event handlers
- */
- [property: string]: any;
-}
-
-/**
- * The basis of a Mithril controller instance.
- */
-interface MithrilController {
- /**
- * An optional handler to call when the associated virtual element is
- * destroyed.
- *
- * @param evt An associated event.
- */
- onunload?(evt: MithrilEvent): any;
-}
-
-/**
- * This represents a controller function.
- *
- * @see MithrilControllerConstructor
- */
-interface MithrilControllerFunction<T extends MithrilController> {
- (): T;
-}
-
-/**
- * This represents a controller constructor.
- *
- * @see MithrilControllerFunction
- */
-interface MithrilControllerConstructor<T extends MithrilController> {
- new(): T;
-}
-
-/**
- * This represents a view factory.
- */
-interface MithrilView<T extends MithrilController> {
- /**
- * Creates a view out of virtual elements.
- */
- (ctrl: T): MithrilVirtualElement<T>;
-}
-
-/**
- * This represents a Mithril component.
- *
- * @see m
- * @see m.component
- */
-interface MithrilComponent<T extends MithrilController> {
- /**
- * The component's controller.
- *
- * @see m.component
- */
- controller?: MithrilControllerFunction<T> |
- MithrilControllerConstructor<T>;
-
- /**
- * Creates a view out of virtual elements.
- *
- * @see m.component
- */
- view(ctrl: T): MithrilVirtualElement<T>;
-}
-
-/**
- * This is the base interface for property getter-setters
- *
- * @see m.prop
- */
-interface MithrilProperty<T> {
- /**
- * Gets the contained value.
- *
- * @return The contained value.
- */
- (): T;
-
- /**
- * Sets the contained value.
- *
- * @param value The new value to set.
- * @return The newly set value.
- */
- (value: T): T;
-}
-
-/**
- * This represents a non-promise getter-setter functions.
- *
- * @see m.prop which returns objects that implement this interface.
- */
-interface MithrilBasicProperty<T> extends MithrilProperty<T> {
- /**
- * Makes this serializable to JSON.
- */
- toJSON(): T;
-}
-
-/**
- * This represents a promise getter-setter function.
- *
- * @see m.prop which returns objects that implement this interface.
- */
-interface MithrilPromiseProperty<T> extends MithrilPromise<T>,
- MithrilProperty<MithrilPromise<T>> {
- /**
- * Gets the contained promise.
- *
- * @return The contained value.
- */
- (): MithrilPromise<T>;
-
- /**
- * Sets the contained promise.
- *
- * @param value The new value to set.
- * @return The newly set value.
- */
- (value: MithrilPromise<T>): MithrilPromise<T>;
-
- /**
- * Sets the contained wrapped value.
- *
- * @param value The new value to set.
- * @return The newly set value.
- */
- (value: T): MithrilPromise<T>;
-}
-
-/**
- * This represents a key-value mapping linking routes to components.
- */
-interface MithrilRoutes {
- /**
- * The key represents the route. The value represents the corresponding
- * component.
- */
- [key: string]: MithrilComponent<MithrilController>;
-}
-
-/**
- * This represents a Mithril deferred object.
- */
-interface MithrilDeferred<T> {
- /**
- * Resolve this deferred's promise with a value.
- *
- * @param value The value to resolve the promise with.
- */
- resolve(value?: T): void;
-
- /**
- * Reject this deferred with an error.
- *
- * @param value The reason for rejecting the promise.
- */
- reject(reason?: any): void;
-
- /**
- * The backing promise.
- *
- * @see MithrilPromise
- */
- promise: MithrilPromise<T>;
-}
-
-/**
- * This represents a thennable success callback.
- */
-interface MithrilSuccessCallback<T, U> {
- (value: T): U | Thennable<U>;
-}
-
-/**
- * This represents a thennable error callback.
- */
-interface MithrilErrorCallback<T> {
- (value: Error): T | Thennable<T>;
-}
-
-/**
- * This represents a thennable.
- */
-interface Thennable<T> {
- then<U>(success: (value: T) => U): Thennable<U>;
- then<U,V>(success: (value: T) => U,
- error: (value: Error) => V): Thennable<U>|Thennable<V>;
- catch?: <U>(error: (value: Error) => U) => Thennable<U>;
-}
-
-/**
- * This represents a Mithril promise object.
- */
-interface MithrilPromise<T> extends Thennable<T>, MithrilProperty<MithrilPromise<T>> {
- /**
- * Chain this promise with a simple success callback, propogating
- * rejections.
- *
- * @param success The callback to call when the promise is resolved.
- * @return The chained promise.
- */
- then<U>(success: MithrilSuccessCallback<T,U>): MithrilPromise<U>;
-
- /**
- * Chain this promise with a success callback and error callback, without
- * propogating rejections.
- *
- * @param success The callback to call when the promise is resolved.
- * @param error The callback to call when the promise is rejected.
- * @return The chained promise.
- */
- then<U, V>(success: MithrilSuccessCallback<T, U>,
- error: MithrilErrorCallback<V>): MithrilPromise<U> | MithrilPromise<V>;
-
- /**
- * Chain this promise with a single error callback, without propogating
- * rejections.
- *
- * @param error The callback to call when the promise is rejected.
- * @return The chained promise.
- */
- catch<U>(error: MithrilErrorCallback<U>): MithrilPromise<T> |
- MithrilPromise<U>;
-}
-
-/**
- * This represents the available options for configuring m.request.
- *
- * @see m.request
- */
-interface MithrilXHROptions<T> {
- /**
- * This represents the HTTP method used, one of the following:
- *
- * - "GET" (default)
- * - "POST"
- * - "PUT"
- * - "DELETE"
- * - "HEAD"
- * - "OPTIONS"
- */
- method?: string;
-
- /**
- * The URL to send the request to.
- */
- url: string;
-
- /**
- * The username for HTTP authentication.
- */
- user?: string;
-
- /**
- * The password for HTTP authentication.
- */
- password?: string;
-
- /**
- * The data to be sent. It's automatically serialized in the right format
- * depending on the method (with exception of HTML5 FormData), and put in
- * the appropriate section of the request.
- */
- data?: any;
-
- /**
- * Whether to run it in the background, i.e. true if it doesn't affect
- * template rendering.
- */
- background?: boolean;
-
- /**
- * Set an initial value while the request is working, to populate the
- * promise getter-setter.
- */
- initialValue?: T;
-
- /**
- * An optional preprocessor function to unwrap a successful response, in
- * case the response contains metadata wrapping the data.
- *
- * @param data The data to unwrap.
- * @return The unwrapped result.
- */
- unwrapSuccess?(data: any): T;
-
- /**
- * An optional preprocessor function to unwrap an unsuccessful response,
- * in case the response contains metadata wrapping the data.
- *
- * @param data The data to unwrap.
- * @return The unwrapped result.
- */
- unwrapError?(data: any): T;
-
- /**
- * An optional function to serialize the data. This defaults to
- * `JSON.stringify`.
- *
- * @param dataToSerialize The data to serialize.
- * @return The serialized form as a string.
- */
- serialize?(dataToSerialize: any): string;
-
- /**
- * An optional function to deserialize the data. This defaults to
- * `JSON.parse`.
- *
- * @param dataToSerialize The data to parse.
- * @return The parsed form.
- */
- deserialize?(dataToDeserialize: string): any;
-
- /**
- * An optional function to extract the data from a raw XMLHttpRequest,
- * useful if the relevant data is in a response header or the status
- * field.
- *
- * @param xhr The associated XMLHttpRequest.
- * @param options The options passed to this request.
- * @return string The serialized format.
- */
- extract?(xhr: XMLHttpRequest, options: MithrilXHROptions<T>): string;
-
- /**
- * The parsed data, or its children if it's an array, will be passed to
- * this class constructor if it's given, to parse it into classes.
- *
- * @param data The data to parse.
- * @return The new instance for the list.
- */
- type?: new (data: Object) => any;
-
- /**
- * An optional function to run between `open` and `send`, useful for
- * adding request headers or using XHR2 features such as the `upload`
- * property. It is even possible to override the XHR altogether with a
- * similar object, such as an XDomainRequest instance.
- *
- * @param xhr The associated XMLHttpRequest.
- * @param options The options passed to this request.
- * @return The new XMLHttpRequest, or nothing if the same one is kept.
- */
- config?(xhr: XMLHttpRequest, options: MithrilXHROptions<T>): any;
-
- /**
- * For JSONP requests, this must be the string "jsonp". Otherwise, it's
- * ignored.
- */
- dataType?: string;
-
- /**
- * For JSONP requests, this is the query string key for the JSONP
- * request. This is useful for APIs that don't use common conventions,
- * such as `www.example.com/?jsonpCallback=doSomething`. It defaults to
- * `callback` for JSONP requests, and is ignored for any other kind of
- * request.
- */
- callbackKey?: string;
-}
+// Mithril type definitions for Typescript + +/** +* This is the module containing all the types/declarations/etc. for Mithril +*/ +declare module _mithril { + interface MithrilStatic { + /** + * Creates a virtual element for use with m.render, m.mount, etc. + * + * @param selector A simple CSS selector. May include SVG tags. Nested + * selectors are not supported. + * @param attributes Attributes to add. Any DOM attribute may be used + * as an attribute, although innerHTML and the like may be overwritten + * silently. + * @param children Child elements, components, and text to add. + * @return A virtual element. + * + * @see m.render + * @see m.mount + * @see m.component + */ + <T extends MithrilController>( + selector: string, + attributes: MithrilAttributes, + ...children: Array<string | + MithrilVirtualElement<T> | + MithrilComponent<T>> + ): MithrilVirtualElement<T>; + + /** + * Initializes a component for use with m.render, m.mount, etc. + * + * @param component A component. + * @param args Arguments to optionally pass to the component. + * @return A component. + * + * @see m.render + * @see m.mount + * @see m + */ + <T extends MithrilController>( + component: MithrilComponent<T>, + ...args: any[] + ): MithrilComponent<T>; + + /** + * Creates a virtual element for use with m.render, m.mount, etc. + * + * @param selector A simple CSS selector. Nested selectors are not + * supported. + * @param children Child elements, components, and text to add. + * @return A virtual element. + * + * @see m.render + * @see m.mount + * @see m.component + */ + <T extends MithrilController>( + selector: string, + ...children: Array<string | + MithrilVirtualElement<T> | + MithrilComponent<T>> + ): MithrilVirtualElement<T>; + + /** + * Initializes a component for use with m.render, m.mount, etc. + * Shorthand for m.component. + * + * @param selector A component. + * @param args Arguments to optionally pass to the component. + * @return A component. + * + * @see m.render + * @see m.mount + * @see m.component + */ + <T extends MithrilController>( + component: MithrilComponent<T>, + ...args: any[] + ): MithrilComponent<T>; + + /** + * Creates a getter-setter function that wraps a Mithril promise. Useful + * for uniform data access, m.withAttr, etc. + * + * @param promise A thennable to initialize the property with. It may + * optionally be a Mithril promise. + * @return A getter-setter function wrapping the promise. + * + * @see m.withAttr + */ + prop<T>(promise: Thennable<T>) : MithrilPromiseProperty<T>; + + /** + * Creates a getter-setter function that wraps a simple value. Useful + * for uniform data access, m.withAttr, etc. + * + * @param value A value to initialize the property with + * @return A getter-setter function wrapping the value. + * + * @see m.withAttr + */ + prop<T>(value: T): MithrilBasicProperty<T>; + + /** + * Creates a getter-setter function that wraps a simple value. Useful + * for uniform data access, m.withAttr, etc. + * + * @return A getter-setter function wrapping the value. + * + * @see m.withAttr + */ + prop<T>(): MithrilBasicProperty<T>; + + /** + * Returns a event handler that can be bound to an element, firing with + * the specified property. + * + * @param property The property to get from the event. + * @param callback The handler to use the value from the event. + * @return A function suitable for listening to an event. + */ + withAttr( + property: string, + callback: (value: any) => void, + callbackThis: any + ): (e: Event) => any; + + /** + * Returns a event handler that can be bound to an element, firing with + * the specified property. + * + * @param attributeName Name of the element's attribute to bind to. + * @param property The property to bind. + * @return A function suitable for listening to an event. + */ + withAttr<T>( + attributeName: string, + property: MithrilBasicProperty<T> + ) : (e: Event) => any; + + /** + * @deprecated Use m.mount instead + */ + module<T extends MithrilController>( + rootElement: Node, + component: MithrilComponent<T> + ): T; + + /** + * Mounts a component to a base DOM node. + * + * @param rootElement The base node. + * @param component The component to mount. + * @return An instance of the top-level component's controller + */ + mount<T extends MithrilController>( + rootElement: Node, + component: MithrilComponent<T> + ): T; + + /** + * Initializes a component for use with m.render, m.mount, etc. + * + * @param selector A component. + * @param args Arguments to optionally pass to the component. + * @return A component. + * + * @see m.render + * @see m.mount + * @see m + */ + component<T extends MithrilController>( + component: MithrilComponent<T>, + ...args: any[] + ): MithrilComponent<T>; + + /** + * Trust this string of HTML. + * + * @param html The HTML to trust + * @return A String object instance with an added internal flag to mark + * it as trusted. + */ + trust(html: string): MithrilTrustedString; + + /** + * Render a virtual DOM tree. + * + * @param rootElement The base element/node to render the tree from. + * @param children One or more child nodes to add to the tree. + * @param forceRecreation If true, overwrite the entire tree without + * diffing against it. + */ + render<T extends MithrilController>( + rootElement: Element, + children: MithrilVirtualElement<T>|MithrilVirtualElement<T>[], + forceRecreation?: boolean + ): void; + + redraw: { + /** + * Force a redraw the active component. It redraws asynchronously by + * default to allow for simultaneous events to run before redrawing, + * such as the event combination keypress + input frequently used for + * input. + * + * @param force If true, redraw synchronously. + */ + (force?: boolean): void; + + strategy: { + /** + * Gets the current redraw strategy, which returns one of the + * following: + * + * "all" - recreates the DOM tree from scratch + * "diff" - recreates the DOM tree from scratch + * "none" - leaves the DOM tree intact + * + * This is useful for event handlers, which may want to cancel + * the next redraw if the event doesn't update the UI. + * + * @return The current strategy + */ + (): string; + + /** + * Sets the current redraw strategy. The parameter must be one of + * the following values: + * + * "all" - recreates the DOM tree from scratch + * "diff" - recreates the DOM tree from scratch + * "none" - leaves the DOM tree intact + * + * This is useful for event handlers, which may want to cancel + * the next redraw if the event doesn't update the UI. + * + * @param value The value to set + * @return The new strategy + */ + (value: string): string; + + /** + * @private + * Implementation detail - it's a MithrilBasicProperty instance + */ + toJSON(): string; + } + } + + route: { + /** + * Enable routing, mounting a controller based on the route. It + * automatically mounts the components for you, starting with the one + * specified by the default route. + * + * @param rootElement The element to mount the active controller to. + * @param defaultRoute The route to start with. + * @param routes A key-value mapping of pathname to controller. + */ + <T extends MithrilController>( + rootElement: Element, + defaultRoute: string, + routes: MithrilRoutes + ): void; + + /** + * This allows m.route to be used as the `config` attribute for a + * virtual element, particularly useful for cases like this: + * + * ```ts + * // Note that the '#' is not required in `href`, thanks to the + * `config` setting. + * m("a[href='/dashboard/alicesmith']", {config: m.route}); + * ``` + */ + <T extends MithrilController>( + element: Element, + isInitialized: boolean, + context?: MithrilContext, + vdom?: MithrilVirtualElement<T> + ): void; + + /** + * Programmatically redirect to another route. + * + * @param path The route to go to. + * @param params Parameters to pass as a query string. + * @param shouldReplaceHistory Whether to replace the current history + * instead of adding a new one. + */ + (path: string, params?: any, shouldReplaceHistory?: boolean): void; + + /** + * Gets the current route. + * + * @return The current route. + */ + (): string; + + /** + * Gets a route parameter. + * + * @param key The key to get. + * @return The value associated with the parameter key. + */ + param(key: string): string; + + /** + * The current routing mode. This may be changed before calling + * m.route to change the part of the URL used to perform the routing. + * + * The value can be set to one of the following, defaulting to + * "hash": + * + * "search" - Uses the query string. This allows for named anchors to + * work on the page, but changes cause IE8 and lower to refresh the + * page. + * + * "hash" - Uses the hash. This is the only routing mode that does + * not cause page refreshes on any browser, but it does not support + * named anchors. + * + * "pathname" - Uses the URL pathname. This requires server-side + * setup to support bookmarking and page refreshes. It always causes + * page refreshes on IE8 and lower. Note that this requires that the + * application to be run from the root of the URL. + */ + mode: string; + + /** + * Serialize an object into a query string. + * + * @param data The data to serialize. + * @return The serialized string. + */ + buildQueryString(data: Object): String + + /** + * Parse a query string into an object. + * + * @param data The data to parse. + * @return The parsed object data. + */ + parseQueryString(data: String): Object + } + + /** + * Send a request to a server to server. Note that the `url` option is + * required. + * + * @param options The options to use + * @return A promise to the returned data for "GET" requests, or a void + * promise for any other request type. + * + * @see MithrilXHROptions for the available options. + */ + request<T>(options: MithrilXHROptions<T>): MithrilPromise<T>; + + deferred: { + /** + * Create a Mithril deferred object. It behaves synchronously if + * possible, an intentional deviation from Promises/A+. Note that + * deferreds are completely separate from the redrawing system, and + * never trigger a redraw on their own. + * + * @return A new Mithril deferred instance. + * + * @see m.deferred.onerror for the error callback called for Error + * subclasses + */ + <T>(): MithrilDeferred<T>; + + /** + * A callback for all uncaught native Error subclasses in deferreds. + * This defaults to synchronously rethrowing all errors, a deviation + * from Promises/A+, but the behavior is configurable. To restore + * Promises/A+-compatible behavior. simply set this to a no-op. + */ + onerror(e: Error): void; + } + + /** + * Takes a list of promises or thennables and returns a Mithril promise + * that resolves once all in the list are resolved, or rejects if any of + * them reject. + * + * @param promises A list of promises to try to resolve. + * @return A promise that resolves to all the promises if all resolve, or + * rejects with the error contained in the first rejection. + */ + sync<T>(promises: Thennable<T>[]): MithrilPromise<T[]>; + + /** + * Use this and endComputation if your views aren't redrawing after + * calls to third-party libraries. For integrating asynchronous code, + * this should be called before any asynchronous work is done. For + * synchronous code, this should be called at the beginning of the + * problematic segment. Note that these calls must be balanced, much like + * braces and parentheses. This is mostly used internally. Prefer + * m.redraw where possible, especially when making repeated calls. + * + * @see endComputation + * @see m.render + */ + startComputation(): void; + + /** + * Use startComputation and this if your views aren't redrawing after + * calls to third-party libraries. For integrating asynchronous code, + * this should be called after all asynchronous work completes. For + * synchronous code, this should be called at the end of the problematic + * segment. Note that these calls must be balanced, much like braces and + * parentheses. This is mostly used internally. Prefer m.redraw where + * possible, especially when making repeated calls. + * + * @see startComputation + * @see m.render + */ + endComputation(): void; + + /** + * This overwrites the internal version of window used by Mithril. + * It's mostly useful for testing, and is also used internally by + * Mithril to test itself. By default Mithril uses `window` for the + * dependency. + * + * @param mockWindow The mock to use for the window. + * @return The mock that was passed in. + */ + deps(mockWindow: Window): Window; + } + + interface MithrilTrustedString extends String { + /** @private Implementation detail. Don't depend on it. */ + $trusted: boolean; + } + + /** + * The interface for a virtual element. It's best to consider this immutable + * for most use cases. + * + * @see m + */ + interface MithrilVirtualElement<T extends MithrilController> { + /** + * A key to optionally associate with this element. + */ + key?: number; + + /** + * The tag name of this element. + */ + tag?: string; + + /** + * The attributes of this element. + */ + attrs?: MithrilAttributes; + + /** + * The children of this element. + */ + children?: Array<string|MithrilVirtualElement<T>|MithrilComponent<T>>; + } + + /** + * An event passed by Mithril to unload event handlers. + */ + interface MithrilEvent { + /** + * Prevent the default behavior of scrolling the page and updating the + * URL on next route change. + */ + preventDefault(): void; + } + + /** + * A context object for configuration functions. + * + * @see MithrilElementConfig + */ + interface MithrilContext { + /** + * A function to call when the node is unloaded. Useful for cleanup. + */ + onunload?(): any; + + /** + * Set true if the backing DOM node needs to be retained between route + * changes if possible. Set false if this node needs to be recreated + * every single time, regardless of how "different" it is. + */ + retain?: boolean; + } + + /** + * This represents a callback function for a virtual element's config + * attribute. It's a low-level function useful for extra cleanup after + * removal from the tree, storing instances of third-party classes that + * need to be associated with the DOM, etc. + * + * @see MithrilAttributes + * @see MithrilContext + */ + interface MithrilElementConfig { + /** + * A callback function for a virtual element's config attribute. + * + * @param element The associated DOM element. + * @param isInitialized Whether this is the first call for the virtual + * element or not. + * @param context The associated context for this element. + * @param vdom The associated virtual element. + */ + <T extends MithrilController>( + element: Element, + isInitialized: boolean, + context: MithrilContext, + vdom: MithrilVirtualElement<T> + ): void; + } + + /** + * This represents the attributes available for configuring virtual elements, + * beyond the applicable DOM attributes. + * + * @see m + */ + interface MithrilAttributes { + /** + * The class name(s) for this virtual element, as a space-separated list. + */ + className?: string; + + /** + * The class name(s) for this virtual element, as a space-separated list. + */ + class?: string; + + /** + * A custom, low-level configuration in case this element needs special + * cleanup after removal from the tree. + * + * @see MithrilElementConfig + */ + config?: MithrilElementConfig; + + /** + * Any other virtual element properties including attributes and + * event handlers + */ + [property: string]: any; + } + + /** + * The basis of a Mithril controller instance. + */ + interface MithrilController { + /** + * An optional handler to call when the associated virtual element is + * destroyed. + * + * @param evt An associated event. + */ + onunload?(evt: MithrilEvent): any; + } + + /** + * This represents a controller function. + * + * @see MithrilControllerConstructor + */ + interface MithrilControllerFunction<T extends MithrilController> { + (opts?: any): T; + } + + /** + * This represents a controller constructor. + * + * @see MithrilControllerFunction + */ + interface MithrilControllerConstructor<T extends MithrilController> { + new(): T; + } + + /** + * This represents a view factory. + */ + interface MithrilView<T extends MithrilController> { + /** + * Creates a view out of virtual elements. + */ + (ctrl: T): MithrilVirtualElement<T>; + } + + /** + * This represents a Mithril component. + * + * @see m + * @see m.component + */ + interface MithrilComponent<T extends MithrilController> { + /** + * The component's controller. + * + * @see m.component + */ + controller: MithrilControllerFunction<T> | + MithrilControllerConstructor<T>; + + /** + * Creates a view out of virtual elements. + * + * @see m.component + */ + view(ctrl?: T, opts?: any): MithrilVirtualElement<T>; + } + + /** + * This is the base interface for property getter-setters + * + * @see m.prop + */ + interface MithrilProperty<T> { + /** + * Gets the contained value. + * + * @return The contained value. + */ + (): T; + + /** + * Sets the contained value. + * + * @param value The new value to set. + * @return The newly set value. + */ + (value: T): T; + } + + /** + * This represents a non-promise getter-setter functions. + * + * @see m.prop which returns objects that implement this interface. + */ + interface MithrilBasicProperty<T> extends MithrilProperty<T> { + /** + * Makes this serializable to JSON. + */ + toJSON(): T; + } + + /** + * This represents a promise getter-setter function. + * + * @see m.prop which returns objects that implement this interface. + */ + interface MithrilPromiseProperty<T> extends MithrilPromise<T>, + MithrilProperty<MithrilPromise<T>> { + /** + * Gets the contained promise. + * + * @return The contained value. + */ + (): MithrilPromise<T>; + + /** + * Sets the contained promise. + * + * @param value The new value to set. + * @return The newly set value. + */ + (value: MithrilPromise<T>): MithrilPromise<T>; + + /** + * Sets the contained wrapped value. + * + * @param value The new value to set. + * @return The newly set value. + */ + (value: T): MithrilPromise<T>; + } + + /** + * This represents a key-value mapping linking routes to components. + */ + interface MithrilRoutes { + /** + * The key represents the route. The value represents the corresponding + * component. + */ + [key: string]: MithrilComponent<MithrilController>; + } + + /** + * This represents a Mithril deferred object. + */ + interface MithrilDeferred<T> { + /** + * Resolve this deferred's promise with a value. + * + * @param value The value to resolve the promise with. + */ + resolve(value?: T): void; + + /** + * Reject this deferred with an error. + * + * @param value The reason for rejecting the promise. + */ + reject(reason?: any): void; + + /** + * The backing promise. + * + * @see MithrilPromise + */ + promise: MithrilPromise<T>; + } + + /** + * This represents a thennable success callback. + */ + interface MithrilSuccessCallback<T, U> { + (value: T): U | Thennable<U>; + } + + /** + * This represents a thennable error callback. + */ + interface MithrilErrorCallback<T> { + (value: Error): T | Thennable<T>; + } + + /** + * This represents a thennable. + */ + interface Thennable<T> { + then<U>(success: (value: T) => U): Thennable<U>; + then<U,V>(success: (value: T) => U, error: (value: Error) => V): Thennable<U>|Thennable<V>; + catch?: <U>(error: (value: Error) => U) => Thennable<U>; + } + + /** + * This represents a Mithril promise object. + */ + interface MithrilPromise<T> extends Thennable<T>, MithrilProperty<MithrilPromise<T>> { + /** + * Chain this promise with a simple success callback, propogating + * rejections. + * + * @param success The callback to call when the promise is resolved. + * @return The chained promise. + */ + then<U>(success: MithrilSuccessCallback<T,U>): MithrilPromise<U>; + + /** + * Chain this promise with a success callback and error callback, without + * propogating rejections. + * + * @param success The callback to call when the promise is resolved. + * @param error The callback to call when the promise is rejected. + * @return The chained promise. + */ + then<U, V>( + success: MithrilSuccessCallback<T, U>, + error: MithrilErrorCallback<V> + ): MithrilPromise<U> | MithrilPromise<V>; + + /** + * Chain this promise with a single error callback, without propogating + * rejections. + * + * @param error The callback to call when the promise is rejected. + * @return The chained promise. + */ + catch<U>(error: MithrilErrorCallback<U>): MithrilPromise<T> | + MithrilPromise<U>; + } + + /** + * This represents the available options for configuring m.request. + * + * @see m.request + */ + interface MithrilXHROptions<T> { + /** + * This represents the HTTP method used, one of the following: + * + * - "GET" (default) + * - "POST" + * - "PUT" + * - "DELETE" + * - "HEAD" + * - "OPTIONS" + */ + method?: string; + + /** + * The URL to send the request to. + */ + url: string; + + /** + * The username for HTTP authentication. + */ + user?: string; + + /** + * The password for HTTP authentication. + */ + password?: string; + + /** + * The data to be sent. It's automatically serialized in the right format + * depending on the method (with exception of HTML5 FormData), and put in + * the appropriate section of the request. + */ + data?: any; + + /** + * Whether to run it in the background, i.e. true if it doesn't affect + * template rendering. + */ + background?: boolean; + + /** + * Set an initial value while the request is working, to populate the + * promise getter-setter. + */ + initialValue?: T; + + /** + * An optional preprocessor function to unwrap a successful response, in + * case the response contains metadata wrapping the data. + * + * @param data The data to unwrap. + * @return The unwrapped result. + */ + unwrapSuccess?(data: any): T; + + /** + * An optional preprocessor function to unwrap an unsuccessful response, + * in case the response contains metadata wrapping the data. + * + * @param data The data to unwrap. + * @return The unwrapped result. + */ + unwrapError?(data: any): T; + + /** + * An optional function to serialize the data. This defaults to + * `JSON.stringify`. + * + * @param dataToSerialize The data to serialize. + * @return The serialized form as a string. + */ + serialize?(dataToSerialize: any): string; + + /** + * An optional function to deserialize the data. This defaults to + * `JSON.parse`. + * + * @param dataToSerialize The data to parse. + * @return The parsed form. + */ + deserialize?(dataToDeserialize: string): any; + + /** + * An optional function to extract the data from a raw XMLHttpRequest, + * useful if the relevant data is in a response header or the status + * field. + * + * @param xhr The associated XMLHttpRequest. + * @param options The options passed to this request. + * @return string The serialized format. + */ + extract?(xhr: XMLHttpRequest, options: MithrilXHROptions<T>): string; + + /** + * The parsed data, or its children if it's an array, will be passed to + * this class constructor if it's given, to parse it into classes. + * + * @param data The data to parse. + * @return The new instance for the list. + */ + type?: new (data: Object) => any; + + /** + * An optional function to run between `open` and `send`, useful for + * adding request headers or using XHR2 features such as the `upload` + * property. It is even possible to override the XHR altogether with a + * similar object, such as an XDomainRequest instance. + * + * @param xhr The associated XMLHttpRequest. + * @param options The options passed to this request. + * @return The new XMLHttpRequest, or nothing if the same one is kept. + */ + config?(xhr: XMLHttpRequest, options: MithrilXHROptions<T>): any; + + /** + * For JSONP requests, this must be the string "jsonp". Otherwise, it's + * ignored. + */ + dataType?: string; + + /** + * For JSONP requests, this is the query string key for the JSONP + * request. This is useful for APIs that don't use common conventions, + * such as `www.example.com/?jsonpCallback=doSomething`. It defaults to + * `callback` for JSONP requests, and is ignored for any other kind of + * request. + */ + callbackKey?: string; + } +} + +declare var Mithril: _mithril.MithrilStatic; +declare var m: _mithril.MithrilStatic; + +declare module "mithril" { + export = m; +} diff --git a/extension/lib/module-trampoline.js b/extension/lib/module-trampoline.js index 4cdc9aab3..22173aa42 100644 --- a/extension/lib/module-trampoline.js +++ b/extension/lib/module-trampoline.js @@ -30,6 +30,16 @@ System.config({ defaultJSExtensions: true, }); + +// Register mithril as a module, +// but only if it is ambient. +if (m) { + let mod = System.newModule({default: m}); + let modName = "mithril"; + System.set(modName, mod); +} + + let me = window.location.protocol + "//" + window.location.host + window.location.pathname.replace(/[.]html$/, ".js"); diff --git a/extension/lib/vendor/mithril.js b/extension/lib/vendor/mithril.js index 89652690a..d023ac34c 100644 --- a/extension/lib/vendor/mithril.js +++ b/extension/lib/vendor/mithril.js @@ -1,1406 +1,2132 @@ -var m = (function app(window, undefined) {
- "use strict";
- var VERSION = "v0.2.2-rc.1";
- function isFunction(object) {
- return typeof object === "function";
- }
- function isObject(object) {
- return type.call(object) === "[object Object]";
- }
- function isString(object) {
- return type.call(object) === "[object String]";
- }
- var isArray = Array.isArray || function (object) {
- return type.call(object) === "[object Array]";
- };
- var type = {}.toString;
- var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
- var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
- var noop = function () {};
-
- // caching commonly used variables
- var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
-
- // self invoking function needed because of the way mocks work
- function initialize(window) {
- $document = window.document;
- $location = window.location;
- $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout;
- $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout;
- }
-
- initialize(window);
-
- m.version = function() {
- return VERSION;
- };
-
- /**
- * @typedef {String} Tag
- * A string that looks like -> div.classname#id[param=one][param2=two]
- * Which describes a DOM node
- */
-
- /**
- *
- * @param {Tag} The DOM node tag
- * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs
- * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional)
- *
- */
- function m(tag, pairs) {
- for (var args = [], i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i];
- }
- if (isObject(tag)) return parameterize(tag, args);
- var hasAttrs = pairs != null && isObject(pairs) && !("tag" in pairs || "view" in pairs || "subtree" in pairs);
- var attrs = hasAttrs ? pairs : {};
- var classAttrName = "class" in attrs ? "class" : "className";
- var cell = {tag: "div", attrs: {}};
- var match, classes = [];
- if (!isString(tag)) throw new Error("selector in m(selector, attrs, children) should be a string");
- while ((match = parser.exec(tag)) != null) {
- if (match[1] === "" && match[2]) cell.tag = match[2];
- else if (match[1] === "#") cell.attrs.id = match[2];
- else if (match[1] === ".") classes.push(match[2]);
- else if (match[3][0] === "[") {
- var pair = attrParser.exec(match[3]);
- cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true);
- }
- }
-
- var children = hasAttrs ? args.slice(1) : args;
- if (children.length === 1 && isArray(children[0])) {
- cell.children = children[0];
- }
- else {
- cell.children = children;
- }
-
- for (var attrName in attrs) {
- if (attrs.hasOwnProperty(attrName)) {
- if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") {
- classes.push(attrs[attrName]);
- cell.attrs[attrName] = ""; //create key in correct iteration order
- }
- else cell.attrs[attrName] = attrs[attrName];
- }
- }
- if (classes.length) cell.attrs[classAttrName] = classes.join(" ");
-
- return cell;
- }
- function forEach(list, f) {
- for (var i = 0; i < list.length && !f(list[i], i++);) {}
- }
- function forKeys(list, f) {
- forEach(list, function (attrs, i) {
- return (attrs = attrs && attrs.attrs) && attrs.key != null && f(attrs, i);
- });
- }
- // This function was causing deopts in Chrome.
- function dataToString(data) {
- //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version)
- try {
- if (data == null || data.toString() == null) return "";
- } catch (e) {
- return "";
- }
- return data;
- }
- // This function was causing deopts in Chrome.
- function injectTextNode(parentElement, first, index, data) {
- try {
- insertNode(parentElement, first, index);
- first.nodeValue = data;
- } catch (e) {} //IE erroneously throws error when appending an empty text node after a null
- }
-
- function flatten(list) {
- //recursively flatten array
- for (var i = 0; i < list.length; i++) {
- if (isArray(list[i])) {
- list = list.concat.apply([], list);
- //check current index again and flatten until there are no more nested arrays at that index
- i--;
- }
- }
- return list;
- }
-
- function insertNode(parentElement, node, index) {
- parentElement.insertBefore(node, parentElement.childNodes[index] || null);
- }
-
- var DELETION = 1, INSERTION = 2, MOVE = 3;
-
- function handleKeysDiffer(data, existing, cached, parentElement) {
- forKeys(data, function (key, i) {
- existing[key = key.key] = existing[key] ? {
- action: MOVE,
- index: i,
- from: existing[key].index,
- element: cached.nodes[existing[key].index] || $document.createElement("div")
- } : {action: INSERTION, index: i};
- });
- var actions = [];
- for (var prop in existing) actions.push(existing[prop]);
- var changes = actions.sort(sortChanges), newCached = new Array(cached.length);
- newCached.nodes = cached.nodes.slice();
-
- forEach(changes, function (change) {
- var index = change.index;
- if (change.action === DELETION) {
- clear(cached[index].nodes, cached[index]);
- newCached.splice(index, 1);
- }
- if (change.action === INSERTION) {
- var dummy = $document.createElement("div");
- dummy.key = data[index].attrs.key;
- insertNode(parentElement, dummy, index);
- newCached.splice(index, 0, {
- attrs: {key: data[index].attrs.key},
- nodes: [dummy]
- });
- newCached.nodes[index] = dummy;
- }
-
- if (change.action === MOVE) {
- var changeElement = change.element;
- var maybeChanged = parentElement.childNodes[index];
- if (maybeChanged !== changeElement && changeElement !== null) {
- parentElement.insertBefore(changeElement, maybeChanged || null);
- }
- newCached[index] = cached[change.from];
- newCached.nodes[index] = changeElement;
- }
- });
-
- return newCached;
- }
-
- function diffKeys(data, cached, existing, parentElement) {
- var keysDiffer = data.length !== cached.length;
- if (!keysDiffer) {
- forKeys(data, function (attrs, i) {
- var cachedCell = cached[i];
- return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key;
- });
- }
-
- return keysDiffer ? handleKeysDiffer(data, existing, cached, parentElement) : cached;
- }
-
- function diffArray(data, cached, nodes) {
- //diff the array itself
-
- //update the list of DOM nodes by collecting the nodes from each item
- forEach(data, function (_, i) {
- if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes);
- })
- //remove items from the end of the array if the new array is shorter than the old one. if errors ever happen here, the issue is most likely
- //a bug in the construction of the `cached` data structure somewhere earlier in the program
- forEach(cached.nodes, function (node, i) {
- if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]);
- })
- if (data.length < cached.length) cached.length = data.length;
- cached.nodes = nodes;
- }
-
- function buildArrayKeys(data) {
- var guid = 0;
- forKeys(data, function () {
- forEach(data, function (attrs) {
- if ((attrs = attrs && attrs.attrs) && attrs.key == null) attrs.key = "__mithril__" + guid++;
- })
- return 1;
- });
- }
-
- function maybeRecreateObject(data, cached, dataAttrKeys) {
- //if an element is different enough from the one in cache, recreate it
- if (data.tag !== cached.tag ||
- dataAttrKeys.sort().join() !== Object.keys(cached.attrs).sort().join() ||
- data.attrs.id !== cached.attrs.id ||
- data.attrs.key !== cached.attrs.key ||
- (m.redraw.strategy() === "all" && (!cached.configContext || cached.configContext.retain !== true)) ||
- (m.redraw.strategy() === "diff" && cached.configContext && cached.configContext.retain === false)) {
- if (cached.nodes.length) clear(cached.nodes);
- if (cached.configContext && isFunction(cached.configContext.onunload)) cached.configContext.onunload();
- if (cached.controllers) {
- forEach(cached.controllers, function (controller) {
- if (controller.unload) controller.onunload({preventDefault: noop});
- });
- }
- }
- }
-
- function getObjectNamespace(data, namespace) {
- return data.attrs.xmlns ? data.attrs.xmlns :
- data.tag === "svg" ? "http://www.w3.org/2000/svg" :
- data.tag === "math" ? "http://www.w3.org/1998/Math/MathML" :
- namespace;
- }
-
- function unloadCachedControllers(cached, views, controllers) {
- if (controllers.length) {
- cached.views = views;
- cached.controllers = controllers;
- forEach(controllers, function (controller) {
- if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old;
- if (pendingRequests && controller.onunload) {
- var onunload = controller.onunload;
- controller.onunload = noop;
- controller.onunload.$old = onunload;
- }
- });
- }
- }
-
- function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) {
- //schedule configs to be called. They are called after `build`
- //finishes running
- if (isFunction(data.attrs.config)) {
- var context = cached.configContext = cached.configContext || {};
-
- //bind
- configs.push(function() {
- return data.attrs.config.call(data, node, !isNew, context, cached);
- });
- }
- }
-
- function buildUpdatedNode(cached, data, editable, hasKeys, namespace, views, configs, controllers) {
- var node = cached.nodes[0];
- if (hasKeys) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace);
- cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs);
- cached.nodes.intact = true;
-
- if (controllers.length) {
- cached.views = views;
- cached.controllers = controllers;
- }
-
- return node;
- }
-
- function handleNonexistentNodes(data, parentElement, index) {
- var nodes;
- if (data.$trusted) {
- nodes = injectHTML(parentElement, index, data);
- }
- else {
- nodes = [$document.createTextNode(data)];
- if (!parentElement.nodeName.match(voidElements)) insertNode(parentElement, nodes[0], index);
- }
-
- var cached = typeof data === "string" || typeof data === "number" || typeof data === "boolean" ? new data.constructor(data) : data;
- cached.nodes = nodes;
- return cached;
- }
-
- function reattachNodes(data, cached, parentElement, editable, index, parentTag) {
- var nodes = cached.nodes;
- if (!editable || editable !== $document.activeElement) {
- if (data.$trusted) {
- clear(nodes, cached)
- nodes = injectHTML(parentElement, index, data)
- } else if (parentTag === "textarea") {
- // <textarea> uses `value` instead of `nodeValue`.
- parentElement.value = data
- } else if (editable) {
- // contenteditable nodes use `innerHTML` instead of `nodeValue`.
- editable.innerHTML = data
- } else {
- // was a trusted string
- if (nodes[0].nodeType === 1 || nodes.length > 1 || (nodes[0].nodeValue.trim && !nodes[0].nodeValue.trim())) {
- clear(cached.nodes, cached)
- nodes = [$document.createTextNode(data)]
- }
- injectTextNode(parentElement, nodes[0], index, data);
- }
- }
- cached = new data.constructor(data);
- cached.nodes = nodes;
- return cached;
- }
-
- function handleText(cached, data, index, parentElement, shouldReattach, editable, parentTag) {
- //handle text nodes
- return cached.nodes.length === 0 ? handleNonexistentNodes(data, parentElement, index) :
- cached.valueOf() !== data.valueOf() || shouldReattach === true ?
- reattachNodes(data, cached, parentElement, editable, index, parentTag) :
- (cached.nodes.intact = true, cached);
- }
-
- function getSubArrayCount(item) {
- if (item.$trusted) {
- //fix offset of next element if item was a trusted string w/ more than one html element
- //the first clause in the regexp matches elements
- //the second clause (after the pipe) matches text nodes
- var match = item.match(/<[^\/]|\>\s*[^<]/g);
- if (match != null) return match.length;
- }
- else if (isArray(item)) {
- return item.length;
- }
- return 1;
- }
-
- function buildArray(data, cached, parentElement, index, parentTag, shouldReattach, editable, namespace, configs) {
- data = flatten(data);
- var nodes = [], intact = cached.length === data.length, subArrayCount = 0;
-
- //keys algorithm: sort elements without recreating them if keys are present
- //1) create a map of all existing keys, and mark all for deletion
- //2) add new keys to map and mark them for addition
- //3) if key exists in new list, change action from deletion to a move
- //4) for each key, handle its corresponding action as marked in previous steps
- var existing = {}, shouldMaintainIdentities = false;
- forKeys(cached, function (attrs, i) {
- shouldMaintainIdentities = true;
- existing[cached[i].attrs.key] = {action: DELETION, index: i};
- });
-
- buildArrayKeys(data);
- if (shouldMaintainIdentities) cached = diffKeys(data, cached, existing, parentElement);
- //end key algorithm
-
- var cacheCount = 0;
- //faster explicitly written
- for (var i = 0, len = data.length; i < len; i++) {
- //diff each item in the array
- var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs);
-
- if (item !== undefined) {
- intact = intact && item.nodes.intact;
- subArrayCount += getSubArrayCount(item);
- cached[cacheCount++] = item;
- }
- }
-
- if (!intact) diffArray(data, cached, nodes);
- return cached
- }
-
- function makeCache(data, cached, index, parentIndex, parentCache) {
- if (cached != null) {
- if (type.call(cached) === type.call(data)) return cached;
-
- if (parentCache && parentCache.nodes) {
- var offset = index - parentIndex, end = offset + (isArray(data) ? data : cached.nodes).length;
- clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end));
- } else if (cached.nodes) {
- clear(cached.nodes, cached);
- }
- }
-
- cached = new data.constructor();
- //if constructor creates a virtual dom element, use a blank object
- //as the base cached node instead of copying the virtual el (#277)
- if (cached.tag) cached = {};
- cached.nodes = [];
- return cached;
- }
-
- function constructNode(data, namespace) {
- return namespace === undefined ?
- data.attrs.is ? $document.createElement(data.tag, data.attrs.is) : $document.createElement(data.tag) :
- data.attrs.is ? $document.createElementNS(namespace, data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag);
- }
-
- function constructAttrs(data, node, namespace, hasKeys) {
- return hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs;
- }
-
- function constructChildren(data, node, cached, editable, namespace, configs) {
- return data.children != null && data.children.length > 0 ?
- build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) :
- data.children;
- }
-
- function reconstructCached(data, attrs, children, node, namespace, views, controllers) {
- var cached = {tag: data.tag, attrs: attrs, children: children, nodes: [node]};
- unloadCachedControllers(cached, views, controllers);
- if (cached.children && !cached.children.nodes) cached.children.nodes = [];
- //edge case: setting value on <select> doesn't work before children exist, so set it again after children have been created
- if (data.tag === "select" && "value" in data.attrs) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace);
- return cached
- }
-
- function getController(views, view, cachedControllers, controller) {
- var controllerIndex = m.redraw.strategy() === "diff" && views ? views.indexOf(view) : -1;
- return controllerIndex > -1 ? cachedControllers[controllerIndex] :
- typeof controller === "function" ? new controller() : {};
- }
-
- function updateLists(views, controllers, view, controller) {
- if (controller.onunload != null) unloaders.push({controller: controller, handler: controller.onunload});
- views.push(view);
- controllers.push(controller);
- }
-
- function checkView(data, view, cached, cachedControllers, controllers, views) {
- var controller = getController(cached.views, view, cachedControllers, data.controller);
- //Faster to coerce to number and check for NaN
- var key = +(data && data.attrs && data.attrs.key);
- data = pendingRequests === 0 || forcing || cachedControllers && cachedControllers.indexOf(controller) > -1 ? data.view(controller) : {tag: "placeholder"};
- if (data.subtree === "retain") return data;
- if (key === key) (data.attrs = data.attrs || {}).key = key;
- updateLists(views, controllers, view, controller);
- return data;
- }
-
- function markViews(data, cached, views, controllers) {
- var cachedControllers = cached && cached.controllers;
- while (data.view != null) data = checkView(data, data.view.$original || data.view, cached, cachedControllers, controllers, views);
- return data;
- }
-
- function buildObject(data, cached, editable, parentElement, index, shouldReattach, namespace, configs) {
- var views = [], controllers = [];
- data = markViews(data, cached, views, controllers);
- if (data.subtree === "retain") return cached;
- if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.");
- data.attrs = data.attrs || {};
- cached.attrs = cached.attrs || {};
- var dataAttrKeys = Object.keys(data.attrs);
- var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0);
- maybeRecreateObject(data, cached, dataAttrKeys);
- if (!isString(data.tag)) return;
- var isNew = cached.nodes.length === 0;
- namespace = getObjectNamespace(data, namespace);
- var node;
- if (isNew) {
- node = constructNode(data, namespace);
- //set attributes first, then create children
- var attrs = constructAttrs(data, node, namespace, hasKeys)
- var children = constructChildren(data, node, cached, editable, namespace, configs);
- cached = reconstructCached(data, attrs, children, node, namespace, views, controllers);
- }
- else {
- node = buildUpdatedNode(cached, data, editable, hasKeys, namespace, views, configs, controllers);
- }
- if (isNew || shouldReattach === true && node != null) insertNode(parentElement, node, index);
- //schedule configs to be called. They are called after `build`
- //finishes running
- scheduleConfigsToBeCalled(configs, data, node, isNew, cached);
- return cached
- }
-
- function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
- //`build` is a recursive function that manages creation/diffing/removal
- //of DOM elements based on comparison between `data` and `cached`
- //the diff algorithm can be summarized as this:
- //1 - compare `data` and `cached`
- //2 - if they are different, copy `data` to `cached` and update the DOM
- // based on what the difference is
- //3 - recursively apply this algorithm for every array and for the
- // children of every virtual element
-
- //the `cached` data structure is essentially the same as the previous
- //redraw's `data` data structure, with a few additions:
- //- `cached` always has a property called `nodes`, which is a list of
- // DOM elements that correspond to the data represented by the
- // respective virtual element
- //- in order to support attaching `nodes` as a property of `cached`,
- // `cached` is *always* a non-primitive object, i.e. if the data was
- // a string, then cached is a String instance. If data was `null` or
- // `undefined`, cached is `new String("")`
- //- `cached also has a `configContext` property, which is the state
- // storage object exposed by config(element, isInitialized, context)
- //- when `cached` is an Object, it represents a virtual element; when
- // it's an Array, it represents a list of elements; when it's a
- // String, Number or Boolean, it represents a text node
-
- //`parentElement` is a DOM element used for W3C DOM API calls
- //`parentTag` is only used for handling a corner case for textarea
- //values
- //`parentCache` is used to remove nodes in some multi-node cases
- //`parentIndex` and `index` are used to figure out the offset of nodes.
- //They're artifacts from before arrays started being flattened and are
- //likely refactorable
- //`data` and `cached` are, respectively, the new and old nodes being
- //diffed
- //`shouldReattach` is a flag indicating whether a parent node was
- //recreated (if so, and if this node is reused, then this node must
- //reattach itself to the new parent)
- //`editable` is a flag that indicates whether an ancestor is
- //contenteditable
- //`namespace` indicates the closest HTML namespace as it cascades down
- //from an ancestor
- //`configs` is a list of config functions to run after the topmost
- //`build` call finishes running
-
- //there's logic that relies on the assumption that null and undefined
- //data are equivalent to empty strings
- //- this prevents lifecycle surprises from procedural helpers that mix
- // implicit and explicit return statements (e.g.
- // function foo() {if (cond) return m("div")}
- //- it simplifies diffing code
- data = dataToString(data);
- if (data.subtree === "retain") return cached;
- cached = makeCache(data, cached, index, parentIndex, parentCache);
- return isArray(data) ? buildArray(data, cached, parentElement, index, parentTag, shouldReattach, editable, namespace, configs) :
- data != null && isObject(data) ? buildObject(data, cached, editable, parentElement, index, shouldReattach, namespace, configs) :
- !isFunction(data) ? handleText(cached, data, index, parentElement, shouldReattach, editable, parentTag) :
- cached;
- }
- function sortChanges(a, b) { return a.action - b.action || a.index - b.index; }
- function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) {
- for (var attrName in dataAttrs) {
- var dataAttr = dataAttrs[attrName];
- var cachedAttr = cachedAttrs[attrName];
- if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr)) {
- cachedAttrs[attrName] = dataAttr;
- try {
- //`config` isn't a real attribute, so ignore it
- if (attrName === "config" || attrName === "key") continue;
- //hook event handlers to the auto-redrawing system
- else if (isFunction(dataAttr) && attrName.slice(0, 2) === "on") {
- node[attrName] = autoredraw(dataAttr, node);
- }
- //handle `style: {...}`
- else if (attrName === "style" && dataAttr != null && isObject(dataAttr)) {
- for (var rule in dataAttr) {
- if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule];
- }
- for (var rule in cachedAttr) {
- if (!(rule in dataAttr)) node.style[rule] = "";
- }
- }
- //handle SVG
- else if (namespace != null) {
- if (attrName === "href") node.setAttributeNS("http://www.w3.org/1999/xlink", "href", dataAttr);
- else node.setAttribute(attrName === "className" ? "class" : attrName, dataAttr);
- }
- //handle cases that are properties (but ignore cases where we should use setAttribute instead)
- //- list and form are typically used as strings, but are DOM element references in js
- //- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
- else if (attrName in node && attrName !== "list" && attrName !== "style" && attrName !== "form" && attrName !== "type" && attrName !== "width" && attrName !== "height") {
- //#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
- try {
- if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr;
- } catch (e) {
- node.setAttribute(attrName, dataAttr);
- }
- }
- else node.setAttribute(attrName, dataAttr);
- }
- catch (e) {
- //swallow IE's invalid argument errors to mimic HTML's fallback-to-doing-nothing-on-invalid-attributes behavior
- if (e.message.indexOf("Invalid argument") < 0) throw e;
- }
- }
- //#348 dataAttr may not be a string, so use loose comparison (double equal) instead of strict (triple equal)
- else if (attrName === "value" && tag === "input" && node.value != dataAttr) {
- node.value = dataAttr;
- }
- }
- return cachedAttrs;
- }
- function clear(nodes, cached) {
- for (var i = nodes.length - 1; i > -1; i--) {
- if (nodes[i] && nodes[i].parentNode) {
- try { nodes[i].parentNode.removeChild(nodes[i]); }
- catch (e) {} //ignore if this fails due to order of events (see http://stackoverflow.com/questions/21926083/failed-to-execute-removechild-on-node)
- cached = [].concat(cached);
- if (cached[i]) unload(cached[i]);
- }
- }
- //release memory if nodes is an array. This check should fail if nodes is a NodeList (see loop above)
- if (nodes.length) nodes.length = 0;
- }
- function unload(cached) {
- if (cached.configContext && isFunction(cached.configContext.onunload)) {
- cached.configContext.onunload();
- cached.configContext.onunload = null;
- }
- if (cached.controllers) {
- forEach(cached.controllers, function (controller) {
- if (isFunction(controller.onunload)) controller.onunload({preventDefault: noop});
- });
- }
- if (cached.children) {
- if (isArray(cached.children)) forEach(cached.children, unload);
- else if (cached.children.tag) unload(cached.children);
- }
- }
- function appendTextFragment(parentElement, data) {
- try {
- parentElement.appendChild($document.createRange().createContextualFragment(data));
- } catch (e) {
- parentElement.insertAdjacentHTML("beforeend", data);
- }
- }
- function injectHTML(parentElement, index, data) {
- var nextSibling = parentElement.childNodes[index];
- if (nextSibling) {
- var isElement = nextSibling.nodeType !== 1;
- var placeholder = $document.createElement("span");
- if (isElement) {
- parentElement.insertBefore(placeholder, nextSibling || null);
- placeholder.insertAdjacentHTML("beforebegin", data);
- parentElement.removeChild(placeholder);
- }
- else nextSibling.insertAdjacentHTML("beforebegin", data);
- }
- else {
- appendTextFragment(parentElement, data);
- }
- var nodes = [];
- while (parentElement.childNodes[index] !== nextSibling) {
- nodes.push(parentElement.childNodes[index]);
- index++;
- }
- return nodes;
- }
- function autoredraw(callback, object) {
- return function(e) {
- e = e || event;
- m.redraw.strategy("diff");
- m.startComputation();
- try { return callback.call(object, e); }
- finally {
- endFirstComputation();
- }
- };
- }
-
- var html;
- var documentNode = {
- appendChild: function(node) {
- if (html === undefined) html = $document.createElement("html");
- if ($document.documentElement && $document.documentElement !== node) {
- $document.replaceChild(node, $document.documentElement);
- }
- else $document.appendChild(node);
- this.childNodes = $document.childNodes;
- },
- insertBefore: function(node) {
- this.appendChild(node);
- },
- childNodes: []
- };
- var nodeCache = [], cellCache = {};
- m.render = function(root, cell, forceRecreation) {
- var configs = [];
- if (!root) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");
- var id = getCellCacheKey(root);
- var isDocumentRoot = root === $document;
- var node = isDocumentRoot || root === $document.documentElement ? documentNode : root;
- if (isDocumentRoot && cell.tag !== "html") cell = {tag: "html", attrs: {}, children: cell};
- if (cellCache[id] === undefined) clear(node.childNodes);
- if (forceRecreation === true) reset(root);
- cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs);
- forEach(configs, function (config) { config(); });
- };
- function getCellCacheKey(element) {
- var index = nodeCache.indexOf(element);
- return index < 0 ? nodeCache.push(element) - 1 : index;
- }
-
- m.trust = function(value) {
- value = new String(value);
- value.$trusted = true;
- return value;
- };
-
- function gettersetter(store) {
- var prop = function() {
- if (arguments.length) store = arguments[0];
- return store;
- };
-
- prop.toJSON = function() {
- return store;
- };
-
- return prop;
- }
-
- m.prop = function (store) {
- //note: using non-strict equality check here because we're checking if store is null OR undefined
- if ((store != null && isObject(store) || isFunction(store)) && isFunction(store.then)) {
- return propify(store);
- }
-
- return gettersetter(store);
- };
-
- var roots = [], components = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePreRedrawHook = null, computePostRedrawHook = null, topComponent, unloaders = [];
- var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms
- function parameterize(component, args) {
- var controller = function() {
- return (component.controller || noop).apply(this, args) || this;
- };
- if (component.controller) controller.prototype = component.controller.prototype;
- var view = function(ctrl) {
- var currentArgs = arguments.length > 1 ? args.concat([].slice.call(arguments, 1)) : args;
- return component.view.apply(component, currentArgs ? [ctrl].concat(currentArgs) : [ctrl]);
- };
- view.$original = component.view;
- var output = {controller: controller, view: view};
- if (args[0] && args[0].key != null) output.attrs = {key: args[0].key};
- return output;
- }
- m.component = function(component) {
- for (var args = [], i = 1; i < arguments.length; i++) args.push(arguments[i]);
- return parameterize(component, args);
- };
- m.mount = m.module = function(root, component) {
- if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
- var index = roots.indexOf(root);
- if (index < 0) index = roots.length;
-
- var isPrevented = false;
- var event = {preventDefault: function() {
- isPrevented = true;
- computePreRedrawHook = computePostRedrawHook = null;
- }};
-
- forEach(unloaders, function (unloader) {
- unloader.handler.call(unloader.controller, event);
- unloader.controller.onunload = null;
- });
-
- if (isPrevented) {
- forEach(unloaders, function (unloader) {
- unloader.controller.onunload = unloader.handler;
- });
- }
- else unloaders = [];
-
- if (controllers[index] && isFunction(controllers[index].onunload)) {
- controllers[index].onunload(event);
- }
-
- var isNullComponent = component === null;
-
- if (!isPrevented) {
- m.redraw.strategy("all");
- m.startComputation();
- roots[index] = root;
- var currentComponent = component ? (topComponent = component) : (topComponent = component = {controller: noop});
- var controller = new (component.controller || noop)();
- //controllers may call m.mount recursively (via m.route redirects, for example)
- //this conditional ensures only the last recursive m.mount call is applied
- if (currentComponent === topComponent) {
- controllers[index] = controller;
- components[index] = component;
- }
- endFirstComputation();
- if (isNullComponent) {
- removeRootElement(root, index);
- }
- return controllers[index];
- }
- if (isNullComponent) {
- removeRootElement(root, index);
- }
- };
-
- function removeRootElement(root, index) {
- roots.splice(index, 1);
- controllers.splice(index, 1);
- components.splice(index, 1);
- reset(root);
- nodeCache.splice(getCellCacheKey(root), 1);
- }
-
- var redrawing = false, forcing = false;
- m.redraw = function(force) {
- if (redrawing) return;
- redrawing = true;
- if (force) forcing = true;
- try {
- //lastRedrawId is a positive number if a second redraw is requested before the next animation frame
- //lastRedrawID is null if it's the first redraw and not an event handler
- if (lastRedrawId && !force) {
- //when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout
- //when rAF: always reschedule redraw
- if ($requestAnimationFrame === window.requestAnimationFrame || new Date - lastRedrawCallTime > FRAME_BUDGET) {
- if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId);
- lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET);
- }
- }
- else {
- redraw();
- lastRedrawId = $requestAnimationFrame(function() { lastRedrawId = null; }, FRAME_BUDGET);
- }
- }
- finally {
- redrawing = forcing = false;
- }
- };
- m.redraw.strategy = m.prop();
- function redraw() {
- if (computePreRedrawHook) {
- computePreRedrawHook();
- computePreRedrawHook = null;
- }
- forEach(roots, function (root, i) {
- var component = components[i];
- if (controllers[i]) {
- var args = [controllers[i]];
- m.render(root, component.view ? component.view(controllers[i], args) : "");
- }
- });
- //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState
- if (computePostRedrawHook) {
- computePostRedrawHook();
- computePostRedrawHook = null;
- }
- lastRedrawId = null;
- lastRedrawCallTime = new Date;
- m.redraw.strategy("diff");
- }
-
- var pendingRequests = 0;
- m.startComputation = function() { pendingRequests++; };
- m.endComputation = function() {
- if (pendingRequests > 1) pendingRequests--;
- else {
- pendingRequests = 0;
- m.redraw();
- }
- }
-
- function endFirstComputation() {
- if (m.redraw.strategy() === "none") {
- pendingRequests--;
- m.redraw.strategy("diff");
- }
- else m.endComputation();
- }
-
- m.withAttr = function(prop, withAttrCallback, callbackThis) {
- return function(e) {
- e = e || event;
- var currentTarget = e.currentTarget || this;
- var _this = callbackThis || this;
- withAttrCallback.call(_this, prop in currentTarget ? currentTarget[prop] : currentTarget.getAttribute(prop));
- };
- };
-
- //routing
- var modes = {pathname: "", hash: "#", search: "?"};
- var redirect = noop, routeParams, currentRoute, isDefaultRoute = false;
- m.route = function(root, arg1, arg2, vdom) {
- //m.route()
- if (arguments.length === 0) return currentRoute;
- //m.route(el, defaultRoute, routes)
- else if (arguments.length === 3 && isString(arg1)) {
- redirect = function(source) {
- var path = currentRoute = normalizeRoute(source);
- if (!routeByValue(root, arg2, path)) {
- if (isDefaultRoute) throw new Error("Ensure the default route matches one of the routes defined in m.route");
- isDefaultRoute = true;
- m.route(arg1, true);
- isDefaultRoute = false;
- }
- };
- var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate";
- window[listener] = function() {
- var path = $location[m.route.mode];
- if (m.route.mode === "pathname") path += $location.search;
- if (currentRoute !== normalizeRoute(path)) redirect(path);
- };
-
- computePreRedrawHook = setScroll;
- window[listener]();
- }
- //config: m.route
- else if (root.addEventListener || root.attachEvent) {
- root.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + vdom.attrs.href;
- if (root.addEventListener) {
- root.removeEventListener("click", routeUnobtrusive);
- root.addEventListener("click", routeUnobtrusive);
- }
- else {
- root.detachEvent("onclick", routeUnobtrusive);
- root.attachEvent("onclick", routeUnobtrusive);
- }
- }
- //m.route(route, params, shouldReplaceHistoryEntry)
- else if (isString(root)) {
- var oldRoute = currentRoute;
- currentRoute = root;
- var args = arg1 || {};
- var queryIndex = currentRoute.indexOf("?");
- var params = queryIndex > -1 ? parseQueryString(currentRoute.slice(queryIndex + 1)) : {};
- for (var i in args) params[i] = args[i];
- var querystring = buildQueryString(params);
- var currentPath = queryIndex > -1 ? currentRoute.slice(0, queryIndex) : currentRoute;
- if (querystring) currentRoute = currentPath + (currentPath.indexOf("?") === -1 ? "?" : "&") + querystring;
-
- var shouldReplaceHistoryEntry = (arguments.length === 3 ? arg2 : arg1) === true || oldRoute === root;
-
- if (window.history.pushState) {
- computePreRedrawHook = setScroll;
- computePostRedrawHook = function() {
- window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
- };
- redirect(modes[m.route.mode] + currentRoute);
- }
- else {
- $location[m.route.mode] = currentRoute;
- redirect(modes[m.route.mode] + currentRoute);
- }
- }
- };
- m.route.param = function(key) {
- if (!routeParams) throw new Error("You must call m.route(element, defaultRoute, routes) before calling m.route.param()");
- if( !key ){
- return routeParams;
- }
- return routeParams[key];
- };
- m.route.mode = "search";
- function normalizeRoute(route) {
- return route.slice(modes[m.route.mode].length);
- }
- function routeByValue(root, router, path) {
- routeParams = {};
-
- var queryStart = path.indexOf("?");
- if (queryStart !== -1) {
- routeParams = parseQueryString(path.substr(queryStart + 1, path.length));
- path = path.substr(0, queryStart);
- }
-
- // Get all routes and check if there's
- // an exact match for the current path
- var keys = Object.keys(router);
- var index = keys.indexOf(path);
- if(index !== -1){
- m.mount(root, router[keys [index]]);
- return true;
- }
-
- for (var route in router) {
- if (route === path) {
- m.mount(root, router[route]);
- return true;
- }
-
- var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$");
-
- if (matcher.test(path)) {
- path.replace(matcher, function() {
- var keys = route.match(/:[^\/]+/g) || [];
- var values = [].slice.call(arguments, 1, -2);
- forEach(keys, function (key, i) {
- routeParams[key.replace(/:|\./g, "")] = decodeURIComponent(values[i]);
- })
- m.mount(root, router[route]);
- });
- return true;
- }
- }
- }
- function routeUnobtrusive(e) {
- e = e || event;
- if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return;
-
- if (e.preventDefault) e.preventDefault();
- else e.returnValue = false;
-
- var currentTarget = e.currentTarget || e.srcElement;
- var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
- while (currentTarget && currentTarget.nodeName.toUpperCase() !== "A") currentTarget = currentTarget.parentNode;
- // clear pendingRequests because we want an immediate route change
- pendingRequests = 0;
- m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args);
- }
- function setScroll() {
- if (m.route.mode !== "hash" && $location.hash) $location.hash = $location.hash;
- else window.scrollTo(0, 0);
- }
- function buildQueryString(object, prefix) {
- var duplicates = {};
- var str = [];
- for (var prop in object) {
- var key = prefix ? prefix + "[" + prop + "]" : prop;
- var value = object[prop];
-
- if (value === null) {
- str.push(encodeURIComponent(key));
- } else if (isObject(value)) {
- str.push(buildQueryString(value, key));
- } else if (isArray(value)) {
- var keys = [];
- duplicates[key] = duplicates[key] || {};
- forEach(value, function (item) {
- if (!duplicates[key][item]) {
- duplicates[key][item] = true;
- keys.push(encodeURIComponent(key) + "=" + encodeURIComponent(item));
- }
- });
- str.push(keys.join("&"));
- } else if (value !== undefined) {
- str.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
- }
- }
- return str.join("&");
- }
- function parseQueryString(str) {
- if (str === "" || str == null) return {};
- if (str.charAt(0) === "?") str = str.slice(1);
-
- var pairs = str.split("&"), params = {};
- forEach(pairs, function (string) {
- var pair = string.split("=");
- var key = decodeURIComponent(pair[0]);
- var value = pair.length === 2 ? decodeURIComponent(pair[1]) : null;
- if (params[key] != null) {
- if (!isArray(params[key])) params[key] = [params[key]];
- params[key].push(value);
- }
- else params[key] = value;
- });
-
- return params;
- }
- m.route.buildQueryString = buildQueryString;
- m.route.parseQueryString = parseQueryString;
-
- function reset(root) {
- var cacheKey = getCellCacheKey(root);
- clear(root.childNodes, cellCache[cacheKey]);
- cellCache[cacheKey] = undefined;
- }
-
- m.deferred = function () {
- var deferred = new Deferred();
- deferred.promise = propify(deferred.promise);
- return deferred;
- };
- function propify(promise, initialValue) {
- var prop = m.prop(initialValue);
- promise.then(prop);
- prop.then = function(resolve, reject) {
- return propify(promise.then(resolve, reject), initialValue);
- };
- prop["catch"] = prop.then.bind(null, null);
- return prop;
- }
- //Promiz.mithril.js | Zolmeister | MIT
- //a modified version of Promiz.js, which does not conform to Promises/A+ for two reasons:
- //1) `then` callbacks are called synchronously (because setTimeout is too slow, and the setImmediate polyfill is too big
- //2) throwing subclasses of Error cause the error to be bubbled up instead of triggering rejection (because the spec does not account for the important use case of default browser error handling, i.e. message w/ line number)
- function Deferred(successCallback, failureCallback) {
- var RESOLVING = 1, REJECTING = 2, RESOLVED = 3, REJECTED = 4;
- var self = this, state = 0, promiseValue = 0, next = [];
-
- self.promise = {};
-
- self.resolve = function(value) {
- if (!state) {
- promiseValue = value;
- state = RESOLVING;
-
- fire();
- }
- return this;
- };
-
- self.reject = function(value) {
- if (!state) {
- promiseValue = value;
- state = REJECTING;
-
- fire();
- }
- return this;
- };
-
- self.promise.then = function(successCallback, failureCallback) {
- var deferred = new Deferred(successCallback, failureCallback)
- if (state === RESOLVED) {
- deferred.resolve(promiseValue);
- }
- else if (state === REJECTED) {
- deferred.reject(promiseValue);
- }
- else {
- next.push(deferred);
- }
- return deferred.promise
- };
-
- function finish(type) {
- state = type || REJECTED;
- next.map(function(deferred) {
- state === RESOLVED ? deferred.resolve(promiseValue) : deferred.reject(promiseValue);
- });
- }
-
- function thennable(then, successCallback, failureCallback, notThennableCallback) {
- if (((promiseValue != null && isObject(promiseValue)) || isFunction(promiseValue)) && isFunction(then)) {
- try {
- // count protects against abuse calls from spec checker
- var count = 0;
- then.call(promiseValue, function(value) {
- if (count++) return;
- promiseValue = value;
- successCallback();
- }, function (value) {
- if (count++) return;
- promiseValue = value;
- failureCallback();
- });
- }
- catch (e) {
- m.deferred.onerror(e);
- promiseValue = e;
- failureCallback();
- }
- } else {
- notThennableCallback();
- }
- }
-
- function fire() {
- // check if it's a thenable
- var then;
- try {
- then = promiseValue && promiseValue.then;
- }
- catch (e) {
- m.deferred.onerror(e);
- promiseValue = e;
- state = REJECTING;
- return fire();
- }
-
- if (state === REJECTING) {
- m.deferred.onerror(promiseValue)
- }
-
- thennable(then, function () {
- state = RESOLVING
- fire()
- }, function () {
- state = REJECTING
- fire()
- }, function () {
- try {
- if (state === RESOLVING && isFunction(successCallback)) {
- promiseValue = successCallback(promiseValue);
- }
- else if (state === REJECTING && isFunction(failureCallback)) {
- promiseValue = failureCallback(promiseValue);
- state = RESOLVING;
- }
- }
- catch (e) {
- m.deferred.onerror(e);
- promiseValue = e;
- return finish();
- }
-
- if (promiseValue === self) {
- promiseValue = TypeError();
- finish();
- } else {
- thennable(then, function () {
- finish(RESOLVED);
- }, finish, function () {
- finish(state === RESOLVING && RESOLVED);
- });
- }
- });
- }
- }
- m.deferred.onerror = function(e) {
- if (type.call(e) === "[object Error]" && !e.constructor.toString().match(/ Error/)) {
- pendingRequests = 0;
- throw e;
- }
- };
-
- m.sync = function(args) {
- var method = "resolve";
-
- function synchronizer(pos, resolved) {
- return function(value) {
- results[pos] = value;
- if (!resolved) method = "reject";
- if (--outstanding === 0) {
- deferred.promise(results);
- deferred[method](results);
- }
- return value;
- };
- }
-
- var deferred = m.deferred();
- var outstanding = args.length;
- var results = new Array(outstanding);
- if (args.length > 0) {
- forEach(args, function (arg, i) {
- arg.then(synchronizer(i, true), synchronizer(i, false));
- });
- }
- else deferred.resolve([]);
-
- return deferred.promise;
- };
- function identity(value) { return value; }
-
- function ajax(options) {
- if (options.dataType && options.dataType.toLowerCase() === "jsonp") {
- var callbackKey = "mithril_callback_" + new Date().getTime() + "_" + (Math.round(Math.random() * 1e16)).toString(36)
- var script = $document.createElement("script");
-
- window[callbackKey] = function(resp) {
- script.parentNode.removeChild(script);
- options.onload({
- type: "load",
- target: {
- responseText: resp
- }
- });
- window[callbackKey] = undefined;
- };
-
- script.onerror = function() {
- script.parentNode.removeChild(script);
-
- options.onerror({
- type: "error",
- target: {
- status: 500,
- responseText: JSON.stringify({
- error: "Error making jsonp request"
- })
- }
- });
- window[callbackKey] = undefined;
-
- return false;
- }
-
- script.onload = function() {
- return false;
- };
-
- script.src = options.url
- + (options.url.indexOf("?") > 0 ? "&" : "?")
- + (options.callbackKey ? options.callbackKey : "callback")
- + "=" + callbackKey
- + "&" + buildQueryString(options.data || {});
- $document.body.appendChild(script);
- }
- else {
- var xhr = new window.XMLHttpRequest();
- xhr.open(options.method, options.url, true, options.user, options.password);
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4) {
- if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr});
- else options.onerror({type: "error", target: xhr});
- }
- };
- if (options.serialize === JSON.stringify && options.data && options.method !== "GET") {
- xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
- }
- if (options.deserialize === JSON.parse) {
- xhr.setRequestHeader("Accept", "application/json, text/*");
- }
- if (isFunction(options.config)) {
- var maybeXhr = options.config(xhr, options);
- if (maybeXhr != null) xhr = maybeXhr;
- }
-
- var data = options.method === "GET" || !options.data ? "" : options.data;
- if (data && (!isString(data) && data.constructor !== window.FormData)) {
- throw new Error("Request data should be either be a string or FormData. Check the `serialize` option in `m.request`");
- }
- xhr.send(data);
- return xhr;
- }
- }
-
- function bindData(xhrOptions, data, serialize) {
- if (xhrOptions.method === "GET" && xhrOptions.dataType !== "jsonp") {
- var prefix = xhrOptions.url.indexOf("?") < 0 ? "?" : "&";
- var querystring = buildQueryString(data);
- xhrOptions.url = xhrOptions.url + (querystring ? prefix + querystring : "");
- }
- else xhrOptions.data = serialize(data);
- return xhrOptions;
- }
-
- function parameterizeUrl(url, data) {
- var tokens = url.match(/:[a-z]\w+/gi);
- if (tokens && data) {
- forEach(tokens, function (token) {
- var key = token.slice(1);
- url = url.replace(token, data[key]);
- delete data[key];
- });
- }
- return url;
- }
-
- m.request = function(xhrOptions) {
- if (xhrOptions.background !== true) m.startComputation();
- var deferred = new Deferred();
- var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp"
- var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
- var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse;
- var extract = isJSONP ? function(jsonp) { return jsonp.responseText } : xhrOptions.extract || function(xhr) {
- if (xhr.responseText.length === 0 && deserialize === JSON.parse) {
- return null
- } else {
- return xhr.responseText
- }
- };
- xhrOptions.method = (xhrOptions.method || "GET").toUpperCase();
- xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
- xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
- xhrOptions.onload = xhrOptions.onerror = function(e) {
- try {
- e = e || event;
- var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity;
- var response = unwrap(deserialize(extract(e.target, xhrOptions)), e.target);
- if (e.type === "load") {
- if (isArray(response) && xhrOptions.type) {
- forEach(response, function (res, i) {
- response[i] = new xhrOptions.type(res);
- });
- } else if (xhrOptions.type) {
- response = new xhrOptions.type(response);
- }
- deferred.resolve(response)
- } else {
- deferred.reject(response)
- }
-
- deferred[e.type === "load" ? "resolve" : "reject"](response);
- }
- catch (e) {
- deferred.reject(e);
- }
- finally {
- if (xhrOptions.background !== true) m.endComputation()
- }
- }
-
- ajax(xhrOptions);
- deferred.promise = propify(deferred.promise, xhrOptions.initialValue);
- return deferred.promise;
- };
-
- //testing API
- m.deps = function(mock) {
- initialize(window = mock || window);
- return window;
- };
- //for internal testing only, do not use `m.deps.factory`
- m.deps.factory = app;
-
- return m;
-})(typeof window !== "undefined" ? window : {});
-
+;(function (global, factory) { // eslint-disable-line + "use strict" + /* eslint-disable no-undef */ + var m = factory(global) + if (typeof module === "object" && module != null && module.exports) { + module.exports = m + } else if (typeof define === "function" && define.amd) { + define(function () { return m }) + } else { + global.m = m + } + /* eslint-enable no-undef */ +})(typeof window !== "undefined" ? window : {}, function (global, undefined) { // eslint-disable-line + "use strict" + + m.version = function () { + return "v0.2.2-rc.1" + } + + var hasOwn = {}.hasOwnProperty + var type = {}.toString + + function isFunction(object) { + return typeof object === "function" + } + + function isObject(object) { + return type.call(object) === "[object Object]" + } + + function isString(object) { + return type.call(object) === "[object String]" + } + + var isArray = Array.isArray || function (object) { + return type.call(object) === "[object Array]" + } + + function noop() {} + + /* eslint-disable max-len */ + var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ + /* eslint-enable max-len */ + + // caching commonly used variables + var $document, $location, $requestAnimationFrame, $cancelAnimationFrame + + // self invoking function needed because of the way mocks work + function initialize(mock) { + $document = mock.document + $location = mock.location + $cancelAnimationFrame = mock.cancelAnimationFrame || mock.clearTimeout + $requestAnimationFrame = mock.requestAnimationFrame || mock.setTimeout + } + + // testing API + m.deps = function (mock) { + initialize(global = mock || window) + return global + } + + m.deps(global) + + /** + * @typedef {String} Tag + * A string that looks like -> div.classname#id[param=one][param2=two] + * Which describes a DOM node + */ + + function parseTagAttrs(cell, tag) { + var classes = [] + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g + var match + + while ((match = parser.exec(tag))) { + if (match[1] === "" && match[2]) { + cell.tag = match[2] + } else if (match[1] === "#") { + cell.attrs.id = match[2] + } else if (match[1] === ".") { + classes.push(match[2]) + } else if (match[3][0] === "[") { + var pair = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3]) + cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true) + } + } + + return classes + } + + function getVirtualChildren(args, hasAttrs) { + var children = hasAttrs ? args.slice(1) : args + + if (children.length === 1 && isArray(children[0])) { + return children[0] + } else { + return children + } + } + + function assignAttrs(target, attrs, classes) { + var classAttr = "class" in attrs ? "class" : "className" + + for (var attrName in attrs) { + if (hasOwn.call(attrs, attrName)) { + if (attrName === classAttr && + attrs[attrName] != null && + attrs[attrName] !== "") { + classes.push(attrs[attrName]) + // create key in correct iteration order + target[attrName] = "" + } else { + target[attrName] = attrs[attrName] + } + } + } + + if (classes.length) target[classAttr] = classes.join(" ") + } + + /** + * + * @param {Tag} The DOM node tag + * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs + * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, + * or splat (optional) + */ + function m(tag, pairs) { + for (var args = [], i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i] + } + + if (isObject(tag)) return parameterize(tag, args) + + if (!isString(tag)) { + throw new Error("selector in m(selector, attrs, children) should " + + "be a string") + } + + var hasAttrs = pairs != null && isObject(pairs) && + !("tag" in pairs || "view" in pairs || "subtree" in pairs) + + var attrs = hasAttrs ? pairs : {} + var cell = { + tag: "div", + attrs: {}, + children: getVirtualChildren(args, hasAttrs) + } + + assignAttrs(cell.attrs, attrs, parseTagAttrs(cell, tag)) + return cell + } + + function forEach(list, f) { + for (var i = 0; i < list.length && !f(list[i], i++);) { + // function called in condition + } + } + + function forKeys(list, f) { + forEach(list, function (attrs, i) { + return (attrs = attrs && attrs.attrs) && + attrs.key != null && + f(attrs, i) + }) + } + // This function was causing deopts in Chrome. + function dataToString(data) { + // data.toString() might throw or return null if data is the return + // value of Console.log in some versions of Firefox (behavior depends on + // version) + try { + if (data != null && data.toString() != null) return data + } catch (e) { + // silently ignore errors + } + return "" + } + + // This function was causing deopts in Chrome. + function injectTextNode(parentElement, first, index, data) { + try { + insertNode(parentElement, first, index) + first.nodeValue = data + } catch (e) { + // IE erroneously throws error when appending an empty text node + // after a null + } + } + + function flatten(list) { + // recursively flatten array + for (var i = 0; i < list.length; i++) { + if (isArray(list[i])) { + list = list.concat.apply([], list) + // check current index again and flatten until there are no more + // nested arrays at that index + i-- + } + } + return list + } + + function insertNode(parentElement, node, index) { + parentElement.insertBefore(node, + parentElement.childNodes[index] || null) + } + + var DELETION = 1 + var INSERTION = 2 + var MOVE = 3 + + function handleKeysDiffer(data, existing, cached, parentElement) { + forKeys(data, function (key, i) { + existing[key = key.key] = existing[key] ? { + action: MOVE, + index: i, + from: existing[key].index, + element: cached.nodes[existing[key].index] || + $document.createElement("div") + } : {action: INSERTION, index: i} + }) + + var actions = [] + for (var prop in existing) if (hasOwn.call(existing, prop)) { + actions.push(existing[prop]) + } + + var changes = actions.sort(sortChanges) + var newCached = new Array(cached.length) + + newCached.nodes = cached.nodes.slice() + + forEach(changes, function (change) { + var index = change.index + if (change.action === DELETION) { + clear(cached[index].nodes, cached[index]) + newCached.splice(index, 1) + } + if (change.action === INSERTION) { + var dummy = $document.createElement("div") + dummy.key = data[index].attrs.key + insertNode(parentElement, dummy, index) + newCached.splice(index, 0, { + attrs: {key: data[index].attrs.key}, + nodes: [dummy] + }) + newCached.nodes[index] = dummy + } + + if (change.action === MOVE) { + var changeElement = change.element + var maybeChanged = parentElement.childNodes[index] + if (maybeChanged !== changeElement && changeElement !== null) { + parentElement.insertBefore(changeElement, + maybeChanged || null) + } + newCached[index] = cached[change.from] + newCached.nodes[index] = changeElement + } + }) + + return newCached + } + + function diffKeys(data, cached, existing, parentElement) { + var keysDiffer = data.length !== cached.length + + if (!keysDiffer) { + forKeys(data, function (attrs, i) { + var cachedCell = cached[i] + return keysDiffer = cachedCell && + cachedCell.attrs && + cachedCell.attrs.key !== attrs.key + }) + } + + if (keysDiffer) { + return handleKeysDiffer(data, existing, cached, parentElement) + } else { + return cached + } + } + + function diffArray(data, cached, nodes) { + // diff the array itself + + // update the list of DOM nodes by collecting the nodes from each item + forEach(data, function (_, i) { + if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) + }) + // remove items from the end of the array if the new array is shorter + // than the old one. if errors ever happen here, the issue is most + // likely a bug in the construction of the `cached` data structure + // somewhere earlier in the program + forEach(cached.nodes, function (node, i) { + if (node.parentNode != null && nodes.indexOf(node) < 0) { + clear([node], [cached[i]]) + } + }) + + if (data.length < cached.length) cached.length = data.length + cached.nodes = nodes + } + + function buildArrayKeys(data) { + var guid = 0 + forKeys(data, function () { + forEach(data, function (attrs) { + if ((attrs = attrs && attrs.attrs) && attrs.key == null) { + attrs.key = "__mithril__" + guid++ + } + }) + return 1 + }) + } + + function isDifferentEnough(data, cached, dataAttrKeys) { + if (data.tag !== cached.tag) return true + + if (dataAttrKeys.sort().join() !== + Object.keys(cached.attrs).sort().join()) { + return true + } + + if (data.attrs.id !== cached.attrs.id) { + return true + } + + if (data.attrs.key !== cached.attrs.key) { + return true + } + + if (m.redraw.strategy() === "all") { + return !cached.configContext || cached.configContext.retain !== true + } + + if (m.redraw.strategy() === "diff") { + return cached.configContext && cached.configContext.retain === false + } + + return false + } + + function maybeRecreateObject(data, cached, dataAttrKeys) { + // if an element is different enough from the one in cache, recreate it + if (isDifferentEnough(data, cached, dataAttrKeys)) { + if (cached.nodes.length) clear(cached.nodes) + + if (cached.configContext && + isFunction(cached.configContext.onunload)) { + cached.configContext.onunload() + } + + if (cached.controllers) { + forEach(cached.controllers, function (controller) { + if (controller.onunload) controller.onunload({preventDefault: noop}); + }); + } + } + } + + function getObjectNamespace(data, namespace) { + if (data.attrs.xmlns) return data.attrs.xmlns + if (data.tag === "svg") return "http://www.w3.org/2000/svg" + if (data.tag === "math") return "http://www.w3.org/1998/Math/MathML" + return namespace + } + + var pendingRequests = 0 + m.startComputation = function () { pendingRequests++ } + m.endComputation = function () { + if (pendingRequests > 1) { + pendingRequests-- + } else { + pendingRequests = 0 + m.redraw() + } + } + + function unloadCachedControllers(cached, views, controllers) { + if (controllers.length) { + cached.views = views + cached.controllers = controllers + forEach(controllers, function (controller) { + if (controller.onunload && controller.onunload.$old) { + controller.onunload = controller.onunload.$old + } + + if (pendingRequests && controller.onunload) { + var onunload = controller.onunload + controller.onunload = noop + controller.onunload.$old = onunload + } + }) + } + } + + function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) { + // schedule configs to be called. They are called after `build` finishes + // running + if (isFunction(data.attrs.config)) { + var context = cached.configContext = cached.configContext || {} + + // bind + configs.push(function () { + return data.attrs.config.call(data, node, !isNew, context, + cached) + }) + } + } + + function buildUpdatedNode( + cached, + data, + editable, + hasKeys, + namespace, + views, + configs, + controllers + ) { + var node = cached.nodes[0] + + if (hasKeys) { + setAttributes(node, data.tag, data.attrs, cached.attrs, namespace) + } + + cached.children = build( + node, + data.tag, + undefined, + undefined, + data.children, + cached.children, + false, + 0, + data.attrs.contenteditable ? node : editable, + namespace, + configs + ) + + cached.nodes.intact = true + + if (controllers.length) { + cached.views = views + cached.controllers = controllers + } + + return node + } + + function handleNonexistentNodes(data, parentElement, index) { + var nodes + if (data.$trusted) { + nodes = injectHTML(parentElement, index, data) + } else { + nodes = [$document.createTextNode(data)] + if (!parentElement.nodeName.match(voidElements)) { + insertNode(parentElement, nodes[0], index) + } + } + + var cached + + if (typeof data === "string" || + typeof data === "number" || + typeof data === "boolean") { + cached = new data.constructor(data) + } else { + cached = data + } + + cached.nodes = nodes + return cached + } + + function reattachNodes( + data, + cached, + parentElement, + editable, + index, + parentTag + ) { + var nodes = cached.nodes + if (!editable || editable !== $document.activeElement) { + if (data.$trusted) { + clear(nodes, cached) + nodes = injectHTML(parentElement, index, data) + } else if (parentTag === "textarea") { + // <textarea> uses `value` instead of `nodeValue`. + parentElement.value = data + } else if (editable) { + // contenteditable nodes use `innerHTML` instead of `nodeValue`. + editable.innerHTML = data + } else { + // was a trusted string + if (nodes[0].nodeType === 1 || nodes.length > 1 || + (nodes[0].nodeValue.trim && + !nodes[0].nodeValue.trim())) { + clear(cached.nodes, cached) + nodes = [$document.createTextNode(data)] + } + + injectTextNode(parentElement, nodes[0], index, data) + } + } + cached = new data.constructor(data) + cached.nodes = nodes + return cached + } + + function handleTextNode( + cached, + data, + index, + parentElement, + shouldReattach, + editable, + parentTag + ) { + if (!cached.nodes.length) { + return handleNonexistentNodes(data, parentElement, index) + } else if (cached.valueOf() !== data.valueOf() || shouldReattach) { + return reattachNodes(data, cached, parentElement, editable, index, + parentTag) + } else { + return (cached.nodes.intact = true, cached) + } + } + + function getSubArrayCount(item) { + if (item.$trusted) { + // fix offset of next element if item was a trusted string w/ more + // than one html element + // the first clause in the regexp matches elements + // the second clause (after the pipe) matches text nodes + var match = item.match(/<[^\/]|\>\s*[^<]/g) + if (match != null) return match.length + } else if (isArray(item)) { + return item.length + } + return 1 + } + + function buildArray( + data, + cached, + parentElement, + index, + parentTag, + shouldReattach, + editable, + namespace, + configs + ) { + data = flatten(data) + var nodes = [] + var intact = cached.length === data.length + var subArrayCount = 0 + + // keys algorithm: sort elements without recreating them if keys are + // present + // + // 1) create a map of all existing keys, and mark all for deletion + // 2) add new keys to map and mark them for addition + // 3) if key exists in new list, change action from deletion to a move + // 4) for each key, handle its corresponding action as marked in + // previous steps + + var existing = {} + var shouldMaintainIdentities = false + + forKeys(cached, function (attrs, i) { + shouldMaintainIdentities = true + existing[cached[i].attrs.key] = {action: DELETION, index: i} + }) + + buildArrayKeys(data) + if (shouldMaintainIdentities) { + cached = diffKeys(data, cached, existing, parentElement) + } + // end key algorithm + + var cacheCount = 0 + // faster explicitly written + for (var i = 0, len = data.length; i < len; i++) { + // diff each item in the array + var item = build( + parentElement, + parentTag, + cached, + index, + data[i], + cached[cacheCount], + shouldReattach, + index + subArrayCount || subArrayCount, + editable, + namespace, + configs) + + if (item !== undefined) { + intact = intact && item.nodes.intact + subArrayCount += getSubArrayCount(item) + cached[cacheCount++] = item + } + } + + if (!intact) diffArray(data, cached, nodes) + return cached + } + + function makeCache(data, cached, index, parentIndex, parentCache) { + if (cached != null) { + if (type.call(cached) === type.call(data)) return cached + + if (parentCache && parentCache.nodes) { + var offset = index - parentIndex + var end = offset + (isArray(data) ? data : cached.nodes).length + clear( + parentCache.nodes.slice(offset, end), + parentCache.slice(offset, end)) + } else if (cached.nodes) { + clear(cached.nodes, cached) + } + } + + cached = new data.constructor() + // if constructor creates a virtual dom element, use a blank object as + // the base cached node instead of copying the virtual el (#277) + if (cached.tag) cached = {} + cached.nodes = [] + return cached + } + + function constructNode(data, namespace) { + if (data.attrs.is) { + if (namespace == null) { + return $document.createElement(data.tag, data.attrs.is) + } else { + return $document.createElementNS(namespace, data.tag, + data.attrs.is) + } + } else if (namespace == null) { + return $document.createElement(data.tag) + } else { + return $document.createElementNS(namespace, data.tag) + } + } + + function constructAttrs(data, node, namespace, hasKeys) { + if (hasKeys) { + return setAttributes(node, data.tag, data.attrs, {}, namespace) + } else { + return data.attrs + } + } + + function constructChildren( + data, + node, + cached, + editable, + namespace, + configs + ) { + if (data.children != null && data.children.length > 0) { + return build( + node, + data.tag, + undefined, + undefined, + data.children, + cached.children, + true, + 0, + data.attrs.contenteditable ? node : editable, + namespace, + configs) + } else { + return data.children + } + } + + function reconstructCached( + data, + attrs, + children, + node, + namespace, + views, + controllers + ) { + var cached = { + tag: data.tag, + attrs: attrs, + children: children, + nodes: [node] + } + + unloadCachedControllers(cached, views, controllers) + + if (cached.children && !cached.children.nodes) { + cached.children.nodes = [] + } + + // edge case: setting value on <select> doesn't work before children + // exist, so set it again after children have been created + if (data.tag === "select" && "value" in data.attrs) { + setAttributes(node, data.tag, {value: data.attrs.value}, {}, + namespace) + } + + return cached + } + + function getController(views, view, cachedControllers, controller) { + var controllerIndex + + if (m.redraw.strategy() === "diff" && views) { + controllerIndex = views.indexOf(view) + } else { + controllerIndex = -1 + } + + if (controllerIndex > -1) { + return cachedControllers[controllerIndex] + } else if (isFunction(controller)) { + return new controller() + } else { + return {} + } + } + + var unloaders = [] + + function updateLists(views, controllers, view, controller) { + if (controller.onunload != null) { + unloaders.push({ + controller: controller, + handler: controller.onunload + }) + } + + views.push(view) + controllers.push(controller) + } + + var forcing = false + function checkView(data, view, cached, cachedControllers, controllers, views) { + var controller = getController(cached.views, view, cachedControllers, data.controller) + var key = data && data.attrs && data.attrs.key + data = pendingRequests === 0 || forcing || cachedControllers && cachedControllers.indexOf(controller) > -1 ? data.view(controller) : {tag: "placeholder"} + if (data.subtree === "retain") return data; + data.attrs = data.attrs || {} + data.attrs.key = key + updateLists(views, controllers, view, controller) + return data + } + + function markViews(data, cached, views, controllers) { + var cachedControllers = cached && cached.controllers + + while (data.view != null) { + data = checkView( + data, + data.view.$original || data.view, + cached, + cachedControllers, + controllers, + views) + } + + return data + } + + function buildObject( // eslint-disable-line max-statements + data, + cached, + editable, + parentElement, + index, + shouldReattach, + namespace, + configs + ) { + var views = [] + var controllers = [] + + data = markViews(data, cached, views, controllers) + + if (data.subtree === "retain") return cached + + if (!data.tag && controllers.length) { + throw new Error("Component template must return a virtual " + + "element, not an array, string, etc.") + } + + data.attrs = data.attrs || {} + cached.attrs = cached.attrs || {} + + var dataAttrKeys = Object.keys(data.attrs) + var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) + + maybeRecreateObject(data, cached, dataAttrKeys) + + if (!isString(data.tag)) return + + var isNew = cached.nodes.length === 0 + + namespace = getObjectNamespace(data, namespace) + + var node + if (isNew) { + node = constructNode(data, namespace) + // set attributes first, then create children + var attrs = constructAttrs(data, node, namespace, hasKeys) + + var children = constructChildren(data, node, cached, editable, + namespace, configs) + + cached = reconstructCached( + data, + attrs, + children, + node, + namespace, + views, + controllers) + } else { + node = buildUpdatedNode( + cached, + data, + editable, + hasKeys, + namespace, + views, + configs, + controllers) + } + + if (isNew || shouldReattach === true && node != null) { + insertNode(parentElement, node, index) + } + + // The configs are called after `build` finishes running + scheduleConfigsToBeCalled(configs, data, node, isNew, cached) + + return cached + } + + function build( + parentElement, + parentTag, + parentCache, + parentIndex, + data, + cached, + shouldReattach, + index, + editable, + namespace, + configs + ) { + /* + * `build` is a recursive function that manages creation/diffing/removal + * of DOM elements based on comparison between `data` and `cached` the + * diff algorithm can be summarized as this: + * + * 1 - compare `data` and `cached` + * 2 - if they are different, copy `data` to `cached` and update the DOM + * based on what the difference is + * 3 - recursively apply this algorithm for every array and for the + * children of every virtual element + * + * The `cached` data structure is essentially the same as the previous + * redraw's `data` data structure, with a few additions: + * - `cached` always has a property called `nodes`, which is a list of + * DOM elements that correspond to the data represented by the + * respective virtual element + * - in order to support attaching `nodes` as a property of `cached`, + * `cached` is *always* a non-primitive object, i.e. if the data was + * a string, then cached is a String instance. If data was `null` or + * `undefined`, cached is `new String("")` + * - `cached also has a `configContext` property, which is the state + * storage object exposed by config(element, isInitialized, context) + * - when `cached` is an Object, it represents a virtual element; when + * it's an Array, it represents a list of elements; when it's a + * String, Number or Boolean, it represents a text node + * + * `parentElement` is a DOM element used for W3C DOM API calls + * `parentTag` is only used for handling a corner case for textarea + * values + * `parentCache` is used to remove nodes in some multi-node cases + * `parentIndex` and `index` are used to figure out the offset of nodes. + * They're artifacts from before arrays started being flattened and are + * likely refactorable + * `data` and `cached` are, respectively, the new and old nodes being + * diffed + * `shouldReattach` is a flag indicating whether a parent node was + * recreated (if so, and if this node is reused, then this node must + * reattach itself to the new parent) + * `editable` is a flag that indicates whether an ancestor is + * contenteditable + * `namespace` indicates the closest HTML namespace as it cascades down + * from an ancestor + * `configs` is a list of config functions to run after the topmost + * `build` call finishes running + * + * there's logic that relies on the assumption that null and undefined + * data are equivalent to empty strings + * - this prevents lifecycle surprises from procedural helpers that mix + * implicit and explicit return statements (e.g. + * function foo() {if (cond) return m("div")} + * - it simplifies diffing code + */ + data = dataToString(data) + if (data.subtree === "retain") return cached + cached = makeCache(data, cached, index, parentIndex, parentCache) + + if (isArray(data)) { + return buildArray( + data, + cached, + parentElement, + index, + parentTag, + shouldReattach, + editable, + namespace, + configs) + } else if (data != null && isObject(data)) { + return buildObject( + data, + cached, + editable, + parentElement, + index, + shouldReattach, + namespace, + configs) + } else if (!isFunction(data)) { + return handleTextNode( + cached, + data, + index, + parentElement, + shouldReattach, + editable, + parentTag) + } else { + return cached + } + } + + function sortChanges(a, b) { + return a.action - b.action || a.index - b.index + } + + function copyStyleAttrs(node, dataAttr, cachedAttr) { + for (var rule in dataAttr) if (hasOwn.call(dataAttr, rule)) { + if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) { + node.style[rule] = dataAttr[rule] + } + } + + for (rule in cachedAttr) if (hasOwn.call(cachedAttr, rule)) { + if (!hasOwn.call(dataAttr, rule)) node.style[rule] = "" + } + } + + function shouldUseSetAttribute(attrName) { + return attrName !== "list" && + attrName !== "style" && + attrName !== "form" && + attrName !== "type" && + attrName !== "width" && + attrName !== "height" + } + + function setSingleAttr( + node, + attrName, + dataAttr, + cachedAttr, + tag, + namespace + ) { + if (attrName === "config" || attrName === "key") { + // `config` isn't a real attribute, so ignore it + return true + } else if (isFunction(dataAttr) && attrName.slice(0, 2) === "on") { + // hook event handlers to the auto-redrawing system + node[attrName] = autoredraw(dataAttr, node) + } else if (attrName === "style" && dataAttr != null && + isObject(dataAttr)) { + // handle `style: {...}` + copyStyleAttrs(node, dataAttr, cachedAttr) + } else if (namespace != null) { + // handle SVG + if (attrName === "href") { + node.setAttributeNS("http://www.w3.org/1999/xlink", + "href", dataAttr) + } else { + node.setAttribute( + attrName === "className" ? "class" : attrName, + dataAttr) + } + } else if (attrName in node && shouldUseSetAttribute(attrName)) { + // handle cases that are properties (but ignore cases where we + // should use setAttribute instead) + // + // - list and form are typically used as strings, but are DOM + // element references in js + // + // - when using CSS selectors (e.g. `m("[style='']")`), style is + // used as a string, but it's an object in js + // + // #348 don't set the value if not needed - otherwise, cursor + // placement breaks in Chrome + try { + if (tag !== "input" || node[attrName] !== dataAttr) { + node[attrName] = dataAttr + } + } catch (e) { + node.setAttribute(attrName, dataAttr) + } + } + else node.setAttribute(attrName, dataAttr) + } + + function trySetAttr( + node, + attrName, + dataAttr, + cachedAttr, + cachedAttrs, + tag, + namespace + ) { + if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr)) { + cachedAttrs[attrName] = dataAttr + try { + return setSingleAttr( + node, + attrName, + dataAttr, + cachedAttr, + tag, + namespace) + } catch (e) { + // swallow IE's invalid argument errors to mimic HTML's + // fallback-to-doing-nothing-on-invalid-attributes behavior + if (e.message.indexOf("Invalid argument") < 0) throw e + } + } else if (attrName === "value" && tag === "input" && + node.value !== dataAttr) { + // #348 dataAttr may not be a string, so use loose comparison + node.value = dataAttr + } + } + + function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) { + for (var attrName in dataAttrs) if (hasOwn.call(dataAttrs, attrName)) { + if (trySetAttr( + node, + attrName, + dataAttrs[attrName], + cachedAttrs[attrName], + cachedAttrs, + tag, + namespace)) { + continue + } + } + return cachedAttrs + } + + function clear(nodes, cached) { + for (var i = nodes.length - 1; i > -1; i--) { + if (nodes[i] && nodes[i].parentNode) { + try { + nodes[i].parentNode.removeChild(nodes[i]) + } catch (e) { + /* eslint-disable max-len */ + // ignore if this fails due to order of events (see + // http://stackoverflow.com/questions/21926083/failed-to-execute-removechild-on-node) + /* eslint-enable max-len */ + } + cached = [].concat(cached) + if (cached[i]) unload(cached[i]) + } + } + // release memory if nodes is an array. This check should fail if nodes + // is a NodeList (see loop above) + if (nodes.length) { + nodes.length = 0 + } + } + + function unload(cached) { + if (cached.configContext && isFunction(cached.configContext.onunload)) { + cached.configContext.onunload() + cached.configContext.onunload = null + } + if (cached.controllers) { + forEach(cached.controllers, function (controller) { + if (isFunction(controller.onunload)) { + controller.onunload({preventDefault: noop}) + } + }) + } + if (cached.children) { + if (isArray(cached.children)) forEach(cached.children, unload) + else if (cached.children.tag) unload(cached.children) + } + } + + function appendTextFragment(parentElement, data) { + try { + parentElement.appendChild( + $document.createRange().createContextualFragment(data)) + } catch (e) { + parentElement.insertAdjacentHTML("beforeend", data) + } + } + + function injectHTML(parentElement, index, data) { + var nextSibling = parentElement.childNodes[index] + if (nextSibling) { + var isElement = nextSibling.nodeType !== 1 + var placeholder = $document.createElement("span") + if (isElement) { + parentElement.insertBefore(placeholder, nextSibling || null) + placeholder.insertAdjacentHTML("beforebegin", data) + parentElement.removeChild(placeholder) + } else { + nextSibling.insertAdjacentHTML("beforebegin", data) + } + } else { + appendTextFragment(parentElement, data) + } + + var nodes = [] + + while (parentElement.childNodes[index] !== nextSibling) { + nodes.push(parentElement.childNodes[index]) + index++ + } + + return nodes + } + + function autoredraw(callback, object) { + return function (e) { + e = e || event + m.redraw.strategy("diff") + m.startComputation() + try { + return callback.call(object, e) + } finally { + endFirstComputation() + } + } + } + + var html + var documentNode = { + appendChild: function (node) { + if (html === undefined) html = $document.createElement("html") + if ($document.documentElement && + $document.documentElement !== node) { + $document.replaceChild(node, $document.documentElement) + } else { + $document.appendChild(node) + } + + this.childNodes = $document.childNodes + }, + + insertBefore: function (node) { + this.appendChild(node) + }, + + childNodes: [] + } + + var nodeCache = [] + var cellCache = {} + + m.render = function (root, cell, forceRecreation) { + if (!root) { + throw new Error("Ensure the DOM element being passed to " + + "m.route/m.mount/m.render is not undefined.") + } + var configs = [] + var id = getCellCacheKey(root) + var isDocumentRoot = root === $document + var node + + if (isDocumentRoot || root === $document.documentElement) { + node = documentNode + } else { + node = root + } + + if (isDocumentRoot && cell.tag !== "html") { + cell = {tag: "html", attrs: {}, children: cell} + } + + if (cellCache[id] === undefined) clear(node.childNodes) + if (forceRecreation === true) reset(root) + + cellCache[id] = build( + node, + null, + undefined, + undefined, + cell, + cellCache[id], + false, + 0, + null, + undefined, + configs) + + forEach(configs, function (config) { config() }) + } + + function getCellCacheKey(element) { + var index = nodeCache.indexOf(element) + return index < 0 ? nodeCache.push(element) - 1 : index + } + + m.trust = function (value) { + value = new String(value) // eslint-disable-line no-new-wrappers + value.$trusted = true + return value + } + + function gettersetter(store) { + function prop() { + if (arguments.length) store = arguments[0] + return store + } + + prop.toJSON = function () { + return store + } + + return prop + } + + m.prop = function (store) { + if ((store != null && isObject(store) || isFunction(store)) && + isFunction(store.then)) { + return propify(store) + } + + return gettersetter(store) + } + + var roots = [] + var components = [] + var controllers = [] + var lastRedrawId = null + var lastRedrawCallTime = 0 + var computePreRedrawHook = null + var computePostRedrawHook = null + var topComponent + var FRAME_BUDGET = 16 // 60 frames per second = 1 call per 16 ms + + function parameterize(component, args) { + function controller() { + /* eslint-disable no-invalid-this */ + return (component.controller || noop).apply(this, args) || this + /* eslint-enable no-invalid-this */ + } + + if (component.controller) { + controller.prototype = component.controller.prototype + } + + function view(ctrl) { + var currentArgs = [ctrl].concat(args) + for (var i = 1; i < arguments.length; i++) { + currentArgs.push(arguments[i]) + } + + return component.view.apply(component, currentArgs) + } + + view.$original = component.view + var output = {controller: controller, view: view} + if (args[0] && args[0].key != null) output.attrs = {key: args[0].key} + return output + } + + m.component = function (component) { + for (var args = [], i = 1; i < arguments.length; i++) { + args.push(arguments[i]) + } + + return parameterize(component, args) + } + + function checkPrevented(component, root, index, isPrevented) { + if (!isPrevented) { + m.redraw.strategy("all") + m.startComputation() + roots[index] = root + var currentComponent + + if (component) { + currentComponent = topComponent = component + } else { + currentComponent = topComponent = component = {controller: noop} + } + + var controller = new (component.controller || noop)() + + // controllers may call m.mount recursively (via m.route redirects, + // for example) + // this conditional ensures only the last recursive m.mount call is + // applied + if (currentComponent === topComponent) { + controllers[index] = controller + components[index] = component + } + endFirstComputation() + if (component === null) { + removeRootElement(root, index) + } + return controllers[index] + } else if (component == null) { + removeRootElement(root, index) + } + } + + m.mount = m.module = function (root, component) { + if (!root) { + throw new Error("Please ensure the DOM element exists before " + + "rendering a template into it.") + } + + var index = roots.indexOf(root) + if (index < 0) index = roots.length + + var isPrevented = false + var event = { + preventDefault: function () { + isPrevented = true + computePreRedrawHook = computePostRedrawHook = null + } + } + + forEach(unloaders, function (unloader) { + unloader.handler.call(unloader.controller, event) + unloader.controller.onunload = null + }) + + if (isPrevented) { + forEach(unloaders, function (unloader) { + unloader.controller.onunload = unloader.handler + }) + } else { + unloaders = [] + } + + if (controllers[index] && isFunction(controllers[index].onunload)) { + controllers[index].onunload(event) + } + + return checkPrevented(component, root, index, isPrevented) + } + + function removeRootElement(root, index) { + roots.splice(index, 1) + controllers.splice(index, 1) + components.splice(index, 1) + reset(root) + nodeCache.splice(getCellCacheKey(root), 1) + } + + var redrawing = false + m.redraw = function (force) { + if (redrawing) return + redrawing = true + if (force) forcing = true + + try { + // lastRedrawId is a positive number if a second redraw is requested + // before the next animation frame + // lastRedrawID is null if it's the first redraw and not an event + // handler + if (lastRedrawId && !force) { + // when setTimeout: only reschedule redraw if time between now + // and previous redraw is bigger than a frame, otherwise keep + // currently scheduled timeout + // when rAF: always reschedule redraw + if ($requestAnimationFrame === global.requestAnimationFrame || + new Date() - lastRedrawCallTime > FRAME_BUDGET) { + if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId) + lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET) + } + } else { + redraw() + lastRedrawId = $requestAnimationFrame(function () { + lastRedrawId = null + }, FRAME_BUDGET) + } + } finally { + redrawing = forcing = false + } + } + + m.redraw.strategy = m.prop() + function redraw() { + if (computePreRedrawHook) { + computePreRedrawHook() + computePreRedrawHook = null + } + forEach(roots, function (root, i) { + var component = components[i] + if (controllers[i]) { + var args = [controllers[i]] + m.render(root, + component.view ? component.view(controllers[i], args) : "") + } + }) + // after rendering within a routed context, we need to scroll back to + // the top, and fetch the document title for history.pushState + if (computePostRedrawHook) { + computePostRedrawHook() + computePostRedrawHook = null + } + lastRedrawId = null + lastRedrawCallTime = new Date() + m.redraw.strategy("diff") + } + + function endFirstComputation() { + if (m.redraw.strategy() === "none") { + pendingRequests-- + m.redraw.strategy("diff") + } else { + m.endComputation() + } + } + + m.withAttr = function (prop, withAttrCallback, callbackThis) { + return function (e) { + e = e || event + /* eslint-disable no-invalid-this */ + var currentTarget = e.currentTarget || this + var _this = callbackThis || this + /* eslint-enable no-invalid-this */ + var target = prop in currentTarget ? + currentTarget[prop] : + currentTarget.getAttribute(prop) + withAttrCallback.call(_this, target) + } + } + + // routing + var modes = {pathname: "", hash: "#", search: "?"} + var redirect = noop + var isDefaultRoute = false + var routeParams, currentRoute + + m.route = function (root, arg1, arg2, vdom) { // eslint-disable-line + // m.route() + if (arguments.length === 0) return currentRoute + // m.route(el, defaultRoute, routes) + if (arguments.length === 3 && isString(arg1)) { + redirect = function (source) { + var path = currentRoute = normalizeRoute(source) + if (!routeByValue(root, arg2, path)) { + if (isDefaultRoute) { + throw new Error("Ensure the default route matches " + + "one of the routes defined in m.route") + } + + isDefaultRoute = true + m.route(arg1, true) + isDefaultRoute = false + } + } + + var listener = m.route.mode === "hash" ? + "onhashchange" : + "onpopstate" + + global[listener] = function () { + var path = $location[m.route.mode] + if (m.route.mode === "pathname") path += $location.search + if (currentRoute !== normalizeRoute(path)) redirect(path) + } + + computePreRedrawHook = setScroll + global[listener]() + + return + } + + // config: m.route + if (root.addEventListener || root.attachEvent) { + var base = m.route.mode !== "pathname" ? $location.pathname : "" + root.href = base + modes[m.route.mode] + vdom.attrs.href + if (root.addEventListener) { + root.removeEventListener("click", routeUnobtrusive) + root.addEventListener("click", routeUnobtrusive) + } else { + root.detachEvent("onclick", routeUnobtrusive) + root.attachEvent("onclick", routeUnobtrusive) + } + + return + } + // m.route(route, params, shouldReplaceHistoryEntry) + if (isString(root)) { + var oldRoute = currentRoute + currentRoute = root + + var args = arg1 || {} + var queryIndex = currentRoute.indexOf("?") + var params + + if (queryIndex > -1) { + params = parseQueryString(currentRoute.slice(queryIndex + 1)) + } else { + params = {} + } + + for (var i in args) if (hasOwn.call(args, i)) { + params[i] = args[i] + } + + var querystring = buildQueryString(params) + var currentPath + + if (queryIndex > -1) { + currentPath = currentRoute.slice(0, queryIndex) + } else { + currentPath = currentRoute + } + + if (querystring) { + currentRoute = currentPath + + (currentPath.indexOf("?") === -1 ? "?" : "&") + + querystring + } + + var replaceHistory = + (arguments.length === 3 ? arg2 : arg1) === true || + oldRoute === root + + if (global.history.pushState) { + var method = replaceHistory ? "replaceState" : "pushState" + computePreRedrawHook = setScroll + computePostRedrawHook = function () { + global.history[method](null, $document.title, + modes[m.route.mode] + currentRoute) + } + redirect(modes[m.route.mode] + currentRoute) + } else { + $location[m.route.mode] = currentRoute + redirect(modes[m.route.mode] + currentRoute) + } + } + } + + m.route.param = function (key) { + if (!routeParams) { + throw new Error("You must call m.route(element, defaultRoute, " + + "routes) before calling m.route.param()") + } + + if (!key) { + return routeParams + } + + return routeParams[key] + } + + m.route.mode = "search" + + function normalizeRoute(route) { + return route.slice(modes[m.route.mode].length) + } + + function routeByValue(root, router, path) { + routeParams = {} + + var queryStart = path.indexOf("?") + if (queryStart !== -1) { + routeParams = parseQueryString( + path.substr(queryStart + 1, path.length)) + path = path.substr(0, queryStart) + } + + // Get all routes and check if there's + // an exact match for the current path + var keys = Object.keys(router) + var index = keys.indexOf(path) + + if (index !== -1){ + m.mount(root, router[keys [index]]) + return true + } + + for (var route in router) if (hasOwn.call(router, route)) { + if (route === path) { + m.mount(root, router[route]) + return true + } + + var matcher = new RegExp("^" + route + .replace(/:[^\/]+?\.{3}/g, "(.*?)") + .replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") + + if (matcher.test(path)) { + /* eslint-disable no-loop-func */ + path.replace(matcher, function () { + var keys = route.match(/:[^\/]+/g) || [] + var values = [].slice.call(arguments, 1, -2) + forEach(keys, function (key, i) { + routeParams[key.replace(/:|\./g, "")] = + decodeURIComponent(values[i]) + }) + m.mount(root, router[route]) + }) + /* eslint-enable no-loop-func */ + return true + } + } + } + + function routeUnobtrusive(e) { + e = e || event + if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return + + if (e.preventDefault) { + e.preventDefault() + } else { + e.returnValue = false + } + + var currentTarget = e.currentTarget || e.srcElement + var args + + if (m.route.mode === "pathname" && currentTarget.search) { + args = parseQueryString(currentTarget.search.slice(1)) + } else { + args = {} + } + + while (currentTarget && !/a/i.test(currentTarget.nodeName)) { + currentTarget = currentTarget.parentNode + } + + // clear pendingRequests because we want an immediate route change + pendingRequests = 0 + m.route(currentTarget[m.route.mode] + .slice(modes[m.route.mode].length), args) + } + + function setScroll() { + if (m.route.mode !== "hash" && $location.hash) { + $location.hash = $location.hash + } else { + global.scrollTo(0, 0) + } + } + + function buildQueryString(object, prefix) { + var duplicates = {} + var str = [] + + for (var prop in object) if (hasOwn.call(object, prop)) { + var key = prefix ? prefix + "[" + prop + "]" : prop + var value = object[prop] + + if (value === null) { + str.push(encodeURIComponent(key)) + } else if (isObject(value)) { + str.push(buildQueryString(value, key)) + } else if (isArray(value)) { + var keys = [] + duplicates[key] = duplicates[key] || {} + /* eslint-disable no-loop-func */ + forEach(value, function (item) { + /* eslint-enable no-loop-func */ + if (!duplicates[key][item]) { + duplicates[key][item] = true + keys.push(encodeURIComponent(key) + "=" + + encodeURIComponent(item)) + } + }) + str.push(keys.join("&")) + } else if (value !== undefined) { + str.push(encodeURIComponent(key) + "=" + + encodeURIComponent(value)) + } + } + return str.join("&") + } + + function parseQueryString(str) { + if (str === "" || str == null) return {} + if (str.charAt(0) === "?") str = str.slice(1) + + var pairs = str.split("&") + var params = {} + + forEach(pairs, function (string) { + var pair = string.split("=") + var key = decodeURIComponent(pair[0]) + var value = pair.length === 2 ? decodeURIComponent(pair[1]) : null + if (params[key] != null) { + if (!isArray(params[key])) params[key] = [params[key]] + params[key].push(value) + } + else params[key] = value + }) + + return params + } + + m.route.buildQueryString = buildQueryString + m.route.parseQueryString = parseQueryString + + function reset(root) { + var cacheKey = getCellCacheKey(root) + clear(root.childNodes, cellCache[cacheKey]) + cellCache[cacheKey] = undefined + } + + m.deferred = function () { + var deferred = new Deferred() + deferred.promise = propify(deferred.promise) + return deferred + } + + function propify(promise, initialValue) { + var prop = m.prop(initialValue) + promise.then(prop) + prop.then = function (resolve, reject) { + return propify(promise.then(resolve, reject), initialValue) + } + + prop.catch = prop.then.bind(null, null) + return prop + } + // Promiz.mithril.js | Zolmeister | MIT + // a modified version of Promiz.js, which does not conform to Promises/A+ + // for two reasons: + // + // 1) `then` callbacks are called synchronously (because setTimeout is too + // slow, and the setImmediate polyfill is too big + // + // 2) throwing subclasses of Error cause the error to be bubbled up instead + // of triggering rejection (because the spec does not account for the + // important use case of default browser error handling, i.e. message w/ + // line number) + + var RESOLVING = 1 + var REJECTING = 2 + var RESOLVED = 3 + var REJECTED = 4 + + function Deferred(onSuccess, onFailure) { + var self = this + var state = 0 + var promiseValue = 0 + var next = [] + + self.promise = {} + + self.resolve = function (value) { + if (!state) { + promiseValue = value + state = RESOLVING + + fire() + } + + return self + } + + self.reject = function (value) { + if (!state) { + promiseValue = value + state = REJECTING + + fire() + } + + return self + } + + self.promise.then = function (onSuccess, onFailure) { + var deferred = new Deferred(onSuccess, onFailure) + + if (state === RESOLVED) { + deferred.resolve(promiseValue) + } else if (state === REJECTED) { + deferred.reject(promiseValue) + } else { + next.push(deferred) + } + + return deferred.promise + } + + function finish(type) { + state = type || REJECTED + next.map(function (deferred) { + if (state === RESOLVED) { + deferred.resolve(promiseValue) + } else { + deferred.reject(promiseValue) + } + }) + } + + function thennable(then, success, failure, notThennable) { + if (((promiseValue != null && isObject(promiseValue)) || + isFunction(promiseValue)) && isFunction(then)) { + try { + // count protects against abuse calls from spec checker + var count = 0 + then.call(promiseValue, function (value) { + if (count++) return + promiseValue = value + success() + }, function (value) { + if (count++) return + promiseValue = value + failure() + }) + } catch (e) { + m.deferred.onerror(e) + promiseValue = e + failure() + } + } else { + notThennable() + } + } + + function fire() { + // check if it's a thenable + var then + try { + then = promiseValue && promiseValue.then + } catch (e) { + m.deferred.onerror(e) + promiseValue = e + state = REJECTING + return fire() + } + + if (state === REJECTING) { + m.deferred.onerror(promiseValue) + } + + thennable(then, function () { + state = RESOLVING + fire() + }, function () { + state = REJECTING + fire() + }, function () { + try { + if (state === RESOLVING && isFunction(onSuccess)) { + promiseValue = onSuccess(promiseValue) + } else if (state === REJECTING && isFunction(onFailure)) { + promiseValue = onFailure(promiseValue) + state = RESOLVING + } + } catch (e) { + m.deferred.onerror(e) + promiseValue = e + return finish() + } + + if (promiseValue === self) { + promiseValue = TypeError() + finish() + } else { + thennable(then, function () { + finish(RESOLVED) + }, finish, function () { + finish(state === RESOLVING && RESOLVED) + }) + } + }) + } + } + + m.deferred.onerror = function (e) { + if (type.call(e) === "[object Error]" && + !e.constructor.toString().match(/ Error/)) { + pendingRequests = 0 + throw e + } + } + + m.sync = function (args) { + var deferred = m.deferred() + var outstanding = args.length + var results = new Array(outstanding) + var method = "resolve" + + function synchronizer(pos, resolved) { + return function (value) { + results[pos] = value + if (!resolved) method = "reject" + if (--outstanding === 0) { + deferred.promise(results) + deferred[method](results) + } + return value + } + } + + if (args.length > 0) { + forEach(args, function (arg, i) { + arg.then(synchronizer(i, true), synchronizer(i, false)) + }) + } else { + deferred.resolve([]) + } + + return deferred.promise + } + + function identity(value) { return value } + + function handleJsonp(options) { + var callbackKey = "mithril_callback_" + + new Date().getTime() + "_" + + (Math.round(Math.random() * 1e16)).toString(36) + + var script = $document.createElement("script") + + global[callbackKey] = function (resp) { + script.parentNode.removeChild(script) + options.onload({ + type: "load", + target: { + responseText: resp + } + }) + global[callbackKey] = undefined + } + + script.onerror = function () { + script.parentNode.removeChild(script) + + options.onerror({ + type: "error", + target: { + status: 500, + responseText: JSON.stringify({ + error: "Error making jsonp request" + }) + } + }) + global[callbackKey] = undefined + + return false + } + + script.onload = function () { + return false + } + + script.src = options.url + + (options.url.indexOf("?") > 0 ? "&" : "?") + + (options.callbackKey ? options.callbackKey : "callback") + + "=" + callbackKey + + "&" + buildQueryString(options.data || {}) + + $document.body.appendChild(script) + } + + function createXhr(options) { + var xhr = new global.XMLHttpRequest() + xhr.open(options.method, options.url, true, options.user, + options.password) + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status >= 200 && xhr.status < 300) { + options.onload({type: "load", target: xhr}) + } else { + options.onerror({type: "error", target: xhr}) + } + } + } + + if (options.serialize === JSON.stringify && + options.data && + options.method !== "GET") { + xhr.setRequestHeader("Content-Type", + "application/json; charset=utf-8") + } + + if (options.deserialize === JSON.parse) { + xhr.setRequestHeader("Accept", "application/json, text/*") + } + + if (isFunction(options.config)) { + var maybeXhr = options.config(xhr, options) + if (maybeXhr != null) xhr = maybeXhr + } + + var data = options.method === "GET" || !options.data ? "" : options.data + + if (data && !isString(data) && data.constructor !== global.FormData) { + throw new Error("Request data should be either be a string or " + + "FormData. Check the `serialize` option in `m.request`") + } + + xhr.send(data) + return xhr + } + + function ajax(options) { + if (options.dataType && options.dataType.toLowerCase() === "jsonp") { + return handleJsonp(options) + } else { + return createXhr(options) + } + } + + function bindData(options, data, serialize) { + if (options.method === "GET" && options.dataType !== "jsonp") { + var prefix = options.url.indexOf("?") < 0 ? "?" : "&" + var querystring = buildQueryString(data) + options.url += (querystring ? prefix + querystring : "") + } else { + options.data = serialize(data) + } + } + + function parameterizeUrl(url, data) { + var tokens = url.match(/:[a-z]\w+/gi) + + if (tokens && data) { + forEach(tokens, function (token) { + var key = token.slice(1) + url = url.replace(token, data[key]) + delete data[key] + }) + } + + return url + } + + m.request = function (options) { + if (options.background !== true) m.startComputation() + var deferred = new Deferred() + var isJSONP = options.dataType && + options.dataType.toLowerCase() === "jsonp" + + var serialize, deserialize, extract + + if (isJSONP) { + serialize = options.serialize = + deserialize = options.deserialize = identity + + extract = function (jsonp) { return jsonp.responseText } + } else { + serialize = options.serialize = options.serialize || JSON.stringify + + deserialize = options.deserialize = + options.deserialize || JSON.parse + extract = options.extract || function (xhr) { + if (xhr.responseText.length || deserialize !== JSON.parse) { + return xhr.responseText + } else { + return null + } + } + } + + options.method = (options.method || "GET").toUpperCase() + options.url = parameterizeUrl(options.url, options.data) + bindData(options, options.data, serialize) + options.onload = options.onerror = function (ev) { + try { + ev = ev || event + var response = deserialize(extract(ev.target, options)) + if (ev.type === "load") { + if (options.unwrapSuccess) { + response = options.unwrapSuccess(response, ev.target) + } + + if (isArray(response) && options.type) { + forEach(response, function (res, i) { + response[i] = new options.type(res) + }) + } else if (options.type) { + response = new options.type(response) + } + + deferred.resolve(response) + } else { + if (options.unwrapError) { + response = options.unwrapError(response, ev.target) + } + + deferred.reject(response) + } + } catch (e) { + deferred.reject(e) + } finally { + if (options.background !== true) m.endComputation() + } + } + + ajax(options) + deferred.promise = propify(deferred.promise, options.initialValue) + return deferred.promise + } + + return m +}) diff --git a/extension/lib/wallet/wxmessaging.js b/extension/lib/wallet/wxmessaging.js index f5819b197..8c74dab4e 100644 --- a/extension/lib/wallet/wxmessaging.js +++ b/extension/lib/wallet/wxmessaging.js @@ -144,6 +144,7 @@ System.register(["./wallet", "./db", "./http"], function(exports_1, context_1) { return handlers[req.type](db, req.detail, onresponse); } console.error("Request type " + JSON.stringify(req) + " unknown, req " + req.type); + onresponse({ error: "request unknown" }); return false; }); }) diff --git a/extension/lib/wallet/wxmessaging.ts b/extension/lib/wallet/wxmessaging.ts index 045145716..b3bc5005d 100644 --- a/extension/lib/wallet/wxmessaging.ts +++ b/extension/lib/wallet/wxmessaging.ts @@ -29,6 +29,7 @@ import {BrowserHttpLib} from "./http"; * @author Florian Dold */ + function makeHandlers(wallet: Wallet) { return { ["balances"]: function(db, detail, sendResponse) { @@ -162,6 +163,7 @@ export function wxMain() { return handlers[req.type](db, req.detail, onresponse); } console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); + onresponse({error: "request unknown"}); return false; }); }) diff --git a/extension/pages/confirm-create-reserve.html b/extension/pages/confirm-create-reserve.html index 522efc872..7bf2b0498 100644 --- a/extension/pages/confirm-create-reserve.html +++ b/extension/pages/confirm-create-reserve.html @@ -24,7 +24,7 @@ <section id="main"> <article> - <div id="mint-selection"></div> + <div class="fade" id="mint-selection"></div> </article> </section> diff --git a/extension/pages/confirm-create-reserve.js b/extension/pages/confirm-create-reserve.js index 124abdf22..d49e709c2 100644 --- a/extension/pages/confirm-create-reserve.js +++ b/extension/pages/confirm-create-reserve.js @@ -13,46 +13,71 @@ You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> */ -System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(exports_1, context_1) { +System.register(["../lib/wallet/helpers", "../lib/wallet/types", "mithril"], function(exports_1, context_1) { "use strict"; var __moduleName = context_1 && context_1.id; - var helpers_1, types_1; + var helpers_1, types_1, mithril_1; var DelayTimer, Controller; + function view(ctrl) { + var controls = []; + var mx = function (x) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + return controls.push(mithril_1.default.apply(void 0, [x].concat(args))); + }; + mx("p", (_a = ["The bank wants to create a reserve over ", "."], _a.raw = ["The bank wants to create a reserve over ", "."], i18n(_a, helpers_1.amountToPretty(ctrl.amount)))); + mx("input", { + className: "url", + type: "text", + spellcheck: false, + value: ctrl.url(), + oninput: mithril_1.default.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), + }); + mx("button", { + onclick: function () { return ctrl.confirmReserve(ctrl.url(), ctrl.amount, ctrl.callbackUrl); }, + disabled: !ctrl.isValidMint + }, "Confirm mint selection"); + if (ctrl.statusString) { + mx("p", ctrl.statusString); + } + else { + mx("p", "Checking URL, please wait ..."); + } + return mithril_1.default("div", controls); + var _a; + } + function getSuggestedMint(currency) { + // TODO: make this request go to the wallet backend + // Right now, this is a stub. + var defaultMint = { + "KUDOS": "http://mint.test.taler.net" + }; + var mint = defaultMint[currency]; + if (!mint) { + mint = ""; + } + return Promise.resolve(mint); + } function main() { var url = URI(document.location.href); var query = URI.parseQuery(url.query()); var amount = types_1.AmountJson.checked(JSON.parse(query.amount)); var callback_url = query.callback_url; - var MintSelection = { - controller: function () { return new Controller(); }, - view: function (ctrl) { - var controls = []; - var mx = function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i - 0] = arguments[_i]; - } - return controls.push(m.apply(void 0, args)); - }; - mx("p", (_a = ["The bank wants to create a reserve over ", "."], _a.raw = ["The bank wants to create a reserve over ", "."], i18n(_a, helpers_1.amountToPretty(amount)))); - mx("input.url", { - type: "text", - spellcheck: false, - oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), - }); - if (ctrl.isValidMint) { - mx("button", { - onclick: function () { return ctrl.confirmReserve(ctrl.url, amount, callback_url); } - }, "Confirm mint selection"); - } - if (ctrl.errorString) { - mx("p", ctrl.errorString); - } - return m("div", controls); - var _a; - } - }; - m.mount(document.getElementById("mint-selection"), MintSelection); + var bank_url = query.bank_url; + getSuggestedMint(amount.currency) + .then(function (suggestedMintUrl) { + var controller = function () { return new Controller(suggestedMintUrl, amount, callback_url); }; + var MintSelection = { controller: controller, view: view }; + mithril_1.default.mount(document.getElementById("mint-selection"), MintSelection); + }) + .catch(function (e) { + // TODO: provide more context information, maybe factor it out into a + // TODO:generic error reporting function or component. + document.body.innerText = "Fatal error: \"" + e.message + "\"."; + console.error("got backend error \"" + e.message + "\""); + }); } exports_1("main", main); return { @@ -62,6 +87,9 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(expor }, function (types_1_1) { types_1 = types_1_1; + }, + function (mithril_1_1) { + mithril_1 = mithril_1_1; }], execute: function() { "use strict"; @@ -77,39 +105,48 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(expor } DelayTimer.prototype.bump = function () { var _this = this; - if (this.timerId !== null) { - window.clearTimeout(this.timerId); - } + this.stop(); var handler = function () { _this.f(); }; this.timerId = window.setTimeout(handler, this.ms); }; + DelayTimer.prototype.stop = function () { + if (this.timerId !== null) { + window.clearTimeout(this.timerId); + } + }; return DelayTimer; }()); Controller = (function () { - function Controller() { + function Controller(initialMintUrl, amount, callbackUrl) { var _this = this; - this.url = null; - this.errorString = null; + this.url = mithril_1.default.prop(); + this.statusString = null; this.isValidMint = false; - this.update(); + this.amount = amount; + this.callbackUrl = callbackUrl; this.timer = new DelayTimer(800, function () { return _this.update(); }); + this.url(initialMintUrl); + this.update(); } Controller.prototype.update = function () { var _this = this; + this.timer.stop(); var doUpdate = function () { - if (!_this.url) { - _this.errorString = (_a = ["Please enter a URL"], _a.raw = ["Please enter a URL"], i18n(_a)); + if (!_this.url()) { + _this.statusString = (_a = ["Please enter a URL"], _a.raw = ["Please enter a URL"], i18n(_a)); + mithril_1.default.endComputation(); return; } - _this.errorString = null; - var parsedUrl = URI(_this.url); + _this.statusString = null; + var parsedUrl = URI(_this.url()); if (parsedUrl.is("relative")) { - _this.errorString = (_b = ["The URL you've entered is not valid (must be absolute)"], _b.raw = ["The URL you've entered is not valid (must be absolute)"], i18n(_b)); + _this.statusString = (_b = ["The URL you've entered is not valid (must be absolute)"], _b.raw = ["The URL you've entered is not valid (must be absolute)"], i18n(_b)); + mithril_1.default.endComputation(); return; } - var keysUrl = URI("/keys").absoluteTo(helpers_1.canonicalizeBaseUrl(_this.url)); + var keysUrl = URI("/keys").absoluteTo(helpers_1.canonicalizeBaseUrl(_this.url())); console.log("requesting keys from '" + keysUrl + "'"); _this.request = new XMLHttpRequest(); _this.request.onreadystatechange = function () { @@ -117,33 +154,33 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(expor switch (_this.request.status) { case 200: _this.isValidMint = true; + _this.statusString = "The mint base URL is valid!"; break; case 0: - _this.errorString = "unknown request error"; + _this.statusString = "unknown request error"; break; default: - _this.errorString = "request failed with status " + _this.request.status; + _this.statusString = "request failed with status " + _this.request.status; break; } - m.redraw(); } + mithril_1.default.endComputation(); }; _this.request.open("get", keysUrl.href()); _this.request.send(); var _a, _b; }; + mithril_1.default.startComputation(); doUpdate(); - m.redraw(); console.log("got update"); }; Controller.prototype.reset = function () { this.isValidMint = false; - this.errorString = null; + this.statusString = null; if (this.request) { this.request.abort(); this.request = null; } - m.redraw(); }; Controller.prototype.confirmReserve = function (mint, amount, callback_url) { var _this = this; @@ -170,7 +207,7 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(expor } else { _this.reset(); - _this.errorString = ("Oops, something went wrong." + + _this.statusString = ("Oops, something went wrong." + ("The wallet responded with error status (" + rawResp.error + ").")); } }; @@ -178,7 +215,7 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(expor }; Controller.prototype.onUrlChanged = function (url) { this.reset(); - this.url = url; + this.url(url); this.timer.bump(); }; return Controller; diff --git a/extension/pages/confirm-create-reserve.tsx b/extension/pages/confirm-create-reserve.tsx index 8cb46559b..0effc752c 100644 --- a/extension/pages/confirm-create-reserve.tsx +++ b/extension/pages/confirm-create-reserve.tsx @@ -14,14 +14,14 @@ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> */ +/// <reference path="../lib/decl/mithril.d.ts" /> + import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers"; import {AmountJson, CreateReserveResponse} from "../lib/wallet/types"; +import m from "mithril"; "use strict"; -declare var m: any; - - /** * Execute something after a delay, with the possibility * to reset the delay. @@ -37,43 +37,55 @@ class DelayTimer { } bump() { - if (this.timerId !== null) { - window.clearTimeout(this.timerId); - } + this.stop(); const handler = () => { this.f(); }; this.timerId = window.setTimeout(handler, this.ms); } + + stop() { + if (this.timerId !== null) { + window.clearTimeout(this.timerId); + } + } } class Controller { - url = null; - errorString = null; + url = m.prop<string>(); + statusString = null; isValidMint = false; private timer: DelayTimer; private request: XMLHttpRequest; + amount: AmountJson; + callbackUrl: string; - constructor() { - this.update(); + constructor(initialMintUrl: string, amount: AmountJson, callbackUrl: string) { + this.amount = amount; + this.callbackUrl = callbackUrl; this.timer = new DelayTimer(800, () => this.update()); + this.url(initialMintUrl); + this.update(); } - update() { + private update() { + this.timer.stop(); const doUpdate = () => { - if (!this.url) { - this.errorString = i18n`Please enter a URL`; + if (!this.url()) { + this.statusString = i18n`Please enter a URL`; + m.endComputation(); return; } - this.errorString = null; - let parsedUrl = URI(this.url); + this.statusString = null; + let parsedUrl = URI(this.url()); if (parsedUrl.is("relative")) { - this.errorString = i18n`The URL you've entered is not valid (must be absolute)`; + this.statusString = i18n`The URL you've entered is not valid (must be absolute)`; + m.endComputation(); return; } - const keysUrl = URI("/keys").absoluteTo(canonicalizeBaseUrl(this.url)); + const keysUrl = URI("/keys").absoluteTo(canonicalizeBaseUrl(this.url())); console.log(`requesting keys from '${keysUrl}'`); @@ -83,34 +95,36 @@ class Controller { switch (this.request.status) { case 200: this.isValidMint = true; + this.statusString = "The mint base URL is valid!"; break; case 0: - this.errorString = `unknown request error`; + this.statusString = `unknown request error`; break; default: - this.errorString = `request failed with status ${this.request.status}`; + this.statusString = `request failed with status ${this.request.status}`; break; } - m.redraw(); } + m.endComputation(); }; this.request.open("get", keysUrl.href()); this.request.send(); }; + m.startComputation(); doUpdate(); - m.redraw(); + + console.log("got update"); } reset() { this.isValidMint = false; - this.errorString = null; + this.statusString = null; if (this.request) { this.request.abort(); this.request = null; } - m.redraw(); } confirmReserve(mint: string, amount: AmountJson, callback_url: string) { @@ -136,7 +150,7 @@ class Controller { document.location.href = url.href(); } else { this.reset(); - this.errorString = ( + this.statusString = ( `Oops, something went wrong.` + `The wallet responded with error status (${rawResp.error}).`); } @@ -146,50 +160,80 @@ class Controller { onUrlChanged(url: string) { this.reset(); - this.url = url; + this.url(url); this.timer.bump(); } } -export function main() { - const url = URI(document.location.href); - const query: any = URI.parseQuery(url.query()); - const amount = AmountJson.checked(JSON.parse(query.amount)); - const callback_url = query.callback_url; +function view(ctrl: Controller) { + let controls = []; + let mx = (x: string, ...args) => controls.push(m(x, ...args)); + + mx("p", + i18n`The bank wants to create a reserve over ${amountToPretty( + ctrl.amount)}.`); + mx("input", + { + className: "url", + type: "text", + spellcheck: false, + value: ctrl.url(), + oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), + }); + + mx("button", { + onclick: () => ctrl.confirmReserve(ctrl.url(), + ctrl.amount, + ctrl.callbackUrl), + disabled: !ctrl.isValidMint + }, + "Confirm mint selection"); + + if (ctrl.statusString) { + mx("p", ctrl.statusString); + } else { + mx("p", "Checking URL, please wait ..."); + } - var MintSelection = { - controller: () => new Controller(), - view(ctrl: Controller) { - let controls = []; - let mx = (...args) => controls.push(m(...args)); - - mx("p", - i18n`The bank wants to create a reserve over ${amountToPretty( - amount)}.`); - mx("input.url", - { - type: "text", - spellcheck: false, - oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), - }); - - if (ctrl.isValidMint) { - mx("button", { - onclick: () => ctrl.confirmReserve(ctrl.url, - amount, - callback_url) - }, - "Confirm mint selection"); - } + return m("div", controls); +} - if (ctrl.errorString) { - mx("p", ctrl.errorString); - } - return m("div", controls); - } +function getSuggestedMint(currency: string): Promise<string> { + // TODO: make this request go to the wallet backend + // Right now, this is a stub. + const defaultMint = { + "KUDOS": "http://mint.test.taler.net" }; - m.mount(document.getElementById("mint-selection"), MintSelection); + let mint = defaultMint[currency]; + + if (!mint) { + mint = "" + } + + return Promise.resolve(mint); +} + + +export function main() { + const url = URI(document.location.href); + const query: any = URI.parseQuery(url.query()); + const amount = AmountJson.checked(JSON.parse(query.amount)); + const callback_url = query.callback_url; + const bank_url = query.bank_url; + + getSuggestedMint(amount.currency) + .then((suggestedMintUrl) => { + const controller = () => new Controller(suggestedMintUrl, amount, callback_url); + var MintSelection = {controller, view}; + m.mount(document.getElementById("mint-selection"), MintSelection); + }) + .catch((e) => { + // TODO: provide more context information, maybe factor it out into a + // TODO:generic error reporting function or component. + document.body.innerText = `Fatal error: "${e.message}".`; + console.error(`got backend error "${e.message}"`); + }); }
\ No newline at end of file diff --git a/extension/style/wallet.css b/extension/style/wallet.css index 01b9cd96f..9f7a7b525 100644 --- a/extension/style/wallet.css +++ b/extension/style/wallet.css @@ -101,3 +101,19 @@ button { font-size: 120%; padding: 0.5em; } + +/* We use fading to hide slower DOM updates */ +.fade { + -webkit-animation: fade 0.7s; + animation: fade 0.7s; + opacity: 1; +} + +@-webkit-keyframes fade { + from {opacity: 0} + to {opacity: 1} +} +@keyframes fade { + from {opacity: 0} + to {opacity: 1} + }
\ No newline at end of file diff --git a/extension/tsconfig.json b/extension/tsconfig.json index 69fab261d..a3eea3a70 100644 --- a/extension/tsconfig.json +++ b/extension/tsconfig.json @@ -29,4 +29,4 @@ "pages/confirm-create-reserve.tsx", "test/tests/taler.ts" ] -}
\ No newline at end of file +} |