<commit_before>import React from 'react'
import { render } from 'react-dom'
import joint from 'jointjs'
import EspNode from './EspNode'
import 'jointjs/dist/joint.css'
import _ from 'lodash'
import svgPanZoom from 'svg-pan-zoom'
import $ from 'jquery'
import classNames from 'classnames';
import NodeDetailsModal from './nodeDetailsModal.js';

import '../../stylesheets/graph.styl'

export default class Graph extends React.Component {

    constructor(props) {
        super(props);
        this.graph = new joint.dia.Graph();
        this.state = {
            clickedNode: {},
            toolboxVisible: false
        };
    }

    componentDidMount() {
        this.processGraphPaper = this.drawProcessGraph()
        this.panAndZoom = this.enablePanZoom();
        this.changeNodeDetailsOnClick(this.processGraphPaper);
        this.forceUpdate()
    }

    directedLayout = () => {
        joint.layout.DirectedGraph.layout(this.graph, {
            nodeSep: 200,
            edgeSep: 500,
            rankSep: 100,
            minLen: 300,
            rankDir: "TB"
        });
    }

    drawProcessGraph = () => {
        var nodes = _.map(this.props.data.nodes, (n) => { return EspNode.makeElement(n) });
        var edges = _.map(this.props.data.edges, (e) => { return EspNode.makeLink(e) });
        var cells = nodes.concat(edges);
        var paper = new joint.dia.Paper({
            el: $('#esp-graph'),
            gridSize: 1,
            height: $('#esp-graph').height(),
            width: $('#esp-graph').width(),
            model: this.graph,
            snapLinks: { radius: 75 },
            interactive: false //remove when editing won't be such a pain
        });

        this.graph.resetCells(cells);
        this.directedLayout();
        return paper;
    }

    enablePanZoom() {
        var panAndZoom = svgPanZoom(this.refs.espGraph.childNodes[0],
            {
                viewportSelector: this.refs.espGraph.childNodes[0].childNodes[0],
                fit: true,
                zoomScaleSensitivity: 0.4,
                controlIconsEnabled: true,
                panEnabled: false
            });
        this.processGraphPaper.on('blank:pointerdown', (evt, x, y) => {
            panAndZoom.enablePan();
        });
        this.processGraphPaper.on('cell:pointerup blank:pointerup', (cellView, event) => {
            panAndZoom.disablePan();
        });
        return panAndZoom
    }

    changeNodeDetailsOnClick () {
        this.processGraphPaper.on('cell:pointerclick', (cellView, evt, x, y) => {
            if (cellView.model.attributes.nodeData) {
                this.setState({clickedNode: cellView.model.attributes.nodeData});
            }
        });
    }

    onDetailsClosed = () => {
        this.setState({clickedNode: {}})
    }

    toggleToolbox = () => {
        this.setState({toolboxVisible: !this.state.toolboxVisible})
    }

    render() {
        var displayedEdges = this.graph.attributes.cells.models.filter(m => m.attributes.source).map(l => l.attributes.edgeData)
        var displayedNodes = this.graph.attributes.cells.models.filter(m => !m.attributes.source).map(n => n.attributes.nodeData)
        return (

{this.props.data.metaData.id}

                <NodeDetailsModal node={this.state.clickedNode} onClose={this.onDetailsClosed}/>
                {this.processGraphPaper && this.panAndZoom && this.state.toolboxVisible ?
                    <Toolbox processGraphPaper={this.processGraphPaper} panAndZoom={this.panAndZoom} graph={this.graph}/> : null
                }

                <button type="button" className="btn btn-default hidden" onClick={this.directedLayout}>Directed layout</button>

        );
    }
}

class Toolbox extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            dragAndDropPaper: {
                visible: false,
                left: null,
                top: null
            }
        };
    }

    componentDidMount() {
        this.drawToolbox(this.props.processGraphPaper, this.props.panAndZoom, this.props.graph);
    }

    drawToolbox(processGraphPaper, panAndZoom, graph) {
        var toolboxPaper = this.initializeToolboxGraph();
        toolboxPaper.on('cell:pointerdown', (cellView, e, x, y) => {
            var originalClickOffset = { x: x - cellView.model.position().x, y: y - cellView.model.position().y};
            this.setState({
                dragAndDropPaper: {
                    visible: true,
                    left: e.pageX - originalClickOffset.x - window.pageXOffset,
                    top: e.pageY - originalClickOffset.y - window.pageYOffset
                }
            })
            var flyShape = this.initializeFlyGraphWithDraggedElement(cellView);

            //todo: sprobowac usuac to jquery stad
            $('body').on('mousemove', (e) => {
                this.setState({
                    dragAndDropPaper: {
                        visible: true,
                        left: e.pageX - originalClickOffset.x - window.pageXOffset + this.refs.dragAndDropPaper.clientLeft,
                        top: e.pageY - originalClickOffset.y - window.pageYOffset + this.refs.dragAndDropPaper.clientTop
                    }})
            });
            $('body').on('mouseup', (e) => {
                var targetPaperOffset = processGraphPaper.$el.offset();
                if (this.isElementDroppedOnTheProcessGraph(e, targetPaperOffset, processGraphPaper)) {
                    var element = this.createDraggedElement(flyShape, panAndZoom, e, targetPaperOffset, originalClickOffset);
                    graph.addCell(element);
                }
                $('body').off('mousemove').off('mouseup');
                flyShape.remove();
                this.setState({dragAndDropPaper: {visible: false }});
            });
        });
    }

    initializeToolboxGraph() {
        var toolboxGraph = new joint.dia.Graph
        var toolboxPaper = new joint.dia.Paper({
            el: this.refs.toolbox,
            height: 60,
            model: toolboxGraph,
            interactive: false
        });

        toolboxGraph.addCells([EspNode.makeElement({type: 'Filter'}), EspNode.makeElement({type: 'Sink'})]);
        joint.layout.DirectedGraph.layout(toolboxGraph, {nodeSep: 200, edgeSep: 500, minLen: 300, rankDir: "TB"})
        return toolboxPaper;
    }

    initializeFlyGraphWithDraggedElement(cellView) {
        var flyGraph = new joint.dia.Graph
        var dragAndDropPaper = new joint.dia.Paper({
            el: this.refs.dragAndDropPaper,
            model: flyGraph,
            interactive: false
        })
        var flyShape = cellView.model.clone()
        flyShape.position(0, 0);
        flyGraph.addCell(flyShape);
        return flyShape;
    }

    createDraggedElement(flyShape, panAndZoom, e, targetPaperOffset, originalClickOffset) {
        var element = flyShape.clone();
        var pan = panAndZoom.getPan()
        var zoom = panAndZoom.getSizes().realZoom
        var resX = (e.pageX - targetPaperOffset.left - originalClickOffset.x - pan.x) / zoom;
        var resY = (e.pageY - targetPaperOffset.top - originalClickOffset.y - pan.y) / zoom;
        element.position(resX, resY);
        return element;
    }

    isElementDroppedOnTheProcessGraph = (e, targetPaperOffset, processGraphPaper) => {
        return e.pageX > targetPaperOffset.left &&
            e.pageX < targetPaperOffset.left + processGraphPaper.$el.width() &&
            e.pageY > targetPaperOffset.top &&
            e.pageY < targetPaperOffset.top + processGraphPaper.$el.height();
    }

    render() {
        var dragAndDropPaperStyles = {
            position: 'fixed', zIndex:100, opacity:.7, pointerEvent: 'none',
            top: this.state.dragAndDropPaper.top, left: this.state.dragAndDropPaper.left
        }

        return (

                {this.state.dragAndDropPaper.visible ?

 : null }

        );

    }
};
<commit_msg>Labels on hover bring to front<commit_after>import React from 'react'
import { render } from 'react-dom'
import joint from 'jointjs'
import EspNode from './EspNode'
import 'jointjs/dist/joint.css'
import _ from 'lodash'
import svgPanZoom from 'svg-pan-zoom'
import $ from 'jquery'
import classNames from 'classnames';
import NodeDetailsModal from './nodeDetailsModal.js';

import '../../stylesheets/graph.styl'

export default class Graph extends React.Component {

    constructor(props) {
        super(props);
        this.graph = new joint.dia.Graph();
        this.state = {
            clickedNode: {},
            toolboxVisible: false
        };
    }

    componentDidMount() {
        this.processGraphPaper = this.drawProcessGraph()
        this.panAndZoom = this.enablePanZoom();
        this.changeNodeDetailsOnClick(this.processGraphPaper);
        this.forceUpdate();
        this.labelToFrontOnHover(this.processGraphPaper);
    }

    directedLayout = () => {
        joint.layout.DirectedGraph.layout(this.graph, {
            nodeSep: 200,
            edgeSep: 500,
            rankSep: 100,
            minLen: 300,
            rankDir: "TB"
        });
    }

    drawProcessGraph = () => {
        var nodes = _.map(this.props.data.nodes, (n) => { return EspNode.makeElement(n) });
        var edges = _.map(this.props.data.edges, (e) => { return EspNode.makeLink(e) });
        var cells = nodes.concat(edges);
        var paper = new joint.dia.Paper({
            el: $('#esp-graph'),
            gridSize: 1,
            height: $('#esp-graph').height(),
            width: $('#esp-graph').width(),
            model: this.graph,
            snapLinks: { radius: 75 },
            interactive: false //remove when editing won't be such a pain
        });

        this.graph.resetCells(cells);
        this.directedLayout();
        return paper;
    }

    enablePanZoom() {
        var panAndZoom = svgPanZoom(this.refs.espGraph.childNodes[0],
            {
                viewportSelector: this.refs.espGraph.childNodes[0].childNodes[0],
                fit: true,
                zoomScaleSensitivity: 0.4,
                controlIconsEnabled: true,
                panEnabled: false
            });
        this.processGraphPaper.on('blank:pointerdown', (evt, x, y) => {
            panAndZoom.enablePan();
        });
        this.processGraphPaper.on('cell:pointerup blank:pointerup', (cellView, event) => {
            panAndZoom.disablePan();
        });
        return panAndZoom
    }

    changeNodeDetailsOnClick () {
        this.processGraphPaper.on('cell:pointerclick', (cellView, evt, x, y) => {
            if (cellView.model.attributes.nodeData) {
                this.setState({clickedNode: cellView.model.attributes.nodeData});
            }
        });
    }

    labelToFrontOnHover () {
        this.processGraphPaper.on('cell:mouseover', (cellView, evt, x, y) => {
          cellView.model.toFront();
        });
    }

    onDetailsClosed = () => {
        this.setState({clickedNode: {}})
    }

    toggleToolbox = () => {
        this.setState({toolboxVisible: !this.state.toolboxVisible})
    }

    render() {
        var displayedEdges = this.graph.attributes.cells.models.filter(m => m.attributes.source).map(l => l.attributes.edgeData)
        var displayedNodes = this.graph.attributes.cells.models.filter(m => !m.attributes.source).map(n => n.attributes.nodeData)
        return (

{this.props.data.metaData.id}

                <NodeDetailsModal node={this.state.clickedNode} onClose={this.onDetailsClosed}/>
                {this.processGraphPaper && this.panAndZoom && this.state.toolboxVisible ?
                    <Toolbox processGraphPaper={this.processGraphPaper} panAndZoom={this.panAndZoom} graph={this.graph}/> : null
                }

                <button type="button" className="btn btn-default hidden" onClick={this.directedLayout}>Directed layout</button>

        );

    }
}

class Toolbox extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            dragAndDropPaper: {
                visible: false,
                left: null,
                top: null
            }
        };
    }

    componentDidMount() {
        this.drawToolbox(this.props.processGraphPaper, this.props.panAndZoom, this.props.graph);
    }

    drawToolbox(processGraphPaper, panAndZoom, graph) {
        var toolboxPaper = this.initializeToolboxGraph();
        toolboxPaper.on('cell:pointerdown', (cellView, e, x, y) => {
            var originalClickOffset = { x: x - cellView.model.position().x, y: y - cellView.model.position().y};
            this.setState({
                dragAndDropPaper: {
                    visible: true,
                    left: e.pageX - originalClickOffset.x - window.pageXOffset,
                    top: e.pageY - originalClickOffset.y - window.pageYOffset
                }
            })
            var flyShape = this.initializeFlyGraphWithDraggedElement(cellView);

            //todo: sprobowac usuac to jquery stad
            $('body').on('mousemove', (e) => {
                this.setState({
                    dragAndDropPaper: {
                        visible: true,
                        left: e.pageX - originalClickOffset.x - window.pageXOffset + this.refs.dragAndDropPaper.clientLeft,
                        top: e.pageY - originalClickOffset.y - window.pageYOffset + this.refs.dragAndDropPaper.clientTop
                    }})
            });
            $('body').on('mouseup', (e) => {
                var targetPaperOffset = processGraphPaper.$el.offset();
                if (this.isElementDroppedOnTheProcessGraph(e, targetPaperOffset, processGraphPaper)) {
                    var element = this.createDraggedElement(flyShape, panAndZoom, e, targetPaperOffset, originalClickOffset);
                    graph.addCell(element);
                }
                $('body').off('mousemove').off('mouseup');
                flyShape.remove();
                this.setState({dragAndDropPaper: {visible: false }});
            });
        });
    }

    initializeToolboxGraph() {
        var toolboxGraph = new joint.dia.Graph
        var toolboxPaper = new joint.dia.Paper({
            el: this.refs.toolbox,
            height: 60,
            model: toolboxGraph,
            interactive: false
        });

        toolboxGraph.addCells([EspNode.makeElement({type: 'Filter'}), EspNode.makeElement({type: 'Sink'})]);
        joint.layout.DirectedGraph.layout(toolboxGraph, {nodeSep: 200, edgeSep: 500, minLen: 300, rankDir: "TB"})
        return toolboxPaper;
    }

    initializeFlyGraphWithDraggedElement(cellView) {
        var flyGraph = new joint.dia.Graph
        var dragAndDropPaper = new joint.dia.Paper({
            el: this.refs.dragAndDropPaper,
            model: flyGraph,
            interactive: false
        })
        var flyShape = cellView.model.clone()
        flyShape.position(0, 0);
        flyGraph.addCell(flyShape);
        return flyShape;
    }

    createDraggedElement(flyShape, panAndZoom, e, targetPaperOffset, originalClickOffset) {
        var element = flyShape.clone();
        var pan = panAndZoom.getPan()
        var zoom = panAndZoom.getSizes().realZoom
        var resX = (e.pageX - targetPaperOffset.left - originalClickOffset.x - pan.x) / zoom;
        var resY = (e.pageY - targetPaperOffset.top - originalClickOffset.y - pan.y) / zoom;
        element.position(resX, resY);
        return element;
    }

    isElementDroppedOnTheProcessGraph = (e, targetPaperOffset, processGraphPaper) => {
        return e.pageX > targetPaperOffset.left &&
            e.pageX < targetPaperOffset.left + processGraphPaper.$el.width() &&
            e.pageY > targetPaperOffset.top &&
            e.pageY < targetPaperOffset.top + processGraphPaper.$el.height();
    }

    render() {
        var dragAndDropPaperStyles = {
            position: 'fixed', zIndex:100, opacity:.7, pointerEvent: 'none',
            top: this.state.dragAndDropPaper.top, left: this.state.dragAndDropPaper.left
        }

        return (

                {this.state.dragAndDropPaper.visible ?

 : null }

        );

    }
};
<|endoftext|>


<commit_before>/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014, Red Hat, Inc. and/or its affiliates, and individual
 * contributors by the @authors tag. See the copyright.txt in the
 * distribution for a full listing of individual contributors.
 *
 * 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.
 */

CONTACTS.namespace("CONTACTS.app.getContacts");
CONTACTS.namespace("CONTACTS.app.buildContactList");
CONTACTS.namespace("CONTACTS.app.getContactById");
CONTACTS.namespace("CONTACTS.app.buildContactDetail");
CONTACTS.namespace("CONTACTS.app.restEndpoint");

//Set this variable for all Contacts REST APIs base URL.
CONTACTS.app.baseUrl = "< backend URL e.g http(s)//host:port >/jboss-contacts-mobile-picketlink-secured"
CONTACTS.app.restEndpoint = CONTACTS.app.baseUrl + "/rest/contacts";

CONTACTS.app.onNotification = function(event) {
    var notify = $('#notification');
    $('#notification span').text(event.alert);
    notify.show();
    setTimeout(function() {
        notify.fadeOut();
    }, 10000);

    CONTACTS.app.contactId = event.payload.id;
    $('#notification a').on("click", function(e) {
        if(e.handled !== true) {
            CONTACTS.app.getContactById(CONTACTS.app.contactId);
            $.mobile.navigate("#contacts-edit-page");
            e.handled = true;
        }
    });
}
/**
 * It is recommended to bind to this event instead of DOM ready() because this will work regardless of whether
 * the page is loaded directly or if the content is pulled into another page as part of the Ajax navigation system.
 *
 * The first thing you learn in jQuery is to call code inside the $(document).ready() function so everything
 * will execute as soon as the DOM is loaded. However, in jQuery Mobile, Ajax is used to load the contents of
 * each page into the DOM as you navigate, and the DOM ready handler only executes for the first page.
 * To execute code whenever a new page is loaded and created, you can bind to the pagecreate event.
 *
 *
 * These functions perform the GET. They display the list, detailed list, and fill in the update form.
 *
 * @author Joshua Wilson
 */
$( document ).on( "pagecreate", function(mainEvent) {
    //Initialize the vars in the beginning so that you will always have access to them.
    var getCurrentTime = CONTACTS.util.getCurrentTime,
        restEndpoint = CONTACTS.app.restEndpoint;

    console.log(getCurrentTime() + " [js/app.js] (document -> pagecreate) - start");

    /*
     * Make sure the Contacts list gets populated but only once.
     *
     * The "pagebeforeshow" event will delay this function until everything is set up.
     *
     * Because of the interesting jQM loading architecture, multiple event triggering is a constant problem.
     * The "e.handled" if statement used here and elsewhere is meant to keep jQM from running this code multiple
     * times for one display.
     */
    $("#contacts-list-page").on( "pagebeforeshow", function(e) {
        if(e.handled !== true) {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-list-page -> pagebeforeshow) - start");

            // Fetches the initial Contact data.
            CONTACTS.app.getContacts();

            // Here we hide the "Role Assignment" menu item for non admin users.
            if (CONTACTS.security.currentUser) {
                $("#user-name-page-message").text(CONTACTS.security.currentUser.account.loginName);
            }

            e.handled = true;
            console.log(getCurrentTime() + " [js/app.js] (#contacts-list-page -> pagebeforeshow) - end");
        }
    });

    // This is called on "pagebeforeshow" above and by the CONTACTS.submissions
    // Uses JAX-RS GET to retrieve current contact list.
    CONTACTS.app.getContacts = function () {
        console.log(getCurrentTime() + " [js/app.js] (getContacts) - start");
        var jqxhr = $.ajax({
            url: restEndpoint,
            xhrFields: {withCredentials: true},
            cache: false,
            type: "GET"
        }).done(function(data, textStatus, jqXHR) {
            console.log(getCurrentTime() + " [js/app.js] (getContacts) - succes on ajax call");
            CONTACTS.app.buildContactList(data);
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.log(getCurrentTime() + " [js/app.js] (getContacts) - error in ajax - " +
                        " - jqXHR = " + jqXHR.status +
                        " - textStatus = " + textStatus +
                        " - errorThrown = " + errorThrown);
            if (jqXHR.status === 401) {
                $("body").pagecontainer("change", "#signin-page");
            }
        });
        console.log(getCurrentTime() + " [js/app.js] (getContacts) - end");
    };

    // This is called by CONTACTS.app.getContacts.
    // Display contact list on page one.
    CONTACTS.app.buildContactList = function (contacts) {
        console.log(getCurrentTime() + " [js/app.js] (buildContactList) - start");
        var contactList = "",
            contactDetailList = "";

        // The data from the AJAX call is not sorted alphabetically, this will fix that.
        contacts.sort(function(a,b){
              var aName = a.firstName.toLowerCase() + a.lastName.toLowerCase();
              var bName = b.firstName.toLowerCase() + b.lastName.toLowerCase();
              return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0));
        });

        // Pull the info out of the Data returned from the AJAX request and create the HTML to be placed on the page.
        $.each(contacts, function(index, contact) {
            // Create the HTML for the List only view.
            contactList = contactList.concat(
                "
" +
                    "" + contact.firstName.toString() + " " + contact.lastName.toString() + "" +
                "
");
            // Create the HTML for the Detailed List view.
            contactDetailList = contactDetailList.concat(
                "
" +
                    "" + contact.firstName.toString() + " " + contact.lastName.toString() + "" +
                    "
" +
                        "
" + contact.email.toString() + "
" +
                        "
" + contact.phoneNumber.toString() + "
" +
                        "
" + contact.birthDate.toString() + "
" +
                    "
" +
                 "
");
        });

        // Start with a clean list element otherwise we would have repeats.
        $("#contacts-display-listview").empty();

        // Check if it is already initialized or not, refresh the list in case it is initialized otherwise trigger create.
        if ( $("#contacts-display-listview").hasClass("ui-listview")) {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - hasClass ui-listview) - append.listview - start");
            $("#contacts-display-listview").append(contactList).listview("refresh", true);
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - hasClass ui-listview) - append.listview - end");
        }
        else {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - !hasClass ui-listview) - append.enhanceWithin - start");
            $("#contacts-display-listview").append(contactList).enhanceWithin();
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - !hasClass ui-listview) - append.enhanceWithin - end");
        }

        // Start with a clean list element otherwise we would have repeats.
        $("#contacts-display-detail-listview").empty();

        // check if it is already initialized or not, refresh the list in case it is initialized otherwise trigger create
        if ( $("#contacts-display-detail-listview").hasClass("ui-listview")) {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - hasClass ui-listview) - append.listview - start");
            $("#contacts-display-detail-listview").append(contactDetailList).listview("refresh", true);
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - hasClass ui-listview) - append.listview - end");
        }
        else {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - !hasClass ui-listview) - append.enhanceWithin - start");
            $("#contacts-display-detail-listview").append(contactDetailList).enhanceWithin();
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - !hasClass ui-listview) - append.enhanceWithin - end");
        }

        // Attach onclick event to each row of the contact list that will open up the contact info to be edited.
        $(".contacts-list-item").on("click", function(event){
            if(event.handled !== true) {
                console.log(getCurrentTime() + " [js/app.js] (.contacts-display-listview -> on click) - start");

                CONTACTS.app.getContactById($(this).attr("id").split("list-contact-ID-").pop());

                event.handled = true;
                console.log(getCurrentTime() + " [js/app.js] (.contacts-display-listview -> on click) - end");
            }
        });

        // Attach onclick event to each row of the contact list detailed page that will open up the contact info to be edited.
        $("li.contacts-detail-list-item").on("click", function(event){
            if(event.handled !== true) {
                console.log(getCurrentTime() + " [js/app.js] (li.contacts-display-listview -> on click) - start");

                CONTACTS.app.getContactById($(this).attr("id").split("detail-contact-ID-").pop());

                // Turn the whole
 into a link.
                $("body").pagecontainer("change", "#contacts-edit-page");

                event.handled = true;
                console.log(getCurrentTime() + " [js/app.js] (li.contacts-display-listview -> on click) - end");
            }
        });

        console.log(getCurrentTime() + " [js/app.js] (buildContactList) - end");
        // Add in a line to visually see when we are done.
        console.log("-----------------------------List Page---------------------------------------");
    };

    // This is called by the on click event list above.
    // Retrieve employee detail based on employee id.
    CONTACTS.app.getContactById = function (contactID) {
        console.log(getCurrentTime() + " [js/app.js] (getContactById) - start");
        console.log(getCurrentTime() + " [js/app.js] (getContactById) - contactID = " + contactID);

        var jqxhr = $.ajax({
            url: restEndpoint + "/" + contactID.toString(),
            xhrFields: {withCredentials: true},
            cache: false,
            type: "GET"
        }).done(function(data, textStatus, jqXHR) {
            console.log(getCurrentTime() + " [js/app.js] (getContactById) - success on ajax call");
            CONTACTS.app.buildContactDetail(data);
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.log(getCurrentTime() + " [js/app.js] (getContactById) - error in ajax" +
                        " - jqXHR = " + jqXHR.status +
                        " - textStatus = " + textStatus +
                        " - errorThrown = " + errorThrown);
        });
        console.log(getCurrentTime() + " [js/app.js] (getContactById) - end");
    };

    // This is called by CONTACTS.app.getContactById.
    // Display contact detail for editing on the Edit page.
    CONTACTS.app.buildContactDetail = function(contact) {
        console.log(getCurrentTime() + " [js/app.js] (buildContactDetail) - start");

        // Put each field value in the text input on the page.
        $("#contacts-edit-input-firstName").val(contact.firstName);
        $("#contacts-edit-input-lastName").val(contact.lastName);
        $("#contacts-edit-input-tel").val(contact.phoneNumber);
        $("#contacts-edit-input-email").val(contact.email);
        $("#contacts-edit-input-date").val(contact.birthDate);
        $("#contacts-edit-input-id").val(contact.id);

        console.log(getCurrentTime() + " [js/app.js] (buildContactDetail) - end");
        // Add in a line to visually see when we are done.
        console.log("-----------------------------Update Page---------------------------------------");
    };

    console.log(getCurrentTime() + " [js/app.js] (document -> pagecreate) - end");
});

$( document ).on( "deviceready", function() {
    var pushConfig = {
        pushServerURL: "<pushServerURL e.g http(s)//host:port/context >",
        alias: "<alias e.g. a username or an email address optional>",
        android: {
          senderID: "<senderID e.g Google Project ID only for android>",
          variantID: "<variantID e.g. 1234456-234320>",
          variantSecret: "<variantSecret e.g. 1234456-234320>"
        },
        ios: {
          variantID: "<variantID e.g. 1234456-234320>",
          variantSecret: "<variantSecret e.g. 1234456-234320>"
        }
    };
    push.register(CONTACTS.app.onNotification, successHandler, errorHandler, pushConfig);

    function successHandler() {
        console.log('successful registered');
    }

    function errorHandler(error) {
        alert('error registering ' + error);
    }
});
<commit_msg>whitespace and semicolons<commit_after>/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014, Red Hat, Inc. and/or its affiliates, and individual
 * contributors by the @authors tag. See the copyright.txt in the
 * distribution for a full listing of individual contributors.
 *
 * 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.
 */

CONTACTS.namespace("CONTACTS.app.getContacts");
CONTACTS.namespace("CONTACTS.app.buildContactList");
CONTACTS.namespace("CONTACTS.app.getContactById");
CONTACTS.namespace("CONTACTS.app.buildContactDetail");
CONTACTS.namespace("CONTACTS.app.restEndpoint");

//Set this variable for all Contacts REST APIs base URL.
CONTACTS.app.baseUrl = "< backend URL e.g http(s)//host:port >/jboss-contacts-mobile-picketlink-secured";
CONTACTS.app.restEndpoint = CONTACTS.app.baseUrl + "/rest/contacts";

CONTACTS.app.onNotification = function(event) {
    var notify = $('#notification');
    $('#notification span').text(event.alert);
    notify.show();
    setTimeout(function() {
        notify.fadeOut();
    }, 10000);

    CONTACTS.app.contactId = event.payload.id;
    $('#notification a').on("click", function(e) {
        if(e.handled !== true) {
            CONTACTS.app.getContactById(CONTACTS.app.contactId);
            $.mobile.navigate("#contacts-edit-page");
            e.handled = true;
        }
    });
};
/**
 * It is recommended to bind to this event instead of DOM ready() because this will work regardless of whether
 * the page is loaded directly or if the content is pulled into another page as part of the Ajax navigation system.
 *
 * The first thing you learn in jQuery is to call code inside the $(document).ready() function so everything
 * will execute as soon as the DOM is loaded. However, in jQuery Mobile, Ajax is used to load the contents of
 * each page into the DOM as you navigate, and the DOM ready handler only executes for the first page.
 * To execute code whenever a new page is loaded and created, you can bind to the pagecreate event.
 *
 *
 * These functions perform the GET. They display the list, detailed list, and fill in the update form.
 *
 * @author Joshua Wilson
 */
$( document ).on( "pagecreate", function(mainEvent) {
    //Initialize the vars in the beginning so that you will always have access to them.
    var getCurrentTime = CONTACTS.util.getCurrentTime,
        restEndpoint = CONTACTS.app.restEndpoint;

    console.log(getCurrentTime() + " [js/app.js] (document -> pagecreate) - start");

    /*
     * Make sure the Contacts list gets populated but only once.
     *
     * The "pagebeforeshow" event will delay this function until everything is set up.
     *
     * Because of the interesting jQM loading architecture, multiple event triggering is a constant problem.
     * The "e.handled" if statement used here and elsewhere is meant to keep jQM from running this code multiple
     * times for one display.
     */
    $("#contacts-list-page").on( "pagebeforeshow", function(e) {
        if(e.handled !== true) {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-list-page -> pagebeforeshow) - start");

            // Fetches the initial Contact data.
            CONTACTS.app.getContacts();

            // Here we hide the "Role Assignment" menu item for non admin users.
            if (CONTACTS.security.currentUser) {
                $("#user-name-page-message").text(CONTACTS.security.currentUser.account.loginName);
            }

            e.handled = true;
            console.log(getCurrentTime() + " [js/app.js] (#contacts-list-page -> pagebeforeshow) - end");
        }
    });

    // This is called on "pagebeforeshow" above and by the CONTACTS.submissions
    // Uses JAX-RS GET to retrieve current contact list.
    CONTACTS.app.getContacts = function () {
        console.log(getCurrentTime() + " [js/app.js] (getContacts) - start");
        var jqxhr = $.ajax({
            url: restEndpoint,
            xhrFields: {withCredentials: true},
            cache: false,
            type: "GET"
        }).done(function(data, textStatus, jqXHR) {
            console.log(getCurrentTime() + " [js/app.js] (getContacts) - succes on ajax call");
            CONTACTS.app.buildContactList(data);
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.log(getCurrentTime() + " [js/app.js] (getContacts) - error in ajax - " +
                        " - jqXHR = " + jqXHR.status +
                        " - textStatus = " + textStatus +
                        " - errorThrown = " + errorThrown);
            if (jqXHR.status === 401) {
                $("body").pagecontainer("change", "#signin-page");
            }
        });
        console.log(getCurrentTime() + " [js/app.js] (getContacts) - end");
    };

    // This is called by CONTACTS.app.getContacts.
    // Display contact list on page one.
    CONTACTS.app.buildContactList = function (contacts) {
        console.log(getCurrentTime() + " [js/app.js] (buildContactList) - start");
        var contactList = "",
            contactDetailList = "";

        // The data from the AJAX call is not sorted alphabetically, this will fix that.
        contacts.sort(function(a,b){
              var aName = a.firstName.toLowerCase() + a.lastName.toLowerCase();
              var bName = b.firstName.toLowerCase() + b.lastName.toLowerCase();
              return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0));
        });

        // Pull the info out of the Data returned from the AJAX request and create the HTML to be placed on the page.
        $.each(contacts, function(index, contact) {
            // Create the HTML for the List only view.
            contactList = contactList.concat(
                "
" +
                    "" + contact.firstName.toString() + " " + contact.lastName.toString() + "" +
                "
");
            // Create the HTML for the Detailed List view.
            contactDetailList = contactDetailList.concat(
                "
" +
                    "" + contact.firstName.toString() + " " + contact.lastName.toString() + "" +
                    "
" +
                        "
" + contact.email.toString() + "
" +
                        "
" + contact.phoneNumber.toString() + "
" +
                        "
" + contact.birthDate.toString() + "
" +
                    "
" +
                 "
");
        });

        // Start with a clean list element otherwise we would have repeats.
        $("#contacts-display-listview").empty();

        // Check if it is already initialized or not, refresh the list in case it is initialized otherwise trigger create.
        if ( $("#contacts-display-listview").hasClass("ui-listview")) {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - hasClass ui-listview) - append.listview - start");
            $("#contacts-display-listview").append(contactList).listview("refresh", true);
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - hasClass ui-listview) - append.listview - end");
        }
        else {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - !hasClass ui-listview) - append.enhanceWithin - start");
            $("#contacts-display-listview").append(contactList).enhanceWithin();
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-listview - !hasClass ui-listview) - append.enhanceWithin - end");
        }

        // Start with a clean list element otherwise we would have repeats.
        $("#contacts-display-detail-listview").empty();

        // check if it is already initialized or not, refresh the list in case it is initialized otherwise trigger create
        if ( $("#contacts-display-detail-listview").hasClass("ui-listview")) {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - hasClass ui-listview) - append.listview - start");
            $("#contacts-display-detail-listview").append(contactDetailList).listview("refresh", true);
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - hasClass ui-listview) - append.listview - end");
        }
        else {
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - !hasClass ui-listview) - append.enhanceWithin - start");
            $("#contacts-display-detail-listview").append(contactDetailList).enhanceWithin();
            console.log(getCurrentTime() + " [js/app.js] (#contacts-display-detail-listview - !hasClass ui-listview) - append.enhanceWithin - end");
        }

        // Attach onclick event to each row of the contact list that will open up the contact info to be edited.
        $(".contacts-list-item").on("click", function(event){
            if(event.handled !== true) {
                console.log(getCurrentTime() + " [js/app.js] (.contacts-display-listview -> on click) - start");

                CONTACTS.app.getContactById($(this).attr("id").split("list-contact-ID-").pop());

                event.handled = true;
                console.log(getCurrentTime() + " [js/app.js] (.contacts-display-listview -> on click) - end");
            }
        });

        // Attach onclick event to each row of the contact list detailed page that will open up the contact info to be edited.
        $("li.contacts-detail-list-item").on("click", function(event){
            if(event.handled !== true) {
                console.log(getCurrentTime() + " [js/app.js] (li.contacts-display-listview -> on click) - start");

                CONTACTS.app.getContactById($(this).attr("id").split("detail-contact-ID-").pop());

                // Turn the whole
 into a link.
                $("body").pagecontainer("change", "#contacts-edit-page");

                event.handled = true;
                console.log(getCurrentTime() + " [js/app.js] (li.contacts-display-listview -> on click) - end");
            }
        });

        console.log(getCurrentTime() + " [js/app.js] (buildContactList) - end");
        // Add in a line to visually see when we are done.
        console.log("-----------------------------List Page---------------------------------------");
    };

    // This is called by the on click event list above.
    // Retrieve employee detail based on employee id.
    CONTACTS.app.getContactById = function (contactID) {
        console.log(getCurrentTime() + " [js/app.js] (getContactById) - start");
        console.log(getCurrentTime() + " [js/app.js] (getContactById) - contactID = " + contactID);

        var jqxhr = $.ajax({
            url: restEndpoint + "/" + contactID.toString(),
            xhrFields: {withCredentials: true},
            cache: false,
            type: "GET"
        }).done(function(data, textStatus, jqXHR) {
            console.log(getCurrentTime() + " [js/app.js] (getContactById) - success on ajax call");
            CONTACTS.app.buildContactDetail(data);
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.log(getCurrentTime() + " [js/app.js] (getContactById) - error in ajax" +
                        " - jqXHR = " + jqXHR.status +
                        " - textStatus = " + textStatus +
                        " - errorThrown = " + errorThrown);
        });
        console.log(getCurrentTime() + " [js/app.js] (getContactById) - end");
    };

    // This is called by CONTACTS.app.getContactById.
    // Display contact detail for editing on the Edit page.
    CONTACTS.app.buildContactDetail = function(contact) {
        console.log(getCurrentTime() + " [js/app.js] (buildContactDetail) - start");

        // Put each field value in the text input on the page.
        $("#contacts-edit-input-firstName").val(contact.firstName);
        $("#contacts-edit-input-lastName").val(contact.lastName);
        $("#contacts-edit-input-tel").val(contact.phoneNumber);
        $("#contacts-edit-input-email").val(contact.email);
        $("#contacts-edit-input-date").val(contact.birthDate);
        $("#contacts-edit-input-id").val(contact.id);

        console.log(getCurrentTime() + " [js/app.js] (buildContactDetail) - end");
        // Add in a line to visually see when we are done.
        console.log("-----------------------------Update Page---------------------------------------");
    };

    console.log(getCurrentTime() + " [js/app.js] (document -> pagecreate) - end");
});

$( document ).on( "deviceready", function() {
    var pushConfig = {
        pushServerURL: "<pushServerURL e.g http(s)//host:port/context >",
        alias: "<alias e.g. a username or an email address optional>",
        android: {
          senderID: "<senderID e.g Google Project ID only for android>",
          variantID: "<variantID e.g. 1234456-234320>",
          variantSecret: "<variantSecret e.g. 1234456-234320>"
        },
        ios: {
          variantID: "<variantID e.g. 1234456-234320>",
          variantSecret: "<variantSecret e.g. 1234456-234320>"
        }
    };
    push.register(CONTACTS.app.onNotification, successHandler, errorHandler, pushConfig);

    function successHandler() {
        console.log('successful registered');
    }

    function errorHandler(error) {
        alert('error registering ' + error);
    }
});
<|endoftext|>


<commit_before><commit_msg>Checking in more gitignore stuff<commit_after><|endoftext|>


<commit_before>var path = require('path')

module.exports = {
  // Webpack aliases
  aliases: {
    quasar: path.resolve(__dirname, '../node_modules/quasar-framework/'),
    src: path.resolve(__dirname, '../src'),
    assets: path.resolve(__dirname, '../src/assets'),
    components: path.resolve(__dirname, '../src/components')
  },

  // Progress Bar Webpack plugin format
  // https://github.com/clessg/progress-bar-webpack-plugin#options
  progressFormat: ' [:bar] ' + ':percent'.bold + ' (:msg)',

  // Default theme to build with ('ios' or 'mat')
  defaultTheme: 'mat',

  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    publicPath: '',
    productionSourceMap: false,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css']
  },
  dev: {
    env: require('./dev.env'),
    cssSourceMap: true,
    // auto open browser or not
    openBrowser: true,
    publicPath: '/',
    port: 8080,

    // If for example you are using Quasar Play
    // to generate a QR code then on each dev (re)compilation
    // you need to avoid clearing out the console, so set this
    // to "false", otherwise you can set it to "true" to always
    // have only the messages regarding your last (re)compilation.
    clearConsoleOnRebuild: false,

    // Proxy your API if using any.
    // Also see /build/script.dev.js and search for "proxy api requests"
    // https://github.com/chimurai/http-proxy-middleware
    proxyTable: {}
  }
}

/*
 * proxyTable example:
 *
   proxyTable: {
      // proxy all requests starting with /api
      '/api': {
        target: 'https://some.address.com/api',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
 */
<commit_msg>add proxy for dev api<commit_after>var path = require('path')

module.exports = {
  // Webpack aliases
  aliases: {
    quasar: path.resolve(__dirname, '../node_modules/quasar-framework/'),
    src: path.resolve(__dirname, '../src'),
    assets: path.resolve(__dirname, '../src/assets'),
    components: path.resolve(__dirname, '../src/components')
  },

  // Progress Bar Webpack plugin format
  // https://github.com/clessg/progress-bar-webpack-plugin#options
  progressFormat: ' [:bar] ' + ':percent'.bold + ' (:msg)',

  // Default theme to build with ('ios' or 'mat')
  defaultTheme: 'mat',

  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    publicPath: '',
    productionSourceMap: false,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css']
  },
  dev: {
    env: require('./dev.env'),
    cssSourceMap: true,
    // auto open browser or not
    openBrowser: true,
    publicPath: '/',
    port: 8080,

    // If for example you are using Quasar Play
    // to generate a QR code then on each dev (re)compilation
    // you need to avoid clearing out the console, so set this
    // to "false", otherwise you can set it to "true" to always
    // have only the messages regarding your last (re)compilation.
    clearConsoleOnRebuild: false,

    // Proxy your API if using any.
    // Also see /build/script.dev.js and search for "proxy api requests"
    // https://github.com/chimurai/http-proxy-middleware
    proxyTable: {
      '/api': {
        target: 'http://localhost:3001',
	changeOrigin: true
      }
    }
  }
}

/*
 * proxyTable example:
 *
   proxyTable: {
      // proxy all requests starting with /api
      '/api': {
        target: 'https://some.address.com/api',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
 */
<|endoftext|>


<commit_before>/**
 * Exports a reducer for dealing with the account of the session user.
 *
 * @module account/reducer
 */
import {
    GET_ACCOUNT,
    UPDATE_ACCOUNT,
    UPDATE_ACCOUNT_SETTINGS,
    CHANGE_ACCOUNT_PASSWORD,
    GET_API_KEYS,
    CREATE_API_KEY,
    CLEAR_API_KEY
} from "../actionTypes";
import { reportAPIError } from "../utils";

/**
 * The state that should initially be stored.
 *
 * @const
 * @type {object}
 */
const initialState = {
    ready: false,
    oldPasswordError: false,
    apiKeys: null,
    newKey: null
};

/**
 * A reducer for dealing with the account of the session user.
 *
 * @param state {object}
 * @param action {object}
 * @returns {object}
 */
export default function accountReducer (state = initialState, action) {

    switch (action.type) {

        case GET_ACCOUNT.SUCCEEDED:
            return {...state, ...action.data, ready: true};

        case UPDATE_ACCOUNT.SUCCEEDED:
            return {...state, ...action.data};

        case GET_API_KEYS.SUCCEEDED:
            return {...state, apiKeys: action.data};

        case CHANGE_ACCOUNT_PASSWORD.SUCCEEDED:
            return {...state, oldPasswordError: false};

        case CHANGE_ACCOUNT_PASSWORD.FAILED:
            if (action.message === "Invalid old password.") {
                return {...state, oldPasswordError: true};
            }

            return reportAPIError(action);

        case CREATE_API_KEY.REQUESTED:
            return {...state, key: null};

        case CREATE_API_KEY.SUCCEEDED:
            return {...state, newKey: action.data.key};

        case CLEAR_API_KEY:
            return {...state, key: null};

        case UPDATE_ACCOUNT_SETTINGS.SUCCEEDED:
            return {...state, settings: action.data};

        default:
            return state;
    }
}
<commit_msg>Fix API key creation<commit_after>/**
 * Exports a reducer for dealing with the account of the session user.
 *
 * @module account/reducer
 */
import {
    GET_ACCOUNT,
    UPDATE_ACCOUNT,
    UPDATE_ACCOUNT_SETTINGS,
    CHANGE_ACCOUNT_PASSWORD,
    GET_API_KEYS,
    CREATE_API_KEY,
    CLEAR_API_KEY
} from "../actionTypes";
import { reportAPIError } from "../utils";

/**
 * The state that should initially be stored.
 *
 * @const
 * @type {object}
 */
const initialState = {
    ready: false,
    oldPasswordError: false,
    apiKeys: null,
    newKey: null
};

/**
 * A reducer for dealing with the account of the session user.
 *
 * @param state {object}
 * @param action {object}
 * @returns {object}
 */
export default function accountReducer (state = initialState, action) {

    switch (action.type) {

        case GET_ACCOUNT.SUCCEEDED:
            return {...state, ...action.data, ready: true};

        case UPDATE_ACCOUNT.SUCCEEDED:
            return {...state, ...action.data};

        case GET_API_KEYS.SUCCEEDED:
            return {...state, apiKeys: action.data};

        case CHANGE_ACCOUNT_PASSWORD.SUCCEEDED:
            return {...state, oldPasswordError: false};

        case CHANGE_ACCOUNT_PASSWORD.FAILED:
            if (action.message === "Invalid old password.") {
                return {...state, oldPasswordError: true};
            }

            return reportAPIError(action);

        case CREATE_API_KEY.REQUESTED:
            return {...state, key: null};

        case CREATE_API_KEY.SUCCEEDED:
            return {...state, newKey: action.data.key};

        case CLEAR_API_KEY:
            return {...state, newKey: null};

        case UPDATE_ACCOUNT_SETTINGS.SUCCEEDED:
            return {...state, settings: action.data};

        default:
            return state;
    }
}
<|endoftext|>


<commit_before>(function(angular, undefined) {
'use strict';

// module: ng.django.forms
// Correct Angular's form.FormController behavior after rendering bound forms.
// Additional validators for form elements.
var djng_forms_module = angular.module('ng.django.forms', []);

// create a simple hash code for the given string
function hashCode(s) {
	return s.split("").reduce(function(a, b) {
		a = (a << 5) - a + b.charCodeAt(0);
		return a & a;
	}, 0);
}

// This directive adds a dummy binding to input fields without attribute ng-model, so that AngularJS
// form validation gets notified whenever the fields content changes.
djng_forms_module.directive('input', ['$compile', function($compile) {
	return {
		restrict: 'E',
		require: '?^form',
		link: function(scope, element, attr, formCtrl) {
			var modelName;
			if (!formCtrl || angular.isUndefined(formCtrl.$name) || element.prop('type') === 'hidden' || angular.isUndefined(attr.name) || angular.isDefined(attr.ngModel))
				return;
			modelName = 'dmy' + Math.abs(hashCode(formCtrl.$name)) +'.' + attr.name;
			attr.$set('ngModel', modelName);
			$compile(element, null, 9999)(scope);
		}
	};
}]);

// Bound fields with invalid input data, shall be marked as ng-invalid-bound, so that
// the input field visibly contains invalid data, even if pristine
djng_forms_module.directive('djngError', function() {
	return {
		restrict: 'A',
		require: '?^form',
		link: function(scope, element, attrs, formCtrl) {
			var boundField;
			if (!formCtrl || angular.isUndefined(attrs.name) || attrs.djngError !== 'bound-field')
				return;
			boundField = formCtrl[attrs.name];
			boundField.$setValidity('bound', false);
			boundField.$parsers.push(function(value) {
				// set bound field into valid state after changing value
				boundField.$setValidity('bound', true);
				element.removeAttr('djng-error');
			});
		}
	};
});

// This directive overrides some of the internal behavior on forms if used together with AngularJS.
// Otherwise, the content of bound forms is not displayed, because AngularJS does not know about
// the concept of bound forms and thus hides values preset by Django while rendering HTML.
djng_forms_module.directive('ngModel', function() {
	function restoreInputField(modelCtrl, field) {
		// restore the field's content from the rendered content of bound fields
		switch (field.type) {
		case 'radio':
			if (field.defaultChecked) {
				modelCtrl.$setViewValue(field.defaultValue);
			}
			break;
		case 'checkbox':
			if (field.defaultChecked) {
				modelCtrl.$setViewValue(true);
			}
			break;
		case 'password':
			// after an (un)successful submission, reset the password field
			modelCtrl.$setViewValue(null);
			break;
		default:
			if (field.defaultValue) {
				modelCtrl.$setViewValue(field.defaultValue);
			}
			break;
		}
	}

	function restoreSelectOptions(modelCtrl, field) {
		var multivalues = [];
		angular.forEach(field.options, function(option) {
			if (option.defaultSelected) {
				// restore the select option to selected
				angular.element(option).prop('selected', 'selected');
				if (field.multiple) {
					multivalues.push(option.value);
				} else {
					modelCtrl.$setViewValue(option.value);
				}
			}
		});
		if (field.multiple) {
			modelCtrl.$setViewValue(multivalues);
		}
	}

	function restoreTextArea(modelCtrl, field) {
		if (field.defaultValue) {
			// restore the field's content from the rendered content of bound fields
			modelCtrl.$setViewValue(field.defaultValue);
		}
	}

	return {
		restrict: 'A',
		// make sure this directive is applied after angular built-in one
		priority: 1,
		require: ['ngModel', '^?form'],
		link: function(scope, element, attrs, ctrls) {
			var field = angular.isElement(element) ? element[0] : null;
			var modelCtrl = ctrls[0], formCtrl = ctrls[1] || null;
			if (!field || !formCtrl)
				return;
			switch (field.tagName) {
			case 'INPUT':
				restoreInputField(modelCtrl, field);
				break;
			case 'SELECT':
				restoreSelectOptions(modelCtrl, field);
				break;
			case 'TEXTAREA':
				restoreTextArea(modelCtrl, field);
				break;
			default:
				console.log('Unknown field type');
				break;
			}
			// restore the form's pristine state
			formCtrl.$setPristine();
		}
	};
});

djng_forms_module.directive('validateMultipleFields', function() {
	return {
		restrict: 'A',
		require: '^?form',
		compile: function(element, attrs) {
			angular.forEach(element.find('input'), function(elem) {
				elem = angular.element(elem)
				elem.attr('ng-change', 'changed()');
			});

			return {

				post: function(scope, element, attrs, controller) {
					var formCtrl, subFields, checkboxElems = [];

					scope.changed = function() {
						validate(true)
					}

					function validate(trigger) {
						var valid = false;
						angular.forEach(checkboxElems, function(checkbox) {
							valid = valid || checkbox.checked;
							console.log('checkbox', checkbox);
							console.log('checkbox.clearRejected', checkbox.clearRejected)
							if(checkbox.clearRejected !== undefined) {
								checkbox.clearRejected('validateMultipleFields');
							}
						});

						formCtrl.$setValidity('required', valid);
						//formCtrl.$setValidity('rejected', true);

						if (trigger && angular.isString(subFields)) {
							formCtrl[subFields].$dirty = true;
							formCtrl[subFields].$pristine = false;
						}
						console.log(formCtrl)
					}

					if (!controller)
						return;
					formCtrl = controller;
					try {
						subFields = angular.fromJson(attrs.validateMultipleFields);
					} catch (SyntaxError) {
						subFields = attrs.validateMultipleFields;
					}
					angular.forEach(element.find('input'), function(elem) {
						if (subFields.indexOf(elem.name) >= 0) {
							checkboxElems.push(elem);
						}
					});

					validate();
				}
			}
		}
	};
});

// This directive can be added to an input field which shall validate inserted dates, for example:
// <input ng-model="a_date" type="text" validate-date="^(\d{4})-(\d{1,2})-(\d{1,2})$" />
// Now, such an input field is only considered valid, if the date is a valid date and if it matches
// against the given regular expression.
djng_forms_module.directive('validateDate', function() {
	var validDatePattern = null;

	function validateDate(date) {
		var matched, dateobj;
		if (!date) // empty field are validated by the "required" validator
			return true;
		dateobj = new Date(date);
		if (isNaN(dateobj))
			return false;
		if (validDatePattern) {
			matched = validDatePattern.exec(date);
			return matched && parseInt(matched[2]) === dateobj.getMonth() + 1;
		}
		return true;
	}

	return {
		require: '?ngModel',
		restrict: 'A',
		link: function(scope, elem, attrs, controller) {
			if (!controller)
				return;

			if (attrs.validateDate) {
				// if a pattern is set, only valid dates with that pattern are accepted
				validDatePattern = new RegExp(attrs.validateDate, 'i');
			}

			var validator = function(value) {
				var validity = controller.$isEmpty(value) || validateDate(value);
				controller.$setValidity('date', validity);
				return validity ? value : undefined;
			};

			controller.$parsers.push(validator);
		}
	};
});

// If forms are validated using Ajax, the server shall return a dictionary of detected errors to the
// client code. The success-handler of this Ajax call, now can set those error messages on their
// prepared list-items. The simplest way, is to add this code snippet into the controllers function
// which is responsible for submitting form data using Ajax:
//  $http.post("/path/to/url", $scope.data).success(function(data) {
//      djangoForm.setErrors($scope.form, data.errors);
//  });
// djangoForm.setErrors returns false, if no errors have been transferred.
djng_forms_module.factory('djangoForm', function() {
	var NON_FIELD_ERRORS = '__all__';

	function isNotEmpty(obj) {
		for (var p in obj) {
			if (obj.hasOwnProperty(p))
				return true;
		}
		return false;
	}

	function resetFieldValidity(field) {
		var pos = field.$viewChangeListeners.push(field.clearRejected = function() {
			field.$message = '';
			field.$setValidity('rejected', true);
			field.$viewChangeListeners.splice(pos - 1, 1);
			delete field.clearRejected;
		})
	}

	return {
		// setErrors takes care of updating prepared placeholder fields for displaying form errors
		// deteced by an AJAX submission. Returns true if errors have been added to the form.
		setErrors: function(form, errors) {
			// remove errors from this form, which may have been rejected by an earlier validation
			form.$message = '';
			if (form.$error.hasOwnProperty('rejected')) {
				// make copy of rejected before we loop as calling field.$setValidity('rejected', true) modifies the error array
				var rejected = form.$error.rejected.concat();
				angular.forEach(rejected, function(rejected) {
					var field, key = rejected.$name;
					if (form.hasOwnProperty(key)) {
						field = form[key];
						if(field.clearRejected) {
							field.clearRejected();
						}
					}
				});
			}
			// add the new upstream errors
			angular.forEach(errors, function(errors, key) {
				console.log('key', key)
				var field;
				if (errors.length > 0) {
					if (key === NON_FIELD_ERRORS) {
						form.$message = errors[0];
						form.$setPristine();
					} else if (form.hasOwnProperty(key)) {
						field = form[key];
						field.$message = errors[0];
						field.$setValidity('rejected', false);
						field.$setPristine();
						if (angular.isArray(field.$viewChangeListeners)) {
							resetFieldValidity(field);
						} else {
							// this field is a composite of input elements
							angular.forEach(field, function(subField, subKey) {
								if (subField && angular.isArray(subField.$viewChangeListeners)) {
									resetFieldValidity(subField);
								}
							});
						}
					}
				}
			});
			return isNotEmpty(errors);
		}
	};
});

djng_forms_module.factory('djangoForm2', function() {

	return {

		setErrors: setErrors
	}

	/* ============================ */

	function setErrors(form, errors) {

		_clearFormMessage(form);
		_displayErrors(form, errors);

		return _isNotEmpty(errors)
	};

	function _clearFormMessage(form) {

		form.$message = '';
	};

	function _displayErrors(form, errors) {

		angular.forEach(errors, function(error, key) {

			var field,

			message = error[0];

			if (form.hasOwnProperty(key)) {

				field = form[key];
				field.$dirty = true;

				/*
				 * check to see if the field already has an error.
				 *
				 * If so return, as,
				 *
				 *  - either the field has set its own error. This can occur
				 *    when a field is 'required' and you submit it with no value.
				 *
				 *  - we already have a 'rejected' error in the field
				 *    and the user has just resubmitted it without changing the
				 *    field value.
				 *
				 *    This also prevents the listener being added multiple times
				 */

				if(_errorCount(field) > 0)
					return;

				field.$setValidity('rejected', false);
				field.$message = message;
				field.$setPristine();

				if (angular.isArray(field.$viewChangeListeners)) {
					_addRejectionResetHandler(field);
				} else {
					// this field is a composite of input elements
					angular.forEach(field, function(subField, subKey) {
						if (subField && angular.isArray(subField.$viewChangeListeners)) {
							_addRejectionResetHandler(subField);
						}
					});
				}

			} else {

				form.$message = message;
				form.$setPristine();
			}
		});
	}

	function _errorCount(field) {
		var error,
			count = 0;

		for(error in field.$error) {
			count++;
		}

		return count;
	}

	function _addRejectionResetHandler(field) {
		var pos = field.$viewChangeListeners.push(function() {
			//console.log(field)
			console.log('field')
			field.$viewChangeListeners.splice(pos - 1, 1);
			field.$setValidity('rejected', true);
			field.$message = "";
		})
	}

	function _isNotEmpty(obj) {
		for (var p in obj) {
			if (obj.hasOwnProperty(p))
				return true;
		}
		return false;
	}
})

})(window.angular);
<commit_msg>changed validate-multiple-field to deal with checkbox controls instead of elements, so that rejected errors can be cleared<commit_after>(function(angular, undefined) {
'use strict';

// module: ng.django.forms
// Correct Angular's form.FormController behavior after rendering bound forms.
// Additional validators for form elements.
var djng_forms_module = angular.module('ng.django.forms', []);

// create a simple hash code for the given string
function hashCode(s) {
	return s.split("").reduce(function(a, b) {
		a = (a << 5) - a + b.charCodeAt(0);
		return a & a;
	}, 0);
}

// This directive adds a dummy binding to input fields without attribute ng-model, so that AngularJS
// form validation gets notified whenever the fields content changes.
djng_forms_module.directive('input', ['$compile', function($compile) {
	return {
		restrict: 'E',
		require: '?^form',
		link: function(scope, element, attr, formCtrl) {
			var modelName;
			if (!formCtrl || angular.isUndefined(formCtrl.$name) || element.prop('type') === 'hidden' || angular.isUndefined(attr.name) || angular.isDefined(attr.ngModel))
				return;
			modelName = 'dmy' + Math.abs(hashCode(formCtrl.$name)) +'.' + attr.name;
			attr.$set('ngModel', modelName);
			$compile(element, null, 9999)(scope);
		}
	};
}]);

// Bound fields with invalid input data, shall be marked as ng-invalid-bound, so that
// the input field visibly contains invalid data, even if pristine
djng_forms_module.directive('djngError', function() {
	return {
		restrict: 'A',
		require: '?^form',
		link: function(scope, element, attrs, formCtrl) {
			var boundField;
			if (!formCtrl || angular.isUndefined(attrs.name) || attrs.djngError !== 'bound-field')
				return;
			boundField = formCtrl[attrs.name];
			boundField.$setValidity('bound', false);
			boundField.$parsers.push(function(value) {
				// set bound field into valid state after changing value
				boundField.$setValidity('bound', true);
				element.removeAttr('djng-error');
			});
		}
	};
});

// This directive overrides some of the internal behavior on forms if used together with AngularJS.
// Otherwise, the content of bound forms is not displayed, because AngularJS does not know about
// the concept of bound forms and thus hides values preset by Django while rendering HTML.
djng_forms_module.directive('ngModel', function() {
	function restoreInputField(modelCtrl, field) {
		// restore the field's content from the rendered content of bound fields
		switch (field.type) {
		case 'radio':
			if (field.defaultChecked) {
				modelCtrl.$setViewValue(field.defaultValue);
			}
			break;
		case 'checkbox':
			if (field.defaultChecked) {
				modelCtrl.$setViewValue(true);
			}
			break;
		case 'password':
			// after an (un)successful submission, reset the password field
			modelCtrl.$setViewValue(null);
			break;
		default:
			if (field.defaultValue) {
				modelCtrl.$setViewValue(field.defaultValue);
			}
			break;
		}
	}

	function restoreSelectOptions(modelCtrl, field) {
		var multivalues = [];
		angular.forEach(field.options, function(option) {
			if (option.defaultSelected) {
				// restore the select option to selected
				angular.element(option).prop('selected', 'selected');
				if (field.multiple) {
					multivalues.push(option.value);
				} else {
					modelCtrl.$setViewValue(option.value);
				}
			}
		});
		if (field.multiple) {
			modelCtrl.$setViewValue(multivalues);
		}
	}

	function restoreTextArea(modelCtrl, field) {
		if (field.defaultValue) {
			// restore the field's content from the rendered content of bound fields
			modelCtrl.$setViewValue(field.defaultValue);
		}
	}

	return {
		restrict: 'A',
		// make sure this directive is applied after angular built-in one
		priority: 1,
		require: ['ngModel', '^?form'],
		link: function(scope, element, attrs, ctrls) {
			var field = angular.isElement(element) ? element[0] : null;
			var modelCtrl = ctrls[0], formCtrl = ctrls[1] || null;
			if (!field || !formCtrl)
				return;
			switch (field.tagName) {
			case 'INPUT':
				restoreInputField(modelCtrl, field);
				break;
			case 'SELECT':
				restoreSelectOptions(modelCtrl, field);
				break;
			case 'TEXTAREA':
				restoreTextArea(modelCtrl, field);
				break;
			default:
				console.log('Unknown field type');
				break;
			}
			// restore the form's pristine state
			formCtrl.$setPristine();
		}
	};
});

djng_forms_module.directive('validateMultipleFields', function($parse) {
	return {
		restrict: 'A',
		require: '^?form',
		compile: function(element, attrs) {
			angular.forEach(element.find('input'), function(elem) {
				elem = angular.element(elem)
				elem.attr('ng-change', 'changed()');
			});

			return {

				post: function(scope, element, attrs, controller) {
					var formCtrl, subFields, checkboxCtrls = [];

					scope.changed = function() {
						validate(true)
					}

					function validate(trigger) {
						var valid = false;
						angular.forEach(checkboxCtrls, function(checkbox) {
							valid = valid || checkbox.$modelValue;
							if(checkbox.clearRejected) {
								checkbox.clearRejected();
							}
						});

						formCtrl.$setValidity('required', valid);
						formCtrl.$setValidity('rejected', true);

						if (trigger && angular.isString(subFields)) {
							formCtrl[subFields].$dirty = true;
							formCtrl[subFields].$pristine = false;
						}
					}

					if (!controller)
						return;
					formCtrl = controller;
					try {
						subFields = angular.fromJson(attrs.validateMultipleFields);
					} catch (SyntaxError) {
						subFields = attrs.validateMultipleFields;
					}
					angular.forEach(element.find('input'), function(elem) {
						if (subFields.indexOf(elem.name) >= 0) {
							checkboxCtrls.push(formCtrl[elem.name]);
						}
					});

					scope.$on('$destroy', function() {

					})

					validate();
				}
			}
		}
	};
});

// This directive can be added to an input field which shall validate inserted dates, for example:
// <input ng-model="a_date" type="text" validate-date="^(\d{4})-(\d{1,2})-(\d{1,2})$" />
// Now, such an input field is only considered valid, if the date is a valid date and if it matches
// against the given regular expression.
djng_forms_module.directive('validateDate', function() {
	var validDatePattern = null;

	function validateDate(date) {
		var matched, dateobj;
		if (!date) // empty field are validated by the "required" validator
			return true;
		dateobj = new Date(date);
		if (isNaN(dateobj))
			return false;
		if (validDatePattern) {
			matched = validDatePattern.exec(date);
			return matched && parseInt(matched[2]) === dateobj.getMonth() + 1;
		}
		return true;
	}

	return {
		require: '?ngModel',
		restrict: 'A',
		link: function(scope, elem, attrs, controller) {
			if (!controller)
				return;

			if (attrs.validateDate) {
				// if a pattern is set, only valid dates with that pattern are accepted
				validDatePattern = new RegExp(attrs.validateDate, 'i');
			}

			var validator = function(value) {
				var validity = controller.$isEmpty(value) || validateDate(value);
				controller.$setValidity('date', validity);
				return validity ? value : undefined;
			};

			controller.$parsers.push(validator);
		}
	};
});

// If forms are validated using Ajax, the server shall return a dictionary of detected errors to the
// client code. The success-handler of this Ajax call, now can set those error messages on their
// prepared list-items. The simplest way, is to add this code snippet into the controllers function
// which is responsible for submitting form data using Ajax:
//  $http.post("/path/to/url", $scope.data).success(function(data) {
//      djangoForm.setErrors($scope.form, data.errors);
//  });
// djangoForm.setErrors returns false, if no errors have been transferred.
djng_forms_module.factory('djangoForm', function() {
	var NON_FIELD_ERRORS = '__all__';

	function isNotEmpty(obj) {
		for (var p in obj) {
			if (obj.hasOwnProperty(p))
				return true;
		}
		return false;
	}

	function resetFieldValidity(field) {
		var pos = field.$viewChangeListeners.push(field.clearRejected = function() {
			field.$message = '';
			field.$setValidity('rejected', true);
			console.log(field.$viewChangeListeners)
			field.$viewChangeListeners.splice(pos - 1, 1);
			delete field.clearRejected;
			console.log('removing rejected')
			console.log(field);
			console.log(field.$viewChangeListeners)
			console.log('------------------------');
		})
	}

	return {
		// setErrors takes care of updating prepared placeholder fields for displaying form errors
		// deteced by an AJAX submission. Returns true if errors have been added to the form.
		setErrors: function(form, errors) {
			// remove errors from this form, which may have been rejected by an earlier validation
			form.$message = '';
			if (form.$error.hasOwnProperty('rejected')) {
				/*
				 * make copy of rejected before we loop as calling
				 * field.$setValidity('rejected', true) modifies the error array
				 * so only every other one was being removed
				 */
				var rejected = form.$error.rejected.concat();
				angular.forEach(rejected, function(rejected) {
					var field, key = rejected.$name;
					if (form.hasOwnProperty(key)) {
						field = form[key];
						if(field.clearRejected) {
							field.clearRejected();
						}
					}
				});
			}
			// add the new upstream errors
			angular.forEach(errors, function(errors, key) {
				var field;
				if (errors.length > 0) {
					if (key === NON_FIELD_ERRORS) {
						form.$message = errors[0];
						form.$setPristine();
					} else if (form.hasOwnProperty(key)) {
						field = form[key];
						field.$message = errors[0];
						field.$setValidity('rejected', false);
						field.$setPristine();
						if (angular.isArray(field.$viewChangeListeners)) {
							resetFieldValidity(field);
						} else {
							// this field is a composite of input elements
							angular.forEach(field, function(subField, subKey) {
								if (subField && angular.isArray(subField.$viewChangeListeners)) {
									resetFieldValidity(subField);
								}
							});
						}
					}
				}
			});
			return isNotEmpty(errors);
		}
	};
});

})(window.angular);
<|endoftext|>


<commit_before>_kiwi.view.MediaMessage = Backbone.View.extend({
    events: {
        'click .media_close': 'close'
    },

    initialize: function () {
        // Get the URL from the data
        this.url = this.$el.data('url');
    },

    toggle: function () {
        if (!this.$content || !this.$content.is(':visible')) {
            this.open();
        } else {
            this.close();
        }
    },

    // Close the media content and remove it from display
    close: function () {
        var that = this;
        this.$content.slideUp('fast', function () {
            that.$content.remove();
        });
    },

    // Open the media content within its wrapper
    open: function () {
        // Create the content div if we haven't already
        if (!this.$content) {
            this.$content = $('
 ' + _kiwi.global.i18n.translate('client_views_mediamessage_close').fetch() + '

');
            this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || _kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch() + ' :(');
        }

        // Now show the content if not already
        if (!this.$content.is(':visible')) {
            // Hide it first so the slideDown always plays
            this.$content.hide();

            // Add the media content and slide it into view
            this.$el.append(this.$content);
            this.$content.slideDown();
        }
    },

    // Generate the media content for each recognised type
    mediaTypes: {
        twitter: function () {
            var tweet_id = this.$el.data('tweetid');
            var that = this;

            $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
                that.$content.find('.content').html(data.html);
            });

            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_tweet').fetch() + '...
');
        },

        image: function () {
            return $('');
        },

        imgur: function () {
            var that = this;
            $.getJSON('https://api.imgur.com/oembed?url=' + this.$el.data('id') + '&maxheight=100&maxwidth=100', function (data) {
                that.$content.find('.content').html(data.html);
            }).fail(function () {
                that.$content.find('.content').html(' No content.');
            });
            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_image').fetch() + '...
');
        },

        reddit: function () {
            var that = this;
            var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);

            $.getJSON('https://www.' + matches[0] + '.json?jsonp=?', function (data) {
                console.log('Loaded reddit data', data);
                var post = data[0].data.children[0].data;
                var thumb = '';

                // Show a thumbnail if there is one
                if (post.thumbnail) {
                    //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
                    var thumbnail = 'https' + post.thumbnail.substr(4);

                    // Hide the thumbnail if an over_18 image
                    if (post.over_18) {
                        thumb = '';
                        thumb += '
Show
NSFW
';
                        thumb += '';
                        thumb += '';
                    } else {
                        thumb = '';
                    }
                }

                // Build the template string up
                var tmpl = '
' + thumb + '<%- title %>
Posted by <%- author %>.    ';
                tmpl += ' <%- ups %>     <%- downs %>
';
                tmpl += '<%- num_comments %> comments made. <a href="https://www.reddit.com<%- permalink %>">View post
';

                that.$content.find('.content').html(_.template(tmpl)(post));
            });

            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_reddit').fetch() + '...
');
        },

        youtube: function () {
            var ytid = this.$el.data('ytid');
            var that = this;
            var yt_html = '';
            that.$content.find('.content').html(yt_html);

            return $('');
        },

        gist: function () {
            var that = this,
                matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(this.url);

            $.getJSON('https://gist.github.com/'+matches[1]+'.json?callback=?' + (matches[2] || ''), function (data) {
                $('body').append('<link rel="stylesheet" href="' + data.stylesheet + '" type="text/css" />');
                that.$content.find('.content').html(data.div);
            });

            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_gist').fetch() + '...
');
        },

        spotify: function () {
            var uri = this.$el.data('uri'),
                method = this.$el.data('method'),
                spot, html;

            switch (method) {
                case "track":
                case "album":
                    spot = {
                        url: 'https://embed.spotify.com/?uri=' + uri,
                        width: 300,
                        height: 80
                    };
                    break;
                case "artist":
                    spot = {
                        url: 'https://embed.spotify.com/follow/1/?uri=' + uri +'&size=detail&theme=dark',
                        width: 300,
                        height: 56
                    };
                    break;
            }

            html = '';

            return $(html);
        },

        soundcloud: function () {
            var url = this.$el.data('url'),
                $content = $('

').text(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());

            $.getJSON('https://soundcloud.com/oembed', { url: url })
                .then(function (data) {
                    $content.empty().append(
                        $(data.html).attr('height', data.height - 100)
                    );
                }, function () {
                    $content.text(_kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch());
                });

            return $content;
        },

        custom: function() {
            var type = this.constructor.types[this.$el.data('index')];

            if (!type)
                return;

            return $(type.buildHtml(this.$el.data('url')));
        }

    }
    }, {

    /**
     * Add a media message type to append HTML after a matching URL
     * match() should return a truthy value if it wants to handle this URL
     * buildHtml() should return the HTML string to be used within the drop down
     */
    addType: function(match, buildHtml) {
        if (typeof match !== 'function' || typeof buildHtml !== 'function')
            return;

        this.types = this.types || [];
        this.types.push({match: match, buildHtml: buildHtml});
    },

    // Build the closed media HTML from a URL
    buildHtml: function (url) {
        var html = '', matches;

        _.each(this.types || [], function(type, type_idx) {
            if (!type.match(url))
                return;

            // Add which media type should handle this media message. Will be read when it's clicked on
            html += '';
        });

        // Is it an image?
        if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
            html += '';
        }

        // Is this an imgur link not picked up by the images regex? Only need the image ID.
        matches = (/imgur\.com(?:.*)?\/([0-9a-zA-Z]+)/ig).exec(url);
        if (matches && !url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
            html += '';
        }

        // Is it a tweet?
        matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
        if (matches) {
            html += '';
        }

        // Is reddit?
        matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
        if (matches) {
            html += '';
        }

        // Is youtube?
        matches = (/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi).exec(url);
        if (matches) {
            html += '';
        }

        // Is a github gist?
        matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(url);
        if (matches) {
            html += '';
        }

        // Is this a spotify link?
        matches = (/http:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i).exec(url);
        if (matches) {
            // Make it a Spotify URI! (spotify:<type>:<id>)
            var method = matches[1],
                uri = "spotify:" + matches[1] + ":" + matches[2];
            html += '';
        }

        matches = (/(?:m\.)?(soundcloud\.com(?:\/.+))/i).exec(url);
        if (matches) {
            html += '';
        }

        return html;
    }
});
<commit_msg>Fix the regex for imgur as it was failing to match albums.<commit_after>_kiwi.view.MediaMessage = Backbone.View.extend({
    events: {
        'click .media_close': 'close'
    },

    initialize: function () {
        // Get the URL from the data
        this.url = this.$el.data('url');
    },

    toggle: function () {
        if (!this.$content || !this.$content.is(':visible')) {
            this.open();
        } else {
            this.close();
        }
    },

    // Close the media content and remove it from display
    close: function () {
        var that = this;
        this.$content.slideUp('fast', function () {
            that.$content.remove();
        });
    },

    // Open the media content within its wrapper
    open: function () {
        // Create the content div if we haven't already
        if (!this.$content) {
            this.$content = $('
 ' + _kiwi.global.i18n.translate('client_views_mediamessage_close').fetch() + '

');
            this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || _kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch() + ' :(');
        }

        // Now show the content if not already
        if (!this.$content.is(':visible')) {
            // Hide it first so the slideDown always plays
            this.$content.hide();

            // Add the media content and slide it into view
            this.$el.append(this.$content);
            this.$content.slideDown();
        }
    },

    // Generate the media content for each recognised type
    mediaTypes: {
        twitter: function () {
            var tweet_id = this.$el.data('tweetid');
            var that = this;

            $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
                that.$content.find('.content').html(data.html);
            });

            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_tweet').fetch() + '...
');
        },

        image: function () {
            return $('');
        },

        imgur: function () {
            var that = this;
            $.getJSON('https://api.imgur.com/oembed?url=' + this.$el.data('id') + '&maxheight=100&maxwidth=100', function (data) {
                that.$content.find('.content').html(data.html);
            }).fail(function () {
                that.$content.find('.content').html(' No content.');
            });
            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_image').fetch() + '...
');
        },

        reddit: function () {
            var that = this;
            var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);

            $.getJSON('https://www.' + matches[0] + '.json?jsonp=?', function (data) {
                console.log('Loaded reddit data', data);
                var post = data[0].data.children[0].data;
                var thumb = '';

                // Show a thumbnail if there is one
                if (post.thumbnail) {
                    //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
                    var thumbnail = 'https' + post.thumbnail.substr(4);

                    // Hide the thumbnail if an over_18 image
                    if (post.over_18) {
                        thumb = '';
                        thumb += '
Show
NSFW
';
                        thumb += '';
                        thumb += '';
                    } else {
                        thumb = '';
                    }
                }

                // Build the template string up
                var tmpl = '
' + thumb + '<%- title %>
Posted by <%- author %>.    ';
                tmpl += ' <%- ups %>     <%- downs %>
';
                tmpl += '<%- num_comments %> comments made. <a href="https://www.reddit.com<%- permalink %>">View post
';

                that.$content.find('.content').html(_.template(tmpl)(post));
            });

            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_reddit').fetch() + '...
');
        },

        youtube: function () {
            var ytid = this.$el.data('ytid');
            var that = this;
            var yt_html = '';
            that.$content.find('.content').html(yt_html);

            return $('');
        },

        gist: function () {
            var that = this,
                matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(this.url);

            $.getJSON('https://gist.github.com/'+matches[1]+'.json?callback=?' + (matches[2] || ''), function (data) {
                $('body').append('<link rel="stylesheet" href="' + data.stylesheet + '" type="text/css" />');
                that.$content.find('.content').html(data.div);
            });

            return $('
' + _kiwi.global.i18n.translate('client_views_mediamessage_load_gist').fetch() + '...
');
        },

        spotify: function () {
            var uri = this.$el.data('uri'),
                method = this.$el.data('method'),
                spot, html;

            switch (method) {
                case "track":
                case "album":
                    spot = {
                        url: 'https://embed.spotify.com/?uri=' + uri,
                        width: 300,
                        height: 80
                    };
                    break;
                case "artist":
                    spot = {
                        url: 'https://embed.spotify.com/follow/1/?uri=' + uri +'&size=detail&theme=dark',
                        width: 300,
                        height: 56
                    };
                    break;
            }

            html = '';

            return $(html);
        },

        soundcloud: function () {
            var url = this.$el.data('url'),
                $content = $('

').text(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());

            $.getJSON('https://soundcloud.com/oembed', { url: url })
                .then(function (data) {
                    $content.empty().append(
                        $(data.html).attr('height', data.height - 100)
                    );
                }, function () {
                    $content.text(_kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch());
                });

            return $content;
        },

        custom: function() {
            var type = this.constructor.types[this.$el.data('index')];

            if (!type)
                return;

            return $(type.buildHtml(this.$el.data('url')));
        }

    }
    }, {

    /**
     * Add a media message type to append HTML after a matching URL
     * match() should return a truthy value if it wants to handle this URL
     * buildHtml() should return the HTML string to be used within the drop down
     */
    addType: function(match, buildHtml) {
        if (typeof match !== 'function' || typeof buildHtml !== 'function')
            return;

        this.types = this.types || [];
        this.types.push({match: match, buildHtml: buildHtml});
    },

    // Build the closed media HTML from a URL
    buildHtml: function (url) {
        var html = '', matches;

        _.each(this.types || [], function(type, type_idx) {
            if (!type.match(url))
                return;

            // Add which media type should handle this media message. Will be read when it's clicked on
            html += '';
        });

        // Is it an image?
        if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
            html += '';
        }

        // Is this an imgur link not picked up by the images regex? Only need the image ID.
        matches = (/imgur.com\/((?:.[^\/]+)|(?:a\/.+)|(?:.*\/(.+)))/gi).exec(url);
        if (matches && !url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
            html += '';
        }

        // Is it a tweet?
        matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
        if (matches) {
            html += '';
        }

        // Is reddit?
        matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
        if (matches) {
            html += '';
        }

        // Is youtube?
        matches = (/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi).exec(url);
        if (matches) {
            html += '';
        }

        // Is a github gist?
        matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(url);
        if (matches) {
            html += '';
        }

        // Is this a spotify link?
        matches = (/http:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i).exec(url);
        if (matches) {
            // Make it a Spotify URI! (spotify:<type>:<id>)
            var method = matches[1],
                uri = "spotify:" + matches[1] + ":" + matches[2];
            html += '';
        }

        matches = (/(?:m\.)?(soundcloud\.com(?:\/.+))/i).exec(url);
        if (matches) {
            html += '';
        }

        return html;
    }
});
<|endoftext|>