<commit_before>/*
Copyright 2012 Twitter, Inc.

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.
*/

/**
 * This module defines the Workflow class, instances of which use a Client to retrieve jobs and
 * events from an Ambrose server. The Workflow acts as a controller and owner of job
 * state. Callbacks may be bound to events triggered on the Workflow to react to state changes.
 */
define(['jquery', 'uri', './core', './client', './graph'], function(
  $, URI, Ambrose, Client, Graph
) {
  // Maximum number of consecutive client failures before event polling is stopped.
  var MAX_CLIENT_FAILURES = 10;

  // return data.job, throwing error if it's undefined
  function getJob(data) {
    var job = data.job;
    if (job == null) throw new Error('Data contains no job entry');
    return job;
  }

  // Workflow ctor
  var Workflow = Ambrose.Workflow = function(id, client) {
    return new Ambrose.Workflow.fn.init(id, client);
  };

  /**
   * Workflow prototype.
   */
  Workflow.fn = Workflow.prototype = {
    /**
     * Constructs a new Workflow.
     *
     * @param id unique id of this workflow. If null, defaults to value of current url's
     * 'workflowId' parameter.
     * @param client client to use when requesting Workflow state from server. If null, defaults to
     * new client instance constructed with default arguments.
     */
    init: function(id, client) {
      if (id == null) {
        var uri = new URI(window.location.href);
        var params = uri.search(true);
        if (params) id = params.workflowId;
      }
      if (client == null) client = Client();
      this.$this = $(this);
      this.id = id;
      this.client = client;
      this.progress = 0;
      this.jobs = [];
      this.jobsByName = {};
      this.jobsById = {};
      this.lastEventId = -1;
      this.current = {
        selected: null,
        mouseover: null,
      };
    },

    /**
     * Triggers an event on this Workflow. Any event handlers bound to this Workflow for the given
     * event will be invoked.
     *
     * @param event name of event.
     * @param data array of extra arguments, if any, to pass to handler functions.
     * @return this.
     */
    trigger: function(event, data) {
      this.$this.trigger(event, data);
      return this;
    },

    /**
     * Binds callback to the given Workflow event.
     *
     * @param event name of event.
     * @param callback the callback to invoke when the Workflow event is triggered.
     * @return this.
     */
    on: function(event, callback) {
      this.$this.on(event, callback);
      return this;
    },

    /**
     * Initiates asynchronous request for Workflow jobs from server. On error the 'error.loadJobs'
     * event is triggered.
     *
     * @return Promise configured with error and success callbacks which update state of this
     * Workflow.
     */
    loadJobs: function() {
      this.trigger('loadingJobs');
      console.log('Loading jobs');

      // define callbacks
      var self = this;
      var handleError = function(textStatus, errorThrown) {
        console.error('Failed to load jobs:', textStatus, errorThrown);
        self.trigger('error.loadJobs', [null, textStatus, errorThrown]);
      };
      var handleSuccess = function(data, textStatus) {
        if (data == null) {
          handleError(textStatus, 'Data is null');
          return;
        }

        // reset job indices
        var jobs = self.jobs = data;
        var jobsByName = self.jobsByName = {};
        var jobsById = self.jobsById = {};

        // initialize job indices
        $.each(jobs, function(i, job) {
          // index job by name
          var name = job.name;
          if (name in jobsByName) {
            console.error("Multiple jobs found with name '" + name + "':", self);
            return;
          }
          jobsByName[name] = job;

          // index job by id (if defined)
          if (job.jobId) {
            var id = job.id = job.jobId;
            delete job.jobId;
            if (id in jobsById) {
              console.error("Multiple jobs found with id '" + id + "':", self);
              return;
            }
            jobsById[id] = job;
          }
        });

        // initialize parent links
        $.each(jobs, function(i, job) {
          var name = job.name;
          $.each(job.successorNames, function(i, childName) {
            var child = jobsByName[childName];
            var predecessorNames = child.predecessorNames || (child.predecessorNames = []);
            predecessorNames.push(name);
          });
        });

        // build job graph and sort
        var graph = self.graph = Graph({
          data: jobs,
          getId: function(d) { return d.name; },
          getParentIds: function(d) { return d.predecessorNames; },
        });
        graph.sort();
        jobs = self.jobs = $.map(graph.nodesByTopologicalIndex, function(node, i) {
          var job = node.data;
          job.index = i;
          job.node = node;
          return job;
        });

        self.trigger('jobsLoaded', [jobs, textStatus, null]);
      };

      // initiate request
      return this.client.getJobs()
        .error(function(jqXHR, textStatus, errorThrown) {
          handleError(textStatus, errorThrown);
        })
        .success(function(data, textStatus, jqXHR) {
          handleSuccess(data, textStatus);
        });
    },

    /**
     * Starts event polling if not already started.
     *
     * @param frequency poll events at this frequency (ms). Defaults to 1000.
     * @param maxEvents max number of events to process on each request. Defaults to 1.
     * @return this.
     */
    startEventPolling: function(frequency, maxEvents) {
      if (this.eventPollingIntervalId != null) return;
      if (frequency == null) frequency = 1000;
      if (maxEvents == null) maxEvents = 1;
      console.info('Starting event polling');
      this.clientFailureCount = 0;
      var self = this;
      this.eventPollingIntervalId = setInterval(function() {
        self.pollEvents(maxEvents);
      }, frequency);
      this.trigger('eventPollingStarted');
      return this;
    },

    /**
     * Stops event polling if running.
     *
     * @return this.
     */
    stopEventPolling: function() {
      if (this.eventPollingIntervalId == null) return;
      console.info('Stopping event polling');
      clearInterval(this.eventPollingIntervalId);
      this.eventPollingIntervalId = null;
      this.trigger('eventPollingStopped');
      return this;
    },

    /**
     * Initiates asynchronous request for new Workflow events from server. First, tests Workflow
     * completion status. If complete, event polling is stopped and 'workflowComplete' event is
     * triggered. Otherwise, events from last event id are requested. On request failure,
     * 'error.pollEvents' event is triggered. On success, one or more Workflow events are processed
     * sequentially, triggering events in set {'workflowProgress', 'jobStarted', 'jobProgress',
     * 'jobComplete', 'jobFailed'}.
     *
     * @param maxEvents max number of events to process. Defaults to 1.
     * @return Promise configured with error and success callbacks which update state of this
     * Workflow and trigger events.
     */
    pollEvents: function(maxEvents) {
      // stop polling if all jobs are done
      if (this.isComplete()) {
        console.info('Workflow complete');
        this.stopEventPolling();
        this.trigger('workflowComplete');
        return;
      }

      // TODO Integer.MAX_VALUE
      if (maxEvents == null) maxEvents = 999999;

      // error handler
      var self = this;
      var handleError = function(textStatus, errorThrown) {
        console.error('Failed to poll events:', self, textStatus, errorThrown);
        if (++self.clientFailureCount > MAX_CLIENT_FAILURES)
          self.stopEventPolling();
        self.trigger('error.pollEvents', [null, textStatus, errorThrown]);
      };

      // success handler
      var handleSuccess = function(data, textStatus) {
        if (data == null) {
          handleError(textStatus, 'Data is null');
          return;
        }

        // reset client failure count
        self.clientFailureCount = 0;

        // process events
        var eventCount = 0;
        $.each(data, function(i, event) {
          // validate event data
          var id = event.eventId;
          var type = event.eventType;
          var job = event.eventData;
          if (!id || !type || !job) {
            console.error('Invalid event data:', self, event);
            return;
          }

          // skip events we've already processed
          if (id <= self.lastEventId) return;

          // don't process more than specified number of events
          if (eventCount > maxEvents) return;

          // check for workflow event
          if (type == 'WORKFLOW_PROGRESS') {
            self.setProgress(job.workflowProgress);
            return;
          }

          // collect job data; JOB_FINISHED and JOB_FAILED events contain job.jobData
          if (job.jobId == null && job.jobData != null && job.jobData.jobId != null) {
            var jobData = job.jobData;
            delete job.jobData;
            $.extend(true, job, jobData);
          }
          job.id = job.jobId;
          delete job.jobId;

          // retrieve and update job with new data
          job = self.updateJob(job);
          self.jobsById[job.id] = job;

          // process job event
          switch (type) {
          case 'JOB_STARTED':
            console.info('Job started:', job);
            job.status = 'RUNNING';
            break;
          case 'JOB_PROGRESS':
            console.info('Job progress:', job);
            if (job.isComplete == 'true') {
              if (job.isSuccessful == 'true') {
                job.status = 'COMPLETE';
              } else {
                job.status = 'FAILED';
              }
            }
            break;
          case 'JOB_FINISHED':
            // TODO(Andy Schlaikjer): rename JOB_FINISHED to JOB_COMPLETE in server
            type = 'JOB_COMPLETE';
            console.info('Job complete:', job);
            job.status = 'COMPLETE';
            break;
          case 'JOB_FAILED':
            console.info('Job failed:', job);
            job.status = 'FAILED';
            break;
          default:
            console.error("Unsupported event type '" + type + "':", self, event);
            return;
          }

          // update state and trigger event
          eventCount++;
          self.lastEventId = id;
          self.trigger(type.toLowerCase().camelCase(), [job, event]);
        });

        // update state and trigger event
        self.trigger('eventsPolled', [data, textStatus, null]);
      };

      // initiate request
      return this.client.getEvents(this.id, this.lastEventId)
        .error(function(jqXHR, textStatus, errorThrown) {
          handleError(textStatus, errorThrown);
        })
        .success(function(data, textStatus, jqXHR) {
          handleSuccess(data, textStatus);
        });
    },

    /**
     * @return true if this Workflow is complete, false otherwise.
     */
    isComplete: function() {
      var complete = false;
      if (this.progress == '100') {
        complete = true;
        var jobs = this.jobs;
        var i = -1, n = jobs.length; while (++i < n) {
          var job = jobs[i];
          var status = job.status;
          if (status != 'COMPLETE' && status != 'FAILED') {
            complete = false;
            break;
          }
        }
      }
      return complete;
    },

    /**
     * Updates existing job whose id matches that defined by data.id, then updates contents of
     * job to include all of data's fields. Any existing job fields are overwritten.
     *
     * @param data object containing job id and other fields.
     * @return the updated job object.
     * @throws Error if no existing job with matching id exists.
     */
    updateJob: function(data) {
      return $.extend(this.findJob(data), data);
    },

    /**
     * Retrieves existing job object given data containing job name or id fields.
     *
     * @param data object containing name and/or id fields used to uniquely identify the job to
     * retrieve.
     * @return job object.
     * @throws Error if data is null or contains neither name or id fields, or no job with
     * matching name or id exists.
     */
    findJob: function(data) {
      var field, ref, job;
      if (data.name != null) {
        field = 'name';
        ref = data.name;
        job = this.jobsByName[ref];
      } else if (data.id != null) {
        field = 'id';
        ref = data.id;
        job = this.jobsById[ref];
      } else {
        throw new Error('Data contains neither name nor id fields');
      }
      if (job == null) throw new Error("Job with " + field + " '" + ref + "' not found");
      return job;
    },

    /**
     * Sets this Workflow's percent completion and triggers 'progress' event.
     *
     * @param progress int in rage [0, 100].
     * @return this.
     */
    setProgress: function(progress) {
      this.progress = progress;
      this.trigger('workflowProgress', [progress]);
      return this;
    },

    /**
     * Updates current "mouseover" job. The mouse may be hovering only one job at a time. If you
     * mouseover the same job twice, the second call does nothing. If you mouseover null, any
     * current mouseover job is cleared.
     *
     * @param data object containing either 'name' or 'id' fields with which to find job. If null, clears any current "mouseover" job.
     * @return selected job.
     * @throws Error if no job exists with associated name or id.
     */
    mouseOverJob: function(data) {
      var job = null;
      if (data != null) job = this.findJob(data);
      var prev = this.current.mouseover;
      if (job === prev) return;
      if (prev != null) prev.mouseover = false;
      if (job != null) job.mouseover = true;
      this.current.mouseover = job;
      //console.debug('Job mouse over:', job, prev);
      this.trigger('jobMouseOver', [job, prev]);
      return job;
    },

    /**
     * Updates current "selected" job. Only one job may be selected at a time. If you select the
     * same job twice, the second select will deselect the job. If you select null, any selected job
     * is deselected.
     *
     * @param data object containing either 'name' or 'id' fields with which to find job. If null, clears any current "selected" job.
     * @return selected job.
     * @throws Error if no job exists with associated name or id.
     */
    selectJob: function(data) {
      var job = null;
      if (data != null) job = this.findJob(data);
      var prev = this.current.selected;
      if (prev != null) prev.selected = false;
      if (job === prev) job = null;
      else if (job != null) job.selected = true;
      this.current.selected = job;
      //console.debug('Job selected:', job, prev);
      this.trigger('jobSelected', [job, prev]);
      return job;
    },
  };

  // bind prototype to ctor
  Workflow.fn.init.prototype = Workflow.fn;
  return Workflow;
});
<commit_msg>Updates workflow to add pseudo nodes to job graph<commit_after>/*
Copyright 2012 Twitter, Inc.

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.
*/

/**
 * This module defines the Workflow class, instances of which use a Client to retrieve jobs and
 * events from an Ambrose server. The Workflow acts as a controller and owner of job
 * state. Callbacks may be bound to events triggered on the Workflow to react to state changes.
 */
define(['jquery', 'uri', './core', './client', './graph'], function(
  $, URI, Ambrose, Client, Graph
) {
  // Maximum number of consecutive client failures before event polling is stopped.
  var MAX_CLIENT_FAILURES = 10;

  // return data.job, throwing error if it's undefined
  function getJob(data) {
    var job = data.job;
    if (job == null) throw new Error('Data contains no job entry');
    return job;
  }

  // Workflow ctor
  var Workflow = Ambrose.Workflow = function(id, client) {
    return new Ambrose.Workflow.fn.init(id, client);
  };

  /**
   * Workflow prototype.
   */
  Workflow.fn = Workflow.prototype = {
    /**
     * Constructs a new Workflow.
     *
     * @param id unique id of this workflow. If null, defaults to value of current url's
     * 'workflowId' parameter.
     * @param client client to use when requesting Workflow state from server. If null, defaults to
     * new client instance constructed with default arguments.
     */
    init: function(id, client) {
      if (id == null) {
        var uri = new URI(window.location.href);
        var params = uri.search(true);
        if (params) id = params.workflowId;
      }
      if (client == null) client = Client();
      this.$this = $(this);
      this.id = id;
      this.client = client;
      this.progress = 0;
      this.jobs = [];
      this.jobsByName = {};
      this.jobsById = {};
      this.lastEventId = -1;
      this.current = {
        selected: null,
        mouseover: null,
      };
    },

    /**
     * Triggers an event on this Workflow. Any event handlers bound to this Workflow for the given
     * event will be invoked.
     *
     * @param event name of event.
     * @param data array of extra arguments, if any, to pass to handler functions.
     * @return this.
     */
    trigger: function(event, data) {
      this.$this.trigger(event, data);
      return this;
    },

    /**
     * Binds callback to the given Workflow event.
     *
     * @param event name of event.
     * @param callback the callback to invoke when the Workflow event is triggered.
     * @return this.
     */
    on: function(event, callback) {
      this.$this.on(event, callback);
      return this;
    },

    /**
     * Initiates asynchronous request for Workflow jobs from server. On error the 'error.loadJobs'
     * event is triggered.
     *
     * @return Promise configured with error and success callbacks which update state of this
     * Workflow.
     */
    loadJobs: function() {
      this.trigger('loadingJobs');
      console.log('Loading jobs');

      // define callbacks
      var self = this;
      var handleError = function(textStatus, errorThrown) {
        console.error('Failed to load jobs:', textStatus, errorThrown);
        self.trigger('error.loadJobs', [null, textStatus, errorThrown]);
      };
      var handleSuccess = function(data, textStatus) {
        if (data == null) {
          handleError(textStatus, 'Data is null');
          return;
        }

        // reset job indices
        var jobs = self.jobs = data;
        var jobsByName = self.jobsByName = {};
        var jobsById = self.jobsById = {};

        // initialize job indices
        $.each(jobs, function(i, job) {
          // index job by name
          var name = job.name;
          if (name in jobsByName) {
            console.error("Multiple jobs found with name '" + name + "':", self);
            return;
          }
          jobsByName[name] = job;

          // index job by id (if defined)
          if (job.jobId) {
            var id = job.id = job.jobId;
            delete job.jobId;
            if (id in jobsById) {
              console.error("Multiple jobs found with id '" + id + "':", self);
              return;
            }
            jobsById[id] = job;
          }
        });

        // initialize parent links
        $.each(jobs, function(i, job) {
          var name = job.name;
          $.each(job.successorNames, function(i, childName) {
            var child = jobsByName[childName];
            var predecessorNames = child.predecessorNames || (child.predecessorNames = []);
            predecessorNames.push(name);
          });
        });

        // build job graph and sort
        var graph = self.graph = Graph({
          data: jobs,
          getId: function(d) { return d.name; },
          getParentIds: function(d) { return d.predecessorNames; },
        });
        graph.sort();
        graph.addPseudoNodes();
        jobs = self.jobs = $.map(graph.nodesByTopologicalIndex, function(node, i) {
          var job = node.data;
          job.index = i;
          job.node = node;
          return job;
        });

        self.trigger('jobsLoaded', [jobs, textStatus, null]);
      };

      // initiate request
      return this.client.getJobs()
        .error(function(jqXHR, textStatus, errorThrown) {
          handleError(textStatus, errorThrown);
        })
        .success(function(data, textStatus, jqXHR) {
          handleSuccess(data, textStatus);
        });
    },

    /**
     * Starts event polling if not already started.
     *
     * @param frequency poll events at this frequency (ms). Defaults to 1000.
     * @param maxEvents max number of events to process on each request. Defaults to 1.
     * @return this.
     */
    startEventPolling: function(frequency, maxEvents) {
      if (this.eventPollingIntervalId != null) return;
      if (frequency == null) frequency = 1000;
      if (maxEvents == null) maxEvents = 1;
      console.info('Starting event polling');
      this.clientFailureCount = 0;
      var self = this;
      this.eventPollingIntervalId = setInterval(function() {
        self.pollEvents(maxEvents);
      }, frequency);
      this.trigger('eventPollingStarted');
      return this;
    },

    /**
     * Stops event polling if running.
     *
     * @return this.
     */
    stopEventPolling: function() {
      if (this.eventPollingIntervalId == null) return;
      console.info('Stopping event polling');
      clearInterval(this.eventPollingIntervalId);
      this.eventPollingIntervalId = null;
      this.trigger('eventPollingStopped');
      return this;
    },

    /**
     * Initiates asynchronous request for new Workflow events from server. First, tests Workflow
     * completion status. If complete, event polling is stopped and 'workflowComplete' event is
     * triggered. Otherwise, events from last event id are requested. On request failure,
     * 'error.pollEvents' event is triggered. On success, one or more Workflow events are processed
     * sequentially, triggering events in set {'workflowProgress', 'jobStarted', 'jobProgress',
     * 'jobComplete', 'jobFailed'}.
     *
     * @param maxEvents max number of events to process. Defaults to 1.
     * @return Promise configured with error and success callbacks which update state of this
     * Workflow and trigger events.
     */
    pollEvents: function(maxEvents) {
      // stop polling if all jobs are done
      if (this.isComplete()) {
        console.info('Workflow complete');
        this.stopEventPolling();
        this.trigger('workflowComplete');
        return;
      }

      // TODO Integer.MAX_VALUE
      if (maxEvents == null) maxEvents = 999999;

      // error handler
      var self = this;
      var handleError = function(textStatus, errorThrown) {
        console.error('Failed to poll events:', self, textStatus, errorThrown);
        if (++self.clientFailureCount > MAX_CLIENT_FAILURES)
          self.stopEventPolling();
        self.trigger('error.pollEvents', [null, textStatus, errorThrown]);
      };

      // success handler
      var handleSuccess = function(data, textStatus) {
        if (data == null) {
          handleError(textStatus, 'Data is null');
          return;
        }

        // reset client failure count
        self.clientFailureCount = 0;

        // process events
        var eventCount = 0;
        $.each(data, function(i, event) {
          // validate event data
          var id = event.eventId;
          var type = event.eventType;
          var job = event.eventData;
          if (!id || !type || !job) {
            console.error('Invalid event data:', self, event);
            return;
          }

          // skip events we've already processed
          if (id <= self.lastEventId) return;

          // don't process more than specified number of events
          if (eventCount > maxEvents) return;

          // check for workflow event
          if (type == 'WORKFLOW_PROGRESS') {
            self.setProgress(job.workflowProgress);
            return;
          }

          // collect job data; JOB_FINISHED and JOB_FAILED events contain job.jobData
          if (job.jobId == null && job.jobData != null && job.jobData.jobId != null) {
            var jobData = job.jobData;
            delete job.jobData;
            $.extend(true, job, jobData);
          }
          job.id = job.jobId;
          delete job.jobId;

          // retrieve and update job with new data
          job = self.updateJob(job);
          self.jobsById[job.id] = job;

          // process job event
          switch (type) {
          case 'JOB_STARTED':
            console.info('Job started:', job);
            job.status = 'RUNNING';
            break;
          case 'JOB_PROGRESS':
            console.info('Job progress:', job);
            if (job.isComplete == 'true') {
              if (job.isSuccessful == 'true') {
                job.status = 'COMPLETE';
              } else {
                job.status = 'FAILED';
              }
            }
            break;
          case 'JOB_FINISHED':
            // TODO(Andy Schlaikjer): rename JOB_FINISHED to JOB_COMPLETE in server
            type = 'JOB_COMPLETE';
            console.info('Job complete:', job);
            job.status = 'COMPLETE';
            break;
          case 'JOB_FAILED':
            console.info('Job failed:', job);
            job.status = 'FAILED';
            break;
          default:
            console.error("Unsupported event type '" + type + "':", self, event);
            return;
          }

          // update state and trigger event
          eventCount++;
          self.lastEventId = id;
          self.trigger(type.toLowerCase().camelCase(), [job, event]);
        });

        // update state and trigger event
        self.trigger('eventsPolled', [data, textStatus, null]);
      };

      // initiate request
      return this.client.getEvents(this.id, this.lastEventId)
        .error(function(jqXHR, textStatus, errorThrown) {
          handleError(textStatus, errorThrown);
        })
        .success(function(data, textStatus, jqXHR) {
          handleSuccess(data, textStatus);
        });
    },

    /**
     * @return true if this Workflow is complete, false otherwise.
     */
    isComplete: function() {
      var complete = false;
      if (this.progress == '100') {
        complete = true;
        var jobs = this.jobs;
        var i = -1, n = jobs.length; while (++i < n) {
          var job = jobs[i];
          var status = job.status;
          if (status != 'COMPLETE' && status != 'FAILED') {
            complete = false;
            break;
          }
        }
      }
      return complete;
    },

    /**
     * Updates existing job whose id matches that defined by data.id, then updates contents of
     * job to include all of data's fields. Any existing job fields are overwritten.
     *
     * @param data object containing job id and other fields.
     * @return the updated job object.
     * @throws Error if no existing job with matching id exists.
     */
    updateJob: function(data) {
      return $.extend(this.findJob(data), data);
    },

    /**
     * Retrieves existing job object given data containing job name or id fields.
     *
     * @param data object containing name and/or id fields used to uniquely identify the job to
     * retrieve.
     * @return job object.
     * @throws Error if data is null or contains neither name or id fields, or no job with
     * matching name or id exists.
     */
    findJob: function(data) {
      var field, ref, job;
      if (data.name != null) {
        field = 'name';
        ref = data.name;
        job = this.jobsByName[ref];
      } else if (data.id != null) {
        field = 'id';
        ref = data.id;
        job = this.jobsById[ref];
      } else {
        throw new Error('Data contains neither name nor id fields');
      }
      if (job == null) throw new Error("Job with " + field + " '" + ref + "' not found");
      return job;
    },

    /**
     * Sets this Workflow's percent completion and triggers 'progress' event.
     *
     * @param progress int in rage [0, 100].
     * @return this.
     */
    setProgress: function(progress) {
      this.progress = progress;
      this.trigger('workflowProgress', [progress]);
      return this;
    },

    /**
     * Updates current "mouseover" job. The mouse may be hovering only one job at a time. If you
     * mouseover the same job twice, the second call does nothing. If you mouseover null, any
     * current mouseover job is cleared.
     *
     * @param data object containing either 'name' or 'id' fields with which to find job. If null, clears any current "mouseover" job.
     * @return selected job.
     * @throws Error if no job exists with associated name or id.
     */
    mouseOverJob: function(data) {
      var job = null;
      if (data != null) job = this.findJob(data);
      var prev = this.current.mouseover;
      if (job === prev) return;
      if (prev != null) prev.mouseover = false;
      if (job != null) job.mouseover = true;
      this.current.mouseover = job;
      //console.debug('Job mouse over:', job, prev);
      this.trigger('jobMouseOver', [job, prev]);
      return job;
    },

    /**
     * Updates current "selected" job. Only one job may be selected at a time. If you select the
     * same job twice, the second select will deselect the job. If you select null, any selected job
     * is deselected.
     *
     * @param data object containing either 'name' or 'id' fields with which to find job. If null, clears any current "selected" job.
     * @return selected job.
     * @throws Error if no job exists with associated name or id.
     */
    selectJob: function(data) {
      var job = null;
      if (data != null) job = this.findJob(data);
      var prev = this.current.selected;
      if (prev != null) prev.selected = false;
      if (job === prev) job = null;
      else if (job != null) job.selected = true;
      this.current.selected = job;
      //console.debug('Job selected:', job, prev);
      this.trigger('jobSelected', [job, prev]);
      return job;
    },
  };

  // bind prototype to ctor
  Workflow.fn.init.prototype = Workflow.fn;
  return Workflow;
});
<|endoftext|>


<commit_before>hqDefine("hqwebapp/js/hq.helpers", [
    'jquery',
    'knockout',
    'underscore',
    'analytix/js/google',
], function (
    $,
    ko,
    _,
    googleAnalytics
) {
    // disable-on-submit is a class for form submit buttons so they're automatically disabled when the form is submitted
    $(document).on('submit', 'form', function (ev) {
        var form = $(ev.target);
        form.find('.disable-on-submit').disableButton();
        form.find('.disable-on-submit-no-spinner').disableButtonNoSpinner();
    });
    $(document).on('submit', 'form.disable-on-submit', function (ev) {
        $(ev.target).find('[type="submit"]').disableButton();
    });
    $(document).on('reset', 'form', function (ev) {
        $(ev.target).find('.disable-on-submit').enableButton();
    });
    $(document).on('reset', 'form.disable-on-submit', function (ev) {
        $(ev.target).enableButton();
    });
    $(document).on('click', '.add-spinner-on-click', function (ev) {
        $(ev.target).addSpinnerToButton();
    });

    $(document).on('click', '.notification-close-btn', function () {
        var note_id = $(this).data('note-id');
        var post_url = $(this).data('url');
        $.post(post_url, {note_id: note_id});
        $(this).parents('.alert').hide(150);
    });

    if ($.timeago) {
        $.timeago.settings.allowFuture = true;
        $(".timeago").timeago();
    }

    window.onerror = function (message, file, line, col, error) {
        var stack = error ? error.stack : null;
        if (!stack && (message === 'Script error' || message === 'Script error.')) {
            return false;
        }
        $.post('/jserror/', {
            message: message,
            page: window.location.href,
            file: file,
            line: line,
            stack: stack,
        });
        return false; // let default handler run
    };

    var oldHide = $.fn.popover.Constructor.prototype.hide;

    $.fn.popover.Constructor.prototype.hide = function () {
        if (this.options.trigger === "hover" && this.tip().is(":hover")) {
            var that = this;
            setTimeout(function () {
                return that.hide.apply(that, arguments);
            }, that.options.delay.hide);
            return;
        }
        oldHide.apply(this, arguments);
    };

    $.fn.hqHelp = function () {
        var self = this;
        self.each(function (i) {
            var $self = $(self),
                $helpElem = $($self.get(i)),
                $link = $helpElem.find('a');

            var options = {
                html: true,
                trigger: 'focus',
                container: 'body',
                sanitize: false,
            };
            if (!$link.data('content')) {
                options.content = function () {
                    return $('#popover_content_wrapper').html();
                };
            }
            if (!$link.data("title")) {
                options.template = '

';
            }
            $link.popover(options);

            // Prevent jumping to the top of the page when link is clicked
            $helpElem.find('a').click(function (event) {
                googleAnalytics.track.event("Clicked Help Bubble", $(this).data('title'), '-');
                event.preventDefault();
            });
        });
    };

    $.fn.addSpinnerToButton = function () {
        $(this).find("i").addClass("hide");
        $(this).prepend(' ');
    };

    $.fn.removeSpinnerFromButton = function () {
        $(this).find("i.hide").removeClass("hide");
        $(this).find('i.fa-spin').remove();
    };

    $.fn.disableButtonNoSpinner = function () {
        $(this).prop('disabled', 'disabled')
            .addClass('disabled');
    };

    $.fn.disableButton = function () {
        $(this).disableButtonNoSpinner();
        $(this).addSpinnerToButton();
    };

    $.fn.enableButton = function () {
        $(this).removeSpinnerFromButton();
        $(this).removeClass('disabled')
            .prop('disabled', false);
    };

    $.fn.koApplyBindings = function (context) {
        if (!this.length) {
            throw new Error("No element passed to koApplyBindings");
        }
        if (this.length > 1) {
            throw new Error("Multiple elements passed to koApplyBindings");
        }
        ko.applyBindings(context, this.get(0));
        this.removeClass('ko-template');
    };

    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            // Don't pass csrftoken cross domain
            // Ignore HTTP methods that do not require CSRF protection
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type) && !this.crossDomain) {
                var $csrf_token = $("#csrfTokenContainer").val();
                xhr.setRequestHeader("X-CSRFToken", $csrf_token);
            }
        },
    });

    // Return something so that hqModules understands that the module has been defined
    return 1;
});
<commit_msg>Silence "ResizeObserver loop limit exceeded" jserror<commit_after>hqDefine("hqwebapp/js/hq.helpers", [
    'jquery',
    'knockout',
    'underscore',
    'analytix/js/google',
], function (
    $,
    ko,
    _,
    googleAnalytics
) {
    // disable-on-submit is a class for form submit buttons so they're automatically disabled when the form is submitted
    $(document).on('submit', 'form', function (ev) {
        var form = $(ev.target);
        form.find('.disable-on-submit').disableButton();
        form.find('.disable-on-submit-no-spinner').disableButtonNoSpinner();
    });
    $(document).on('submit', 'form.disable-on-submit', function (ev) {
        $(ev.target).find('[type="submit"]').disableButton();
    });
    $(document).on('reset', 'form', function (ev) {
        $(ev.target).find('.disable-on-submit').enableButton();
    });
    $(document).on('reset', 'form.disable-on-submit', function (ev) {
        $(ev.target).enableButton();
    });
    $(document).on('click', '.add-spinner-on-click', function (ev) {
        $(ev.target).addSpinnerToButton();
    });

    $(document).on('click', '.notification-close-btn', function () {
        var note_id = $(this).data('note-id');
        var post_url = $(this).data('url');
        $.post(post_url, {note_id: note_id});
        $(this).parents('.alert').hide(150);
    });

    if ($.timeago) {
        $.timeago.settings.allowFuture = true;
        $(".timeago").timeago();
    }

    window.onerror = function (message, file, line, col, error) {
        var stack = error ? error.stack : null;
        if (!stack && (
                message === 'Script error'
                || message === 'Script error.'
                || message === 'ResizeObserver loop limit exceeded'
        )) {
            return false;
        }
        $.post('/jserror/', {
            message: message,
            page: window.location.href,
            file: file,
            line: line,
            stack: stack,
        });
        return false; // let default handler run
    };

    var oldHide = $.fn.popover.Constructor.prototype.hide;

    $.fn.popover.Constructor.prototype.hide = function () {
        if (this.options.trigger === "hover" && this.tip().is(":hover")) {
            var that = this;
            setTimeout(function () {
                return that.hide.apply(that, arguments);
            }, that.options.delay.hide);
            return;
        }
        oldHide.apply(this, arguments);
    };

    $.fn.hqHelp = function () {
        var self = this;
        self.each(function (i) {
            var $self = $(self),
                $helpElem = $($self.get(i)),
                $link = $helpElem.find('a');

            var options = {
                html: true,
                trigger: 'focus',
                container: 'body',
                sanitize: false,
            };
            if (!$link.data('content')) {
                options.content = function () {
                    return $('#popover_content_wrapper').html();
                };
            }
            if (!$link.data("title")) {
                options.template = '

';
            }
            $link.popover(options);

            // Prevent jumping to the top of the page when link is clicked
            $helpElem.find('a').click(function (event) {
                googleAnalytics.track.event("Clicked Help Bubble", $(this).data('title'), '-');
                event.preventDefault();
            });
        });
    };

    $.fn.addSpinnerToButton = function () {
        $(this).find("i").addClass("hide");
        $(this).prepend(' ');
    };

    $.fn.removeSpinnerFromButton = function () {
        $(this).find("i.hide").removeClass("hide");
        $(this).find('i.fa-spin').remove();
    };

    $.fn.disableButtonNoSpinner = function () {
        $(this).prop('disabled', 'disabled')
            .addClass('disabled');
    };

    $.fn.disableButton = function () {
        $(this).disableButtonNoSpinner();
        $(this).addSpinnerToButton();
    };

    $.fn.enableButton = function () {
        $(this).removeSpinnerFromButton();
        $(this).removeClass('disabled')
            .prop('disabled', false);
    };

    $.fn.koApplyBindings = function (context) {
        if (!this.length) {
            throw new Error("No element passed to koApplyBindings");
        }
        if (this.length > 1) {
            throw new Error("Multiple elements passed to koApplyBindings");
        }
        ko.applyBindings(context, this.get(0));
        this.removeClass('ko-template');
    };

    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            // Don't pass csrftoken cross domain
            // Ignore HTTP methods that do not require CSRF protection
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type) && !this.crossDomain) {
                var $csrf_token = $("#csrfTokenContainer").val();
                xhr.setRequestHeader("X-CSRFToken", $csrf_token);
            }
        },
    });

    // Return something so that hqModules understands that the module has been defined
    return 1;
});
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const {generateAndCompile} = require('RelayModernTestUtils');

import validateMutation from '../validateMutation';

jest.mock('warning', () => {
  return (dontWarn, message, ...args) => {
    if (dontWarn) {
      return;
    }
    throw new Error(`${message} ${args.join(' ')}`);
  };
});
describe('validateOptimisticResponse', () => {
  [
    {
      name: 'Does not log a warning in the positive case',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'User',
            id: 0,
            name: 'B-dizzle',
          },
        },
      },
      variables: null,
      shouldWarn: false,
    },
    {
      name: 'Logs a warning when a field is undefined',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {},
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Logs a warning when an id is undefined',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'User',
            name: 'Zuck',
          },
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Logs a warning when a object is undefined',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {},
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Uses type names to filter inline fragment warnings',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'Page',
            id: '3',
            username: 'Zuck',
          },
        },
      },
      variables: null,
      shouldWarn: false,
    },
    {
      name: 'Logs a warning for errors contained in inline fragments',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'User',
            id: '3',
            username: 'Zuck',
          },
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name:
        'Logs a warning when there are unused fields in an `optimisticResponse`',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'Page',
            id: '3',
            username: 'Zuck',
            unusedField: true,
          },
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Passes when fields are null',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            id: 3,
            __typename: 'User',
            name: null,
            birthdate: null,
          },
        },
      },
      variables: null,
      shouldWarn: false,
    },
    {
      name: 'Handles include and skip directives when var is true',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!,
            $myVar: Boolean!,
          ) {
            actorNameChange(input: $input) {
              actor {
                ... @include(if: $myVar) {
                  ... on Page {
                    username
                  }
                }
                ... @skip(if: $myVar) {
                  ... on Page {
                    canViewerLike
                  }
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            id: 3,
            __typename: 'Page',
            username: null,
          },
        },
      },
      variables: {
        myVar: true,
      },
      shouldWarn: false,
    },
    {
      name: 'Handles include directive and skip directives when var is false',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!,
            $myVar: Boolean!,
          ) {
            actorNameChange(input: $input) {
              actor {
                ... @include(if: $myVar) {
                  ... on Page {
                    username
                  }
                }
                ... @skip(if: $myVar) {
                  ... on Page {
                    canViewerLike
                  }
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            id: 3,
            __typename: 'Page',
            username: null,
          },
        },
      },
      variables: {
        myVar: false,
      },
      shouldWarn: true,
    },
  ].forEach(({name, mutation, optimisticResponse, shouldWarn, variables}) => {
    it(name, () => {
      jest.clearAllMocks();
      if (shouldWarn) {
        expect(() =>
          validateMutation(optimisticResponse, mutation, variables),
        ).toThrow(Error);
      } else {
        expect(() =>
          validateMutation(optimisticResponse, mutation, variables),
        ).not.toThrow(Error);
      }
    });
  });
});
<commit_msg>Remove error to fix flow error in www<commit_after>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const {generateAndCompile} = require('RelayModernTestUtils');

import validateMutation from '../validateMutation';

jest.mock('warning', () => {
  return (dontWarn, message, ...args) => {
    if (dontWarn) {
      return;
    }
    throw new Error(`${message} ${args.join(' ')}`);
  };
});
describe('validateOptimisticResponse', () => {
  [
    {
      name: 'Does not log a warning in the positive case',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'User',
            id: 0,
            name: 'B-dizzle',
          },
        },
      },
      variables: null,
      shouldWarn: false,
    },
    {
      name: 'Logs a warning when a field is undefined',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {},
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Logs a warning when an id is undefined',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'User',
            name: 'Zuck',
          },
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Logs a warning when a object is undefined',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {},
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Uses type names to filter inline fragment warnings',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'Page',
            id: '3',
            username: 'Zuck',
          },
        },
      },
      variables: null,
      shouldWarn: false,
    },
    {
      name: 'Logs a warning for errors contained in inline fragments',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'User',
            id: '3',
            username: 'Zuck',
          },
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name:
        'Logs a warning when there are unused fields in an `optimisticResponse`',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            __typename: 'Page',
            id: '3',
            username: 'Zuck',
            unusedField: true,
          },
        },
      },
      variables: null,
      shouldWarn: true,
    },
    {
      name: 'Passes when fields are null',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!
          ) {
            actorNameChange(input: $input) {
              actor {
                name
                ... on User {
                  birthdate {
                    day
                    month
                    year
                  }
                }
                ... on Page {
                  username
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            id: 3,
            __typename: 'User',
            name: null,
            birthdate: null,
          },
        },
      },
      variables: null,
      shouldWarn: false,
    },
    {
      name: 'Handles include and skip directives when var is true',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!,
            $myVar: Boolean!,
          ) {
            actorNameChange(input: $input) {
              actor {
                ... @include(if: $myVar) {
                  ... on Page {
                    username
                  }
                }
                ... @skip(if: $myVar) {
                  ... on Page {
                    canViewerLike
                  }
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            id: 3,
            __typename: 'Page',
            username: null,
          },
        },
      },
      variables: {
        myVar: true,
      },
      shouldWarn: false,
    },
    {
      name: 'Handles include directive and skip directives when var is false',
      mutation: generateAndCompile(`
          mutation ChangeNameMutation(
            $input: ActorNameChangeInput!,
            $myVar: Boolean!,
          ) {
            actorNameChange(input: $input) {
              actor {
                ... @include(if: $myVar) {
                  ... on Page {
                    username
                  }
                }
                ... @skip(if: $myVar) {
                  ... on Page {
                    canViewerLike
                  }
                }
              }
            }
          }
          `).ChangeNameMutation,
      optimisticResponse: {
        actorNameChange: {
          actor: {
            id: 3,
            __typename: 'Page',
            username: null,
          },
        },
      },
      variables: {
        myVar: false,
      },
      shouldWarn: true,
    },
  ].forEach(({name, mutation, optimisticResponse, shouldWarn, variables}) => {
    it(name, () => {
      jest.clearAllMocks();
      if (shouldWarn) {
        expect(() =>
          validateMutation(optimisticResponse, mutation, variables),
        ).toThrow();
      } else {
        expect(() =>
          validateMutation(optimisticResponse, mutation, variables),
        ).not.toThrow();
      }
    });
  });
});
<|endoftext|>


<commit_before>var text = document.getElementById('textArea');
var sumbit = document.getElementById('submitButtonArea');

function arrows() {
   thingie = '

';
   // row 1
   thingie += '

	 <button onclick="up()"> ⇑ </button>		';
   thingie += '<button onclick="talk()">talk</button>	';
   thingie += '<button onclick="items()">items</button>
';
   // row 2
   thingie += '
<button onclick="left()">⇐</button>	';
   thingie += '<button onclick="down()"> ⇓ </button>	';
   thingie += '<button onclick="right()">⇒</button>	';
   thingie += '<button onclick="check()">check</button>	';
   thingie += '<button onclick="stats()">status</button>	';
   thingie += '

';

   sumbit.innerHTML = thingie;
}

function dialog(n) {
  var thingie = '';
  var buttons = '';
  switch(n) {
    case 1:
      thingie = 'Fred Paz: Our world has been invaded by aliens. They have been here for centuries. ';
      thingie += 'They have brainwashed much of the populus to believe that they are not capable of doing mathematics. ';
      thingie += 'We have been fortunate enough to have a segment of the population that has been immune to this sinister ';
      thingie += 'plot, and we were able to carry on without too many hiccups in the past.';
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(3)">Continue</button> <button onclick="dialog(2)">Ask to be dismissed.</button>';
      sumbit.innerHTML = buttons;
      break;
    case 2:
      thingie = 'Fred Paz: That is understandable. Please return when you are ready.';
      text.innerHTML = thingie;
      arrows();
      break;
    case 3:
      thingie = 'Fred Paz: Unfortunately, the aliens have hacked into our cell phone and mobile devices and have been using ';
      thingie += 'the electronic signals from these devices to convince people that they do not need to know how to do math. ';
      thingie += 'This has created a dire problem for our future. It will not be long before the aliens take over.';
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(3.1)">Continue</button> <button onclick="dialog(2)">Ask to be dismissed.</button>';
      sumbit.innerHTML = buttons;
      break;
    case 3.1:
      thingie = "Fred Paz: We must work to overcome the alien's brainwashing. ";
      thingie += 'Unfortunately, the people who dearly need our help will fight against our efforts.';
      thingie += 'Like the incredibly long fight scene in the movie They Live, the most difficult fight will be ';
      thingie += 'to convince people to challenge their negative thoughts on mathematics. ';
      thingie += 'However, defeating the alines will be a cakewalk  once we do. ';
      thingie += "At least that's what happened in They Live.";
      thingie += "You've seen the movie right?";
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(4)">Yes</button> <button onclick="dialog(5)">No</button>';
      sumbit.innerHTML = buttons;
      break;
    case 4:
      thingie = "Fred Paz: No, I am not going to give you a pair of sunglasses. ";
      thingie += "What gave you that idea? ";
      thingie += 'However, I do have a business card with the url for ';

      thingie += 'withmathican.com
';
      thingie += '(You recieve the With Math I Can Business Card)
';
      player.items.push({'item':'WMIC card',
        'n':1,
        'desc':'A business card with the withMathIcan.com url. You can carry it in your right hand. Grants 9 attack, 6 defense, and 6 evasion.',
        'atk':9,
        'def':6,
        'eva':6,
         use(){
           text.innerHTML = 'You carry your With Math I Can card in your right hand.';
           player.rightHand = {'item':'WMIC card','atk':9,'def':6,'eva':6};
           player.atk = player.leftHand.atk + player.rightHand.atk;
           player.def = player.head.def + player.leftHand.def + player.rightHand.def + player.torso.def;
           player.def += player.legs.def + player.feet.def;
           player.eva = player.head.eva + player.leftHand.eva + player.rightHand.eva + player.torso.eva;
           player.eva += player.legs.eva + player.feet.eva;
         }
      });
      thingie += " Would you like to equip it?";
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(6)">Yes</button> <button onclick="dialog(7)">No</button>';
      sumbit.innerHTML = buttons;
      player.questLog.push('Talked to Fred Paz.');
      break;

  case 5:
      thingie = "Fred Paz: I am dreadfully sorry. I did not mean to spoil the movie. ";
      thingie += 'However, I would still recommend watching it.';
      thingie += "With your parents' permission of course.";
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(5)">Continue</button>';
      sumbit.innerHTML = buttons;
      break;

  case 6:
       thingie = '(You carry your With Math I Can card in your right hand.)';
       player.rightHand = {'item':'WMIC card','atk':9,'def':6,'eva':6};
       player.atk = player.leftHand.atk + player.rightHand.atk;
       player.def = player.head.def + player.leftHand.def + player.rightHand.def + player.torso.def;
       player.def += player.legs.def + player.feet.def;
       player.eva = player.head.eva + player.leftHand.eva + player.rightHand.eva + player.torso.eva;
       player.eva += player.legs.eva + player.feet.eva;

   case 7:
       thingie += 'Fred Paz: Alright, now get out there and change some minds about math.';
       text.innerHTML = thingie;
       arrows();
       break;
     default:
        thingie = 'Error: My name is Error.';
        text.innerHTML = thingie;
        arrows();
  }

}
<commit_msg>Update dialog.js<commit_after>var text = document.getElementById('textArea');
var sumbit = document.getElementById('submitButtonArea');

function arrows() {
   thingie = '

';
   // row 1
   thingie += '

	 <button onclick="up()"> ⇑ </button>		';
   thingie += '<button onclick="talk()">talk</button>	';
   thingie += '<button onclick="items()">items</button>
';
   // row 2
   thingie += '
<button onclick="left()">⇐</button>	';
   thingie += '<button onclick="down()"> ⇓ </button>	';
   thingie += '<button onclick="right()">⇒</button>	';
   thingie += '<button onclick="check()">check</button>	';
   thingie += '<button onclick="stats()">status</button>	';
   thingie += '

';

   sumbit.innerHTML = thingie;
}

function dialog(n) {
  var thingie = '';
  var buttons = '';
  switch(n) {
    case 1:
      thingie = 'Fred Paz: Our world has been invaded by aliens. They have been here for centuries. ';
      thingie += 'They have brainwashed much of the populus to believe that they are not capable of doing mathematics. ';
      thingie += 'We have been fortunate enough to have a segment of the population that has been immune to this sinister ';
      thingie += 'plot, and we were able to carry on without too many hiccups in the past.';
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(3)">Continue</button> <button onclick="dialog(2)">Ask to be dismissed.</button>';
      sumbit.innerHTML = buttons;
      break;
    case 2:
      thingie = 'Fred Paz: That is understandable. Please return when you are ready.';
      text.innerHTML = thingie;
      arrows();
      break;
    case 3:
      thingie = 'Fred Paz: Unfortunately, the aliens have hacked into our cell phone and mobile devices and have been using ';
      thingie += 'the electronic signals from these devices to convince people that they do not need to know how to do math. ';
      thingie += 'This has created a dire problem for our future. It will not be long before the aliens take over.';
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(3.1)">Continue</button> <button onclick="dialog(2)">Ask to be dismissed.</button>';
      sumbit.innerHTML = buttons;
      break;
    case 3.1:
      thingie = "Fred Paz: We must work to overcome the alien's brainwashing. ";
      thingie += 'Unfortunately, the people who dearly need our help will fight against our efforts.';
      thingie += 'Like the incredibly long fight scene in the movie They Live, the most difficult fight will be ';
      thingie += 'to convince people to challenge their negative thoughts on mathematics. ';
      thingie += 'However, defeating the alines will be a cakewalk  once we do. ';
      thingie += "At least that's what happened in They Live.";
      thingie += "You've seen the movie right?";
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(4)">Yes</button> <button onclick="dialog(5)">No</button>';
      sumbit.innerHTML = buttons;
      break;
    case 4:
      thingie = "Fred Paz: No, I am not going to give you a pair of sunglasses. ";
      thingie += "What gave you that idea? ";
      thingie += 'However, I do have a business card with the url for ';

      thingie += 'withmathican.com
';
      thingie += '(You recieve the With Math I Can Business Card)
';
      player.items.push({'item':'WMIC card',
        'n':1,
        'desc':'A business card with the withMathIcan.com url. You can carry it in your right hand. Grants 9 attack, 6 defense, and 6 evasion.',
        'atk':9,
        'def':6,
        'eva':6,
         use(){
           text.innerHTML = 'You carry your With Math I Can card in your right hand.';
           player.rightHand = {'item':'WMIC card','atk':9,'def':6,'eva':6};
           player.atk = player.leftHand.atk + player.rightHand.atk;
           player.def = player.head.def + player.leftHand.def + player.rightHand.def + player.torso.def;
           player.def += player.legs.def + player.feet.def;
           player.eva = player.head.eva + player.leftHand.eva + player.rightHand.eva + player.torso.eva;
           player.eva += player.legs.eva + player.feet.eva;
         }
      });
      thingie += " Would you like to equip it?";
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(6)">Yes</button> <button onclick="dialog(7)">No</button>';
      sumbit.innerHTML = buttons;
      player.questLog.push('Talked to Fred Paz.');
      break;

  case 5:
      thingie = "Fred Paz: I am dreadfully sorry. I did not mean to spoil the movie. ";
      thingie += 'However, I would still recommend watching it.';
      thingie += "With your parents' permission of course.";
      text.innerHTML = thingie;
      buttons = '<button onclick="dialog(5)">Continue</button>';
      sumbit.innerHTML = buttons;
      break;

  case 6:
       thingie = '(You carry your With Math I Can card in your right hand.)
';
       player.rightHand = {'item':'WMIC card','atk':9,'def':6,'eva':6};
       player.atk = player.leftHand.atk + player.rightHand.atk;
       player.def = player.head.def + player.leftHand.def + player.rightHand.def + player.torso.def;
       player.def += player.legs.def + player.feet.def;
       player.eva = player.head.eva + player.leftHand.eva + player.rightHand.eva + player.torso.eva;
       player.eva += player.legs.eva + player.feet.eva;

   case 7:
       thingie += 'Fred Paz: Alright, now get out there and change some minds about math.';
       text.innerHTML = thingie;
       arrows();
       break;
     default:
        thingie = 'Error: My name is Error.';
        text.innerHTML = thingie;
        arrows();
  }

}
<|endoftext|>


<commit_before>/**
 * Различные настройки
 */

;var ls = ls || {};

ls.settings = (function ($) {

    this.presets = {};

    this.get = function (editor, preset) {
        if (this.presets[editor]) {
            if (preset && this.presets[editor][preset]) {
                return this.presets[editor][preset]()
            }
            return this.presets[editor]['default']()
        } else {
            return {};
        }
    };

    // *** markitup *** //
    this.presets.markitup = {};
    this.presets.markitup['default'] = function(){
        return     {
            onShiftEnter:   {keepDefault: false, replaceWith: '
\n'},
            onCtrlEnter:    {keepDefault: false, openWith: '\n
', closeWith: '
'},
            onTab:          {keepDefault: false, replaceWith: '    '},
            markupSet: [
                {name: ls.lang.get('panel_title_h4'), className: 'editor-h4', openWith: '
', closeWith: '
' },
                {name: ls.lang.get('panel_title_h5'), className: 'editor-h5', openWith: '
', closeWith: '
' },
                {name: ls.lang.get('panel_title_h6'), className: 'editor-h6', openWith: '
', closeWith: '
' },
                {separator: '---------------' },
                { name:'snippets',  className: 'snippet-menu', dropMenu: [
                    {name: ls.lang.get('panel_user'),   className: 'editor-user', replaceWith: '<alto:user login="[![' + ls.lang.get('panel_user_promt') + ']!]"/>' },
                    {name: ls.lang.get('panel_photoset'),   className: 'editor-photoset', replaceWith: '<alto:photoset/>' },
                    {name: ls.lang.get('panel_spoiler'),  className: 'editor-spoiler', openWith: '<alto:spoiler title="">', closeWith: '</alto:spoiler>' }
                ]
                },
                {separator: '---------------' },
                {name: ls.lang.get('panel_b'),  className: 'editor-bold', key: 'B', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)' },
                {name: ls.lang.get('panel_i'),  className: 'editor-italic', key: 'I', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)'  },
                {name: ls.lang.get('panel_s'),  className: 'editor-stroke', key: 'S', openWith: '<s>', closeWith: '</s>' },
                {name: ls.lang.get('panel_u'),  className: 'editor-underline', key: 'U', openWith: '', closeWith: '' },
                {name: ls.lang.get('panel_quote'), className: 'editor-quote', key: 'Q', replaceWith: function (m) {
                    if (m.selectionOuter) return '
' + m.selectionOuter + '
'; else if (m.selection) return '
' + m.selection + '
'; else return '

'
                } },
                {name: ls.lang.get('panel_code'),   className: 'editor-code', openWith: '<(!(code|!|codeline)!)>', closeWith: '</(!(code|!|codeline)!)>' },
                {separator: '---------------' },
                {name: ls.lang.get('panel_list'),   className: 'editor-ul', openWith: '
', closeWith: '
', multiline: true, openBlockWith: '
\n', closeBlockWith: '\n
' },
                {name: ls.lang.get('panel_list'),   className: 'editor-ol', openWith: '
', closeWith: '
', multiline: true, openBlockWith: '
\n', closeBlockWith: '\n
' },
                {name: ls.lang.get('panel_list_li'), className: 'editor-li', openWith: '
', closeWith: '
' },
                {separator: '---------------' },
                //{name: ls.lang.get('panel_image'),  className: 'editor-picture', key: 'P', beforeInsert: function (h) {
                //    jQuery('#modal-upload_img').modal();
                //} },
                {name: ls.lang.get('panel_insert_image'),  className: 'editor-insert-picture', key: 'K', beforeInsert: function (h) {
                    jQuery('#js-alto-image-manager').modal();
                } },
                {name: ls.lang.get('panel_video'),  className: 'editor-video', replaceWith: '<video>[![' + ls.lang.get('panel_video_promt') + ':!:http://]!]</video>' },
                {name: ls.lang.get('panel_url'),    className: 'editor-link', key: 'L', openWith: '', closeWith: '', placeHolder: 'Your text to link...' },

                {separator: '---------------' },
                {name: ls.lang.get('panel_clear_tags'), className: 'editor-clean', replaceWith: function (markitup) {
                    return markitup.selection.replace(/<(.*?)>/g, "")
                } },
                {name: ls.lang.get('panel_cut'), className: 'editor-cut', replaceWith: function (markitup) {
                    if (markitup.selection) return '<cut name="' + markitup.selection + '">'; else return '<cut>'
                }}
            ]
        };
    };

    this.presets.markitup['comment'] = function(){
        return {
            onShiftEnter:   {keepDefault: false, replaceWith: '
\n'},
            onTab:          {keepDefault: false, replaceWith: '    '},
            markupSet: [
                {name: ls.lang.get('panel_b'), className: 'editor-bold', key: 'B', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)' },
                {name: ls.lang.get('panel_i'), className: 'editor-italic', key: 'I', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)'  },
                {name: ls.lang.get('panel_s'), className: 'editor-stroke', key: 'S', openWith: '<s>', closeWith: '</s>' },
                {name: ls.lang.get('panel_u'), className: 'editor-underline', key: 'U', openWith: '', closeWith: '' },
                {separator: '---------------' },
                {name: ls.lang.get('panel_quote'), className: 'editor-quote', key: 'Q', replaceWith: function (m) {
                    if (m.selectionOuter) return '
' + m.selectionOuter + '
'; else if (m.selection) return '
' + m.selection + '
'; else return '

'
                } },
                {name: ls.lang.get('panel_code'), className: 'editor-code', openWith: '<(!(code|!|codeline)!)>', closeWith: '</(!(code|!|codeline)!)>' },
                //{name: ls.lang.get('panel_image'), className: 'editor-picture', key: 'P', beforeInsert: function (h) {
                //    jQuery('#modal-upload_img').modal();
                //} },
                {name: ls.lang.get('panel_insert_image'),  className: 'editor-insert-picture', key: 'K', beforeInsert: function (h) {
                    jQuery('#js-alto-image-manager').modal();
                } },
                {name: ls.lang.get('panel_url'), className: 'editor-link', key: 'L', openWith: '', closeWith: '', placeHolder: 'Your text to link...' },
                {name: ls.lang.get('panel_user'), className: 'editor-user', replaceWith: '<ls user="[![' + ls.lang.get('panel_user_promt') + ']!]" />' },
                {separator: '---------------' },
                {name: ls.lang.get('panel_clear_tags'), className: 'editor-clean', replaceWith: function (markitup) {
                    return markitup.selection.replace(/<(.*?)>/g, "")
                } }
            ]
        }
    };

    // *** tinimce *** //
    this.presets.tinymce = {};
    this.presets.tinymce['default'] = function(){
        return {
            selector:           '.js-editor-wysiwyg',
            theme:              'modern',
            relative_urls:      false,
            menubar:            false,
            toolbar: "undo styleselect | bold italic strikethrough underline blockquote snippets | alignleft aligncenter alignright | bullist numlist table | link unlink | altoimage media | code pagebreak ",
            toolbar_items_size: 'small',
            image_advtab: false,
            style_formats: [
                {title: ls.lang.get('panel_title_h4'), block: 'h4'},
                {title: ls.lang.get('panel_title_h5'), block: 'h5'},
                {title: ls.lang.get('panel_title_h6'), block: 'h6'}
            ],
            object_resizing:    true,
            forced_root_block:  '', // Needed for 3.x
            force_p_newlines:   true,
            custom_elements : "alto:photoset",
            self_closing_elements: "alto:photoset",
            force_br_newlines:  false,
            plugins: "advlist autolink autosave link lists media pagebreak autoresize table code altoimage snippets photoset username spoiler",
            //convert_urls: false,
            extended_valid_elements: "embed[src|type|allowscriptaccess|allowfullscreen|width|height],alto:photoset[from|to|style|position|topic],alto:user[login|style],alto:spoiler[style|title]",
            pagebreak_separator: "<cut>",
            media_strict: false,
            language: 'ru',
            inline_styles: false,
            formats: {
                underline:      {inline: 'u', exact: true},
                strikethrough:  {inline: 's', exact: true}
            }
        }
    };

    this.presets.tinymce['comment'] = function(){
        return {
            selector:           '.js-editor-wysiwyg',
            theme:              'modern',
            relative_urls:      false,
            menubar:            false,
            toolbar: "undo redo | styleselect | bold italic strikethrough underline blockquote | alignleft aligncenter alignright | bullist numlist table | link unlink | altoimage media | code ",
            toolbar_items_size: 'small',
            image_advtab: false,
            style_formats: [
                {title: 'Head 1', block: 'h4'},
                {title: 'Head 2', block: 'h5'},
                {title: 'Head 3', block: 'h6'}
            ],
            object_resizing:    'table,img.mce-object-video,div',
            forced_root_block:  '', // Needed for 3.x
            force_p_newlines:   true,
            force_br_newlines:  false,
            plugins: "advlist autolink autosave link lists media pagebreak autoresize table code altoimage",
            extended_valid_elements: "embed[src|type|allowscriptaccess|allowfullscreen|width|height]",
            pagebreak_separator: "<cut>",
            media_strict: false,
            language: 'ru',
            inline_styles: false,
            formats: {
                underline:      {inline: 'u', exact: true},
                strikethrough:  {inline: 's', exact: true}
            },
            autoresize_min_height: '80px',
            setup: function (editor) {
                editor.on('keyup', function (e) {
                    var key = e.keyCode || e.which;
                    if (e.ctrlKey && (key == 13)) {
                        $('#comment-button-submit').click();
                    }
                    return false;
                });
            }
        }
    };

    this.getMarkitup = function() {
        return this.get('markitup');
    };

    this.getMarkitupComment = function() {
        return this.get('markitup', 'comment');
    };

    this.getTinymce = function() {
        return this.get('tinymce');
    };

    this.getTinymceComment = function() {
        return this.get('tinymce', 'comment');
    };

    return this;
}).call(ls.settings || {},jQuery);<commit_msg>Исправлен предпросмотр для топиков у редактора tinymce, #343<commit_after>/**
 * Различные настройки
 */

;var ls = ls || {};

ls.settings = (function ($) {

    this.presets = {};

    this.get = function (editor, preset) {
        if (this.presets[editor]) {
            if (preset && this.presets[editor][preset]) {
                return this.presets[editor][preset]()
            }
            return this.presets[editor]['default']()
        } else {
            return {};
        }
    };

    // *** markitup *** //
    this.presets.markitup = {};
    this.presets.markitup['default'] = function(){
        return     {
            onShiftEnter:   {keepDefault: false, replaceWith: '
\n'},
            onCtrlEnter:    {keepDefault: false, openWith: '\n
', closeWith: '
'},
            onTab:          {keepDefault: false, replaceWith: '    '},
            markupSet: [
                {name: ls.lang.get('panel_title_h4'), className: 'editor-h4', openWith: '
', closeWith: '
' },
                {name: ls.lang.get('panel_title_h5'), className: 'editor-h5', openWith: '
', closeWith: '
' },
                {name: ls.lang.get('panel_title_h6'), className: 'editor-h6', openWith: '
', closeWith: '
' },
                {separator: '---------------' },
                { name:'snippets',  className: 'snippet-menu', dropMenu: [
                    {name: ls.lang.get('panel_user'),   className: 'editor-user', replaceWith: '<alto:user login="[![' + ls.lang.get('panel_user_promt') + ']!]"/>' },
                    {name: ls.lang.get('panel_photoset'),   className: 'editor-photoset', replaceWith: '<alto:photoset/>' },
                    {name: ls.lang.get('panel_spoiler'),  className: 'editor-spoiler', openWith: '<alto:spoiler title="">', closeWith: '</alto:spoiler>' }
                ]
                },
                {separator: '---------------' },
                {name: ls.lang.get('panel_b'),  className: 'editor-bold', key: 'B', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)' },
                {name: ls.lang.get('panel_i'),  className: 'editor-italic', key: 'I', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)'  },
                {name: ls.lang.get('panel_s'),  className: 'editor-stroke', key: 'S', openWith: '<s>', closeWith: '</s>' },
                {name: ls.lang.get('panel_u'),  className: 'editor-underline', key: 'U', openWith: '', closeWith: '' },
                {name: ls.lang.get('panel_quote'), className: 'editor-quote', key: 'Q', replaceWith: function (m) {
                    if (m.selectionOuter) return '
' + m.selectionOuter + '
'; else if (m.selection) return '
' + m.selection + '
'; else return '

'
                } },
                {name: ls.lang.get('panel_code'),   className: 'editor-code', openWith: '<(!(code|!|codeline)!)>', closeWith: '</(!(code|!|codeline)!)>' },
                {separator: '---------------' },
                {name: ls.lang.get('panel_list'),   className: 'editor-ul', openWith: '
', closeWith: '
', multiline: true, openBlockWith: '
\n', closeBlockWith: '\n
' },
                {name: ls.lang.get('panel_list'),   className: 'editor-ol', openWith: '
', closeWith: '
', multiline: true, openBlockWith: '
\n', closeBlockWith: '\n
' },
                {name: ls.lang.get('panel_list_li'), className: 'editor-li', openWith: '
', closeWith: '
' },
                {separator: '---------------' },
                //{name: ls.lang.get('panel_image'),  className: 'editor-picture', key: 'P', beforeInsert: function (h) {
                //    jQuery('#modal-upload_img').modal();
                //} },
                {name: ls.lang.get('panel_insert_image'),  className: 'editor-insert-picture', key: 'K', beforeInsert: function (h) {
                    jQuery('#js-alto-image-manager').modal();
                } },
                {name: ls.lang.get('panel_video'),  className: 'editor-video', replaceWith: '<video>[![' + ls.lang.get('panel_video_promt') + ':!:http://]!]</video>' },
                {name: ls.lang.get('panel_url'),    className: 'editor-link', key: 'L', openWith: '', closeWith: '', placeHolder: 'Your text to link...' },

                {separator: '---------------' },
                {name: ls.lang.get('panel_clear_tags'), className: 'editor-clean', replaceWith: function (markitup) {
                    return markitup.selection.replace(/<(.*?)>/g, "")
                } },
                {name: ls.lang.get('panel_cut'), className: 'editor-cut', replaceWith: function (markitup) {
                    if (markitup.selection) return '<cut name="' + markitup.selection + '">'; else return '<cut>'
                }}
            ]
        };
    };

    this.presets.markitup['comment'] = function(){
        return {
            onShiftEnter:   {keepDefault: false, replaceWith: '
\n'},
            onTab:          {keepDefault: false, replaceWith: '    '},
            markupSet: [
                {name: ls.lang.get('panel_b'), className: 'editor-bold', key: 'B', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)' },
                {name: ls.lang.get('panel_i'), className: 'editor-italic', key: 'I', openWith: '(!(|!|)!)', closeWith: '(!(|!|)!)'  },
                {name: ls.lang.get('panel_s'), className: 'editor-stroke', key: 'S', openWith: '<s>', closeWith: '</s>' },
                {name: ls.lang.get('panel_u'), className: 'editor-underline', key: 'U', openWith: '', closeWith: '' },
                {separator: '---------------' },
                {name: ls.lang.get('panel_quote'), className: 'editor-quote', key: 'Q', replaceWith: function (m) {
                    if (m.selectionOuter) return '
' + m.selectionOuter + '
'; else if (m.selection) return '
' + m.selection + '
'; else return '

'
                } },
                {name: ls.lang.get('panel_code'), className: 'editor-code', openWith: '<(!(code|!|codeline)!)>', closeWith: '</(!(code|!|codeline)!)>' },
                //{name: ls.lang.get('panel_image'), className: 'editor-picture', key: 'P', beforeInsert: function (h) {
                //    jQuery('#modal-upload_img').modal();
                //} },
                {name: ls.lang.get('panel_insert_image'),  className: 'editor-insert-picture', key: 'K', beforeInsert: function (h) {
                    jQuery('#js-alto-image-manager').modal();
                } },
                {name: ls.lang.get('panel_url'), className: 'editor-link', key: 'L', openWith: '', closeWith: '', placeHolder: 'Your text to link...' },
                {name: ls.lang.get('panel_user'), className: 'editor-user', replaceWith: '<ls user="[![' + ls.lang.get('panel_user_promt') + ']!]" />' },
                {separator: '---------------' },
                {name: ls.lang.get('panel_clear_tags'), className: 'editor-clean', replaceWith: function (markitup) {
                    return markitup.selection.replace(/<(.*?)>/g, "")
                } }
            ]
        }
    };

    // *** tinimce *** //
    this.presets.tinymce = {};
    this.presets.tinymce['default'] = function(){
        return {
            selector:           '.js-editor-wysiwyg',
            theme:              'modern',
            relative_urls:      false,
            menubar:            false,
            toolbar: "undo styleselect | bold italic strikethrough underline blockquote snippets | alignleft aligncenter alignright | bullist numlist table | link unlink | altoimage media | code pagebreak ",
            toolbar_items_size: 'small',
            image_advtab: false,
            style_formats: [
                {title: ls.lang.get('panel_title_h4'), block: 'h4'},
                {title: ls.lang.get('panel_title_h5'), block: 'h5'},
                {title: ls.lang.get('panel_title_h6'), block: 'h6'}
            ],
            object_resizing:    true,
            forced_root_block:  '', // Needed for 3.x
            force_p_newlines:   true,
            custom_elements : "alto:photoset",
            self_closing_elements: "alto:photoset",
            force_br_newlines:  false,
            plugins: "advlist autolink autosave link lists media pagebreak autoresize table code altoimage snippets photoset username spoiler",
            //convert_urls: false,
            extended_valid_elements: "embed[src|type|allowscriptaccess|allowfullscreen|width|height],alto:photoset[from|to|style|position|topic],alto:user[login|style],alto:spoiler[style|title]",
            pagebreak_separator: "<cut>",
            media_strict: false,
            language: 'ru',
            inline_styles: false,
            setup: function (editor) {
                editor.on('change', function() {
                    tinyMCE.triggerSave();
                });
            },
            formats: {
                underline:      {inline: 'u', exact: true},
                strikethrough:  {inline: 's', exact: true}
            }
        }
    };

    this.presets.tinymce['comment'] = function(){
        return {
            selector:           '.js-editor-wysiwyg',
            theme:              'modern',
            relative_urls:      false,
            menubar:            false,
            toolbar: "undo redo | styleselect | bold italic strikethrough underline blockquote | alignleft aligncenter alignright | bullist numlist table | link unlink | altoimage media | code ",
            toolbar_items_size: 'small',
            image_advtab: false,
            style_formats: [
                {title: 'Head 1', block: 'h4'},
                {title: 'Head 2', block: 'h5'},
                {title: 'Head 3', block: 'h6'}
            ],
            object_resizing:    'table,img.mce-object-video,div',
            forced_root_block:  '', // Needed for 3.x
            force_p_newlines:   true,
            force_br_newlines:  false,
            plugins: "advlist autolink autosave link lists media pagebreak autoresize table code altoimage",
            extended_valid_elements: "embed[src|type|allowscriptaccess|allowfullscreen|width|height]",
            pagebreak_separator: "<cut>",
            media_strict: false,
            language: 'ru',
            inline_styles: false,
            formats: {
                underline:      {inline: 'u', exact: true},
                strikethrough:  {inline: 's', exact: true}
            },
            autoresize_min_height: '80px',
            setup: function (editor) {
                editor.on('keyup', function (e) {
                    var key = e.keyCode || e.which;
                    if (e.ctrlKey && (key == 13)) {
                        $('#comment-button-submit').click();
                    }
                    return false;
                });
            }
        }
    };

    this.getMarkitup = function() {
        return this.get('markitup');
    };

    this.getMarkitupComment = function() {
        return this.get('markitup', 'comment');
    };

    this.getTinymce = function() {
        return this.get('tinymce');
    };

    this.getTinymceComment = function() {
        return this.get('tinymce', 'comment');
    };

    return this;
}).call(ls.settings || {},jQuery);<|endoftext|>