<commit_before>'use strict';

angular.module('nnConsumerUi')

  .config(function($routeProvider) {
    $routeProvider.when('/', {
      templateUrl: 'components/main/main.html',
      controller: 'MainCtrl'
    });
  })

  // Service that handles all logic related to the main view.
  .service('mainService', function($templateRequest, $compile, $window, $timeout, $filter, $rootScope, $log, storageService, DEBUG, APP_NAME, FROM_EMAIL, apiService, kutiService) {

    this.autocomplete = autocomplete;
    this.findSlide = findSlide;
    this.findElements = findElements;
    this.validateForms = validateForms;
    this.sendSummaryMail = sendSummaryMail;
    this.print = print;

    /**
     * Loads autocomplete data via the server.
     * @param {string} keyword
     * @param {object} item
     * @param {object} notices
     */
    function autocomplete(keyword, item, notices) {
      if (angular.isDefined(item.notices)) {
        var found = false;
        angular.forEach(item.notices, function(value) {
          if (keyword.toLowerCase().indexOf(value.keyword.toLowerCase()) !== -1) {
            notices[item.id] = value;
            found = true;
          }
        });
        if (!found && angular.isDefined(notices[item.id])) {
          delete notices[item.id];
        }
      }

      return apiService.autocomplete(item.source ? item.source.replace('$keyword', encodeURIComponent(keyword)) : '')
        .then(function(response) {
          $log.info('Received autocomplete data.', response.data);
          return response.data;
        }, function(response) {
          $log.error('Failed to get autocomplete data.', response);
          return [];
        });
    }

    /**
     * Finds a slide by its name.
     * @param {array} slides
     * @param {string} name
     * @returns {object}
     */
    function findSlide(slides, name) {
      var slide = null;
      angular.forEach(slides, function(value) {
        if (value.name === name) {
          slide = value;
        }
      });
      return slide;
    }

    /**
     * Finds all elements of a specific type in the given slide.
     * @param {object} slide
     * @param {string} type
     * @returns {array}
     */
    function findElements(slide, type) {
      var elements = [];
      if (angular.isDefined(slide.elements)) {
        angular.forEach(slide.elements, function(value, key) {
          if (value.type === type) {
            elements.push(value);
          }
        });
      }
      return elements;
    }

    /**
     * Validates the given forms as a group and returns the result.
     * @param {object} forms
     * @param {object} slide
     * @param {object} model
     * @returns {boolean}
     */
    function validateForms(forms, slide, model) {
      var valid = true;
      angular.forEach(forms, function(value) {
        valid = value.$valid && valid;
      });
      return valid;
    }

    /**
     * Sends the summary email.
     * @param {object} scope
     */
    function sendSummaryMail(scope) {
      $templateRequest('components/summary/summary.html')
        .then(function(template) {
          var body = $compile('
' + template + '
')(scope);

          $timeout(function() {
            var email = '';

            angular.forEach(scope.session.model, function(slideData, slideName) {
              if (true || slideName.indexOf('yhteystiedot') !== -1) {
                angular.forEach(slideData, function(elementData, elementName) {
                  angular.forEach(elementData, function(itemData, itemName) {
                    if (itemName === 'sahkoposti') {
                      email = itemData;
                    }
                  });
                });
              }
            });

            if (angular.isUndefined(email)) {
              $log.warn('Mail not sent (no contact information)');
              return;
            }

            var config = {
              subject: APP_NAME + ': ' + $filter('translate')('SUMMARY_HEADING'),
              from_email: FROM_EMAIL,
              from_name: APP_NAME,
              to: [{ email: email, type: 'to' }],
              html: body.html(),
              text: body.text()
            };
            apiService.sendMail(config)
              .then(function(response) {
                $log.debug('Mail sent', config, scope.session.model);
              });
          });
        }, function(error) {
          $log.error('Failed to get the summary template', error);
        });
    }

    function print() {
      $window.print();
    }
  })

  // Controller that connects the necessary services to the main view.
  .controller('MainCtrl', function($scope, $q, $timeout, $document, $window, $interval, $log, $modal, $translate, DEBUG, ENVIRONMENT, sessionService, slideService, InfoService, mainService, apiService, languageService) {

    var firstSlide = 'etusivu';
    var loadDelay = 350;
    var scrollDelay = 150;
    var pagerElement = angular.element('.slide-pager');

    $scope.loading = false;
    $scope.showSummary = false;
    $scope.session = sessionService.load();
    $scope.slides = [];
    $scope.forms = {};
    $scope.service = mainService;
    $scope.infoService = InfoService;
    $scope.debug = DEBUG;
    $scope.notices = {};
    $scope.activeLanguage = $scope.session.language || 'fi';
    $scope.languages = [];
    $scope.showSentMessage = false;

    /**
     * Scrolls to a specific slide.
     * @param {string} name
     */
    function scrollToElement(name) {
      $timeout(function() {
        var element = angular.element('#' + name);
        if (element.length) {
          $document.scrollToElementAnimated(element);
        }
      }, scrollDelay);
    }

    /**
     * Adjusts the pager's position so that it's always vertically centered.
     */
    function centerPager() {
      pagerElement.css({ marginTop: -(pagerElement.height() / 2) });
    }

    /**
     * Loads a slide from the slide service.
     * @param {string} name
     * @param {number} index
     */
    function loadSlide(name, index) {
      if ($scope.loading) {
        return;
      }
      if (!name) {
        return;
      }
      var current = $scope.slides[index];
      if (current) {
        // Handle multiple-choice as a special case (could probably be refactored).
        var choices = mainService.findElements(current, 'choice');
        if (choices.length && angular.isDefined($scope.session.model) && angular.isDefined($scope.session.model[current.name])) {
          var choiceNextSlide = getNextSlideFromChoices(choices, $scope.session.model[current.name]);
          if (choiceNextSlide) {
            $log.debug('Changed next slide to', choiceNextSlide);
            name = choiceNextSlide;
          }
        }
      }
      // Make sure that the model only contains the correct slides (used when the route is changed).
      var newModel = {};
      angular.forEach($scope.session.model, function(value, key) {
        if (angular.isDefined(value.index) && value.index <= index) {
          newModel[key] = value;
        }
      });
      $scope.session.model = newModel;
      $scope.slides.splice(index + 1);
      // var slide = mainService.findSlide($scope.slides, name);
      // if (slide !== null) {
      //   $log.debug('Slide exists, scrolling to element', name);
      //   scrollToElement(slide.name);
      //   return;
      // }
      var next = slideService.getByName(name);
      if (!next) {
        $log.error('Failed to find slide', name);
        return;
      }
      if (current) {
        if (current.save_after) {
          sessionService.saveRemote($scope.session);
        }
      }
      $scope.loading = true;
      $timeout(function() {
        $scope.loading = false;
        $log.debug('Loaded slide', next);
        var elements = mainService.findElements(next, 'next');
        if (elements.length) {
          // Ensure that the current slides next element points to the next slide.
          if (current) {
            current.next_slide = next.name;
          }
          // Point the next slide to the 'summary' slide if the 'summary_after' flag is set.
          if (next.summary_after) {
            elements[0].next_slide = 'summary';
          }
        }
        // Copy existing answers from the session if there is any.
        if (angular.isDefined($scope.session.answers[name])) {
          $scope.session.model[name] = angular.copy($scope.session.answers[name]);
        } else {
          $scope.session.model[name] = {};
        }
        // Copy the current model answers to another collection for save keeping purposes.
        if (current && angular.isDefined($scope.session.model[current.name])) {
          $scope.session.answers[current.name] = angular.copy($scope.session.model[current.name]);
        }
        $scope.session.model[next.name].index = index + 1;
        $scope.slides.push(next);
        centerPager();
        scrollToElement(next.name);
      }, loadDelay);
    }

    /**
     * Returns the name of the next slide from the given set of choices.
     *
     * @param {array} choices
     * @param {object} model
     * @returns {string|null}
     */
    function getNextSlideFromChoices(choices, model) {
      var nextSlide = null;
      angular.forEach(choices, function(choice) {
        if (choice.multiple) {
          angular.forEach(choice.items, function(item) {
            if (item.next_slide && angular.isDefined(model[choice.name][item.name]) && model[choice.name][item.name]) {
              nextSlide = item.next_slide;
            }
          });
        }
      });
      return nextSlide;
    }

    /**
     * Initializes the main view by loading the first slide.
     */
    function init() {
      $scope.loading = true;

      // Create the session if not created already.
      if (angular.isUndefined($scope.session.id)) {
        createSession();
      }

      // Load the slides
      slideService.load()
        .then(function() {
          $scope.loading = false;
          loadSlide(firstSlide, -1);
        });

      // Get languages.
      apiService.getLanguages()
        .then(function(response) {
          $scope.languages = response.data;
        });

      // Setup a watcher for session.
      $scope.$watch('session', function(value) {
        if (!value) {
          return;
        }
        sessionService.saveLocal(value);
      }, true/* objectEquality */);

      changeLanguage($scope.session.language);
    }

    /**
     * Creates the user session.
     */
    function createSession() {
      sessionService.init()
        .then(function(session) {
          $scope.session = session;
        }, function() {
          $log.error('Failed to create session.');
        });
    }

    /**
     * Changes the active language.
     *
     * @param {string} language
     */
    function changeLanguage(language) {
      $scope.activeLanguage = $scope.session.language = language;
      $translate.use(language);
    }

    /**
     * Changes the next slide for the given slide.
     *
     * @param {string} name
     * @param {object} slide
     */
    $scope.changeNextSlide = function(name, slide) {
      var elements = mainService.findElements(slide, 'next');
      if (elements.length) {
        elements[0].next_slide = name;
      }
    };

    /**
     * Clears the model for debugging purposes.
     */
    $scope.clear = function() {
      delete $scope.session;
      sessionService.clear();
      createSession();
    };

    /**
     * Saves the session to remote location and then sends summary mail.
     */
    $scope.sendMail = function() {
      $scope.showSentMessage = true;

      sessionService.saveRemote($scope.session)
        .then(function() {
          mainService.sendSummaryMail($scope);

          $timeout(function() {
            $scope.showSentMessage = false;
          }, 5000);
        });
    };

    /**
     * Translates the given item to the active language.
     *
     * @param {object} item
     * @returns {string}
     */
    $scope.translate = function(item) {
      return languageService.translate(item, $scope.activeLanguage);
    };

    /**
     * Calculates the characters remaining for a text area.
     *
     * @param {string} string
     * @param {number} maxLength
     * @returns {number}
     */
    $scope.calculateCharactersRemaining = function(string, maxLength) {
      if (angular.isUndefined(string)) {
        return maxLength;
      }

      return string.length < maxLength ? maxLength - string.length : 0;
    };

    /**
     * Called when a file is uploaded.
     *
     * @param {object} file
     * @param {object} session
     * @param {object} slide
     * @param {object} element
     * @param {object} item
     */
    $scope.uploadFile = function(file, session, slide, element, item) {
      apiService.uploadFile(file)
        .then(function(response) {
          session.model[slide.name][element.name][item.name] = response.data;
        });
    };

    /**
     * Called when a multiple choice is toggled.
     *
     * @param {object} slide
     * @param {object} element
     * @param {object} item
     * @param {number} slide_idx
     */
    $scope.singleChoiceToggled = function(slide, element, item, slide_idx) {
      if (angular.isDefined(element.items)) {
        angular.forEach(element.items, function(choice) {
          if (choice.next_slide) {
            delete $scope.session.model[choice.next_slide];
            removeSlide(choice.next_slide);
          }
        });

        if (item.next_slide) {
          loadSlide(item.next_slide, slide_idx);
        }
      }
    };

    /**
     *
     * @param {object} slide
     * @param {object} element
     * @param {object} item
     */
    $scope.multipleChoiceToggled = function(slide, element, item) {
      if (angular.isDefined($scope.session.model[slide.name][element.name][item.name])) {
        if ($scope.session.model[slide.name][element.name][item.name]) {
          delete $scope.session.model[item.next_slide];
          removeSlide(item.next_slide);
        }
      }
    };

    /**
     *
     * @param {string} name
     */
    function removeSlide(name) {
      var slideIndex = null;
      angular.forEach($scope.slides, function(value, index) {
        if (value.name === name) {
          slideIndex = index;
        }
      });
      if (slideIndex) {
        $scope.slides.splice(slideIndex, 1);
      }
    }

    $scope.scrollToElement = scrollToElement;
    $scope.loadSlide = loadSlide;
    $scope.changeLanguage = changeLanguage;

    init();

  });
<commit_msg>Improve logic for toggling a single slide<commit_after>'use strict';

angular.module('nnConsumerUi')

  .config(function($routeProvider) {
    $routeProvider.when('/', {
      templateUrl: 'components/main/main.html',
      controller: 'MainCtrl'
    });
  })

  // Service that handles all logic related to the main view.
  .service('mainService', function($templateRequest, $compile, $window, $timeout, $filter, $rootScope, $log, storageService, DEBUG, APP_NAME, FROM_EMAIL, apiService, kutiService) {

    this.autocomplete = autocomplete;
    this.findSlide = findSlide;
    this.findElements = findElements;
    this.validateForms = validateForms;
    this.sendSummaryMail = sendSummaryMail;
    this.print = print;

    /**
     * Loads autocomplete data via the server.
     * @param {string} keyword
     * @param {object} item
     * @param {object} notices
     */
    function autocomplete(keyword, item, notices) {
      if (angular.isDefined(item.notices)) {
        var found = false;
        angular.forEach(item.notices, function(value) {
          if (keyword.toLowerCase().indexOf(value.keyword.toLowerCase()) !== -1) {
            notices[item.id] = value;
            found = true;
          }
        });
        if (!found && angular.isDefined(notices[item.id])) {
          delete notices[item.id];
        }
      }

      return apiService.autocomplete(item.source ? item.source.replace('$keyword', encodeURIComponent(keyword)) : '')
        .then(function(response) {
          $log.info('Received autocomplete data.', response.data);
          return response.data;
        }, function(response) {
          $log.error('Failed to get autocomplete data.', response);
          return [];
        });
    }

    /**
     * Finds a slide by its name.
     * @param {array} slides
     * @param {string} name
     * @returns {object}
     */
    function findSlide(slides, name) {
      var slide = null;
      angular.forEach(slides, function(value) {
        if (value.name === name) {
          slide = value;
        }
      });
      return slide;
    }

    /**
     * Finds all elements of a specific type in the given slide.
     * @param {object} slide
     * @param {string} type
     * @returns {array}
     */
    function findElements(slide, type) {
      var elements = [];
      if (angular.isDefined(slide.elements)) {
        angular.forEach(slide.elements, function(value, key) {
          if (value.type === type) {
            elements.push(value);
          }
        });
      }
      return elements;
    }

    /**
     * Validates the given forms as a group and returns the result.
     * @param {object} forms
     * @param {object} slide
     * @param {object} model
     * @returns {boolean}
     */
    function validateForms(forms, slide, model) {
      var valid = true;
      angular.forEach(forms, function(value) {
        valid = value.$valid && valid;
      });
      return valid;
    }

    /**
     * Sends the summary email.
     * @param {object} scope
     */
    function sendSummaryMail(scope) {
      $templateRequest('components/summary/summary.html')
        .then(function(template) {
          var body = $compile('
' + template + '
')(scope);

          $timeout(function() {
            var email = '';

            angular.forEach(scope.session.model, function(slideData, slideName) {
              if (true || slideName.indexOf('yhteystiedot') !== -1) {
                angular.forEach(slideData, function(elementData, elementName) {
                  angular.forEach(elementData, function(itemData, itemName) {
                    if (itemName === 'sahkoposti') {
                      email = itemData;
                    }
                  });
                });
              }
            });

            if (angular.isUndefined(email)) {
              $log.warn('Mail not sent (no contact information)');
              return;
            }

            var config = {
              subject: APP_NAME + ': ' + $filter('translate')('SUMMARY_HEADING'),
              from_email: FROM_EMAIL,
              from_name: APP_NAME,
              to: [{ email: email, type: 'to' }],
              html: body.html(),
              text: body.text()
            };
            apiService.sendMail(config)
              .then(function(response) {
                $log.debug('Mail sent', config, scope.session.model);
              });
          });
        }, function(error) {
          $log.error('Failed to get the summary template', error);
        });
    }

    function print() {
      $window.print();
    }
  })

  // Controller that connects the necessary services to the main view.
  .controller('MainCtrl', function($scope, $q, $timeout, $document, $window, $interval, $log, $modal, $translate, DEBUG, ENVIRONMENT, sessionService, slideService, InfoService, mainService, apiService, languageService) {

    var firstSlide = 'etusivu';
    var loadDelay = 350;
    var scrollDelay = 150;
    var pagerElement = angular.element('.slide-pager');

    $scope.loading = false;
    $scope.showSummary = false;
    $scope.session = sessionService.load();
    $scope.slides = [];
    $scope.forms = {};
    $scope.service = mainService;
    $scope.infoService = InfoService;
    $scope.debug = DEBUG;
    $scope.notices = {};
    $scope.activeLanguage = $scope.session.language || 'fi';
    $scope.languages = [];
    $scope.showSentMessage = false;

    /**
     * Scrolls to a specific slide.
     * @param {string} name
     */
    function scrollToElement(name) {
      $timeout(function() {
        var element = angular.element('#' + name);
        if (element.length) {
          $document.scrollToElementAnimated(element);
        }
      }, scrollDelay);
    }

    /**
     * Adjusts the pager's position so that it's always vertically centered.
     */
    function centerPager() {
      pagerElement.css({ marginTop: -(pagerElement.height() / 2) });
    }

    /**
     * Loads a slide from the slide service.
     * @param {string} name
     * @param {number} index
     */
    function loadSlide(name, index) {
      if ($scope.loading) {
        return;
      }
      if (!name) {
        return;
      }
      var current = $scope.slides[index];
      if (current) {
        // Handle multiple-choice as a special case (could probably be refactored).
        var choices = mainService.findElements(current, 'choice');
        if (choices.length && angular.isDefined($scope.session.model) && angular.isDefined($scope.session.model[current.name])) {
          var choiceNextSlide = getNextSlideFromChoices(choices, $scope.session.model[current.name]);
          if (choiceNextSlide) {
            $log.debug('Changed next slide to', choiceNextSlide);
            name = choiceNextSlide;
          }
        }
      }
      // Make sure that the model only contains the correct slides (used when the route is changed).
      var newModel = {};
      angular.forEach($scope.session.model, function(value, key) {
        if (angular.isDefined(value.index) && value.index <= index) {
          newModel[key] = value;
        }
      });
      $scope.session.model = newModel;
      $scope.slides.splice(index + 1);
      // var slide = mainService.findSlide($scope.slides, name);
      // if (slide !== null) {
      //   $log.debug('Slide exists, scrolling to element', name);
      //   scrollToElement(slide.name);
      //   return;
      // }
      var next = slideService.getByName(name);
      if (!next) {
        $log.error('Failed to find slide', name);
        return;
      }
      if (current) {
        if (current.save_after) {
          sessionService.saveRemote($scope.session);
        }
      }
      $scope.loading = true;
      $timeout(function() {
        $scope.loading = false;
        $log.debug('Loaded slide', next);
        var elements = mainService.findElements(next, 'next');
        if (elements.length) {
          // Ensure that the current slides next element points to the next slide.
          if (current) {
            current.next_slide = next.name;
          }
          // Point the next slide to the 'summary' slide if the 'summary_after' flag is set.
          if (next.summary_after) {
            elements[0].next_slide = 'summary';
          }
        }
        // Copy existing answers from the session if there is any.
        if (angular.isDefined($scope.session.answers[name])) {
          $scope.session.model[name] = angular.copy($scope.session.answers[name]);
        } else {
          $scope.session.model[name] = {};
        }
        // Copy the current model answers to another collection for save keeping purposes.
        if (current && angular.isDefined($scope.session.model[current.name])) {
          $scope.session.answers[current.name] = angular.copy($scope.session.model[current.name]);
        }
        $scope.session.model[next.name].index = index + 1;
        $scope.slides.push(next);
        centerPager();
        scrollToElement(next.name);
      }, loadDelay);
    }

    /**
     * Returns the name of the next slide from the given set of choices.
     *
     * @param {array} choices
     * @param {object} model
     * @returns {string|null}
     */
    function getNextSlideFromChoices(choices, model) {
      var nextSlide = null;
      angular.forEach(choices, function(choice) {
        if (choice.multiple) {
          angular.forEach(choice.items, function(item) {
            if (item.next_slide && angular.isDefined(model[choice.name][item.name]) && model[choice.name][item.name]) {
              nextSlide = item.next_slide;
            }
          });
        }
      });
      return nextSlide;
    }

    /**
     * Initializes the main view by loading the first slide.
     */
    function init() {
      $scope.loading = true;

      // Create the session if not created already.
      if (angular.isUndefined($scope.session.id)) {
        createSession();
      }

      // Load the slides
      slideService.load()
        .then(function() {
          $scope.loading = false;
          loadSlide(firstSlide, -1);
        });

      // Get languages.
      apiService.getLanguages()
        .then(function(response) {
          $scope.languages = response.data;
        });

      // Setup a watcher for session.
      $scope.$watch('session', function(value) {
        if (!value) {
          return;
        }
        sessionService.saveLocal(value);
      }, true/* objectEquality */);

      changeLanguage($scope.session.language);
    }

    /**
     * Creates the user session.
     */
    function createSession() {
      sessionService.init()
        .then(function(session) {
          $scope.session = session;
        }, function() {
          $log.error('Failed to create session.');
        });
    }

    /**
     * Changes the active language.
     *
     * @param {string} language
     */
    function changeLanguage(language) {
      $scope.activeLanguage = $scope.session.language = language;
      $translate.use(language);
    }

    /**
     * Changes the next slide for the given slide.
     *
     * @param {string} name
     * @param {object} slide
     */
    $scope.changeNextSlide = function(name, slide) {
      var elements = mainService.findElements(slide, 'next');
      if (elements.length) {
        elements[0].next_slide = name;
      }
    };

    /**
     * Clears the model for debugging purposes.
     */
    $scope.clear = function() {
      delete $scope.session;
      sessionService.clear();
      createSession();
    };

    /**
     * Saves the session to remote location and then sends summary mail.
     */
    $scope.sendMail = function() {
      $scope.showSentMessage = true;

      sessionService.saveRemote($scope.session)
        .then(function() {
          mainService.sendSummaryMail($scope);

          $timeout(function() {
            $scope.showSentMessage = false;
          }, 5000);
        });
    };

    /**
     * Translates the given item to the active language.
     *
     * @param {object} item
     * @returns {string}
     */
    $scope.translate = function(item) {
      return languageService.translate(item, $scope.activeLanguage);
    };

    /**
     * Calculates the characters remaining for a text area.
     *
     * @param {string} string
     * @param {number} maxLength
     * @returns {number}
     */
    $scope.calculateCharactersRemaining = function(string, maxLength) {
      if (angular.isUndefined(string)) {
        return maxLength;
      }

      return string.length < maxLength ? maxLength - string.length : 0;
    };

    /**
     * Called when a file is uploaded.
     *
     * @param {object} file
     * @param {object} session
     * @param {object} slide
     * @param {object} element
     * @param {object} item
     */
    $scope.uploadFile = function(file, session, slide, element, item) {
      apiService.uploadFile(file)
        .then(function(response) {
          session.model[slide.name][element.name][item.name] = response.data;
        });
    };

    /**
     * Called when a multiple choice is toggled.
     *
     * @param {object} slide
     * @param {object} element
     * @param {object} item
     * @param {number} slide_idx
     */
    $scope.singleChoiceToggled = function(slide, element, item, slide_idx) {
      if (angular.isDefined(element.items)) {
        angular.forEach(element.items, function(choice) {
          if (choice.name !== item.name && choice.next_slide) {
            delete $scope.session.model[choice.next_slide];
            removeSlide(choice.next_slide);
          }
        });

        if (item.next_slide) {
          loadSlide(item.next_slide, slide_idx);
        }
      }
    };

    /**
     *
     * @param {object} slide
     * @param {object} element
     * @param {object} item
     */
    $scope.multipleChoiceToggled = function(slide, element, item) {
      if (angular.isDefined($scope.session.model[slide.name][element.name][item.name])) {
        if ($scope.session.model[slide.name][element.name][item.name]) {
          delete $scope.session.model[item.next_slide];
          removeSlide(item.next_slide);
        }
      }
    };

    /**
     *
     * @param {string} name
     */
    function removeSlide(name) {
      var slideIndex = null;
      angular.forEach($scope.slides, function(value, index) {
        if (value.name === name) {
          slideIndex = index;
        }
      });
      if (slideIndex) {
        $scope.slides.splice(slideIndex, 1);
      }
    }

    $scope.scrollToElement = scrollToElement;
    $scope.loadSlide = loadSlide;
    $scope.changeLanguage = changeLanguage;

    init();

  });
<|endoftext|>


<commit_before>/*
  jQuery Colorbox language configuration
  language: Japanaese (ja)
  translated by: Hajime Fujimoto
*/
jQuery.extend(jQuery.colorbox.settings, {
	current: "{total}枚中{current}枚目",
	previous: "前",
	next: "次",
	close: "閉じる",
	xhrError: "コンテンツの読み込みに失敗しました",
	imgError: "画像の読み込みに失敗しました",
	slideshowStart: "スライドショー開始",
	slideshowStop: "スライドショー終了"
});
<commit_msg>Deleted jquery.colorbox-ja.js<commit_after><|endoftext|>


<commit_before>/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactFiberBeginWork
 * @flow
 */

'use strict';

import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';
import type { PriorityLevel } from 'ReactPriorityLevel';
import ReactCurrentOwner from 'ReactCurrentOwner';

var {
  mountChildFibersInPlace,
  reconcileChildFibers,
  reconcileChildFibersInPlace,
  cloneChildFibers,
} = require('ReactChildFiber');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
  IndeterminateComponent,
  FunctionalComponent,
  ClassComponent,
  HostContainer,
  HostComponent,
  HostText,
  CoroutineComponent,
  CoroutineHandlerPhase,
  YieldComponent,
  Fragment,
} = ReactTypeOfWork;
var {
  NoWork,
  OffscreenPriority,
} = require('ReactPriorityLevel');
var {
  Placement,
} = require('ReactTypeOfSideEffect');
var ReactFiberClassComponent = require('ReactFiberClassComponent');

module.exports = function<T, P, I, TI, C>(
  config : HostConfig<T, P, I, TI, C>,
  scheduleUpdate : (fiber: Fiber) => void
) {

  const {
    adoptClassInstance,
    constructClassInstance,
    mountClassInstance,
    resumeMountClassInstance,
    updateClassInstance,
  } = ReactFiberClassComponent(scheduleUpdate);

  function markChildAsProgressed(current, workInProgress, priorityLevel) {
    // We now have clones. Let's store them as the currently progressed work.
    workInProgress.progressedChild = workInProgress.child;
    workInProgress.progressedPriority = priorityLevel;
    if (current) {
      // We also store it on the current. When the alternate swaps in we can
      // continue from this point.
      current.progressedChild = workInProgress.progressedChild;
      current.progressedPriority = workInProgress.progressedPriority;
    }
  }

  function clearDeletions(workInProgress) {
    workInProgress.progressedFirstDeletion =
      workInProgress.progressedLastDeletion =
        null;
  }

  function transferDeletions(workInProgress) {
    // Any deletions get added first into the effect list.
    workInProgress.firstEffect = workInProgress.progressedFirstDeletion;
    workInProgress.lastEffect = workInProgress.progressedLastDeletion;
  }

  function reconcileChildren(current, workInProgress, nextChildren) {
    const priorityLevel = workInProgress.pendingWorkPriority;
    reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel);
  }

  function reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel) {
    // At this point any memoization is no longer valid since we'll have changed
    // the children.
    workInProgress.memoizedProps = null;
    if (!current) {
      // If this is a fresh new component that hasn't been rendered yet, we
      // won't update its child set by applying minimal side-effects. Instead,
      // we will add them all to the child before it gets rendered. That means
      // we can optimize this reconciliation pass by not tracking side-effects.
      workInProgress.child = mountChildFibersInPlace(
        workInProgress,
        workInProgress.child,
        nextChildren,
        priorityLevel
      );
    } else if (current.child === workInProgress.child) {
      // If the current child is the same as the work in progress, it means that
      // we haven't yet started any work on these children. Therefore, we use
      // the clone algorithm to create a copy of all the current children.

      // If we had any progressed work already, that is invalid at this point so
      // let's throw it out.
      clearDeletions(workInProgress);

      workInProgress.child = reconcileChildFibers(
        workInProgress,
        workInProgress.child,
        nextChildren,
        priorityLevel
      );

      transferDeletions(workInProgress);
    } else {
      // If, on the other hand, it is already using a clone, that means we've
      // already begun some work on this tree and we can continue where we left
      // off by reconciling against the existing children.
      workInProgress.child = reconcileChildFibersInPlace(
        workInProgress,
        workInProgress.child,
        nextChildren,
        priorityLevel
      );

      transferDeletions(workInProgress);
    }
    markChildAsProgressed(current, workInProgress, priorityLevel);
  }

  function updateFragment(current, workInProgress) {
    var nextChildren = workInProgress.pendingProps;
    reconcileChildren(current, workInProgress, nextChildren);
  }

  function updateFunctionalComponent(current, workInProgress) {
    var fn = workInProgress.type;
    var props = workInProgress.pendingProps;

    // TODO: Disable this before release, since it is not part of the public API
    // I use this for testing to compare the relative overhead of classes.
    if (typeof fn.shouldComponentUpdate === 'function') {
      if (workInProgress.memoizedProps !== null) {
        if (!fn.shouldComponentUpdate(workInProgress.memoizedProps, props)) {
          return bailoutOnAlreadyFinishedWork(current, workInProgress);
        }
      }
    }

    var nextChildren;

    if (__DEV__) {
      ReactCurrentOwner.current = workInProgress;
      nextChildren = fn(props);
    } else {
      nextChildren = fn(props);
    }
    reconcileChildren(current, workInProgress, nextChildren);
    return workInProgress.child;
  }

  function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
    let shouldUpdate;
    if (!current) {
      if (!workInProgress.stateNode) {
        // In the initial pass we might need to construct the instance.
        constructClassInstance(workInProgress);
        mountClassInstance(workInProgress);
        shouldUpdate = true;
      } else {
        // In a resume, we'll already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(workInProgress);
      }
    } else {
      shouldUpdate = updateClassInstance(current, workInProgress);
    }
    if (!shouldUpdate) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress);
    }
    // Rerender
    const instance = workInProgress.stateNode;
    ReactCurrentOwner.current = workInProgress;
    const nextChildren = instance.render();
    reconcileChildren(current, workInProgress, nextChildren);
    return workInProgress.child;
  }

  function updateHostComponent(current, workInProgress) {
    let nextChildren = workInProgress.pendingProps.children;
    if (typeof nextChildren === 'string' || typeof nextChildren === 'number') {
      // We special case a direct text child of a host node. This is a common
      // case. We won't handle it as a reified child. We will instead handle
      // this in the host environment that also have access to this prop. That
      // avoids allocating another HostText fiber and traversing it.
      nextChildren = null;
    }
    if (workInProgress.pendingProps.hidden &&
        workInProgress.pendingWorkPriority !== OffscreenPriority) {
      // If this host component is hidden, we can bail out on the children.
      // We'll rerender the children later at the lower priority.

      // It is unfortunate that we have to do the reconciliation of these
      // children already since that will add them to the tree even though
      // they are not actually done yet. If this is a large set it is also
      // confusing that this takes time to do right now instead of later.

      if (workInProgress.progressedPriority === OffscreenPriority) {
        // If we already made some progress on the offscreen priority before,
        // then we should continue from where we left off.
        workInProgress.child = workInProgress.progressedChild;
      }

      // Reconcile the children and stash them for later work.
      reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority);
      workInProgress.child = current ? current.child : null;

      if (!current) {
        // If this doesn't have a current we won't track it for placement
        // effects. However, when we come back around to this we have already
        // inserted the parent which means that we'll infact need to make this a
        // placement.
        // TODO: There has to be a better solution to this problem.
        let child = workInProgress.progressedChild;
        while (child) {
          child.effectTag = Placement;
          child = child.sibling;
        }
      }

      // Abort and don't process children yet.
      return null;
    } else {
      reconcileChildren(current, workInProgress, nextChildren);
      return workInProgress.child;
    }
  }

  function mountIndeterminateComponent(current, workInProgress) {
    if (current) {
      throw new Error('An indeterminate component should never have mounted.');
    }
    var fn = workInProgress.type;
    var props = workInProgress.pendingProps;
    var value;

    if (__DEV__) {
      ReactCurrentOwner.current = workInProgress;
      value = fn(props);
    } else {
      value = fn(props);
    }

    if (typeof value === 'object' && value && typeof value.render === 'function') {
      // Proceed under the assumption that this is a class instance
      workInProgress.tag = ClassComponent;
      adoptClassInstance(workInProgress, value);
      mountClassInstance(workInProgress);
      ReactCurrentOwner.current = workInProgress;
      value = value.render();
    } else {
      // Proceed under the assumption that this is a functional component
      workInProgress.tag = FunctionalComponent;
    }
    reconcileChildren(current, workInProgress, value);
    return workInProgress.child;
  }

  function updateCoroutineComponent(current, workInProgress) {
    var coroutine = (workInProgress.pendingProps : ?ReactCoroutine);
    if (!coroutine) {
      throw new Error('Should be resolved by now');
    }
    reconcileChildren(current, workInProgress, coroutine.children);
  }

  /*
  function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
    let child = firstChild;
    do {
      // Ensure that the first and last effect of the parent corresponds
      // to the children's first and last effect.
      if (!returnFiber.firstEffect) {
        returnFiber.firstEffect = child.firstEffect;
      }
      if (child.lastEffect) {
        if (returnFiber.lastEffect) {
          returnFiber.lastEffect.nextEffect = child.firstEffect;
        }
        returnFiber.lastEffect = child.lastEffect;
      }
    } while (child = child.sibling);
  }
  */

  function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber {
    const priorityLevel = workInProgress.pendingWorkPriority;

    if (workInProgress.tag === HostComponent &&
        workInProgress.memoizedProps.hidden &&
        workInProgress.pendingWorkPriority !== OffscreenPriority) {
      // This subtree still has work, but it should be deprioritized so we need
      // to bail out and not do any work yet.
      // TODO: It would be better if this tree got its correct priority set
      // during scheduleUpdate instead because otherwise we'll start a higher
      // priority reconciliation first before we can get down here. However,
      // that is a bit tricky since workInProgress and current can have
      // different "hidden" settings.
      let child = workInProgress.progressedChild;
      while (child) {
        // To ensure that this subtree gets its priority reset, the children
        // need to be reset.
        child.pendingWorkPriority = OffscreenPriority;
        child = child.sibling;
      }
      return null;
    }

    // TODO: We should ideally be able to bail out early if the children have no
    // more work to do. However, since we don't have a separation of this
    // Fiber's priority and its children yet - we don't know without doing lots
    // of the same work we do anyway. Once we have that separation we can just
    // bail out here if the children has no more work at this priority level.
    // if (workInProgress.priorityOfChildren <= priorityLevel) {
    //   // If there are side-effects in these children that have not yet been
    //   // committed we need to ensure that they get properly transferred up.
    //   if (current && current.child !== workInProgress.child) {
    //     reuseChildrenEffects(workInProgress, child);
    //   }
    //   return null;
    // }

    if (current && workInProgress.child === current.child) {
      // If we had any progressed work already, that is invalid at this point so
      // let's throw it out.
      clearDeletions(workInProgress);
    }

    cloneChildFibers(current, workInProgress);
    markChildAsProgressed(current, workInProgress, priorityLevel);
    return workInProgress.child;
  }

  function bailoutOnLowPriority(current, workInProgress) {
    // TODO: What if this is currently in progress?
    // How can that happen? How is this not being cloned?
    return null;
  }

  function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
    if (workInProgress.pendingWorkPriority === NoWork ||
        workInProgress.pendingWorkPriority > priorityLevel) {
      return bailoutOnLowPriority(current, workInProgress);
    }

    // If we don't bail out, we're going be recomputing our children so we need
    // to drop our effect list.
    workInProgress.firstEffect = null;
    workInProgress.lastEffect = null;

    if (workInProgress.progressedPriority === priorityLevel) {
      // If we have progressed work on this priority level already, we can
      // proceed this that as the child.
      workInProgress.child = workInProgress.progressedChild;
    }

    if ((workInProgress.pendingProps === null || (
      workInProgress.memoizedProps !== null &&
      workInProgress.pendingProps === workInProgress.memoizedProps
      )) &&
      workInProgress.updateQueue === null) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress);
    }

    switch (workInProgress.tag) {
      case IndeterminateComponent:
        return mountIndeterminateComponent(current, workInProgress);
      case FunctionalComponent:
        return updateFunctionalComponent(current, workInProgress);
      case ClassComponent:
        return updateClassComponent(current, workInProgress);
      case HostContainer:
        reconcileChildren(current, workInProgress, workInProgress.pendingProps);
        // A yield component is just a placeholder, we can just run through the
        // next one immediately.
        return workInProgress.child;
      case HostComponent:
        if (workInProgress.stateNode && typeof config.beginUpdate === 'function') {
          config.beginUpdate(workInProgress.stateNode);
        }
        return updateHostComponent(current, workInProgress);
      case HostText:
        // Nothing to do here. This is terminal. We'll do the completion step
        // immediately after.
        return null;
      case CoroutineHandlerPhase:
        // This is a restart. Reset the tag to the initial phase.
        workInProgress.tag = CoroutineComponent;
        // Intentionally fall through since this is now the same.
      case CoroutineComponent:
        updateCoroutineComponent(current, workInProgress);
        // This doesn't take arbitrary time so we could synchronously just begin
        // eagerly do the work of workInProgress.child as an optimization.
        return workInProgress.child;
      case YieldComponent:
        // A yield component is just a placeholder, we can just run through the
        // next one immediately.
        return null;
      case Fragment:
        updateFragment(current, workInProgress);
        return workInProgress.child;
      default:
        throw new Error('Unknown unit of work tag');
    }
  }

  return {
    beginWork,
  };

};
<commit_msg>ES import -> require (#8185)<commit_after>/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactFiberBeginWork
 * @flow
 */

'use strict';

import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';
import type { PriorityLevel } from 'ReactPriorityLevel';

var {
  mountChildFibersInPlace,
  reconcileChildFibers,
  reconcileChildFibersInPlace,
  cloneChildFibers,
} = require('ReactChildFiber');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
  IndeterminateComponent,
  FunctionalComponent,
  ClassComponent,
  HostContainer,
  HostComponent,
  HostText,
  CoroutineComponent,
  CoroutineHandlerPhase,
  YieldComponent,
  Fragment,
} = ReactTypeOfWork;
var {
  NoWork,
  OffscreenPriority,
} = require('ReactPriorityLevel');
var {
  Placement,
} = require('ReactTypeOfSideEffect');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactFiberClassComponent = require('ReactFiberClassComponent');

module.exports = function<T, P, I, TI, C>(
  config : HostConfig<T, P, I, TI, C>,
  scheduleUpdate : (fiber: Fiber) => void
) {

  const {
    adoptClassInstance,
    constructClassInstance,
    mountClassInstance,
    resumeMountClassInstance,
    updateClassInstance,
  } = ReactFiberClassComponent(scheduleUpdate);

  function markChildAsProgressed(current, workInProgress, priorityLevel) {
    // We now have clones. Let's store them as the currently progressed work.
    workInProgress.progressedChild = workInProgress.child;
    workInProgress.progressedPriority = priorityLevel;
    if (current) {
      // We also store it on the current. When the alternate swaps in we can
      // continue from this point.
      current.progressedChild = workInProgress.progressedChild;
      current.progressedPriority = workInProgress.progressedPriority;
    }
  }

  function clearDeletions(workInProgress) {
    workInProgress.progressedFirstDeletion =
      workInProgress.progressedLastDeletion =
        null;
  }

  function transferDeletions(workInProgress) {
    // Any deletions get added first into the effect list.
    workInProgress.firstEffect = workInProgress.progressedFirstDeletion;
    workInProgress.lastEffect = workInProgress.progressedLastDeletion;
  }

  function reconcileChildren(current, workInProgress, nextChildren) {
    const priorityLevel = workInProgress.pendingWorkPriority;
    reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel);
  }

  function reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel) {
    // At this point any memoization is no longer valid since we'll have changed
    // the children.
    workInProgress.memoizedProps = null;
    if (!current) {
      // If this is a fresh new component that hasn't been rendered yet, we
      // won't update its child set by applying minimal side-effects. Instead,
      // we will add them all to the child before it gets rendered. That means
      // we can optimize this reconciliation pass by not tracking side-effects.
      workInProgress.child = mountChildFibersInPlace(
        workInProgress,
        workInProgress.child,
        nextChildren,
        priorityLevel
      );
    } else if (current.child === workInProgress.child) {
      // If the current child is the same as the work in progress, it means that
      // we haven't yet started any work on these children. Therefore, we use
      // the clone algorithm to create a copy of all the current children.

      // If we had any progressed work already, that is invalid at this point so
      // let's throw it out.
      clearDeletions(workInProgress);

      workInProgress.child = reconcileChildFibers(
        workInProgress,
        workInProgress.child,
        nextChildren,
        priorityLevel
      );

      transferDeletions(workInProgress);
    } else {
      // If, on the other hand, it is already using a clone, that means we've
      // already begun some work on this tree and we can continue where we left
      // off by reconciling against the existing children.
      workInProgress.child = reconcileChildFibersInPlace(
        workInProgress,
        workInProgress.child,
        nextChildren,
        priorityLevel
      );

      transferDeletions(workInProgress);
    }
    markChildAsProgressed(current, workInProgress, priorityLevel);
  }

  function updateFragment(current, workInProgress) {
    var nextChildren = workInProgress.pendingProps;
    reconcileChildren(current, workInProgress, nextChildren);
  }

  function updateFunctionalComponent(current, workInProgress) {
    var fn = workInProgress.type;
    var props = workInProgress.pendingProps;

    // TODO: Disable this before release, since it is not part of the public API
    // I use this for testing to compare the relative overhead of classes.
    if (typeof fn.shouldComponentUpdate === 'function') {
      if (workInProgress.memoizedProps !== null) {
        if (!fn.shouldComponentUpdate(workInProgress.memoizedProps, props)) {
          return bailoutOnAlreadyFinishedWork(current, workInProgress);
        }
      }
    }

    var nextChildren;

    if (__DEV__) {
      ReactCurrentOwner.current = workInProgress;
      nextChildren = fn(props);
    } else {
      nextChildren = fn(props);
    }
    reconcileChildren(current, workInProgress, nextChildren);
    return workInProgress.child;
  }

  function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
    let shouldUpdate;
    if (!current) {
      if (!workInProgress.stateNode) {
        // In the initial pass we might need to construct the instance.
        constructClassInstance(workInProgress);
        mountClassInstance(workInProgress);
        shouldUpdate = true;
      } else {
        // In a resume, we'll already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(workInProgress);
      }
    } else {
      shouldUpdate = updateClassInstance(current, workInProgress);
    }
    if (!shouldUpdate) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress);
    }
    // Rerender
    const instance = workInProgress.stateNode;
    ReactCurrentOwner.current = workInProgress;
    const nextChildren = instance.render();
    reconcileChildren(current, workInProgress, nextChildren);
    return workInProgress.child;
  }

  function updateHostComponent(current, workInProgress) {
    let nextChildren = workInProgress.pendingProps.children;
    if (typeof nextChildren === 'string' || typeof nextChildren === 'number') {
      // We special case a direct text child of a host node. This is a common
      // case. We won't handle it as a reified child. We will instead handle
      // this in the host environment that also have access to this prop. That
      // avoids allocating another HostText fiber and traversing it.
      nextChildren = null;
    }
    if (workInProgress.pendingProps.hidden &&
        workInProgress.pendingWorkPriority !== OffscreenPriority) {
      // If this host component is hidden, we can bail out on the children.
      // We'll rerender the children later at the lower priority.

      // It is unfortunate that we have to do the reconciliation of these
      // children already since that will add them to the tree even though
      // they are not actually done yet. If this is a large set it is also
      // confusing that this takes time to do right now instead of later.

      if (workInProgress.progressedPriority === OffscreenPriority) {
        // If we already made some progress on the offscreen priority before,
        // then we should continue from where we left off.
        workInProgress.child = workInProgress.progressedChild;
      }

      // Reconcile the children and stash them for later work.
      reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority);
      workInProgress.child = current ? current.child : null;

      if (!current) {
        // If this doesn't have a current we won't track it for placement
        // effects. However, when we come back around to this we have already
        // inserted the parent which means that we'll infact need to make this a
        // placement.
        // TODO: There has to be a better solution to this problem.
        let child = workInProgress.progressedChild;
        while (child) {
          child.effectTag = Placement;
          child = child.sibling;
        }
      }

      // Abort and don't process children yet.
      return null;
    } else {
      reconcileChildren(current, workInProgress, nextChildren);
      return workInProgress.child;
    }
  }

  function mountIndeterminateComponent(current, workInProgress) {
    if (current) {
      throw new Error('An indeterminate component should never have mounted.');
    }
    var fn = workInProgress.type;
    var props = workInProgress.pendingProps;
    var value;

    if (__DEV__) {
      ReactCurrentOwner.current = workInProgress;
      value = fn(props);
    } else {
      value = fn(props);
    }

    if (typeof value === 'object' && value && typeof value.render === 'function') {
      // Proceed under the assumption that this is a class instance
      workInProgress.tag = ClassComponent;
      adoptClassInstance(workInProgress, value);
      mountClassInstance(workInProgress);
      ReactCurrentOwner.current = workInProgress;
      value = value.render();
    } else {
      // Proceed under the assumption that this is a functional component
      workInProgress.tag = FunctionalComponent;
    }
    reconcileChildren(current, workInProgress, value);
    return workInProgress.child;
  }

  function updateCoroutineComponent(current, workInProgress) {
    var coroutine = (workInProgress.pendingProps : ?ReactCoroutine);
    if (!coroutine) {
      throw new Error('Should be resolved by now');
    }
    reconcileChildren(current, workInProgress, coroutine.children);
  }

  /*
  function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
    let child = firstChild;
    do {
      // Ensure that the first and last effect of the parent corresponds
      // to the children's first and last effect.
      if (!returnFiber.firstEffect) {
        returnFiber.firstEffect = child.firstEffect;
      }
      if (child.lastEffect) {
        if (returnFiber.lastEffect) {
          returnFiber.lastEffect.nextEffect = child.firstEffect;
        }
        returnFiber.lastEffect = child.lastEffect;
      }
    } while (child = child.sibling);
  }
  */

  function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber {
    const priorityLevel = workInProgress.pendingWorkPriority;

    if (workInProgress.tag === HostComponent &&
        workInProgress.memoizedProps.hidden &&
        workInProgress.pendingWorkPriority !== OffscreenPriority) {
      // This subtree still has work, but it should be deprioritized so we need
      // to bail out and not do any work yet.
      // TODO: It would be better if this tree got its correct priority set
      // during scheduleUpdate instead because otherwise we'll start a higher
      // priority reconciliation first before we can get down here. However,
      // that is a bit tricky since workInProgress and current can have
      // different "hidden" settings.
      let child = workInProgress.progressedChild;
      while (child) {
        // To ensure that this subtree gets its priority reset, the children
        // need to be reset.
        child.pendingWorkPriority = OffscreenPriority;
        child = child.sibling;
      }
      return null;
    }

    // TODO: We should ideally be able to bail out early if the children have no
    // more work to do. However, since we don't have a separation of this
    // Fiber's priority and its children yet - we don't know without doing lots
    // of the same work we do anyway. Once we have that separation we can just
    // bail out here if the children has no more work at this priority level.
    // if (workInProgress.priorityOfChildren <= priorityLevel) {
    //   // If there are side-effects in these children that have not yet been
    //   // committed we need to ensure that they get properly transferred up.
    //   if (current && current.child !== workInProgress.child) {
    //     reuseChildrenEffects(workInProgress, child);
    //   }
    //   return null;
    // }

    if (current && workInProgress.child === current.child) {
      // If we had any progressed work already, that is invalid at this point so
      // let's throw it out.
      clearDeletions(workInProgress);
    }

    cloneChildFibers(current, workInProgress);
    markChildAsProgressed(current, workInProgress, priorityLevel);
    return workInProgress.child;
  }

  function bailoutOnLowPriority(current, workInProgress) {
    // TODO: What if this is currently in progress?
    // How can that happen? How is this not being cloned?
    return null;
  }

  function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
    if (workInProgress.pendingWorkPriority === NoWork ||
        workInProgress.pendingWorkPriority > priorityLevel) {
      return bailoutOnLowPriority(current, workInProgress);
    }

    // If we don't bail out, we're going be recomputing our children so we need
    // to drop our effect list.
    workInProgress.firstEffect = null;
    workInProgress.lastEffect = null;

    if (workInProgress.progressedPriority === priorityLevel) {
      // If we have progressed work on this priority level already, we can
      // proceed this that as the child.
      workInProgress.child = workInProgress.progressedChild;
    }

    if ((workInProgress.pendingProps === null || (
      workInProgress.memoizedProps !== null &&
      workInProgress.pendingProps === workInProgress.memoizedProps
      )) &&
      workInProgress.updateQueue === null) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress);
    }

    switch (workInProgress.tag) {
      case IndeterminateComponent:
        return mountIndeterminateComponent(current, workInProgress);
      case FunctionalComponent:
        return updateFunctionalComponent(current, workInProgress);
      case ClassComponent:
        return updateClassComponent(current, workInProgress);
      case HostContainer:
        reconcileChildren(current, workInProgress, workInProgress.pendingProps);
        // A yield component is just a placeholder, we can just run through the
        // next one immediately.
        return workInProgress.child;
      case HostComponent:
        if (workInProgress.stateNode && typeof config.beginUpdate === 'function') {
          config.beginUpdate(workInProgress.stateNode);
        }
        return updateHostComponent(current, workInProgress);
      case HostText:
        // Nothing to do here. This is terminal. We'll do the completion step
        // immediately after.
        return null;
      case CoroutineHandlerPhase:
        // This is a restart. Reset the tag to the initial phase.
        workInProgress.tag = CoroutineComponent;
        // Intentionally fall through since this is now the same.
      case CoroutineComponent:
        updateCoroutineComponent(current, workInProgress);
        // This doesn't take arbitrary time so we could synchronously just begin
        // eagerly do the work of workInProgress.child as an optimization.
        return workInProgress.child;
      case YieldComponent:
        // A yield component is just a placeholder, we can just run through the
        // next one immediately.
        return null;
      case Fragment:
        updateFragment(current, workInProgress);
        return workInProgress.child;
      default:
        throw new Error('Unknown unit of work tag');
    }
  }

  return {
    beginWork,
  };

};
<|endoftext|>


<commit_before>(function() {

/**
 * @class Echo.StreamServer.Controls.Stream.Item.Plugins.Like
 * Adds extra controls Like/Unlike to each item in the Echo Stream control. Note that these controls will appear only for authenticated users.
 *
 *     new Echo.StreamServer.Controls.Stream({
 *         "target": document.getElementById("echo-stream"),
 *         "appkey": "test.echoenabled.com",
 *         "plugins": [{
 *             "name": "Like"
 *         }]
 *     });
 *
 * @extends Echo.Plugin
 */
var plugin = Echo.Plugin.manifest("Like", "Echo.StreamServer.Controls.Stream.Item");

plugin.init = function() {
	this.extendTemplate("insertAsLastChild", "data", plugin.template);
	this.component.addButtonSpec("Like", this._assembleButton("Like"));
	this.component.addButtonSpec("Like", this._assembleButton("Unlike"));
};

plugin.labels = {
	/**
	 * @echo_label
	 */
	"likeThis": " like this.",
	/**
	 * @echo_label
	 */
	"likesThis": " likes this.",
	/**
	 * @echo_label
	 */
	"likeControl": "Like",
	/**
	 * @echo_label
	 */
	"unlikeControl": "Unlike",
	/**
	 * @echo_label
	 */
	"likeProcessing": "Liking...",
	/**
	 * @echo_label
	 */
	"unlikeProcessing": "Unliking..."
};

plugin.events = {
	"Echo.StreamServer.Controls.FacePile.Item.Plugins.Like.onUnlike": function(topic, args) {
		this._sendActivity("Unlike", this.component);
		return {"stop": ["bubble"]};
	}
};

plugin.template = '

';

plugin.renderers.likedBy = function(element) {
	var plugin = this;
	var item = this.component;
	if (!item.get("data.object.likes").length) {
		return element.hide();
	}
	var likesPerPage = 5;
	var visibleUsersCount = plugin.get("facePile")
		? plugin.get("facePile").getVisibleUsersCount()
		: likesPerPage;
	var youLike = false;
	var userId = item.user.get("id");
	var users = item.get("data.object.likes");
	$.each(users, function(i, like) {
		if (like.actor.id === userId) {
			youLike = true;
			return false; // break
		}
	});
	var config = plugin.config.assemble({
		"target": element.get(0),
		"data": {
			"itemsPerPage": likesPerPage,
			"entries": users
		},
		"initialUsersCount": visibleUsersCount,
		"totalUsersCount": item.get("data.object.accumulators.likesCount"),
		"suffixText": plugin.labels.get(users.length > 1 || youLike ? "likeThis" : "likesThis")
	});
	config.plugins.push({"name": "Like"});
	if (item.user.is("admin")) {
		element.addClass(plugin.cssPrefix + "highlight");
	}
	var facePile = new Echo.StreamServer.Controls.FacePile(config);
	plugin.set("facePile", facePile);
	return element.show();
};

plugin.methods._sendRequest = function(data, callback) {
	Echo.StreamServer.API.request({
		"endpoint": "submit",
		"submissionProxyURL": this.component.config.get("submissionProxyURL"),
		"onData": callback,
		"data": data
	}).send();
};

plugin.methods._sendActivity = function(name, item) {
	var plugin = this;
	var activity = {
		"verbs": ["http://activitystrea.ms/schema/1.0/" + name.toLowerCase()],
		"targets": [{"id": item.get("data.object.id")}]
	};
	this._sendRequest({
		"content": activity,
		"appkey": item.config.get("appkey"),
		"sessionID": item.user.get("sessionID"),
		"target-query": item.config.get("parent.query")
	}, function() {
		/**
		 * @event onLikeComplete
		 * @echo_event Echo.StreamServer.Controls.Stream.Item.Plugins.Like.onLikeComplete
		 * Triggered when the Like operation is finished.
		 */
		/**
		 * @event onUnlikeComplete
		 * @echo_event Echo.StreamServer.Controls.Stream.Item.Plugins.Like.onUnlikeComplete
		 * Triggered when the reverse Like operation is finished.
		 */
		plugin.events.publish({
			"topic": "on" + name + "Complete",
			"data": {
				"item": {
					"data": item.get("data"),
					"target": item.config.get("target")
				}
			}
		});
		plugin.requestDataRefresh();
	});
};

plugin.methods._assembleButton = function(name) {
	var plugin = this;
	var callback = function() {
		var item = this;
		item.get("buttons." + plugin.name + "." + name + ".element")
			.empty()
			.append(plugin.labels.get(name.toLowerCase() + "Processing"));
		plugin._sendActivity(name, item);
	};
	return function() {
		var item = this;
		var action =
			($.map(item.get("data.object.likes"), function(entry) {
				if (item.user.has("identity", entry.actor.id)) return entry;
			})).length > 0 ? "Unlike" : "Like";
		return {
			"name": name,
			"label": plugin.labels.get(name.toLowerCase() + "Control"),
			"visible": item.user.is("logged") && action === name,
			"once": true,
			"callback": callback
		};
	};
};

plugin.css =
	'.{plugin.class:likedBy} { background: url(' + Echo.Loader.getURL("sdk/images/likes.png") + ') no-repeat 0px 4px; padding: 0px 0px 4px 21px; }' +
	'.{plugin.class:highlight} { line-height: 23px; }' +
	($.browser.msie ?
		'.{plugin.class:highlight} span { vertical-align: middle; }' +
		'.{plugin.class:likedBy} { background-position: 0px 2px; }'
		: ''
	);

Echo.Plugin.create(plugin);

})();

(function() {

var plugin = Echo.Plugin.manifest("Like", "Echo.StreamServer.Controls.FacePile.Item");

plugin.init = function() {
	this.extendTemplate("insertAsLastChild", "container", plugin.template);
};

plugin.labels = {
	"unlikeOnBehalf": "Unlike on behalf of this user"
};

plugin.template = '';

plugin.component.renderers.container = function(element) {
	this.parentRenderer("container", arguments);
	if (this.component.user.is("admin")) {
		element.addClass(this.cssPrefix + "highlight");
	}
};

plugin.renderers.adminUnlike = function(element) {
	var plugin = this;
	var item = this.component;
	if (!item.user.is("admin")) {
		return element.remove();
	}
	return element.one("click", function() {
		item.dom.get("container").css("opacity", 0.3);
		/**
		 * @event onUnlike
		 * @echo_event Echo.StreamServer.Controls.FacePile.Item.Plugins.Like.onUnlike
		 * Triggered when the item is "unliked" by admin on behalf of a user.
		 */
		plugin.events.publish({
			"topic": "onUnlike",
			"actor": item.get("data"),
			"data": {
				"actor": item.get("data"),
				"target": item.config.get("parent.target").get(0)
			},
			"global": false,
			"propagation": false
		});
	});
};

plugin.css =
	'.{plugin.class:adminUnlike} { cursor: pointer; margin-left: 3px; }' +
	'.{plugin.class:highlight} { display: inline-block; line-height: 16px; background-color: #EEEEEE; padding: 1px 3px; border: 1px solid #D2D2D2; border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; margin: 0px 2px; }' +

Echo.Plugin.create(plugin);

})();
<commit_msg>[docs] like<commit_after>(function() {

/**
 * @class Echo.StreamServer.Controls.Stream.Item.Plugins.Like
 * Adds extra controls Like/Unlike to each item in the Echo Stream control.
 * Note that these controls will appear only for authenticated users.
 *
 * 	new Echo.StreamServer.Controls.Stream({
 * 		"target": document.getElementById("echo-stream"),
 * 		"appkey": "test.echoenabled.com",
 * 		"plugins": [{
 * 			"name": "Like"
 * 		}]
 * 	});
 *
 * @extends Echo.Plugin
 */
var plugin = Echo.Plugin.manifest("Like", "Echo.StreamServer.Controls.Stream.Item");

plugin.init = function() {
	this.extendTemplate("insertAsLastChild", "data", plugin.template);
	this.component.addButtonSpec("Like", this._assembleButton("Like"));
	this.component.addButtonSpec("Like", this._assembleButton("Unlike"));
};

plugin.labels = {
	/**
	 * @echo_label
	 */
	"likeThis": " like this.",
	/**
	 * @echo_label
	 */
	"likesThis": " likes this.",
	/**
	 * @echo_label
	 */
	"likeControl": "Like",
	/**
	 * @echo_label
	 */
	"unlikeControl": "Unlike",
	/**
	 * @echo_label
	 */
	"likeProcessing": "Liking...",
	/**
	 * @echo_label
	 */
	"unlikeProcessing": "Unliking..."
};

plugin.events = {
	"Echo.StreamServer.Controls.FacePile.Item.Plugins.Like.onUnlike": function(topic, args) {
		this._sendActivity("Unlike", this.component);
		return {"stop": ["bubble"]};
	}
};

plugin.template = '

';

/**
 * @echo_renderer
 */
plugin.renderers.likedBy = function(element) {
	var plugin = this;
	var item = this.component;
	if (!item.get("data.object.likes").length) {
		return element.hide();
	}
	var likesPerPage = 5;
	var visibleUsersCount = plugin.get("facePile")
		? plugin.get("facePile").getVisibleUsersCount()
		: likesPerPage;
	var youLike = false;
	var userId = item.user.get("id");
	var users = item.get("data.object.likes");
	$.each(users, function(i, like) {
		if (like.actor.id === userId) {
			youLike = true;
			return false; // break
		}
	});
	var config = plugin.config.assemble({
		"target": element.get(0),
		"data": {
			"itemsPerPage": likesPerPage,
			"entries": users
		},
		"initialUsersCount": visibleUsersCount,
		"totalUsersCount": item.get("data.object.accumulators.likesCount"),
		"suffixText": plugin.labels.get(users.length > 1 || youLike ? "likeThis" : "likesThis")
	});
	config.plugins.push({"name": "Like"});
	if (item.user.is("admin")) {
		element.addClass(plugin.cssPrefix + "highlight");
	}
	var facePile = new Echo.StreamServer.Controls.FacePile(config);
	plugin.set("facePile", facePile);
	return element.show();
};

plugin.methods._sendRequest = function(data, callback) {
	Echo.StreamServer.API.request({
		"endpoint": "submit",
		"submissionProxyURL": this.component.config.get("submissionProxyURL"),
		"onData": callback,
		"data": data
	}).send();
};

plugin.methods._sendActivity = function(name, item) {
	var plugin = this;
	var activity = {
		"verbs": ["http://activitystrea.ms/schema/1.0/" + name.toLowerCase()],
		"targets": [{"id": item.get("data.object.id")}]
	};
	this._sendRequest({
		"content": activity,
		"appkey": item.config.get("appkey"),
		"sessionID": item.user.get("sessionID"),
		"target-query": item.config.get("parent.query")
	}, function() {
		/**
		 * @event onLikeComplete
		 * @echo_event Echo.StreamServer.Controls.Stream.Item.Plugins.Like.onLikeComplete
		 * Triggered when the Like operation is finished.
		 */
		/**
		 * @event onUnlikeComplete
		 * @echo_event Echo.StreamServer.Controls.Stream.Item.Plugins.Like.onUnlikeComplete
		 * Triggered when the reverse Like operation is finished.
		 */
		plugin.events.publish({
			"topic": "on" + name + "Complete",
			"data": {
				"item": {
					"data": item.get("data"),
					"target": item.config.get("target")
				}
			}
		});
		plugin.requestDataRefresh();
	});
};

plugin.methods._assembleButton = function(name) {
	var plugin = this;
	var callback = function() {
		var item = this;
		item.get("buttons." + plugin.name + "." + name + ".element")
			.empty()
			.append(plugin.labels.get(name.toLowerCase() + "Processing"));
		plugin._sendActivity(name, item);
	};
	return function() {
		var item = this;
		var action =
			($.map(item.get("data.object.likes"), function(entry) {
				if (item.user.has("identity", entry.actor.id)) return entry;
			})).length > 0 ? "Unlike" : "Like";
		return {
			"name": name,
			"label": plugin.labels.get(name.toLowerCase() + "Control"),
			"visible": item.user.is("logged") && action === name,
			"once": true,
			"callback": callback
		};
	};
};

plugin.css =
	'.{plugin.class:likedBy} { background: url(' + Echo.Loader.getURL("sdk/images/likes.png") + ') no-repeat 0px 4px; padding: 0px 0px 4px 21px; }' +
	'.{plugin.class:highlight} { line-height: 23px; }' +
	($.browser.msie ?
		'.{plugin.class:highlight} span { vertical-align: middle; }' +
		'.{plugin.class:likedBy} { background-position: 0px 2px; }'
		: ''
	);

Echo.Plugin.create(plugin);

})();

(function() {

/**
 * @class Echo.StreamServer.Controls.Facepile.Item.Plugins.Like
 * Adds extra controls to items in the Echo Facepile control.
 *
 * @extends Echo.Plugin
 */
var plugin = Echo.Plugin.manifest("Like", "Echo.StreamServer.Controls.FacePile.Item");

plugin.init = function() {
	this.extendTemplate("insertAsLastChild", "container", plugin.template);
};

plugin.labels = {
	/**
	 * @echo_label
	 */
	"unlikeOnBehalf": "Unlike on behalf of this user"
};

plugin.template = '';

/**
 * @echo_renderer
 */
plugin.component.renderers.container = function(element) {
	this.parentRenderer("container", arguments);
	if (this.component.user.is("admin")) {
		element.addClass(this.cssPrefix + "highlight");
	}
};

/**
 * @echo_renderer
 */
plugin.renderers.adminUnlike = function(element) {
	var plugin = this;
	var item = this.component;
	if (!item.user.is("admin")) {
		return element.remove();
	}
	return element.one("click", function() {
		item.dom.get("container").css("opacity", 0.3);
		/**
		 * @event onUnlike
		 * @echo_event Echo.StreamServer.Controls.FacePile.Item.Plugins.Like.onUnlike
		 * Triggered when the item is "unliked" by admin on behalf of a user.
		 */
		plugin.events.publish({
			"topic": "onUnlike",
			"actor": item.get("data"),
			"data": {
				"actor": item.get("data"),
				"target": item.config.get("parent.target").get(0)
			},
			"global": false,
			"propagation": false
		});
	});
};

plugin.css =
	'.{plugin.class:adminUnlike} { cursor: pointer; margin-left: 3px; }' +
	'.{plugin.class:highlight} { display: inline-block; line-height: 16px; background-color: #EEEEEE; padding: 1px 3px; border: 1px solid #D2D2D2; border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; margin: 0px 2px; }' +

Echo.Plugin.create(plugin);

})();
<|endoftext|>


<commit_before>import $ from 'jquery'
import Editable from '../src/core'
import Highlighting from '../src/highlighting'
import WordHighlighter from '../src/plugins/highlighting/text-highlighting'

function setupHighlightEnv (context, text) {
  context.text = text
  context.$div = $('
' + context.text + '
').appendTo(document.body)
  context.editable = new Editable()
  context.editable.add(context.$div)

  context.highlightRange = (highlightId, start, end) => {
    return context.editable.highlight({
      editableHost: context.$div[0],
      text: context.text.substring(start, end),
      highlightId,
      textRange: { start, end }
    })
  }

  context.extract = () => {
    return context.editable.getHighlightPositions({editableHost: context.$div[0]})
  }
}

function teardownHighlightEnv (context) {
  context.$div.remove()
  context.editable.off()
  context.editable = undefined
  context.highlightRange = undefined
  context.assertUniqueSpan = undefined
}

describe('Highlighting', function () {
  beforeEach(() => {
    this.editable = new Editable()
  })

  describe('new Highlighting()', () => {
    it('creates an instance with a reference to editable', () => {
      const highlighting = new Highlighting(this.editable, {})
      expect(highlighting.editable).toEqual(this.editable)
    })
  })

  describe('WordHighlighter', () => {
    beforeEach(() => {
      const markerNode = $('')[0]
      this.highlighter = new WordHighlighter(markerNode)
    })

    it('finds the word "a"', () => {
      const text = 'a'
      const matches = this.highlighter.findMatches(text, ['a'])

      const firstMatch = matches[0]
      expect(firstMatch.match).toEqual('a')
      expect(firstMatch.startIndex).toEqual(0)
      expect(firstMatch.endIndex).toEqual(1)
    })

    it('does not find the word "b"', () => {
      const text = 'a'
      const matches = this.highlighter.findMatches(text, ['b'])
      expect(matches.length).toEqual(0)
    })

    it('finds the word "juice"', () => {
      const text = 'Some juice.'
      const matches = this.highlighter.findMatches(text, ['juice'])
      const firstMatch = matches[0]
      expect(firstMatch.match).toEqual('juice')
      expect(firstMatch.startIndex).toEqual(5)
      expect(firstMatch.endIndex).toEqual(10)
    })
  })

  describe('highlightSupport', () => {
    beforeEach(() => {
      setupHighlightEnv(this, 'People Make The
 World Go Round')
    })

    afterEach(() => {
      teardownHighlightEnv(this)
    })

    it('can handle a single highlight', () => {
      const startIndex = this.highlightRange('myId', 3, 7)
      expect(
        this.extract()
      ).toEqual({
        myId: {
          start: 3,
          end: 7
        }
      })
      expect(startIndex).toEqual(3)
    })

    it('can handle adjaccent highlights', () => {
      this.highlightRange('first', 0, 1)
      this.highlightRange('second', 1, 2)
      this.highlightRange('third', 2, 3)
      this.highlightRange('fourth', 3, 4)

      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 0,
          end: 1
        },
        second: {
          start: 1,
          end: 2
        },
        third: {
          start: 2,
          end: 3
        },
        fourth: {
          start: 3,
          end: 4
        }
      })
    })

    it('can handle nested highlights', () => {
      this.highlightRange('first', 0, 1)
      this.highlightRange('second', 1, 2)
      this.highlightRange('third', 2, 6)
      this.highlightRange('fourth', 0, 6)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 0,
          end: 1
        },
        second: {
          start: 1,
          end: 2
        },
        third: {
          start: 2,
          end: 6
        },
        fourth: {
          start: 0,
          end: 6
        }
      })
    })

    it('can handle intersecting highlights', () => {
      this.highlightRange('first', 0, 3)
      this.highlightRange('second', 3, 7)
      this.highlightRange('third', 4, 6)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 0,
          end: 3
        },
        second: {
          start: 3,
          end: 7
        },
        third: {
          start: 4,
          end: 6
        }
      })
    })

    it('can handle highlights containing newlines', () => {
      this.highlightRange('first', 11, 22)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 11,
          end: 22
        }
      })
    })

    it('can handle identical ranges', () => {
      this.highlightRange('first', 11, 22)
      this.highlightRange('second', 11, 22)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 11,
          end: 22
        },
        second: {
          start: 11,
          end: 22
        }
      })
    })

    it('will update any existing range found under `highlightId` aka upsert', () => {
      this.highlightRange('first', 11, 22)
      this.highlightRange('first', 8, 9)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 8,
          end: 9
        }
      })
    })

    it('can handle all cases combined and creates consistent output', () => {
      const expectedRanges = {
        first: {
          start: 0,
          end: 24
        },
        second: {
          start: 2,
          end: 10
        },
        third: {
          start: 4,
          end: 5
        },
        fourth: {
          start: 20,
          end: 32
        },
        fifth: {
          start: 15,
          end: 16
        },
        sixth: {
          start: 15,
          end: 16
        }
      }

      this.highlightRange('first', 4, 8)
      this.highlightRange('second', 2, 10)
      this.highlightRange('third', 4, 5)
      this.highlightRange('first', 0, 24)
      this.highlightRange('fourth', 20, 32)
      this.highlightRange('fifth', 15, 16)
      this.highlightRange('sixth', 15, 16)
      const ranges = this.extract()

      expect(
        ranges
      ).toEqual(expectedRanges)
      const content = this.editable.getContent(this.$div[0])
      this.$div.html(content)
      expect(content).toEqual(this.text)

      for (let highlightId in ranges) {
        this.highlightRange(
          highlightId,
          ranges[highlightId].start,
          ranges[highlightId].end
        )
      }
      expect(
        this.extract()
      ).toEqual(expectedRanges)
    })

    it('skips and warns if an invalid range object was passed', () => {
      this.editable.highlight({
        editableHost: this.$div[0],
        text: 'ple ',
        highlightId: 'myId',
        textRange: { foo: 3, bar: 7 }
      })
      const highlightSpan = this.$div.find('[data-word-id="myId"]')
      expect(highlightSpan.length).toEqual(0)
    })

    it('skips and warns if the range object represents a cursor', () => {
      this.editable.highlight({
        editableHost: this.$div[0],
        text: 'ple ',
        highlightId: 'myId',
        textRange: { start: 3, end: 3 }
      })

      const highlightSpan = this.$div.find('[data-word-id="myId"]')
      expect(highlightSpan.length).toEqual(0)
    })
  })
})
<commit_msg>test: request 'natural' offsets<commit_after>import $ from 'jquery'
import Editable from '../src/core'
import Highlighting from '../src/highlighting'
import WordHighlighter from '../src/plugins/highlighting/text-highlighting'

function setupHighlightEnv (context, text) {
  context.text = text
  context.$div = $('
' + context.text + '
').appendTo(document.body)
  context.editable = new Editable()
  context.editable.add(context.$div)

  context.highlightRange = (highlightId, start, end) => {
    return context.editable.highlight({
      editableHost: context.$div[0],
      text: context.text.substring(start, end),
      highlightId,
      textRange: { start, end }
    })
  }

  context.extract = () => {
    return context.editable.getHighlightPositions({editableHost: context.$div[0]})
  }
}

function teardownHighlightEnv (context) {
  context.$div.remove()
  context.editable.off()
  context.editable = undefined
  context.highlightRange = undefined
  context.assertUniqueSpan = undefined
}

describe('Highlighting', function () {
  beforeEach(() => {
    this.editable = new Editable()
  })

  describe('new Highlighting()', () => {
    it('creates an instance with a reference to editable', () => {
      const highlighting = new Highlighting(this.editable, {})
      expect(highlighting.editable).toEqual(this.editable)
    })
  })

  describe('WordHighlighter', () => {
    beforeEach(() => {
      const markerNode = $('')[0]
      this.highlighter = new WordHighlighter(markerNode)
    })

    it('finds the word "a"', () => {
      const text = 'a'
      const matches = this.highlighter.findMatches(text, ['a'])

      const firstMatch = matches[0]
      expect(firstMatch.match).toEqual('a')
      expect(firstMatch.startIndex).toEqual(0)
      expect(firstMatch.endIndex).toEqual(1)
    })

    it('does not find the word "b"', () => {
      const text = 'a'
      const matches = this.highlighter.findMatches(text, ['b'])
      expect(matches.length).toEqual(0)
    })

    it('finds the word "juice"', () => {
      const text = 'Some juice.'
      const matches = this.highlighter.findMatches(text, ['juice'])
      const firstMatch = matches[0]
      expect(firstMatch.match).toEqual('juice')
      expect(firstMatch.startIndex).toEqual(5)
      expect(firstMatch.endIndex).toEqual(10)
    })
  })

  describe('highlightSupport', () => {
    beforeEach(() => {
      setupHighlightEnv(this, 'People Make The
 World Go Round')
    })

    afterEach(() => {
      teardownHighlightEnv(this)
    })

    it('can handle a single highlight', () => {
      const startIndex = this.highlightRange('myId', 3, 7)
      expect(
        this.extract()
      ).toEqual({
        myId: {
          start: 3,
          end: 7
        }
      })
      expect(startIndex).toEqual(3)
    })

    it('can handle adjaccent highlights', () => {
      this.highlightRange('first', 0, 1)
      this.highlightRange('second', 1, 2)
      this.highlightRange('third', 2, 3)
      this.highlightRange('fourth', 3, 4)

      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 0,
          end: 1
        },
        second: {
          start: 1,
          end: 2
        },
        third: {
          start: 2,
          end: 3
        },
        fourth: {
          start: 3,
          end: 4
        }
      })
    })

    it('can handle nested highlights', () => {
      this.highlightRange('first', 0, 1)
      this.highlightRange('second', 1, 2)
      this.highlightRange('third', 2, 6)
      this.highlightRange('fourth', 0, 6)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 0,
          end: 1
        },
        second: {
          start: 1,
          end: 2
        },
        third: {
          start: 2,
          end: 6
        },
        fourth: {
          start: 0,
          end: 6
        }
      })
    })

    it('can handle intersecting highlights', () => {
      this.highlightRange('first', 0, 3)
      this.highlightRange('second', 3, 7)
      this.highlightRange('third', 4, 6)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 0,
          end: 3
        },
        second: {
          start: 3,
          end: 7
        },
        third: {
          start: 4,
          end: 6
        }
      })
    })

    it('can handle highlights containing newlines', () => {
      this.highlightRange('first', 11, 22)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 11,
          end: 22
        }
      })
    })

    it('can handle identical ranges', () => {
      this.highlightRange('first', 11, 22)
      this.highlightRange('second', 11, 22)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 11,
          end: 22
        },
        second: {
          start: 11,
          end: 22
        }
      })
    })

    it('will update any existing range found under `highlightId` aka upsert', () => {
      this.highlightRange('first', 11, 22)
      this.highlightRange('first', 8, 9)
      expect(
        this.extract()
      ).toEqual({
        first: {
          start: 8,
          end: 9
        }
      })
    })

    it('can handle all cases combined and creates consistent output', () => {
      const expectedRanges = {
        first: {
          start: 0,
          end: 24
        },
        second: {
          start: 2,
          end: 10
        },
        third: {
          start: 4,
          end: 5
        },
        fourth: {
          start: 20,
          end: 31
        },
        fifth: {
          start: 15,
          end: 16
        },
        sixth: {
          start: 15,
          end: 16
        }
      }

      this.highlightRange('first', 4, 8)
      this.highlightRange('second', 2, 10)
      this.highlightRange('third', 4, 5)
      this.highlightRange('first', 0, 24)
      this.highlightRange('fourth', 20, 31)
      this.highlightRange('fifth', 15, 16)
      this.highlightRange('sixth', 15, 16)
      const ranges = this.extract()

      expect(
        ranges
      ).toEqual(expectedRanges)
      const content = this.editable.getContent(this.$div[0])
      this.$div.html(content)
      expect(content).toEqual(this.text)

      for (let highlightId in ranges) {
        this.highlightRange(
          highlightId,
          ranges[highlightId].start,
          ranges[highlightId].end
        )
      }

      expect(
        this.extract()
      ).toEqual(expectedRanges)
    })

    it('skips and warns if an invalid range object was passed', () => {
      this.editable.highlight({
        editableHost: this.$div[0],
        text: 'ple ',
        highlightId: 'myId',
        textRange: { foo: 3, bar: 7 }
      })
      const highlightSpan = this.$div.find('[data-word-id="myId"]')
      expect(highlightSpan.length).toEqual(0)
    })

    it('skips and warns if the range object represents a cursor', () => {
      this.editable.highlight({
        editableHost: this.$div[0],
        text: 'ple ',
        highlightId: 'myId',
        textRange: { start: 3, end: 3 }
      })

      const highlightSpan = this.$div.find('[data-word-id="myId"]')
      expect(highlightSpan.length).toEqual(0)
    })
  })
})
<|endoftext|>