<commit_before>import _extend from 'lodash-es/extend';
import _filter from 'lodash-es/filter';
import _groupBy from 'lodash-es/groupBy';

import {
    event as d3_event,
    select as d3_select
} from 'd3-selection';

import { d3combobox as d3_combobox } from '../lib/d3.combobox.js';

import { t } from '../util/locale';

import {
    actionAddEntity,
    actionAddMember,
    actionChangeMember,
    actionDeleteMember
} from '../actions';

import { modeSelect } from '../modes';
import { osmEntity, osmRelation } from '../osm';
import { services } from '../services';
import { svgIcon } from '../svg';
import { uiDisclosure } from './disclosure';
import { utilDisplayName, utilNoAuto } from '../util';

export function uiRawMembershipEditor(context) {
    var taginfo = services.taginfo,
        _entityID,
        _showBlank;

    function selectRelation(d) {
        d3_event.preventDefault();
        context.enter(modeSelect(context, [d.relation.id]));
    }

    function changeRole(d) {
        var role = d3_select(this).property('value');
        context.perform(
            actionChangeMember(d.relation.id, _extend({}, d.member, { role: role }), d.index),
            t('operations.change_role.annotation')
        );
    }

    function addMembership(d, role) {
        _showBlank = false;

        var member = { id: _entityID, type: context.entity(_entityID).type, role: role };

        if (d.relation) {
            context.perform(
                actionAddMember(d.relation.id, member),
                t('operations.add_member.annotation')
            );

        } else {
            var relation = osmRelation();
            context.perform(
                actionAddEntity(relation),
                actionAddMember(relation.id, member),
                t('operations.add.annotation.relation')
            );

            context.enter(modeSelect(context, [relation.id]));
        }
    }

    function deleteMembership(d) {
        context.perform(
            actionDeleteMember(d.relation.id, d.index),
            t('operations.delete_member.annotation')
        );
    }

    function relations(q) {
        var newRelation = { relation: null, value: t('inspector.new_relation') };
        var result = [];
        var graph = context.graph();

        context.intersects(context.extent()).forEach(function(entity) {
            if (entity.type !== 'relation' || entity.id === _entityID)
                return;

            var matched = context.presets().match(entity, graph),
                presetName = (matched && matched.name()) || t('inspector.relation'),
                entityName = utilDisplayName(entity) || '';

            var value = presetName + ' ' + entityName;
            if (q && value.toLowerCase().indexOf(q.toLowerCase()) === -1)
                return;

            result.push({ relation: entity, value: value });
        });

        result.sort(function(a, b) {
            return osmRelation.creationOrder(a.relation, b.relation);
        });

        // Dedupe identical names by appending relation id - see #2891
        var dupeGroups = _filter(
            _groupBy(result, 'value'),
            function(v) { return v.length > 1; }
        );

        dupeGroups.forEach(function(group) {
            group.forEach(function(obj) {
                obj.value += ' ' + obj.relation.id;
            });
        });

        result.unshift(newRelation);
        return result;
    }

    function rawMembershipEditor(selection) {
        var entity = context.entity(_entityID),
            parents = context.graph().parentRelations(entity),
            memberships = [];

        parents.slice(0, 1000).forEach(function(relation) {
            relation.members.forEach(function(member, index) {
                if (member.id === entity.id) {
                    memberships.push({ relation: relation, member: member, index: index });
                }
            });
        });

        var gt = parents.length > 1000 ? '>' : '';
        selection.call(uiDisclosure(context, 'raw_membership_editor', true)
            .title(t('inspector.all_relations') + ' (' + gt + memberships.length + ')')
            .expanded(true)
            .updatePreference(false)
            .on('toggled', function(expanded) {
                if (expanded) { selection.node().parentNode.scrollTop += 200; }
            })
            .content(content)
        );

        function content(wrap) {
            var list = wrap.selectAll('.member-list')
                .data([0]);

            list = list.enter()
                .append('ul')
                .attr('class', 'member-list')
                .merge(list);

            var items = list.selectAll('li.member-row-normal')
                .data(memberships, function(d) {
                    return osmEntity.key(d.relation) + ',' + d.index;
                });

            items.exit()
                .each(unbind)
                .remove();

            var enter = items.enter()
                .append('li')
                .attr('class', 'member-row member-row-normal form-field');

            var label = enter
                .append('label')
                .attr('class', 'form-label')
                .append('a')
                .attr('href', '#')
                .on('click', selectRelation);

            label
                .append('span')
                .attr('class', 'member-entity-type')
                .text(function(d) {
                    var matched = context.presets().match(d.relation, context.graph());
                    return (matched && matched.name()) || t('inspector.relation');
                });

            label
                .append('span')
                .attr('class', 'member-entity-name')
                .text(function(d) { return utilDisplayName(d.relation); });

            enter
                .append('input')
                .attr('class', 'member-role')
                .property('type', 'text')
                .attr('maxlength', 255)
                .attr('placeholder', t('inspector.role'))
                .call(utilNoAuto)
                .property('value', function(d) { return d.member.role; })
                .on('change', changeRole);

            enter
                .append('button')
                .attr('tabindex', -1)
                .attr('class', 'remove button-input-action member-delete minor')
                .on('click', deleteMembership)
                .call(svgIcon('#operation-delete'));

            if (taginfo) {
                enter.each(bindTypeahead);
            }

            var newrow = list.selectAll('.member-row-new')
                .data(_showBlank ? [0] : []);

            newrow.exit()
                .remove();

            enter = newrow.enter()
                .append('li')
                .attr('class', 'member-row member-row-new form-field');

            enter
                .append('input')
                .attr('type', 'text')
                .attr('class', 'member-entity-input')
                .call(utilNoAuto);

            enter
                .append('input')
                .attr('class', 'member-role')
                .property('type', 'text')
                .attr('maxlength', 255)
                .attr('placeholder', t('inspector.role'))
                .call(utilNoAuto)
                .on('change', changeRole);

            enter
                .append('button')
                .attr('tabindex', -1)
                .attr('class', 'remove button-input-action member-delete minor')
                .on('click', deleteMembership)
                .call(svgIcon('#operation-delete'));

            newrow = newrow
                .merge(enter);

            newrow.selectAll('.member-entity-input')
                .call(d3_combobox()
                    .container(context.container())
                    .minItems(1)
                    .fetcher(function(value, callback) { callback(relations(value)); })
                    .on('accept', onAccept)
                );

            var addrel = wrap.selectAll('.add-relation')
                .data([0]);

            addrel = addrel.enter()
                .append('button')
                .attr('class', 'add-relation')
                .merge(addrel);

            addrel
                .call(svgIcon('#icon-plus', 'light'))
                .on('click', function() {
                    _showBlank = true;
                    content(wrap);
                    list.selectAll('.member-entity-input').node().focus();
                });

            function onAccept(d) {
                var role = list.selectAll('.member-row-new .member-role').property('value');
                addMembership(d, role);
            }

            function bindTypeahead(d) {
                var row = d3_select(this),
                    role = row.selectAll('input.member-role');

                function sort(value, data) {
                    var sameletter = [],
                        other = [];
                    for (var i = 0; i < data.length; i++) {
                        if (data[i].value.substring(0, value.length) === value) {
                            sameletter.push(data[i]);
                        } else {
                            other.push(data[i]);
                        }
                    }
                    return sameletter.concat(other);
                }

                role.call(d3_combobox()
                    .container(context.container())
                    .fetcher(function(role, callback) {
                        var rtype = d.relation.tags.type;
                        taginfo.roles({
                            debounce: true,
                            rtype: rtype || '',
                            geometry: context.geometry(_entityID),
                            query: role
                        }, function(err, data) {
                            if (!err) callback(sort(role, data));
                        });
                    }));
            }

            function unbind() {
                var row = d3_select(this);

                row.selectAll('input.member-role')
                    .call(d3_combobox.off);
            }
        }
    }

    rawMembershipEditor.entityID = function(_) {
        if (!arguments.length) return _entityID;
        _entityID = _;
        return rawMembershipEditor;
    };

    return rawMembershipEditor;
}
<commit_msg>Describe relation in tooltip in add membership list<commit_after>import _extend from 'lodash-es/extend';
import _filter from 'lodash-es/filter';
import _forEach from 'lodash-es/forEach';
import _groupBy from 'lodash-es/groupBy';

import {
    event as d3_event,
    select as d3_select
} from 'd3-selection';

import { d3combobox as d3_combobox } from '../lib/d3.combobox.js';

import { t } from '../util/locale';

import {
    actionAddEntity,
    actionAddMember,
    actionChangeMember,
    actionDeleteMember
} from '../actions';

import { modeSelect } from '../modes';
import { osmEntity, osmRelation } from '../osm';
import { services } from '../services';
import { svgIcon } from '../svg';
import { uiDisclosure } from './disclosure';
import { utilDisplayName, utilNoAuto } from '../util';

export function uiRawMembershipEditor(context) {
    var taginfo = services.taginfo,
        _entityID,
        _showBlank;

    function selectRelation(d) {
        d3_event.preventDefault();
        context.enter(modeSelect(context, [d.relation.id]));
    }

    function changeRole(d) {
        var role = d3_select(this).property('value');
        context.perform(
            actionChangeMember(d.relation.id, _extend({}, d.member, { role: role }), d.index),
            t('operations.change_role.annotation')
        );
    }

    function addMembership(d, role) {
        _showBlank = false;

        var member = { id: _entityID, type: context.entity(_entityID).type, role: role };

        if (d.relation) {
            context.perform(
                actionAddMember(d.relation.id, member),
                t('operations.add_member.annotation')
            );

        } else {
            var relation = osmRelation();
            context.perform(
                actionAddEntity(relation),
                actionAddMember(relation.id, member),
                t('operations.add.annotation.relation')
            );

            context.enter(modeSelect(context, [relation.id]));
        }
    }

    function deleteMembership(d) {
        context.perform(
            actionDeleteMember(d.relation.id, d.index),
            t('operations.delete_member.annotation')
        );
    }

    function relations(q) {
        var newRelation = { relation: null, value: t('inspector.new_relation') };
        var result = [];
        var graph = context.graph();

        context.intersects(context.extent()).forEach(function(entity) {
            if (entity.type !== 'relation' || entity.id === _entityID)
                return;

            var matched = context.presets().match(entity, graph),
                presetName = (matched && matched.name()) || t('inspector.relation'),
                entityName = utilDisplayName(entity) || '';

            var value = presetName + ' ' + entityName;
            if (q && value.toLowerCase().indexOf(q.toLowerCase()) === -1)
                return;

            result.push({ relation: entity, value: value });
        });

        result.sort(function(a, b) {
            return osmRelation.creationOrder(a.relation, b.relation);
        });

        // Dedupe identical names by appending relation id - see #2891
        var dupeGroups = _filter(
            _groupBy(result, 'value'),
            function(v) { return v.length > 1; }
        );

        dupeGroups.forEach(function(group) {
            group.forEach(function(obj) {
                obj.value += ' ' + obj.relation.id;
            });
        });

        _forEach(result, function(obj) {
            obj.title = obj.value;
        });

        result.unshift(newRelation);
        return result;
    }

    function rawMembershipEditor(selection) {
        var entity = context.entity(_entityID),
            parents = context.graph().parentRelations(entity),
            memberships = [];

        parents.slice(0, 1000).forEach(function(relation) {
            relation.members.forEach(function(member, index) {
                if (member.id === entity.id) {
                    memberships.push({ relation: relation, member: member, index: index });
                }
            });
        });

        var gt = parents.length > 1000 ? '>' : '';
        selection.call(uiDisclosure(context, 'raw_membership_editor', true)
            .title(t('inspector.all_relations') + ' (' + gt + memberships.length + ')')
            .expanded(true)
            .updatePreference(false)
            .on('toggled', function(expanded) {
                if (expanded) { selection.node().parentNode.scrollTop += 200; }
            })
            .content(content)
        );

        function content(wrap) {
            var list = wrap.selectAll('.member-list')
                .data([0]);

            list = list.enter()
                .append('ul')
                .attr('class', 'member-list')
                .merge(list);

            var items = list.selectAll('li.member-row-normal')
                .data(memberships, function(d) {
                    return osmEntity.key(d.relation) + ',' + d.index;
                });

            items.exit()
                .each(unbind)
                .remove();

            var enter = items.enter()
                .append('li')
                .attr('class', 'member-row member-row-normal form-field');

            var label = enter
                .append('label')
                .attr('class', 'form-label')
                .append('a')
                .attr('href', '#')
                .on('click', selectRelation);

            label
                .append('span')
                .attr('class', 'member-entity-type')
                .text(function(d) {
                    var matched = context.presets().match(d.relation, context.graph());
                    return (matched && matched.name()) || t('inspector.relation');
                });

            label
                .append('span')
                .attr('class', 'member-entity-name')
                .text(function(d) { return utilDisplayName(d.relation); });

            enter
                .append('input')
                .attr('class', 'member-role')
                .property('type', 'text')
                .attr('maxlength', 255)
                .attr('placeholder', t('inspector.role'))
                .call(utilNoAuto)
                .property('value', function(d) { return d.member.role; })
                .on('change', changeRole);

            enter
                .append('button')
                .attr('tabindex', -1)
                .attr('class', 'remove button-input-action member-delete minor')
                .on('click', deleteMembership)
                .call(svgIcon('#operation-delete'));

            if (taginfo) {
                enter.each(bindTypeahead);
            }

            var newrow = list.selectAll('.member-row-new')
                .data(_showBlank ? [0] : []);

            newrow.exit()
                .remove();

            enter = newrow.enter()
                .append('li')
                .attr('class', 'member-row member-row-new form-field');

            enter
                .append('input')
                .attr('type', 'text')
                .attr('class', 'member-entity-input')
                .call(utilNoAuto);

            enter
                .append('input')
                .attr('class', 'member-role')
                .property('type', 'text')
                .attr('maxlength', 255)
                .attr('placeholder', t('inspector.role'))
                .call(utilNoAuto)
                .on('change', changeRole);

            enter
                .append('button')
                .attr('tabindex', -1)
                .attr('class', 'remove button-input-action member-delete minor')
                .on('click', deleteMembership)
                .call(svgIcon('#operation-delete'));

            newrow = newrow
                .merge(enter);

            newrow.selectAll('.member-entity-input')
                .call(d3_combobox()
                    .container(context.container())
                    .minItems(1)
                    .fetcher(function(value, callback) { callback(relations(value)); })
                    .on('accept', onAccept)
                );

            var addrel = wrap.selectAll('.add-relation')
                .data([0]);

            addrel = addrel.enter()
                .append('button')
                .attr('class', 'add-relation')
                .merge(addrel);

            addrel
                .call(svgIcon('#icon-plus', 'light'))
                .on('click', function() {
                    _showBlank = true;
                    content(wrap);
                    list.selectAll('.member-entity-input').node().focus();
                });

            function onAccept(d) {
                var role = list.selectAll('.member-row-new .member-role').property('value');
                addMembership(d, role);
            }

            function bindTypeahead(d) {
                var row = d3_select(this),
                    role = row.selectAll('input.member-role');

                function sort(value, data) {
                    var sameletter = [],
                        other = [];
                    for (var i = 0; i < data.length; i++) {
                        if (data[i].value.substring(0, value.length) === value) {
                            sameletter.push(data[i]);
                        } else {
                            other.push(data[i]);
                        }
                    }
                    return sameletter.concat(other);
                }

                role.call(d3_combobox()
                    .container(context.container())
                    .fetcher(function(role, callback) {
                        var rtype = d.relation.tags.type;
                        taginfo.roles({
                            debounce: true,
                            rtype: rtype || '',
                            geometry: context.geometry(_entityID),
                            query: role
                        }, function(err, data) {
                            if (!err) callback(sort(role, data));
                        });
                    }));
            }

            function unbind() {
                var row = d3_select(this);

                row.selectAll('input.member-role')
                    .call(d3_combobox.off);
            }
        }
    }

    rawMembershipEditor.entityID = function(_) {
        if (!arguments.length) return _entityID;
        _entityID = _;
        return rawMembershipEditor;
    };

    return rawMembershipEditor;
}
<|endoftext|>


<commit_before>'use strict';

define([
  'jquery', 'handlebars', 'track',
  'api', 'auth', 'notification',
  'playlistmanager', 'playermanager'
], function ($, hb, Track, api, auth, notify,
             getPlaylistManager, getPlayerManager) {

var playerManager = getPlayerManager(),
    playlistManager = getPlaylistManager();

/**
 * It binds user actions for playlist manipulation.
 */

function PlaylistEventListener () {
  var that = this,
      root = $('#dropbeat').find('.play-controls .my-playlist');

  this.elems = {
    root: root,
    closeButton: root.find('.close-button'),
    createList: root.find('.create-new-playlist-button'),
    myPlaylists: root.find('.playlists-wrapper .playlists'),
    playlistTmpl: $('#playlist-template').html()
  };

  // Open my playlists view.
  this.openPlaylistView = function () {
    that.elems.myPlaylists.empty();
    that.elems.root.show();

    var template = hb.compile(that.elems.playlistTmpl),
        children = $(template(playlistManager));

    children.appendTo(that.elems.myPlaylists);
    that.bindEvents(children);
  };

  this.openPlaylistCreationView = function () {
    if (playlistManager.prepared()) {
      return;
    }

    var emptyList = playlistManager.prepare();

    // To use same temaplate for `playlists` and `playlist`,
    // this container obj should wrap a playlist.
    var container = {playlists: [emptyList]},
        template = hb.compile(that.elems.playlistTmpl),
        child = $(template(container)).appendTo(that.elems.myPlaylists);

    that.bindEvents(child);
  };

  this.bindEvents = function (elems) {
    var editValue = '.edit-mode-view form input[type=text]';

    // select playlist.
    elems.find('.nonedit-mode-view .title').click(function () {
      var uid = $(this).closest('.playlist').attr('data-uid'),
          selectedList = playlistManager.getPlaylist(uid);

      playlistManager.playOrderControl.reloadQueue();

      if (selectedList !== null) {
        playlistManager.loadPlaylist(uid, true);
      }

      that.elems.root.hide();
    });

    // edit playlist's name
    elems.find('.nonedit-mode-view .menus .rename-button')
      .click(function () {
        $(this).closest('.playlist').addClass('edit-mode');
      });

    // cancel renaming playlist
    elems.find('.edit-mode-view .menus .cancel-edit-button')
      .click(function () {
        var list = $(this).closest('.playlist')
        list.removeClass('edit-mode');
        list.find(editValue).closest().removeClass('warning');

        if (list.attr('data-uid').length === 0) {
          list.remove();
          playlistManager.commit({cancel: true});
        }
      });

    // remove playlist
    elems.find('.nonedit-mode-view .menus .remove-button')
      .click(function () {
        var list = $(this).closest('.playlist'),
            uid = list.attr('data-uid');

        if (uid.length !== 0) {
          $.ajax({
            url: api.Router.getPath('playlist'),
            type: 'DELETE',
            data: JSON.stringify({uid: uid}),
            contentType: 'application/json; charset=utf-8'
          }).done(function () {
            playlistManager.removePlaylist(uid);
            list.remove();
          });
        }
      });

    // apply creation or renaming playlist.
    elems.find('.edit-mode-view .menus .apply-edit-button').click(function () {
      var list = $(this).closest('.playlist'),
          uid = list.attr('data-uid'),
          name = list.find(editValue).val(),
          data = {name: name, uid: uid},
          method = uid.length === 0 ? 'POST' : 'PUT';

      $.ajax({
        url: api.Router.getPath('playlist'),
        type: method,
        data: JSON.stringify(data),
        contentType: 'application/json; charset=utf-8'
      }).done(function (resp) {
        var editContainer = '.edit-mode-view .title-input-field';

        if (!resp.success) {
          list.find(editValue).closest(editContainer).addClass('warning');
          return;
        }

        list.removeClass('edit-mode');
        list.find('.nonedit-mode-view .title').text(name);
        list.find(editValue).closest(editContainer).removeClass('warning');

        if (list.attr('data-uid').length === 0) {

          list.attr('data-uid', resp.playlist.uid);
          playlistManager.commit(resp.playlist);

        } else {
          var playlist = playlistManager.getPlaylist(uid);
          playlist.name = name;
        }
      });
    });

    elems.find('.edit-mode-view .title-input-field form')
      .on('submit', function (event) {
        $(event.currentTarget).closest('.edit-mode-view')
          .find('.menus .apply-edit-button').click();
        return false;
      });
  };

  this.init = function () {
    this.elems.closeButton.click(function () {
      that.elems.root.hide();
    });

    this.elems.createList.click(function () {
      that.openPlaylistCreationView();
    });
  };

};

/**
 * It binds user actions for tracks in playlist.
 */

function PlaylistTracksEventListener () {
  var that = this,
      baseQuery = '.playlist-track-inner ',
      trackTitleQuery = baseQuery + '.track-title-wrapper',
      trackRemoveQuery = baseQuery + '.menus .track-remove',

  // To bold & unbold tracks it should be calculated
  // every time when it called.
  // Because many tracks attached & detached.
  boldCurrentTrack = function () {
    var track = playerManager.getCurrentTrack(),
        elem = $(baseQuery);

    elem.parent().removeClass('on');

    elem = elem.filter('[data-uid="' + track.uid + '"]');
    elem.parent().addClass('on');
  },

  unboldCurrentTrack = function () {
    var track = playerManager.getCurrentTrack(),
        elem = $(baseQuery)
          .filter('[data-uid="' + track.uid + '"]');

    elem.parent().removeClass('on');
  };

  // Contrary to tracks' queries, this elements need not to fetched twice.
  // So pre-fetched elements are more efficient.
  var root = $('#dropbeat').find('.play-controls .playlist-section');
  this.elems = {
    playlistName: root.find('.playlist-name'),
    playlistSearch: root.find('.playlist-search'),
    addByUrl: root.find('.add-by-url-section'),
    playlistInner: root.find('.playlist .playlist-inner'),
    openPlaylist: root.find('.playlist-footer .my-playlist-button'),
    playlistTmpl: $('#playlist-track-template').html()
  };

  this.loadTracksView = function (playlist) {
    var currentPlaylist = playlistManager.currentPlaylist,
        template = hb.compile(that.elems.playlistTmpl);

    if (currentPlaylist !== null) {
      currentPlaylist.selected = false;
    }

    playlist.selected = true;

    playlistManager.currentPlaylist = playlist;
    that.elems.playlistName.text(playlist.name);

    var children = that.elems.playlistInner.html(template(playlist));

    this.bindEvents(children);
  };

  this.addNewTrack = function (track) {
    var playlist = playlistManager.currentPlaylist,
        data = {
          uid: track.uid,
          name: track.name,
          playlist_uid: playlist.uid
        };

    $.ajax({
      url: api.Router.getPath('track'),
      type: 'POST',
      dataType: 'json',
      data: JSON.stringify(data),
      contentType: 'application/json; charset=utf-8',
    }).done(function (resp) {
      if (resp.success) {
        notify.onTrackAdded();
        playlist.push(resp.track);
        that.loadNewTrackView(resp.track);
      }
    });
  };

  this.loadNewTrackView = function (track) {
    var currentPlaylist = playlistManager.currentPlaylist,
        template = hb.compile(that.elems.playlistTmpl),
        trackDump = Object.create(track);

    trackDump.idx = currentPlaylist.index(track.uid);

    if (currentPlaylist === null) {
      return;
    }

    var child = $(template({tracks: [trackDump]}))
      .appendTo(that.elems.playlistInner);

    this.bindEvents(child);
  };

  this.bindEvents = function (elems) {
    elems.find(trackTitleQuery).click(function () {
      var elem = $(this),
          title = elem.find('.track-title').text(),
          uid = elem.parent().attr('data-uid'),
          source = elem.parent().attr('data-source');

      playerManager.play(new Track(uid, title, source));
    });

    elems.find(trackRemoveQuery).click(function () {
      var elem = $(this),
          uid = elem.closest('.playlist-track-inner').attr('data-uid'),
          playlist = playlistManager.currentPlaylist,
          data = {uid: uid, playlist_uid: playlist.uid};

      $.ajax({
        url: api.Router.getPath('track'),
        type: 'DELETE',
        data: JSON.stringify(data),
        dataType: 'json',
        contentType: 'application/json; charset=utf-8'
      }).done(function (resp) {
        if (resp.success) {
          notify.onTrackRemoved();
          playlist.remove(uid);
          that.loadTracksView(playlist);
        }
      });

    });
  };

  this.init = function () {
    that.elems.openPlaylist.click(function () {
      if (auth.currentUser !== null) {
        playlistListener.openPlaylistView();
      }
    });

    playlistManager.setPlaylistCallbacks({
      onPlaylistChange: function (playlist) {
        that.loadTracksView(playlist);
        playlistManager.playOrderControl.reloadQueue();
      },
      onTrackAdded: function (track) {
        that.loadNewTrack(track);
      }
    });

    playerManager.addPlayerCallbacks({
      onPlay: function () {
        boldCurrentTrack();
      },
      onFinish: function () {
        unboldCurrentTrack();
      }
    });
  };
};

var playlistListener = new PlaylistEventListener(),
    tracksListener = new PlaylistTracksEventListener();

return {
  playlist: playlistListener,
  tracks: tracksListener
};

});
<commit_msg>reload playQueue when playlist changes<commit_after>'use strict';

define([
  'jquery', 'handlebars', 'track',
  'api', 'auth', 'notification',
  'playlistmanager', 'playermanager'
], function ($, hb, Track, api, auth, notify,
             getPlaylistManager, getPlayerManager) {

var playerManager = getPlayerManager(),
    playlistManager = getPlaylistManager();

/**
 * It binds user actions for playlist manipulation.
 */

function PlaylistEventListener () {
  var that = this,
      root = $('#dropbeat').find('.play-controls .my-playlist');

  this.elems = {
    root: root,
    closeButton: root.find('.close-button'),
    createList: root.find('.create-new-playlist-button'),
    myPlaylists: root.find('.playlists-wrapper .playlists'),
    playlistTmpl: $('#playlist-template').html()
  };

  // Open my playlists view.
  this.openPlaylistView = function () {
    that.elems.myPlaylists.empty();
    that.elems.root.show();

    var template = hb.compile(that.elems.playlistTmpl),
        children = $(template(playlistManager));

    children.appendTo(that.elems.myPlaylists);
    that.bindEvents(children);
  };

  this.openPlaylistCreationView = function () {
    if (playlistManager.prepared()) {
      return;
    }

    var emptyList = playlistManager.prepare();

    // To use same temaplate for `playlists` and `playlist`,
    // this container obj should wrap a playlist.
    var container = {playlists: [emptyList]},
        template = hb.compile(that.elems.playlistTmpl),
        child = $(template(container)).appendTo(that.elems.myPlaylists);

    that.bindEvents(child);
  };

  this.bindEvents = function (elems) {
    var editValue = '.edit-mode-view form input[type=text]';

    // select playlist.
    elems.find('.nonedit-mode-view .title').click(function () {
      var uid = $(this).closest('.playlist').attr('data-uid'),
          selectedList = playlistManager.getPlaylist(uid);

      if (selectedList !== null) {
        playlistManager.loadPlaylist(uid, true);
      }

      that.elems.root.hide();
    });

    // edit playlist's name
    elems.find('.nonedit-mode-view .menus .rename-button')
      .click(function () {
        $(this).closest('.playlist').addClass('edit-mode');
      });

    // cancel renaming playlist
    elems.find('.edit-mode-view .menus .cancel-edit-button')
      .click(function () {
        var list = $(this).closest('.playlist')
        list.removeClass('edit-mode');
        list.find(editValue).closest().removeClass('warning');

        if (list.attr('data-uid').length === 0) {
          list.remove();
          playlistManager.commit({cancel: true});
        }
      });

    // remove playlist
    elems.find('.nonedit-mode-view .menus .remove-button')
      .click(function () {
        var list = $(this).closest('.playlist'),
            uid = list.attr('data-uid');

        if (uid.length !== 0) {
          $.ajax({
            url: api.Router.getPath('playlist'),
            type: 'DELETE',
            data: JSON.stringify({uid: uid}),
            contentType: 'application/json; charset=utf-8'
          }).done(function () {
            playlistManager.removePlaylist(uid);
            list.remove();
          });
        }
      });

    // apply creation or renaming playlist.
    elems.find('.edit-mode-view .menus .apply-edit-button').click(function () {
      var list = $(this).closest('.playlist'),
          uid = list.attr('data-uid'),
          name = list.find(editValue).val(),
          data = {name: name, uid: uid},
          method = uid.length === 0 ? 'POST' : 'PUT';

      $.ajax({
        url: api.Router.getPath('playlist'),
        type: method,
        data: JSON.stringify(data),
        contentType: 'application/json; charset=utf-8'
      }).done(function (resp) {
        var editContainer = '.edit-mode-view .title-input-field';

        if (!resp.success) {
          list.find(editValue).closest(editContainer).addClass('warning');
          return;
        }

        list.removeClass('edit-mode');
        list.find('.nonedit-mode-view .title').text(name);
        list.find(editValue).closest(editContainer).removeClass('warning');

        if (list.attr('data-uid').length === 0) {

          list.attr('data-uid', resp.playlist.uid);
          playlistManager.commit(resp.playlist);

        } else {
          var playlist = playlistManager.getPlaylist(uid);
          playlist.name = name;
        }
      });
    });

    elems.find('.edit-mode-view .title-input-field form')
      .on('submit', function (event) {
        $(event.currentTarget).closest('.edit-mode-view')
          .find('.menus .apply-edit-button').click();
        return false;
      });
  };

  this.init = function () {
    this.elems.closeButton.click(function () {
      that.elems.root.hide();
    });

    this.elems.createList.click(function () {
      that.openPlaylistCreationView();
    });
  };

};

/**
 * It binds user actions for tracks in playlist.
 */

function PlaylistTracksEventListener () {
  var that = this,
      baseQuery = '.playlist-track-inner ',
      trackTitleQuery = baseQuery + '.track-title-wrapper',
      trackRemoveQuery = baseQuery + '.menus .track-remove',

  // To bold & unbold tracks it should be calculated
  // every time when it called.
  // Because many tracks attached & detached.
  boldCurrentTrack = function () {
    var track = playerManager.getCurrentTrack(),
        elem = $(baseQuery);

    elem.parent().removeClass('on');

    elem = elem.filter('[data-uid="' + track.uid + '"]');
    elem.parent().addClass('on');
  },

  unboldCurrentTrack = function () {
    var track = playerManager.getCurrentTrack(),
        elem = $(baseQuery)
          .filter('[data-uid="' + track.uid + '"]');

    elem.parent().removeClass('on');
  };

  // Contrary to tracks' queries, this elements need not to fetched twice.
  // So pre-fetched elements are more efficient.
  var root = $('#dropbeat').find('.play-controls .playlist-section');
  this.elems = {
    playlistName: root.find('.playlist-name'),
    playlistSearch: root.find('.playlist-search'),
    addByUrl: root.find('.add-by-url-section'),
    playlistInner: root.find('.playlist .playlist-inner'),
    openPlaylist: root.find('.playlist-footer .my-playlist-button'),
    playlistTmpl: $('#playlist-track-template').html()
  };

  this.loadTracksView = function (playlist) {
    var currentPlaylist = playlistManager.currentPlaylist,
        template = hb.compile(that.elems.playlistTmpl);

    if (currentPlaylist !== null) {
      currentPlaylist.selected = false;
    }

    playlist.selected = true;

    playlistManager.currentPlaylist = playlist;
    that.elems.playlistName.text(playlist.name);

    var children = that.elems.playlistInner.html(template(playlist));

    this.bindEvents(children);

    playlistManager.playOrderControl.reloadQueue();
  };

  this.addNewTrack = function (track) {
    var playlist = playlistManager.currentPlaylist,
        data = {
          uid: track.uid,
          name: track.name,
          playlist_uid: playlist.uid
        };

    $.ajax({
      url: api.Router.getPath('track'),
      type: 'POST',
      dataType: 'json',
      data: JSON.stringify(data),
      contentType: 'application/json; charset=utf-8',
    }).done(function (resp) {
      if (resp.success) {
        notify.onTrackAdded();
        playlist.push(resp.track);
        that.loadTracksView(playlist);
      }
    });
  };

  this.bindEvents = function (elems) {
    elems.find(trackTitleQuery).click(function () {
      var elem = $(this),
          title = elem.find('.track-title').text(),
          uid = elem.parent().attr('data-uid'),
          source = elem.parent().attr('data-source');

      playerManager.play(new Track(uid, title, source));
    });

    elems.find(trackRemoveQuery).click(function () {
      var elem = $(this),
          uid = elem.closest('.playlist-track-inner').attr('data-uid'),
          playlist = playlistManager.currentPlaylist,
          data = {uid: uid, playlist_uid: playlist.uid};

      $.ajax({
        url: api.Router.getPath('track'),
        type: 'DELETE',
        data: JSON.stringify(data),
        dataType: 'json',
        contentType: 'application/json; charset=utf-8'
      }).done(function (resp) {
        if (resp.success) {
          notify.onTrackRemoved();
          playlist.remove(uid);
          that.loadTracksView(playlist);
        }
      });

    });
  };

  this.init = function () {
    that.elems.openPlaylist.click(function () {
      if (auth.currentUser !== null) {
        playlistListener.openPlaylistView();
      }
    });

    playlistManager.setPlaylistCallbacks({
      onPlaylistChange: function (playlist) {
        that.loadTracksView(playlist);
      },
      onTrackAdded: function (track) {
        that.loadNewTrack(track);
      }
    });

    playerManager.addPlayerCallbacks({
      onPlay: function () {
        boldCurrentTrack();
      },
      onFinish: function () {
        unboldCurrentTrack();
      }
    });
  };
};

var playlistListener = new PlaylistEventListener(),
    tracksListener = new PlaylistTracksEventListener();

return {
  playlist: playlistListener,
  tracks: tracksListener
};

});
<|endoftext|>


<commit_before>'use strict';

const env = process.env.BABEL_ENV || process.env.NODE_ENV;

const plugins = [
  [require.resolve('react-loadable/babel'), { webpack: true, babel: true }],
  require.resolve('babel-plugin-transform-class-properties'),
  [
    require.resolve('babel-plugin-transform-object-rest-spread'),
    {
      useBuiltIns: true,
    },
  ],
  [
    require.resolve('babel-plugin-transform-react-jsx'),
    {
      useBuiltIns: true,
    },
  ],
  [
    require.resolve('babel-plugin-transform-runtime'),
    {
      helpers: false,
      polyfill: false,
      regenerator: true,
    },
  ],
  require.resolve('babel-plugin-idx'),
  require.resolve('babel-plugin-syntax-dynamic-import'),
];

if (env === 'development' || env === 'test') {
  const devPlugins = [
    require.resolve('babel-plugin-transform-react-jsx-source'),
    require.resolve('babel-plugin-transform-react-jsx-self'),
  ];

  plugins.push(...devPlugins);
}

const clientEnvPresets = [
  [
    require.resolve('babel-preset-env'),
    {
      modules: false,
      targets: {
        ie: 10,
        // We currently minify with uglify
        // Remove after https://github.com/mishoo/UglifyJS2/issues/448
        uglify: true,
      },
      // Disable polyfill transforms
      useBuiltIns: false,
      // Do not transform modules to CJS
      modules: false,
    },
  ],
];

const serverEnvPresets = [
  [
    require.resolve('babel-preset-env'),
    {
      modules: false,
      targets: {
        node: 'current',
      },
      // Disable polyfill transforms
      useBuiltIns: false,
      // Do not transform modules to CJS
      modules: false,
    },
  ],
];

const serverPlugins = [require.resolve('babel-plugin-dynamic-import-node')];

module.exports = {
  env: {
    client: {
      presets: [require.resolve('babel-preset-react'), ...clientEnvPresets],
      plugins: [
        ...plugins,
        [require.resolve('babel-plugin-transform-regenerator'), { async: false }],
      ],
    },
    server: {
      presets: [require.resolve('babel-preset-react'), ...serverEnvPresets],
      plugins: [...plugins, ...serverPlugins],
    },
  },
};
<commit_msg>🐛 transform es6 modules in test environment<commit_after>'use strict';

const env = process.env.BABEL_ENV || process.env.NODE_ENV;

const plugins = [
  [require.resolve('react-loadable/babel'), { webpack: true, babel: true }],
  require.resolve('babel-plugin-transform-class-properties'),
  [
    require.resolve('babel-plugin-transform-object-rest-spread'),
    {
      useBuiltIns: true,
    },
  ],
  [
    require.resolve('babel-plugin-transform-react-jsx'),
    {
      useBuiltIns: true,
    },
  ],
  [
    require.resolve('babel-plugin-transform-runtime'),
    {
      helpers: false,
      polyfill: false,
      regenerator: true,
    },
  ],
  require.resolve('babel-plugin-idx'),
  require.resolve('babel-plugin-syntax-dynamic-import'),
];

if (env === 'development' || env === 'test') {
  const devPlugins = [
    require.resolve('babel-plugin-transform-react-jsx-source'),
    require.resolve('babel-plugin-transform-react-jsx-self'),
  ];

  plugins.push(...devPlugins);
}

const clientEnvPresets = [
  [
    require.resolve('babel-preset-env'),
    {
      targets: {
        ie: 10,
        // We currently minify with uglify
        // Remove after https://github.com/mishoo/UglifyJS2/issues/448
        uglify: true,
      },
      // Disable polyfill transforms
      useBuiltIns: false,
      // Do not transform modules to CJS
      modules: false,
    },
  ],
];

const serverEnvPresets = [
  [
    require.resolve('babel-preset-env'),
    {
      targets: {
        node: 'current',
      },
      // Disable polyfill transforms
      useBuiltIns: false,
      // Do not transform modules to CJS in non test env
      modules: env === 'test' ? undefined : false,
    },
  ],
];

const serverPlugins = [require.resolve('babel-plugin-dynamic-import-node')];

module.exports = {
  env: {
    client: {
      presets: [require.resolve('babel-preset-react'), ...clientEnvPresets],
      plugins: [
        ...plugins,
        [require.resolve('babel-plugin-transform-regenerator'), { async: false }],
      ],
    },
    server: {
      presets: [require.resolve('babel-preset-react'), ...serverEnvPresets],
      plugins: [...plugins, ...serverPlugins],
    },
  },
};
<|endoftext|>


<commit_before>require('./server.js');
var dischord = require('discord.js');

var bot = new dischord.Client();

/*function searchGrimoire() {
    bot.on("message", function(message) {
        var input = message.content;
        var stripeCmd = input.substr('8');
        var inputCheck = input.startsWith('!search');

        if(inputCheck) {
            bot.reply(message, "http://www.ishtar-collective.net/search/" + encodeURIComponent(stripeCmd));
        }

    });
}*/

bot.loginWithToken(process.env.CLIENT_ID, function (token, err) {
    if(err){
        console.log(err);
    }

    bot.on("message", function(message) {
        var input = message.content;
        var stripeCmd = input.substr('8');
        var inputCheck = input.startsWith('!search');

        if(inputCheck) {
            bot.reply(message, "http://www.ishtar-collective.net/search/" + encodeURIComponent(stripeCmd));
        }

    });

    //searchGrimoire();
});<commit_msg>replace method calls... again... damn heroku<commit_after>require('./server.js');
var dischord = require('discord.js');

var bot = new dischord.Client();

function searchGrimoire() {
    bot.on("message", function(message) {
        var input = message.content;
        var stripeCmd = input.substr('8');
        var inputCheck = input.startsWith('!search');

        if(inputCheck) {
            bot.reply(message, "http://www.ishtar-collective.net/search/" + encodeURIComponent(stripeCmd));
        }

    });
}

bot.loginWithToken(process.env.CLIENT_ID, function (token, err) {
    if(err){
        console.log(err);
    }

    searchGrimoire();
});<|endoftext|>


<commit_before>/*
 * #%L
 * ACS AEM Commons Package
 * %%
 * Copyright (C) 2013 Adobe
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 *
 * A sample component dialog using the Touch UI Multi Field
 * Note the usage of empty valued acs-commons-nested property
 *
 *
 <?xml version="1.0" encoding="UTF-8"?>
    <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="ACS AEM Commions Multifield TouchUI Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
        <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
            <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
            <items jcr:primaryType="nt:unstructured">
                <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                    <items jcr:primaryType="nt:unstructured">
                        <fieldset
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Sample Dashboard"
                        sling:resourceType="granite/ui/components/foundation/form/fieldset">
                            <layout
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                            <items jcr:primaryType="nt:unstructured">
                                <column
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/foundation/container">
                                    <items jcr:primaryType="nt:unstructured">
                                        <dashboard
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/textfield"
                                        fieldDescription="Enter Dashboard name"
                                        fieldLabel="Dashboard name"
                                        name="./dashboard"/>
                                        <pages
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/multifield"
                                        class="full-width"
                                        fieldDescription="Click '+' to add a new page"
                                        fieldLabel="Pages">
                                            <field
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                            acs-commons-nested=""
                                            name="./pages">
                                                <layout
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
                                                method="absolute"/>
                                                <items jcr:primaryType="nt:unstructured">
                                                    <column
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/foundation/container">
                                                        <items jcr:primaryType="nt:unstructured">
                                                            <page
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                            fieldDescription="Name of Page"
                                                            fieldLabel="Page Name"
                                                            name="./page"/>
                                                            <path
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
                                                            fieldDescription="Select Path"
                                                            fieldLabel="Path"
                                                            name="./path"
                                                            rootPath="/content"/>
                                                        </items>
                                                    </column>
                                                </items>
                                            </field>
                                        </pages>
                                    </items>
                                </column>
                            </items>
                        </fieldset>
                    </items>
                </column>
            </items>
        </content>
    </jcr:root>

*/

(function () {
    var DATA_ACS_COMMONS_NESTED = "data-acs-commons-nested",
        CFFW = ".coral-Form-fieldwrapper",
        _ = window._, CUI = window.CUI,
        Class = window.Class;

    //reads multifield data from server, creates the nested composite multifields and fills them
    function addDataInFields() {
        $(document).on("dialog-ready", function() {
            var mNames = [],
                $fieldSets = $("[" + DATA_ACS_COMMONS_NESTED + "][class='coral-Form-fieldset']"),
                $form = $fieldSets.closest("form.foundation-form"),
                actionUrl = $form.attr("action") + ".json",
                mValues, $field, name;

            if(_.isEmpty($fieldSets)){
                return;
            }

            $fieldSets.each(function (i, fieldSet) {
                mNames.push($(fieldSet).data("name"));
            });

            mNames = _.uniq(mNames);

            //creates & fills the nested multifield with data
            function fillNestedFields($multifield, valueArr){
                _.each(valueArr, function(record, index){
                    $multifield.find(".js-coral-Multifield-add").click();

                    //a setTimeout may be needed
                    _.each(record, function(value, key){
                        var $field = $($multifield.find("[name='./" + key + "']")[index]);
                        $field.val(value);
                    });
                });
            }

            function postProcess(data){
                _.each(mNames, function(mName){
                    if(_.isEmpty(mName)){
                        return;
                    }

                    $fieldSets = $("[data-name='" + mName + "']");

                    //strip ./
                    mName = mName.substring(2);

                    mValues = data[mName];

                    if(_.isString(mValues)){
                        mValues = [ JSON.parse(mValues) ];
                    }

                    _.each(mValues, function (record, i) {
                        if (!record) {
                            return;
                        }

                        if(_.isString(record)){
                            record = JSON.parse(record);
                        }

                        _.each(record, function(rValue, rKey){
                            $field = $($fieldSets[i]).find("[name='./" + rKey + "']");

                            if(_.isArray(rValue) && !_.isEmpty(rValue)){
                                fillNestedFields( $($fieldSets[i]).find("[data-init='multifield']"), rValue);
                            }else{
                                $field.val(rValue);
                            }
                        });
                    });
                });
            }

            $.ajax(actionUrl).done(postProcess);
        });
    }

    function fillValue($field, record){
        var name = $field.attr("name");

        if (!name) {
            return;
        }

        //strip ./
        if (name.indexOf("./") === 0) {
            name = name.substring(2);
        }

        record[name] = $field.val();

        //remove the field, so that individual values are not POSTed
        $field.remove();
    }

    //for getting the nested multifield data as js objects
    function getRecordFromMultiField($multifield){
        var $fieldSets = $multifield.find("[class='coral-Form-fieldset']"),
            records = [], record, $fields, name;

        $fieldSets.each(function (i, fieldSet) {
            $fields = $(fieldSet).find("[name]");

            record = {};

            $fields.each(function (j, field) {
                fillValue($(field), record);
            });

            if(!$.isEmptyObject(record)){
                records.push(record);
            }
        });

        return records;
    }

    //collect data from widgets in multifield and POST them to CRX as JSON
    function collectDataFromFields() {
        var $form = $(this).closest("form.foundation-form"),
            $fieldSets = $("[" + DATA_ACS_COMMONS_NESTED + "][class='coral-Form-fieldset']"),
            record, $fields, $field, name, $nestedMultiField;

        $fieldSets.each(function (i, fieldSet) {
            $fields = $(fieldSet).children().children(CFFW);

            record = {};

            $fields.each(function (j, field) {
                $field = $(field);

                //may be a nested multifield
                $nestedMultiField = $field.find("[data-init='multifield']");

                if($nestedMultiField.length === 0){
                    fillValue($field.find("[name]"), record);
                }else{
                    name = $nestedMultiField.find("[class='coral-Form-fieldset']").data("name");

                    if(!name){
                        return;
                    }

                    //strip ./
                    name = name.substring(2);

                    record[name] = getRecordFromMultiField($nestedMultiField);
                }
            });

            if ($.isEmptyObject(record)) {
                return;
            }

            //add the record JSON in a hidden field as string
            $('<input />').attr('type', 'hidden')
                .attr('name',  $(fieldSet).data("name"))
                .attr('value', JSON.stringify(record))
                .appendTo($form);
        });
    }

    $(document).ready(function () {
        addDataInFields();

        $(document).on("dialog-ready", function() {
            //$(document).on("click", ".cq-dialog-submit", submitAction);
            var dialog = document.querySelector('form.cq-dialog');

            if(dialog){
                dialog.addEventListener('submit', collectDataFromFields, true);
            }
        });
    });

    //extend otb multifield for adjusting event propagation when there are nested multifields
    //for working around the nested multifield add and reorder
    CUI.Multifield = new Class({
        toString: "Multifield",
        extend: CUI.Multifield,

        construct: function (options) {
            this.script = this.$element.find(".js-coral-Multifield-input-template:last");
        },

        _addListeners: function () {
            this.superClass._addListeners.call(this);

            //otb coral event handler is added on selector .js-coral-Multifield-add
            //any nested multifield add click events are propagated to the parent multifield;
            //to prevent adding a new composite field in both nested multifield and parent multifield
            //when user clicks on add of nested multifield, stop the event propagation to parent multifield
            this.$element.on("click", ".js-coral-Multifield-add", function (e) {
                e.stopPropagation();
            });

            this.$element.on("drop", function (e) {
                e.stopPropagation();
            });
        }
    });

    CUI.Widget.registry.register("multifield", CUI.Multifield);
}());<commit_msg>touchui-composite-multifield doesn't load values for several field types e.g. checkbox, dropdown selection #477<commit_after>/*
 * #%L
 * ACS AEM Commons Package
 * %%
 * Copyright (C) 2013 Adobe
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 *
 * A sample component dialog using the Touch UI Multi Field
 * Note the usage of empty valued acs-commons-nested property
 *
 *
 <?xml version="1.0" encoding="UTF-8"?>
    <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="ACS AEM Commions Multifield TouchUI Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
        <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
            <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
            <items jcr:primaryType="nt:unstructured">
                <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                    <items jcr:primaryType="nt:unstructured">
                        <fieldset
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Sample Dashboard"
                        sling:resourceType="granite/ui/components/foundation/form/fieldset">
                            <layout
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                            <items jcr:primaryType="nt:unstructured">
                                <column
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/foundation/container">
                                    <items jcr:primaryType="nt:unstructured">
                                        <dashboard
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/textfield"
                                        fieldDescription="Enter Dashboard name"
                                        fieldLabel="Dashboard name"
                                        name="./dashboard"/>
                                        <pages
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/multifield"
                                        class="full-width"
                                        fieldDescription="Click '+' to add a new page"
                                        fieldLabel="Pages">
                                            <field
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                            acs-commons-nested=""
                                            name="./pages">
                                                <layout
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
                                                method="absolute"/>
                                                <items jcr:primaryType="nt:unstructured">
                                                    <column
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/foundation/container">
                                                        <items jcr:primaryType="nt:unstructured">
                                                            <page
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                            fieldDescription="Name of Page"
                                                            fieldLabel="Page Name"
                                                            name="./page"/>
                                                            <path
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
                                                            fieldDescription="Select Path"
                                                            fieldLabel="Path"
                                                            name="./path"
                                                            rootPath="/content"/>
                                                        </items>
                                                    </column>
                                                </items>
                                            </field>
                                        </pages>
                                    </items>
                                </column>
                            </items>
                        </fieldset>
                    </items>
                </column>
            </items>
        </content>
    </jcr:root>

*/
(function ($, $document) {
    var DATA_ACS_COMMONS_NESTED = "data-acs-commons-nested",
        CFFW = ".coral-Form-fieldwrapper",
        _ = window._, CUI = window.CUI,
        Class = window.Class;

    function isSelectOne($field){
        return !_.isEmpty($field) && ($field.prop("type") === "select-one");
    }

    function setSelectOne($field, value){
        var select = $field.closest(".coral-Select").data("select");

        if(select){
            select.setValue(value);
        }
    }

    function isCheckbox($field){
        return !_.isEmpty($field) && ($field.prop("type") === "checkbox");
    }

    function setCheckBox($field, value){
        $field.prop( "checked", $field.attr("value") == value);
    }

    function setWidgetValue($field, value){
        if(_.isEmpty($field)){
            return;
        }

        if(isSelectOne($field)){
            setSelectOne($field, value);
        }else if(isCheckbox($field)){
            setCheckBox($field, value);
        }else{
            $field.val(value);
        }
    }

    //reads multifield data from server, creates the nested composite multifields and fills them
    function addDataInFields() {
        $document.on("dialog-ready", dlgReadyHandler);

        function dlgReadyHandler() {
            var mNames = [],
                $fieldSets = $("[" + DATA_ACS_COMMONS_NESTED + "][class='coral-Form-fieldset']"),
                $form = $fieldSets.closest("form.foundation-form"),
                actionUrl = $form.attr("action") + ".json",
                mValues, $field, name;

            if(_.isEmpty($fieldSets)){
                return;
            }

            $fieldSets.each(function (i, fieldSet) {
                mNames.push($(fieldSet).data("name"));
            });

            mNames = _.uniq(mNames);

            //creates & fills the nested multifield with data
            function fillNestedFields($multifield, valueArr){
                _.each(valueArr, function(record, index){
                    $multifield.find(".js-coral-Multifield-add").click();

                    //a setTimeout may be needed
                    _.each(record, function(value, key){
                        setWidgetValue($($multifield.find("[name='./" + key + "']")[index]), value);
                    });
                });
            }

            function postProcess(data){
                _.each(mNames, function(mName){
                    if(_.isEmpty(mName)){
                        return;
                    }

                    $fieldSets = $("[data-name='" + mName + "']");

                    //strip ./
                    mName = mName.substring(2);

                    mValues = data[mName];

                    if(_.isString(mValues)){
                        mValues = [ JSON.parse(mValues) ];
                    }

                    _.each(mValues, function (record, i) {
                        if (!record) {
                            return;
                        }

                        if(_.isString(record)){
                            record = JSON.parse(record);
                        }

                        _.each(record, function(rValue, rKey){
                            $field = $($fieldSets[i]).find("[name='./" + rKey + "']");

                            if(_.isArray(rValue) && !_.isEmpty(rValue)){
                                fillNestedFields( $($fieldSets[i]).find("[data-init='multifield']"), rValue);
                            }else{
                                setWidgetValue($field, rValue);
                            }
                        });
                    });
                });
            }

            $.ajax(actionUrl).done(postProcess);
        }
    }

    function fillValue($field, record){
        var name = $field.attr("name"), value;

        if (!name) {
            return;
        }

        //strip ./
        if (name.indexOf("./") === 0) {
            name = name.substring(2);
        }

        value = $field.val();

        if( isCheckbox($field) ){
            value = $field.prop("checked") ? $field.val() : "";
        }

        record[name] = value;

        //remove the field, so that individual values are not POSTed
        $field.remove();
    }

    //for getting the nested multifield data as js objects
    function getRecordFromMultiField($multifield){
        var $fieldSets = $multifield.find("[class='coral-Form-fieldset']"),
            records = [], record, $fields, name;

        $fieldSets.each(function (i, fieldSet) {
            $fields = $(fieldSet).find("[name]");

            record = {};

            $fields.each(function (j, field) {
                fillValue($(field), record);
            });

            if(!$.isEmptyObject(record)){
                records.push(record);
            }
        });

        return records;
    }

    //collect data from widgets in multifield and POST them to CRX as JSON
    function collectDataFromFields() {
        var $form = $(this).closest("form.foundation-form"),
            $fieldSets = $("[" + DATA_ACS_COMMONS_NESTED + "][class='coral-Form-fieldset']"),
            record, $fields, $field, name, $nestedMultiField;

        $fieldSets.each(function (i, fieldSet) {
            $fields = $(fieldSet).children().children(CFFW);

            record = {};

            $fields.each(function (j, field) {
                $field = $(field);

                //may be a nested multifield
                $nestedMultiField = $field.find("[data-init='multifield']");

                if($nestedMultiField.length === 0){
                    fillValue($field.find("[name]"), record);
                }else{
                    name = $nestedMultiField.find("[class='coral-Form-fieldset']").data("name");

                    if(!name){
                        return;
                    }

                    //strip ./
                    name = name.substring(2);

                    record[name] = getRecordFromMultiField($nestedMultiField);
                }
            });

            if ($.isEmptyObject(record)) {
                return;
            }

            //add the record JSON in a hidden field as string
            $('<input />').attr('type', 'hidden')
                .attr('name',  $(fieldSet).data("name"))
                .attr('value', JSON.stringify(record))
                .appendTo($form);
        });
    }

    $document.ready(function () {
        addDataInFields();

        $document.on("dialog-ready", function() {
            //$(document).on("click", ".cq-dialog-submit", submitAction);
            var dialog = document.querySelector('form.cq-dialog');

            if(dialog){
                dialog.addEventListener('submit', collectDataFromFields, true);
            }
        });
    });

    //extend otb multifield for adjusting event propagation when there are nested multifields
    //for working around the nested multifield add and reorder
    CUI.Multifield = new Class({
        toString: "Multifield",
        extend: CUI.Multifield,

        construct: function (options) {
            this.script = this.$element.find(".js-coral-Multifield-input-template:last");
        },

        _addListeners: function () {
            this.superClass._addListeners.call(this);

            //otb coral event handler is added on selector .js-coral-Multifield-add
            //any nested multifield add click events are propagated to the parent multifield;
            //to prevent adding a new composite field in both nested multifield and parent multifield
            //when user clicks on add of nested multifield, stop the event propagation to parent multifield
            this.$element.on("click", ".js-coral-Multifield-add", function (e) {
                e.stopPropagation();
            });

            this.$element.on("drop", function (e) {
                e.stopPropagation();
            });
        }
    });

    CUI.Widget.registry.register("multifield", CUI.Multifield);
}(jQuery, jQuery(document)));<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

MODEL({
  name: 'DetailView',
  extendsModel: 'View',

  imports: [ 'data' ],
  exports: [ 'data' ],

  documentation:function() {/*
    When a view based on $$DOC{ref:'Property'} values is desired, $$DOC{ref:'DetailView'}
    is the place to start. Either using $$DOC{ref:'DetailView'} directly, implementing
    a .toDetailHTML() $$DOC{ref:'Method'} in your model, or extending
    $$DOC{ref:'DetailView'} to add custom formatting.

Set the $$DOC{ref:'.data'} $$DOC{ref:'Property'} to the $$DOC{ref:'Model'} instance
    you want to display. $$DOC{ref:'DetailView'} will extract the $$DOC{ref:'Model'}
    definition, create editors for the $$DOC{ref:'Property',usePlural:true}, and
    display the current values of your instance. Set $$DOC{ref:'.mode',usePlural:true}
    to indicate read-only if desired.

$$DOC{ref:'Model',usePlural:true} may specify a .toDetailHTML() $$DOC{ref:'Method'} or
    $$DOC{ref:'Template'} to render their contents instead of
    $$DOC{ref:'DetailView.defaultToHTML'}.

For each $$DOC{ref:'Property'} in the $$DOC{ref:'.data'} instance specified,
    a $$DOC{ref:'PropertyView'} is created that selects the appropriate $$DOC{ref:'View'}
    to construct.
  */},

  properties: [
    {
      name: 'className',
      defaultValue: 'detailView',
      documentation: function() {/*
          The CSS class names to use for HTML $$DOC{ref:'View',usePlural:true}.
          Separate class names with spaces. Each instance of a $$DOC{ref:'DetailView'}
          may have different classes specified.
      */}
    },
    {
      name:  'data',
      postSet: function(_, data) {
        if ( data && data.model_ && this.model !== data.model_ ) {
          this.model = data.model_;
        }
        this.onValueChange_();
      },
      documentation: function() {/*
        The $$DOC{ref:'Model'} to view. The $$DOC{ref:'Property',usePlural:true}
        of this $$DOC{ref:'Model'} instance will be examined and a $$DOC{ref:'PropertyView'}
        created for each with editors for the current value.

Sub-views of $$DOC{ref:'DetailView'} are passed this $$DOC{ref:'.data'}
        property, from which $$DOC{ref:'PropertyView'} will extract its named
        $$DOC{ref:'Property'}
        and bind the property to the sub-view $$DOC{ref:'DetailView.data'}.

      */}
    },
    {
      name:  'model',
      type:  'Model',
      postSet: function(_, m) {
        if ( this.$ ) {
          this.children = [];
          this.$.outerHTML = this.toHTML();
          this.initHTML();
        }
      },
      documentation: function() {/*
        The $$DOC{ref:'.model'} is extracted from $$DOC{ref:'.data'}, or can be
        set in advance when the type of $$DOC{ref:'.data'} is known. The $$DOC{ref:'Model'}
        is used to set up the structure of the $$DOC{ref:'DetailView'}, by examining the
        $$DOC{ref:'Property',usePlural:true}. Changing the $$DOC{ref:'.data'} out
        for another instance of the same $$DOC{ref:'Model'} will refresh the contents
        of the sub-views without destroying and re-creating them.

      */}
    },
    {
      name: 'title',
      defaultValueFn: function() { return "Edit " + this.model.label; },
      documentation: function() {/*

The display title for the $$DOC{ref:'View'}.

      */}
    },
    {
      model_: 'StringProperty',
      name: 'mode',
      defaultValue: 'read-write',
      documentation: function() {/*
        The editing mode. To disable editing set to 'read-only'.

      */}
    },
    {
      model_: 'BooleanProperty',
      name: 'showRelationships',
      defaultValue: false,
      documentation: function() {/*
        Set true to create sub-views to display $$DOC{ref:'Relationship',usePlural:true}
        for the $$DOC{ref:'.model'}.

      */}
    }
  ],

  methods: {
    // Template Method
    onValueChange_: function() { /* Override with value update code. */ },

    viewModel: function() { /* The $$DOC{ref:'Model'} type of the $$DOC{ref:'.data'}. */
       return this.model;
    },

    createTemplateView: function(name, opt_args) {
      /* Overridden here to set the new View.$$DOC{ref:'.data'} to this.$$DOC{ref:'.data'}.
         See $$DOC{ref:'View.createTemplateView'}. */
      var o = this.viewModel()[name];
      if ( o ) {
        var v;

        if ( Action.isInstance(o) )
          v = this.createActionView(o, opt_args);
        else if ( Relationship.isInstance(o) )
          v = this.createRelationshipView(o, opt_args);
        else
          v = this.createView(o, opt_args);

        v.data$ = this.data$;

        return v;
      }

      return this.SUPER(name, opt_args);
    },

    titleHTML: function() {
      /* Title text HTML formatter */
      var title = this.title;

      return title ?
        '
' + title + '
' :
        '';
    },

    startForm: function() { /* HTML formatter */ return '
'; },
    endForm: function() { /* HTML formatter */ return '
'; },

    startColumns: function() { /* HTML formatter */ return '

'; },
    nextColumn:   function() { /* HTML formatter */ return '

'; },
    endColumns:   function() { /* HTML formatter */ return '

'; },

    rowToHTML: function(prop, view) {
      /* HTML formatter for each $$DOC{ref:'Property'} row. */
      var str = "";

      if ( prop.detailViewPreRow ) str += prop.detailViewPreRow(this);

      str += '
';
      if ( DAOController.isInstance(view) ) {
        str += "
" + prop.label + "
";
        str += view.toHTML();
        str += '	';
      } else {
        str += "" + prop.label + "	";
        str += '';
        str += view.toHTML();
        str += '	';
      }
      str += '
';

      if ( prop.detailViewPostRow ) str += prop.detailViewPostRow(this);

      return str;
    },

    // If the Model supplies a toDetailHTML method, then use it instead.
    toHTML: function() {
      /* Overridden to create the complete HTML content for the $$DOC{ref:'View'}.

$$DOC{ref:'Model',usePlural:true} may specify a .toDetailHTML() $$DOC{ref:'Method'} or
         $$DOC{ref:'Template'} to render their contents instead of the
          $$DOC{ref:'DetailView.defaultToHTML'} we supply here.

        */

      if ( ! this.model ) throw "DetailView: either 'data' or 'model' must be specified.";

      return (this.model.getPrototype().toDetailHTML || this.defaultToHTML).call(this);
    },

    defaultToHTML: function() {
      /* For $$DOC{ref:'Model',usePlural:true} that don't supply a .toDetailHTML()
        $$DOC{ref:'Method'} or $$DOC{ref:'Template'}, a default listing of
        $$DOC{ref:'Property'} editors is implemented here.
        */
      this.children = [];
      var model = this.model;
      var str  = "";

      str += '
';
      str += this.startForm();
      str += this.titleHTML();

      for ( var i = 0 ; i < model.properties.length ; i++ ) {
        var prop = model.properties[i];

        if ( prop.hidden ) continue;

        var view = this.createView(prop);
        view.data$ = this.data$;
        str += this.rowToHTML(prop, view);
      }

      str += this.endForm();

      if ( this.showRelationships ) {
        var view = this.X.RelationshipsView.create({
          data: this.data
        });
        view.data$ = this.data$;
        str += view.toHTML();
        this.addChild(view);
      }

      str += '
';

      return str;
    }
  }
});

MODEL({
  name: 'UpdateDetailView',
  extendsModel: 'DetailView',

  imports: [
    'DAO as dao'
  ],

  properties: [
    {
      name: 'rawData',
      documentation: 'The uncloned original input data.',
      postSet: function(old, nu) {
        if ( old ) old.removeListener(this.rawUpdate);
        if ( nu ) nu.addListener(this.rawUpdate);
      }
    },
    {
      name: 'originalData',
      documentation: 'A clone of the input data, for comparison with edits.'
    },
    {
      name: 'data',
      preSet: function(_, v) {
        if ( ! v ) return;
        this.rawData = v;
        return v.deepClone();
      },
      postSet: function(_, data) {
        if ( ! data ) return;
        this.originalData = data.deepClone();
        if ( ! this.model && data && data.model_ ) this.model = data.model_;
        data.addListener(function() {
          // The user is making edits. Drop rawData, since we no longer want
          // to react to updates to it.
          this.version++;
          this.rawData = '';
        }.bind(this));
      }
    },
    {
      name: 'dao'
    },
    {
      name: 'stack',
      defaultValueFn: function() { return this.X.stack; }
    },
    {
      name: 'view'
    },
    {
      // Version of the data which changes whenever any property of the data is updated.
      // Used to help trigger isEnabled / isAvailable in Actions.
      model_: 'IntProperty',
      name: 'version'
    }
  ],

  listeners: [
    {
      name: 'rawUpdate',
      code: function() {
        // If this listener fires, the raw data updated and the user hasn't
        // changed anything.
        this.data = this.rawData;
      }
    }
  ],

  actions: [
    {
      name:  'save',
      help:  'Save updates.',

      isAvailable: function() { this.version; return ! this.originalData.equals(this.data); },
      action: function() {
        var self = this;
        var obj  = this.data;
        this.stack.back();

        this.dao.put(obj, {
          put: function() {
            console.log("Saving: ", obj.toJSON());
            self.originalData.copyFrom(obj);
          },
          error: function() {
            console.error("Error saving", arguments);
          }
        });
      }
    },
    {
      name:  'cancel',
      help:  'Cancel update.',
      isAvailable: function() { this.version; return ! this.originalData.equals(this.data); },
      action: function() { this.stack.back(); }
    },
    {
      name:  'back',
      isAvailable: function() { this.version; return this.originalData.equals(this.data); },
      action: function() { this.stack.back(); }
    },
    {
      name: 'reset',
      isAvailable: function() { this.version; return ! this.originalData.equals(this.data); },
      action: function() { this.data.copyFrom(this.originalData); }
    }
  ]
});

MODEL({
  name: 'RelationshipView',
  extendsModel: 'View',

  properties: [
    {
      name: 'relationship',
      required: true
    },
    {
      name: 'args'
    },
    {
      model_: 'ViewFactoryProperty',
      name: 'viewModel',
      defaultValue: 'DAOController'
    },
    {
      name: 'data',
      postSet: function() {
        this.updateView();
      }
    },
    {
      name: 'view'
    }
  ],

  methods: {
    init: function(args) {
      this.SUPER(args);
      if ( this.args && this.args.model_ ) this.viewModel = this.args.model_
    },
    updateView: function() {
      if ( this.view ) this.view.destroy();
      this.view = this.viewModel({
        dao: this.data[this.relationship.name],
        model: this.relationship.relatedModel
      }, this.X).copyFrom(this.args);
      if ( this.$ ) {
        this.updateHTML();
      }
    }
  },
  templates: [
    function toInnerHTML() {/* %%view */}
  ]
});

MODEL({
  name: 'RelationshipsView',
  extendsModel: 'DetailView',

  templates: [
    function toHTML() {/*
      <%
        for ( var i = 0, relationship; relationship = this.model.relationships[i]; i++ ) {
          out(this.X.RelationshipView.create({
            data$: this.data$,
            relationship: relationship
          }));
        }
      %>
    */}
  ]
});
<commit_msg>DetailView code and comments cleanup.<commit_after>/**
 * @license
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

MODEL({
  name: 'DetailView',
  extendsModel: 'View',

  imports: [ 'data' ],
  exports: [ 'data' ],

  documentation:function() {/*
    When a view based on $$DOC{ref:'Property'} values is desired, $$DOC{ref:'DetailView'}
    is the place to start. Either using $$DOC{ref:'DetailView'} directly, implementing
    a .toDetailHTML() $$DOC{ref:'Method'} in your model, or extending
    $$DOC{ref:'DetailView'} to add custom formatting.

Set the $$DOC{ref:'.data'} $$DOC{ref:'Property'} to the $$DOC{ref:'Model'} instance
    you want to display. $$DOC{ref:'DetailView'} will extract the $$DOC{ref:'Model'}
    definition, create editors for the $$DOC{ref:'Property',usePlural:true}, and
    display the current values of your instance. Set $$DOC{ref:'.mode',usePlural:true}
    to indicate read-only if desired.

$$DOC{ref:'Model',usePlural:true} may specify a .toDetailHTML() $$DOC{ref:'Method'} or
    $$DOC{ref:'Template'} to render their contents instead of
    $$DOC{ref:'DetailView.defaultToHTML'}.

For each $$DOC{ref:'Property'} in the $$DOC{ref:'.data'} instance specified,
    a $$DOC{ref:'PropertyView'} is created that selects the appropriate $$DOC{ref:'View'}
    to construct.
  */},

  properties: [
    {
      name: 'className',
      defaultValue: 'detailView',
      documentation: function() {/*
          The CSS class names to use for HTML $$DOC{ref:'View',usePlural:true}.
          Separate class names with spaces. Each instance of a $$DOC{ref:'DetailView'}
          may have different classes specified.
      */}
    },
    {
      name:  'data',
      postSet: function(_, data) {
        if ( data && data.model_ && this.model !== data.model_ ) {
          this.model = data.model_;
        }
        this.onValueChange_();
      },
      documentation: function() {/*
        The $$DOC{ref:'Model'} to view. The $$DOC{ref:'Property',usePlural:true}
        of this $$DOC{ref:'Model'} instance will be examined and a $$DOC{ref:'PropertyView'}
        created for each with editors for the current value.

Sub-views of $$DOC{ref:'DetailView'} are passed this $$DOC{ref:'.data'}
        property, from which $$DOC{ref:'PropertyView'} will extract its named
        $$DOC{ref:'Property'}
        and bind the property to the sub-view $$DOC{ref:'DetailView.data'}.
      */}
    },
    {
      name:  'model',
      type:  'Model',
      postSet: function(_, m) {
        if ( this.$ ) {
          this.children = [];
          this.$.outerHTML = this.toHTML();
          this.initHTML();
        }
      },
      documentation: function() {/*
        The $$DOC{ref:'.model'} is extracted from $$DOC{ref:'.data'}, or can be
        set in advance when the type of $$DOC{ref:'.data'} is known. The $$DOC{ref:'Model'}
        is used to set up the structure of the $$DOC{ref:'DetailView'}, by examining the
        $$DOC{ref:'Property',usePlural:true}. Changing the $$DOC{ref:'.data'} out
        for another instance of the same $$DOC{ref:'Model'} will refresh the contents
        of the sub-views without destroying and re-creating them.
      */}
    },
    {
      name: 'title',
      defaultValueFn: function() { return "Edit " + this.model.label; },
      documentation: function() {/*

The display title for the $$DOC{ref:'View'}.

      */}
    },
    {
      model_: 'StringProperty',
      name: 'mode',
      defaultValue: 'read-write',
      documentation: function() {/*
        The editing mode. To disable editing set to 'read-only'.
      */}
    },
    {
      model_: 'BooleanProperty',
      name: 'showRelationships',
      defaultValue: false,
      documentation: function() {/*
        Set true to create sub-views to display $$DOC{ref:'Relationship',usePlural:true}
        for the $$DOC{ref:'.model'}.
      */}
    }
  ],

  methods: {
    // Template Method
    onValueChange_: function() { /* Override with value update code. */ },

    viewModel: function() { /* The $$DOC{ref:'Model'} type of the $$DOC{ref:'.data'}. */
       return this.model;
    },

    createTemplateView: function(name, opt_args) {
      /* Overridden here to set the new View.$$DOC{ref:'.data'} to this.$$DOC{ref:'.data'}.
         See $$DOC{ref:'View.createTemplateView'}. */
      var o = this.viewModel()[name];
      if ( o ) {
        var v;

        if ( Action.isInstance(o) )
          v = this.createActionView(o, opt_args);
        else if ( Relationship.isInstance(o) )
          v = this.createRelationshipView(o, opt_args);
        else
          v = this.createView(o, opt_args);

        v.data$ = this.data$;

        return v;
      }

      return this.SUPER(name, opt_args);
    },

    titleHTML: function() {
      /* Title text HTML formatter */
      var title = this.title;

      return title ?
        '
' + title + '
' :
        '';
    },

    startForm: function() { /* HTML formatter */ return '
'; },
    endForm: function() { /* HTML formatter */ return '
'; },

    startColumns: function() { /* HTML formatter */ return '

'; },
    nextColumn:   function() { /* HTML formatter */ return '

'; },
    endColumns:   function() { /* HTML formatter */ return '

'; },

    rowToHTML: function(prop, view) {
      /* HTML formatter for each $$DOC{ref:'Property'} row. */
      var str = "";

      if ( prop.detailViewPreRow ) str += prop.detailViewPreRow(this);

      str += '
';
      if ( DAOController.isInstance(view) ) {
        str += "
" + prop.label + "
";
        str += view.toHTML();
        str += '	';
      } else {
        str += "" + prop.label + "	";
        str += '';
        str += view.toHTML();
        str += '	';
      }
      str += '
';

      if ( prop.detailViewPostRow ) str += prop.detailViewPostRow(this);

      return str;
    },

    // If the Model supplies a toDetailHTML method, then use it instead.
    toHTML: function() {
      /* Overridden to create the complete HTML content for the $$DOC{ref:'View'}.

$$DOC{ref:'Model',usePlural:true} may specify a .toDetailHTML() $$DOC{ref:'Method'} or
         $$DOC{ref:'Template'} to render their contents instead of the
          $$DOC{ref:'DetailView.defaultToHTML'} we supply here.
      */

      if ( ! this.model ) throw "DetailView: either 'data' or 'model' must be specified.";

      return (this.model.getPrototype().toDetailHTML || this.defaultToHTML).call(this);
    },

    defaultToHTML: function() {
      /* For $$DOC{ref:'Model',usePlural:true} that don't supply a .toDetailHTML()
        $$DOC{ref:'Method'} or $$DOC{ref:'Template'}, a default listing of
        $$DOC{ref:'Property'} editors is implemented here.
        */
      this.children = [];
      var model = this.model;
      var str  = "";

      str += '
';
      str += this.startForm();
      str += this.titleHTML();

      for ( var i = 0 ; i < model.properties.length ; i++ ) {
        var prop = model.properties[i];

        if ( prop.hidden ) continue;

        var view = this.createView(prop);
        view.data$ = this.data$;
        str += this.rowToHTML(prop, view);
      }

      str += this.endForm();

      if ( this.showRelationships ) {
        var view = this.X.RelationshipsView.create({
          data: this.data
        });
        view.data$ = this.data$;
        str += view.toHTML();
        this.addChild(view);
      }

      str += '
';

      return str;
    }
  }
});

MODEL({
  name: 'UpdateDetailView',
  extendsModel: 'DetailView',

  imports: [
    'DAO as dao'
  ],

  properties: [
    {
      name: 'rawData',
      documentation: 'The uncloned original input data.',
      postSet: function(old, nu) {
        if ( old ) old.removeListener(this.rawUpdate);
        if ( nu ) nu.addListener(this.rawUpdate);
      }
    },
    {
      name: 'originalData',
      documentation: 'A clone of the input data, for comparison with edits.'
    },
    {
      name: 'data',
      preSet: function(_, v) {
        if ( ! v ) return;
        this.rawData = v;
        return v.deepClone();
      },
      postSet: function(_, data) {
        if ( ! data ) return;
        this.originalData = data.deepClone();
        if ( ! this.model && data && data.model_ ) this.model = data.model_;
        data.addListener(function() {
          // The user is making edits. Drop rawData, since we no longer want
          // to react to updates to it.
          this.version++;
          this.rawData = '';
        }.bind(this));
      }
    },
    {
      name: 'dao'
    },
    {
      name: 'stack',
      defaultValueFn: function() { return this.X.stack; }
    },
    {
      name: 'view'
    },
    {
      // Version of the data which changes whenever any property of the data is updated.
      // Used to help trigger isEnabled / isAvailable in Actions.
      model_: 'IntProperty',
      name: 'version'
    }
  ],

  listeners: [
    {
      name: 'rawUpdate',
      code: function() {
        // If this listener fires, the raw data updated and the user hasn't
        // changed anything.
        this.data = this.rawData;
      }
    }
  ],

  actions: [
    {
      name:  'save',
      help:  'Save updates.',

      isAvailable: function() { this.version; return ! this.originalData.equals(this.data); },
      action: function() {
        var self = this;
        var obj  = this.data;
        this.stack.back();

        this.dao.put(obj, {
          put: function() {
            console.log("Saving: ", obj.toJSON());
            self.originalData.copyFrom(obj);
          },
          error: function() {
            console.error("Error saving", arguments);
          }
        });
      }
    },
    {
      name:  'cancel',
      help:  'Cancel update.',
      isAvailable: function() { this.version; return ! this.originalData.equals(this.data); },
      action: function() { this.stack.back(); }
    },
    {
      name:  'back',
      isAvailable: function() { this.version; return this.originalData.equals(this.data); },
      action: function() { this.stack.back(); }
    },
    {
      name: 'reset',
      isAvailable: function() { this.version; return ! this.originalData.equals(this.data); },
      action: function() { this.data.copyFrom(this.originalData); }
    }
  ]
});

MODEL({
  name: 'RelationshipView',
  extendsModel: 'View',

  properties: [
    {
      name: 'relationship',
      required: true
    },
    {
      name: 'args'
    },
    {
      model_: 'ViewFactoryProperty',
      name: 'viewModel',
      defaultValue: 'DAOController'
    },
    {
      name: 'data',
      postSet: function() {
        this.updateView();
      }
    },
    {
      name: 'view'
    }
  ],

  methods: {
    init: function(args) {
      this.SUPER(args);
      if ( this.args && this.args.model_ ) this.viewModel = this.args.model_
    },
    updateView: function() {
      if ( this.view ) this.view.destroy();
      this.view = this.viewModel({
        dao: this.data[this.relationship.name],
        model: this.relationship.relatedModel
      }, this.X).copyFrom(this.args);
      if ( this.$ ) {
        this.updateHTML();
      }
    }
  },
  templates: [
    function toInnerHTML() {/* %%view */}
  ]
});

MODEL({
  name: 'RelationshipsView',
  extendsModel: 'DetailView',

  templates: [
    function toHTML() {/*
      <%
        for ( var i = 0, relationship; relationship = this.model.relationships[i]; i++ ) {
          out(this.X.RelationshipView.create({
            data$: this.data$,
            relationship: relationship
          }));
        }
      %>
    */}
  ]
});
<|endoftext|>