core/View.js

/*
 *  View.js
 *  PureMVC JavaScript Multicore
 *
 *  Copyright(c) 2023 Saad Shams <saad.shams@puremvc.org>
 *  Your reuse is governed by the BSD License
*/

import {Observer} from "../patterns/observer/Observer.js";

/**
 * A Multiton `View` implementation.
 *
 * <P>In PureMVC, the `View` class assumes these responsibilities:</P>
 *
 * <UL>
 * <LI>Maintain a cache of `Mediator` instances.</LI>
 * <LI>Provide methods for registering, retrieving, and removing `Mediators`.</LI>
 * <LI>Notifying `Mediators` when they are registered or removed.</LI>
 * <LI>Managing the observer lists for each `Notification` in the application.</LI>
 * <LI>Providing a method for attaching `Observers` to a `Notification`'s observer list.</LI>
 * <LI>Providing a method for broadcasting a `Notification`.</LI>
 * <LI>Notifying the `Observers` of a given `Notification` when it broadcast.</LI>
 * </UL>
 *
 * @see Mediator Mediator
 * @see Observer Observer
 * @see Notification Notification
 *
 * @class View
 */
class View {

    /**
     * Constructor.
     *
     * <P>This `View` implementation is a Multiton,
     * so you should not call the constructor
     * directly, but instead call the static Multiton
     * Factory method `View.getInstance( multitonKey )`
     *
     * @constructor
     * @param {string} key
     *
     * @throws {Error} Error if instance for this Multiton key has already been constructed
     */
    constructor(key) {
        if (View.instanceMap.get(key) != null) throw new Error(View.MULTITON_MSG);
        /** @protected
         * @type {string} */
        this.multitonKey = key;
        View.instanceMap.set(this.multitonKey, this);
        /** @protected
         * @type {Map<string, Mediator>} */
        this.mediatorMap = new Map();
        /** @protected
         * @type {Map.<string, Array.<Observer>>} */
        this.observerMap = new Map();
        this.initializeView();
    }

    /**
     * <P>Initialize the Multiton View instance.</P>
     *
     * <P>Called automatically by the constructor, this
     * is your opportunity to initialize the Multiton
     * instance in your subclass without overriding the
     * constructor.</P>
     */
    initializeView() {

    }

    /**
     * View Multiton factory method.
     *
     * @static
     * @param {string} key
     * @param {function(string):View} factory
     * @returns {View} the Multiton instance of `View`
     */
    static getInstance(key, factory) {
        if (View.instanceMap == null)
            /** @static
             * @type {Map<string, View>} */
            View.instanceMap = new Map();
        if (View.instanceMap.get(key) == null) View.instanceMap.set(key, factory(key));
        return View.instanceMap.get(key);
    }

    /**
     * <P>Register an `Observer` to be notified
     * of `Notifications` with a given name.</P>
     *
     * @param {string} notificationName the name of the `Notifications` to notify this `Observer` of
     * @param {Observer} observer the `Observer` to register
     */
    registerObserver(notificationName, observer) {
        if (this.observerMap.get(notificationName) != null) {
            let observers = this.observerMap.get(notificationName);
            observers.push(observer);
        } else {
            this.observerMap.set(notificationName, new Array(observer));
        }
    }

    /**
     * <P>Notify the `Observers` for a particular `Notification`.</P>
     *
     * <P>All previously attached `Observers` for this `Notification`'s
     * list are notified and are passed a reference to the `Notification` in
     * the order in which they were registered.</P>
     *
     * @param {Notification} notification the `Notification` to notify `Observers` of.
     */
    notifyObservers(notification) {
        if (this.observerMap.has(notification.name)) {
            // Copy observers from reference array to working array,
            // since the reference array may change during the notification loop
            let observers = this.observerMap.get(notification.name).slice();

            // Notify Observers from the working array
            for(let i = 0; i < observers.length; i++) {
                observers[i].notifyObserver(notification);
            }
        }
    }

    /**
     * <P>Remove the observer for a given notifyContext from an observer list for a given Notification name.</P>
     *
     * @param {string} notificationName which observer list to remove from
     * @param {Object} notifyContext remove the observer with this object as its notifyContext
     */
    removeObserver(notificationName, notifyContext) {
        // the observer list for the notification under inspection
        let observers = this.observerMap.get(notificationName);

        // find the observer for the notifyContext
        for (let i = 0; i < observers.length; i++) {
            if (observers[i].compareNotifyContext(notifyContext) === true) {
                // there can only be one Observer for a given notifyContext
                // in any given Observer list, so remove it and break
                observers.splice(i, 1);
                break;
            }
        }

        // Also, when a Notification's Observer list length falls to
        // zero, delete the notification key from the observer map
        if (observers.length === 0) {
            this.observerMap.delete(notificationName);
        }
    }

    /**
     * Register a `Mediator` instance with the `View`.
     *
     * <P>Registers the `Mediator` so that it can be retrieved by name,
     * and further interrogates the `Mediator` for its
     * `Notification` interests.</P>
     *
     * <P>If the `Mediator` returns any `Notification`
     * names to be notified about, an `Observer` is created encapsulating
     * the `Mediator` instance's `handleNotification` method
     * and registering it as an `Observer` for all `Notifications` the
     * `Mediator` is interested in.</p>
     *
     * @param {Mediator} mediator a reference to the `Mediator` instance
     */
    registerMediator(mediator) {
        // do not allow re-registration (you must to removeMediator fist)
        if (this.mediatorMap.has(mediator.mediatorName) !== false) return;

        mediator.initializeNotifier(this.multitonKey);

        // Register the Mediator for retrieval by name
        this.mediatorMap.set(mediator.mediatorName, mediator);

        // Get Notification interests, if any.
        let interests = mediator.listNotificationInterests();

        // Register Mediator as an observer for each notification of interests
        if (interests.length > 0) {
            // Create Observer referencing this mediator's handleNotification method
            let observer = new Observer(mediator.handleNotification.bind(mediator), mediator); // check bind

            // Register Mediator as Observer for its list of Notification interests
            for (let i = 0; i < interests.length; i++) {
                this.registerObserver(interests[i], observer);
            }
        }

        // alert the mediator that it has been registered
        mediator.onRegister();
    }

    /**
     * Retrieve a `Mediator` from the `View`.
     *
     * @param {string} mediatorName the name of the `Mediator` instance to retrieve.
     * @returns {Mediator} the `Mediator` instance previously registered with the given `mediatorName`.
     */
    retrieveMediator(mediatorName) {
        return this.mediatorMap.get(mediatorName) || null;
    }

    /**
     * Remove a `Mediator` from the `View`.
     *
     * @param {string} mediatorName name of the `Mediator` instance to be removed.
     * @returns {Mediator} the `Mediator` that was removed from the `View`
     */
    removeMediator(mediatorName) {
        // Retrieve the named mediator
        let mediator = this.mediatorMap.get(mediatorName);

        if (mediator) {
            // for every notification this mediator is interested in...
            let interests = mediator.listNotificationInterests();
            for (let i = 0; i < interests.length; i++) {
                // remove the observer linking the mediator
                // to the notification interest
                this.removeObserver(interests[i], mediator);
            }

            // remove the mediator from the map
            this.mediatorMap.delete(mediatorName);

            // alert the mediator that it has been removed
            mediator.onRemove();
        }

        return mediator;
    }

    /**
     * Check if a Mediator is registered or not
     *
     * @param {string} mediatorName
     * @returns {boolean} whether a Mediator is registered with the given `mediatorName`.
     */
    hasMediator(mediatorName) {
        return this.mediatorMap.has(mediatorName);
    }

    /**
     * Remove a View instance
     *
     * @static
     * @param key multitonKey of View instance to remove
     */
    static removeView(key) {
        this.instanceMap.delete(key);
    }

    /**
     * Message Constants
     *
     * @static
     * @type {string}
     */
    static get MULTITON_MSG() { return "View instance for this Multiton key already constructed!" };

}
export { View }