survey-analytics
Version:
SurveyJS analytics Library.
12,798 lines • 411 kB
JavaScript
/*!
* surveyjs - SurveyJS Dashboard library v2.0.4
* Copyright (c) 2015-2025 Devsoft Baltic OÜ - http://surveyjs.io/
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
import { D as DocumentHelper, l as localization, c as createLoadingIndicator, a as DataHelper, s as svgTemplate, b as createCommercialLicenseLink, t as toPrecision } from './shared.mjs';
export { d as surveyStrings } from './shared.mjs';
import { Event, QuestionCommentModel, settings, ItemValue, hasLicense, surveyLocalization, IsTouch, Helpers } from 'survey-core';
import Plotly from 'plotly.js-dist-min';
class DataProvider {
constructor(_data = []) {
this._data = _data;
this.filterValues = {};
/**
* Fires when data has been changed.
*/
this.onDataChanged = new Event();
}
get data() {
if (Array.isArray(this._data)) {
return this._data;
}
return undefined;
}
set data(data) {
if (Array.isArray(data)) {
this._data = [].concat(data);
}
else {
this._data = data;
}
this.raiseDataChanged();
}
get dataFn() {
if (typeof this._data === "function") {
return this._data;
}
return undefined;
}
get filteredData() {
if (this._filteredData === undefined) {
let filterKeys = Object.keys(this.filterValues);
if (filterKeys.length > 0) {
this._filteredData = this.data.filter((item) => {
return !Object.keys(this.filterValues).some((key) => {
const filterValue = this.filterValues[key];
const filterValueType = typeof filterValue;
const questionValue = item[key];
if (Array.isArray(questionValue)) {
if (filterValueType !== "object")
return questionValue.indexOf(filterValue) == -1;
}
if (typeof questionValue === "object") {
if (filterValueType !== "object")
return true;
return !questionContainsValue(questionValue, filterValue);
}
const seriesValue = item[DataProvider.seriesMarkerKey];
if (!!seriesValue && filterValueType === "object") {
return questionValue !== filterValue[seriesValue];
}
if (filterValueType === "object" && filterValue.start !== undefined && filterValue.end !== undefined) {
let continioiusValue = typeof questionValue === "number" ? questionValue : Date.parse(questionValue);
if (isNaN(continioiusValue)) {
continioiusValue = parseFloat(questionValue);
if (isNaN(continioiusValue)) {
return true;
}
}
return continioiusValue < filterValue.start || continioiusValue >= filterValue.end;
}
return item[key] !== this.filterValues[key];
});
});
}
else {
this._filteredData = this.data;
}
}
return this._filteredData;
}
/**
* Sets filter by question name and value.
*/
setFilter(questionName, selectedValue) {
var filterChanged = true;
if (selectedValue !== undefined) {
filterChanged = this.filterValues[questionName] !== selectedValue;
this.filterValues[questionName] = selectedValue;
}
else {
filterChanged = this.filterValues[questionName] !== undefined;
delete this.filterValues[questionName];
}
if (filterChanged) {
this.raiseDataChanged();
}
}
raiseDataChanged(questionName) {
this._filteredData = undefined;
if (!this.onDataChanged.isEmpty) {
this.onDataChanged.fire(this, { questionName });
}
}
getFilters() {
return Object.keys(this.filterValues).map(key => ({ field: key, type: "=", value: this.filterValues[key] }));
}
}
DataProvider.seriesMarkerKey = "__sa_series_name";
function questionContainsValue(questionValue, filterValue) {
const questionValueKeys = Object.keys(questionValue);
const filterValueKeys = Object.keys(filterValue);
if (filterValueKeys.length > questionValueKeys.length)
return false;
for (var key of filterValueKeys) {
if (filterValue[key] != questionValue[key])
return false;
}
return true;
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
class PostponeHelper {
static postpone(fn, timeout) {
if (PostponeHelper.postponeFunction) {
return PostponeHelper.postponeFunction(fn, timeout);
}
else {
return setTimeout(fn, timeout);
}
}
}
/**
* A base object for all visualizers. Use it to implement a custom visualizer.
*
* Constructor parameters:
*
* - `question`: [`Question`](https://surveyjs.io/form-library/documentation/api-reference/question)\
* A survey question to visualize.
* - `data`: `Array<any>`\
* Survey results.
* - `options`\
* An object with the following properties:
* - `dataProvider`: `DataProvider`\
* A data provider for this visualizer.
* - `renderContent`: `(contentContainer: HTMLElement, visualizer: VisualizerBase) => void`\
* A function that renders the visualizer's HTML markup. Append the markup to `contentContainer`.
* - `survey`: [`SurveyModel`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model)\
* Pass a `SurveyModel` instance if you want to use locales from the survey JSON schema.
* - `seriesValues`: `Array<string>`\
* Series values used to group data.
* - `seriesLabels`: `Array<string>`\
* Series labels to display. If this property is not set, `seriesValues` are used as labels.
* - `type`: `string`\
* *(Optional)* The visualizer's type.
*
* [View Demo](https://surveyjs.io/dashboard/examples/how-to-plot-survey-data-in-custom-bar-chart/ (linkStyle))
*/
class VisualizerBase {
afterRender(contentContainer) {
this.onAfterRender.fire(this, { htmlElement: contentContainer });
}
stateChanged(name, value) {
if (this._settingState) {
return;
}
this.onStateChanged.fire(this, this.getState());
}
constructor(question, data, options = {}, _type) {
var _a;
this.question = question;
this.options = options;
this._type = _type;
this._showToolbar = true;
this._footerVisualizer = undefined;
this._dataProvider = undefined;
this._getDataCore = undefined;
this.labelTruncateLength = 27;
this.renderResult = undefined;
this.toolbarContainer = undefined;
this.headerContainer = undefined;
this.contentContainer = undefined;
this.footerContainer = undefined;
this._supportSelection = false;
/**
* An event that is raised after the visualizer's content is rendered.
*
* Parameters:
*
* - `sender`: `VisualizerBase`\
* A `VisualizerBase` instance that raised the event.
*
* - `options.htmlElement`: `HTMLElement`\
* A page element with the visualizer's content.
* @see render
* @see refresh
**/
this.onAfterRender = new Event();
/**
* An event that is raised after a new locale is set.
*
* Parameters:
*
* - `sender`: `VisualizerBase`\
* A `VisualizerBase` instance that raised the event.
*
* - `options.locale`: `string`\
* The indentifier of a new locale (for example, "en").
* @see locale
*/
this.onLocaleChanged = new Event();
// public onStateChanged = new Event<
// (sender: VisualizationPanel, state: IState) => any,
// VisualizationPanel,
// any
// >();
/**
* An event that is raised when the visualizer's state has changed.
*
* The state includes selected chart types, chart layout, sorting, filtering, and other customizations that a user has made while using the dashboard. Handle the `onStateChanged` event to save these customizations, for example, in `localStorage` and restore them when the user reloads the page.
*
* Parameters:
*
* - `sender`: `VisualizerBase`\
* A `VisualizerBase` instance that raised the event.
*
* - `state`: `any`\
* A new state of the visualizer. Includes information about the visualized elements and current locale.
*
* [View Demo](/dashboard/examples/save-dashboard-state-to-local-storage/ (linkStyle))
* @see getState
* @see setState
*/
this.onStateChanged = new Event();
this.toolbarItemCreators = {};
this._backgroundColor = "#f7f7f7";
this._calculationsCache = undefined;
this.loadingData = false;
this._settingState = false;
this._getDataCore = (_a = this.questionOptions) === null || _a === void 0 ? void 0 : _a.getDataCore;
this._dataProvider = options.dataProvider || new DataProvider(data);
this._dataProvider.onDataChanged.add(() => this.onDataChanged());
this.loadingData = !!this._dataProvider.dataFn;
if (typeof options.labelTruncateLength !== "undefined") {
this.labelTruncateLength = options.labelTruncateLength;
}
}
get questionOptions() {
var _a;
return this.options[(_a = this.question) === null || _a === void 0 ? void 0 : _a.name];
}
onDataChanged() {
this._calculationsCache = undefined;
this.loadingData = !!this._dataProvider.dataFn;
this.refresh();
}
/**
* Returns the identifier of a visualized question.
*/
get name() {
return this.question.valueName || this.question.name;
}
get dataNames() {
return [this.name];
}
/**
* Indicates whether the visualizer displays a header. This property is `true` when a visualized question has a correct answer.
* @see hasFooter
*/
get hasHeader() {
if (!this.options || !this.options.showCorrectAnswers) {
return false;
}
return !!this.question && !!this.question.correctAnswer;
}
/**
* Indicates whether the visualizer displays a footer. This property is `true` when a visualized question has a comment.
* @see hasHeader
*/
get hasFooter() {
return (!!this.question && (this.question.hasComment || this.question.hasOther));
}
createVisualizer(question, options) {
let visualizerOptions = Object.assign({}, options || this.options);
if (visualizerOptions.dataProvider === undefined) {
visualizerOptions.dataProvider = this.dataProvider;
}
return VisualizerFactory.createVisualizer(question, this.data, visualizerOptions);
}
/**
* Allows you to access the footer visualizer. Returns `undefined` if the footer is absent.
* @see hasFooter
*/
get footerVisualizer() {
if (!this.hasFooter) {
return undefined;
}
if (!this._footerVisualizer) {
const question = new QuestionCommentModel(this.question.name + (settings || {}).commentPrefix);
question.title = this.processText(this.question.title);
let visualizerOptions = Object.assign({}, this.options);
visualizerOptions.renderContent = undefined;
this._footerVisualizer = this.createVisualizer(question, visualizerOptions);
if (!!this._footerVisualizer) {
this._footerVisualizer.onUpdate = () => this.invokeOnUpdate();
}
}
return this._footerVisualizer;
}
/**
* Indicates whether users can select series points to cross-filter charts. To allow or disallow selection, set the [`allowSelection`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowSelection) property of the `IVisualizationPanelOptions` object in the [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel) constructor.
*/
get supportSelection() {
return ((this.options.allowSelection === undefined ||
this.options.allowSelection) &&
this._supportSelection);
}
getSeriesValues() {
return this.options.seriesValues || [];
}
getSeriesLabels() {
return this.options.seriesLabels || this.getSeriesValues();
}
getValues() {
throw new Error("Method not implemented.");
}
getLabels() {
return this.getValues();
}
/**
* Registers a function used to create a toolbar item for this visualizer.
*
* The following code shows how to add a custom button and drop-down menu to the toolbar:
*
* ```js
* import { VisualizationPanel, DocumentHelper } from "survey-analytics";
*
* const vizPanel = new VisualizationPanel( ... );
*
* // Add a custom button to the toolbar
* vizPanel.visualizers[0].registerToolbarItem("my-toolbar-button", () => {
* return DocumentHelper.createButton(
* // A button click event handler
* () => {
* alert("Custom toolbar button is clicked");
* },
* // Button caption
* "Button"
* );
* });
*
* // Add a custom drop-down menu to the toolbar
* vizPanel.visualizers[0].registerToolbarItem("my-toolbar-dropdown", () => {
* return DocumentHelper.createSelector(
* // Menu items
* [
* { value: 1, text: "One" },
* { value: 2, text: "Two" },
* { value: 3, text: "Three" }
* ],
* // A function that specifies initial selection
* (option) => false,
* // An event handler that is executed when selection is changed
* (e) => {
* alert(e.target.value);
* }
* );
* });
* ```
* @param name A custom name for the toolbar item.
* @param creator A function that accepts the toolbar and should return an `HTMLElement` with the toolbar item.
* @see unregisterToolbarItem
*/
registerToolbarItem(name, creator) {
this.toolbarItemCreators[name] = creator;
}
/**
*
* Unregisters a function used to create a toolbar item. Allows you to remove a toolbar item.
* @param name A toolbar item name.
* @returns A function previously used to [register](#registerToolbarItem) the removed toolbar item.
* @see registerToolbarItem
*/
unregisterToolbarItem(name) {
if (this.toolbarItemCreators[name] !== undefined) {
const creator = this.toolbarItemCreators[name];
delete this.toolbarItemCreators[name];
return creator;
}
return undefined;
}
/**
* Returns the visualizer's type.
*/
get type() {
return this._type || "visualizer";
}
/**
* @deprecated Use [`surveyData`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#surveyData) instead.
*/
get data() {
return this.dataProvider.filteredData;
}
/**
* Returns an array of survey results used to calculate values for visualization. If a user applies a filter, the array is also filtered.
*
* To get an array of calculated and visualized values, call the [`getCalculatedValues()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase#getCalculatedValues) method.
*/
get surveyData() {
return this.dataProvider.filteredData;
}
get dataProvider() {
return this._dataProvider;
}
/**
* Updates visualized data.
* @param data A data array with survey results to be visualized.
*/
updateData(data) {
if (!this.options.dataProvider) {
this.dataProvider.data = data;
}
if (this.hasFooter) {
this.footerVisualizer.updateData(data);
}
}
invokeOnUpdate() {
this.onUpdate && this.onUpdate();
}
/**
* Deletes the visualizer and all its elements from the DOM.
* @see clear
*/
destroy() {
if (!!this.renderResult) {
this.clear();
this.toolbarContainer = undefined;
this.headerContainer = undefined;
this.contentContainer = undefined;
this.footerContainer = undefined;
this.renderResult.innerHTML = "";
this.renderResult = undefined;
}
if (!!this._footerVisualizer) {
this._footerVisualizer.destroy();
this._footerVisualizer.onUpdate = undefined;
this._footerVisualizer = undefined;
}
}
/**
* Empties the toolbar, header, footer, and content containers.
*
* If you want to empty and delete the visualizer and all its elements from the DOM, call the [`destroy()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase#destroy) method instead.
*/
clear() {
if (!!this.toolbarContainer) {
this.destroyToolbar(this.toolbarContainer);
}
if (!!this.headerContainer) {
this.destroyHeader(this.headerContainer);
}
if (!!this.contentContainer) {
this.destroyContent(this.contentContainer);
}
if (!!this.footerContainer) {
this.destroyFooter(this.footerContainer);
}
}
createToolbarItems(toolbar) {
Object.keys(this.toolbarItemCreators || {}).forEach((toolbarItemName) => {
let toolbarItem = this.toolbarItemCreators[toolbarItemName](toolbar);
if (!!toolbarItem) {
toolbar.appendChild(toolbarItem);
}
});
}
getCorrectAnswerText() {
return !!this.question ? this.question.correctAnswer : "";
}
destroyToolbar(container) {
container.innerHTML = "";
}
renderToolbar(container) {
if (this.showToolbar) {
const toolbar = (DocumentHelper.createElement("div", "sa-toolbar"));
this.createToolbarItems(toolbar);
container.appendChild(toolbar);
}
}
destroyHeader(container) {
if (!!this.options && typeof this.options.destroyHeader === "function") {
this.options.destroyHeader(container, this);
}
else {
container.innerHTML = "";
}
}
destroyContent(container) {
if (!!this.options && typeof this.options.destroyContent === "function") {
this.options.destroyContent(container, this);
}
else {
container.innerHTML = "";
}
}
renderHeader(container) {
if (!!this.options && typeof this.options.renderHeader === "function") {
this.options.renderHeader(container, this);
}
else {
const correctAnswerElement = DocumentHelper.createElement("div", "sa-visualizer__correct-answer");
correctAnswerElement.innerText = localization.getString("correctAnswer") + this.getCorrectAnswerText();
container.appendChild(correctAnswerElement);
}
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
container.innerText = localization.getString("noVisualizerForQuestion");
resolve(container);
});
});
}
renderContent(container) {
if (!!this.options && typeof this.options.renderContent === "function") {
const rendered = this.options.renderContent(container, this);
if (rendered !== false) {
this.afterRender(container);
}
}
else {
if (this.loadingData) {
this.renderLoadingIndicator(this.contentContainer);
}
this.renderContentAsync(container).then(el => this.afterRender(el));
}
}
destroyFooter(container) {
container.innerHTML = "";
}
renderFooter(container) {
container.innerHTML = "";
if (this.hasFooter) {
const footerTitleElement = DocumentHelper.createElement("h4", "sa-visualizer__footer-title", { innerText: localization.getString("otherCommentTitle") });
container.appendChild(footerTitleElement);
const footerContentElement = DocumentHelper.createElement("div", "sa-visualizer__footer-content");
footerContentElement.style.display = VisualizerBase.otherCommentCollapsed
? "none"
: "block";
const visibilityButton = DocumentHelper.createButton(() => {
if (footerContentElement.style.display === "none") {
footerContentElement.style.display = "block";
visibilityButton.innerText = localization.getString("hideButton");
}
else {
footerContentElement.style.display = "none";
visibilityButton.innerText = localization.getString(VisualizerBase.otherCommentCollapsed ? "showButton" : "hideButton");
}
this.footerVisualizer.invokeOnUpdate();
}, localization.getString("showButton") /*, "sa-toolbar__button--right"*/);
container.appendChild(visibilityButton);
container.appendChild(footerContentElement);
this.footerVisualizer.render(footerContentElement);
}
}
/**
* Renders the visualizer in a specified container.
* @param targetElement An `HTMLElement` or an `id` of a page element in which you want to render the visualizer.
*/
render(targetElement) {
if (typeof targetElement === "string") {
targetElement = document.getElementById(targetElement);
}
this.renderResult = targetElement;
this.toolbarContainer = DocumentHelper.createElement("div", "sa-visualizer__toolbar");
targetElement.appendChild(this.toolbarContainer);
this.renderToolbar(this.toolbarContainer);
if (this.hasHeader) {
this.headerContainer = DocumentHelper.createElement("div", "sa-visualizer__header");
targetElement.appendChild(this.headerContainer);
this.renderHeader(this.headerContainer);
}
this.contentContainer = DocumentHelper.createElement("div", "sa-visualizer__content");
targetElement.appendChild(this.contentContainer);
this.renderContent(this.contentContainer);
this.footerContainer = DocumentHelper.createElement("div", "sa-visualizer__footer");
targetElement.appendChild(this.footerContainer);
this.renderFooter(this.footerContainer);
}
updateContent() {
this.destroyContent(this.contentContainer);
this.renderContent(this.contentContainer);
}
/**
* Re-renders the visualizer and its content.
*/
refresh() {
if (!!this.headerContainer) {
PostponeHelper.postpone(() => {
this.destroyHeader(this.headerContainer);
this.renderHeader(this.headerContainer);
this.invokeOnUpdate();
});
}
if (!!this.contentContainer) {
PostponeHelper.postpone(() => {
this.updateContent();
this.invokeOnUpdate();
});
}
if (!!this.footerContainer) {
PostponeHelper.postpone(() => {
this.destroyFooter(this.footerContainer);
this.renderFooter(this.footerContainer);
this.invokeOnUpdate();
});
}
}
processText(text) {
if (this.options.stripHtmlFromTitles !== false) {
let originalText = text || "";
let processedText = originalText.replace(/(<([^>]+)>)/gi, "");
return processedText;
}
return text;
}
getRandomColor() {
const colors = this.getColors();
return colors[Math.floor(Math.random() * colors.length)];
}
get backgroundColor() { return this.getBackgroundColorCore(); }
set backgroundColor(value) { this.setBackgroundColorCore(value); }
getBackgroundColorCore() {
return this._backgroundColor;
}
setBackgroundColorCore(color) {
this._backgroundColor = color;
if (this.footerVisualizer)
this.footerVisualizer.backgroundColor = color;
}
getColors(count = 10) {
const colors = Array.isArray(VisualizerBase.customColors) &&
VisualizerBase.customColors.length > 0
? VisualizerBase.customColors
: VisualizerBase.colors;
let manyColors = [];
for (let index = 0; index < count; index++) {
manyColors = manyColors.concat(colors);
}
return manyColors;
}
/**
* Gets or sets the visibility of the visualizer's toolbar.
*
* Default value: `true`
*/
get showToolbar() {
return this._showToolbar;
}
set showToolbar(newValue) {
if (newValue != this._showToolbar) {
this._showToolbar = newValue;
if (!!this.toolbarContainer) {
this.destroyToolbar(this.toolbarContainer);
this.renderToolbar(this.toolbarContainer);
}
}
}
/**
* @deprecated Use [`getCalculatedValues()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#getCalculatedValues) instead.
*/
getData() {
return this.getCalculatedValuesCore();
}
getCalculatedValuesCore() {
if (!!this._getDataCore) {
return this._getDataCore(this);
}
return defaultStatisticsCalculator(this.surveyData, this);
}
renderLoadingIndicator(contentContainer) {
contentContainer.appendChild(createLoadingIndicator());
}
convertFromExternalData(externalCalculatedData) {
return externalCalculatedData;
}
/**
* Returns an array of calculated and visualized values. If a user applies a filter, the array is also filtered.
*
* To get an array of source survey results, use the [`surveyData`](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase#surveyData) property.
*/
getCalculatedValues() {
return new Promise((resolve, reject) => {
if (this._calculationsCache !== undefined) {
resolve(this._calculationsCache);
}
if (!!this.dataProvider.dataFn) {
this.loadingData = true;
const dataLoadingPromise = this.dataProvider.dataFn({
visualizer: this,
filter: this.dataProvider.getFilters(),
callback: (loadedData) => {
this.loadingData = false;
if (!loadedData.error && Array.isArray(loadedData.data)) {
this._calculationsCache = this.convertFromExternalData(loadedData.data);
resolve(this._calculationsCache);
}
else {
reject();
}
}
});
if (dataLoadingPromise) {
dataLoadingPromise
.then(calculatedData => {
this.loadingData = false;
this._calculationsCache = this.convertFromExternalData(calculatedData);
resolve(this._calculationsCache);
})
.catch(() => {
this.loadingData = false;
reject();
});
}
}
else {
this._calculationsCache = this.getCalculatedValuesCore();
resolve(this._calculationsCache);
}
});
}
/**
* Returns an object with properties that describe a current visualizer state. The properties are different for each individual visualizer.
*
* > This method is overriden in classes descendant from `VisualizerBase`.
* @see setState
* @see onStateChanged
*/
getState() {
return {};
}
/**
* Sets the visualizer's state.
*
* > This method is overriden in classes descendant from `VisualizerBase`.
* @see getState
* @see onStateChanged
*/
setState(state) {
}
/**
* Gets or sets the current locale.
*
* If you want to inherit the locale from a visualized survey, assign a [`SurveyModel`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model) instance to the [`survey`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#survey) property of the `IVisualizationPanelOptions` object in the [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel) constructor.
*
* If the survey is [translated into more than one language](https://surveyjs.io/form-library/examples/survey-localization/), the toolbar displays a language selection drop-down menu.
* @see onLocaleChanged
*/
get locale() {
var survey = this.options.survey;
if (!!survey) {
return survey.locale;
}
return localization.currentLocale;
}
set locale(newLocale) {
this.setLocale(newLocale);
this.onLocaleChanged.fire(this, { locale: newLocale });
this.refresh();
}
setLocale(newLocale) {
localization.currentLocale = newLocale;
var survey = this.options.survey;
if (!!survey && survey.locale !== newLocale) {
survey.locale = newLocale;
}
}
}
VisualizerBase.suppressVisualizerStubRendering = false;
// public static otherCommentQuestionType = "comment"; // TODO: make it configureable - allow choose what kind of question/visualizer will be used for comments/others
VisualizerBase.otherCommentCollapsed = true;
VisualizerBase.customColors = [];
VisualizerBase.colors = [
"#86e1fb",
"#3999fb",
"#ff6771",
"#1eb496",
"#ffc152",
"#aba1ff",
"#7d8da5",
"#4ec46c",
"#cf37a6",
"#4e6198",
];
function defaultStatisticsCalculator(data, dataInfo) {
const dataNames = dataInfo.dataNames;
const statistics = [];
const values = dataInfo.getValues();
const valuesIndex = {};
values.forEach((val, index) => {
valuesIndex[val] = index;
});
const processMissingAnswers = values.indexOf(undefined) !== -1;
const series = dataInfo.getSeriesValues();
const seriesIndex = {};
series.forEach((val, index) => {
seriesIndex[val] = index;
});
const seriesLength = series.length || 1;
for (var i = 0; i < dataNames.length; ++i) {
const dataNameStatistics = new Array();
for (var j = 0; j < seriesLength; ++j) {
dataNameStatistics.push(new Array(values.length).fill(0));
}
statistics.push(dataNameStatistics);
}
data.forEach((row) => {
dataNames.forEach((dataName, index) => {
const rowValue = row[dataName];
if (rowValue !== undefined || processMissingAnswers) {
const rowValues = Array.isArray(rowValue) ? rowValue : [rowValue];
if (series.length > 0) {
if (row[DataProvider.seriesMarkerKey] !== undefined) {
// Series are labelled by seriesMarkerKey in row data
const seriesNo = seriesIndex[row[DataProvider.seriesMarkerKey]] || 0;
rowValues.forEach((val) => {
statistics[index][seriesNo][valuesIndex[val]]++;
});
}
else {
// Series are the keys in question value (matrix question)
// TODO: think about the de-normalization and combine with the previous case
rowValues.forEach((val) => {
series.forEach((seriesName) => {
if (val[seriesName] !== undefined) {
const seriesNo = seriesIndex[seriesName] || 0;
statistics[index][seriesNo][valuesIndex[val[seriesName]]]++;
}
});
});
}
}
else {
// No series
rowValues.forEach((val) => statistics[0][0][valuesIndex[val]]++);
}
}
});
});
return dataInfo.dataNames.length > 1 ? statistics : statistics[0];
}
/**
* An object with methods used to register and unregister visualizers for individual question types.
*
* [View Demo](https://surveyjs.io/dashboard/examples/visualize-answers-from-text-entry-fields-with-charts/ (linkStyle))
*/
class VisualizationManager {
/**
* Registers a visualizer for a specified question type.
*
* [View Demo](https://surveyjs.io/dashboard/examples/visualize-answers-from-text-entry-fields-with-charts/ (linkStyle))
* @param questionType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType).
* @param constructor A function that returns a visualizer constructor to register.
* @param index A zero-based index that specifies the visualizer's position in the visualizer list for the specified question type. Pass `0` to insert the visualizer at the beginning of the list and use it by default. If `index` is not specified, the visualizer is added to the end of the list.
*/
static registerVisualizer(questionType, constructor, index = Number.MAX_VALUE) {
let visualizers = VisualizationManager.vizualizers[questionType];
if (!visualizers) {
visualizers = [];
VisualizationManager.vizualizers[questionType] = visualizers;
}
visualizers.push({ ctor: constructor, index });
}
/**
* Unregisters a visualizer for a specified question type.
*
* [View Demo](https://surveyjs.io/dashboard/examples/visualize-answers-from-text-entry-fields-with-charts/ (linkStyle))
* @param questionType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType).
* @param constructor A function that returns a visualizer constructor to unregister.
*/
static unregisterVisualizer(questionType, constructor) {
let questionTypes = [questionType];
if (!questionType) {
questionTypes = Object.keys(VisualizationManager.vizualizers);
}
questionTypes.forEach(qType => {
if (constructor) {
let visualizers = VisualizationManager.vizualizers[qType];
if (!!visualizers) {
const vDescr = visualizers.filter(v => v.ctor === constructor)[0];
if (!!vDescr) {
let index = visualizers.indexOf(vDescr);
if (index !== -1) {
visualizers.splice(index, 1);
}
}
}
}
else {
VisualizationManager.vizualizers[qType] = [];
}
});
}
/**
* @deprecated Call the [`unregisterVisualizer()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationmanager#unregisterVisualizer) method instead.
* @param constructor A function that returns a visualizer constructor to unregister.
*/
static unregisterVisualizerForAll(constructor) {
VisualizationManager.unregisterVisualizer(undefined, constructor);
}
/**
* Returns all visualizer constructors for a specified question type.
* @param questionType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType).
*/
static getVisualizersByType(questionType) {
let vDescrs = VisualizationManager.vizualizers[questionType];
if (!vDescrs) {
if (VisualizerBase.suppressVisualizerStubRendering) {
return [];
}
return [VisualizerBase];
}
vDescrs = [].concat(vDescrs);
vDescrs.sort((v1, v2) => v1.index - v2.index);
return vDescrs.map(v => v.ctor);
}
/**
* Returns a constructor for an alternative visualizer selector.
* @see registerAltVisualizerSelector
*/
static getAltVisualizerSelector() {
return VisualizationManager.alternativesVisualizer || VisualizerBase;
}
/**
* Registers an alternative visualizer selector.
* @param constructor A function that returns a constructor for an alternative visualizer selector.
*/
static registerAltVisualizerSelector(constructor) {
VisualizationManager.alternativesVisualizer = constructor;
}
}
VisualizationManager.alternativesVisualizer = undefined;
VisualizationManager.vizualizers = {};
/**
* An object that allows you to create individual visualizers without creating a [visualization panel](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel).
*/
class VisualizerFactory {
/**
* Creates a visualizer for a single question.
*
* ```js
* import { VisualizerFactory } from "survey-analytics";
*
* const visualizer = new VisualizerFactory.createVisualizer(
* question,
* data,
* options
* );
*
* visualizer.render("containerId")
* ```
*
* If a question has more than one [registered](https://surveyjs.io/dashboard/documentation/api-reference/visualizationmanager#registerVisualizer) visualizer, users can switch between them using a drop-down menu.
* @param question A question for which to create a visualizer.
* @param data A data array with survey results to be visualized.
* @param options An object with any custom properties you need within the visualizer.
*/
static createVisualizer(question, data, options) {
let type;
if (question.getType() === "text" && question.inputType) {
type = question.inputType;
}
else {
type = question.getType();
}
var creators = VisualizationManager.getVisualizersByType(type);
var visualizers = creators.map((creator) => new creator(question, data, options));
if (visualizers.length > 1) {
const alternativesVisualizerConstructor = VisualizationManager.getAltVisualizerSelector();
let visualizer = new alternativesVisualizerConstructor(visualizers, question, data, options);
return visualizer;
}
return visualizers[0];
}
}
class NumberModel extends VisualizerBase {
constructor(question, data, options = {}, name) {
super(question, data, options, name || "number");
this.registerToolbarItem("changeChartType", () => {
if (this.chartTypes.length > 1) {
return DocumentHelper.createSelector(this.chartTypes.map((chartType) => {
return {
value: chartType,
text: localization.getString("chartType_" + chartType),
};
}), (option) => this.chartType === option.value, (e) => {
this.setChartType(e.target.value);
});
}
return null;
});
}
onDataChanged() {
this._resultAverage = undefined;
this._resultMin = undefined;
this._resultMax = undefined;
super.onDataChanged();
}
onChartTypeChanged() { }
setChartType(chartType) {
if (this.chartTypes.indexOf(chartType) !== -1 &&
this.chartType !== chartType) {
this.chartType = chartType;
this.onChartTypeChanged();
if (!!this.contentContainer) {
this.destroyContent(this.contentContainer);
this.renderContent(this.contentContainer);
}
this.invokeOnUpdate();
}
}
destroy() {
this._resultAverage = undefined;
this._resultMin = undefined;
this._resultMax = undefined;
super.destroy();
}
generateText(maxValue, minValue, stepsCount) {
let texts = [];
if (stepsCount === 5) {
texts = [
"very high (" + maxValue + ")",
"high",
"medium",
"low",
"very low (" + minValue + ")",
];
}
else {
texts.push(maxValue);
for (let i = 0; i < stepsCount - 2; i++) {
texts.push("");
}
texts.push(minValue);
}
if (!!NumberModel.generateTextsCallback) {
return NumberModel.generateTextsCallback(this.question, maxValue, minValue, stepsCount, texts);
}
return texts;
}
generateValues(maxValue, stepsCount) {
const values = [];
for (let i = 0; i < stepsCount; i++) {
values.push(maxValue / stepsCount);
}
values.push(maxValue);
return values;
}
generateColors(maxValue, minValue, stepsCount) {
const palette = this.getColors();
const colors = [];
for (let i = 0; i < stepsCount; i++) {
colors.push(palette[i]);
}
colors.push("rgba(255, 255, 255, 0)");
return colors;
}
convertFromExternalData(externalCalculatedData) {
return [externalCalculatedData.value || 0, externalCalculatedData.minValue || 0, externalCalculatedData.maxValue || 0];
}
getCalculatedValuesCore() {
if (this._resultAverage === undefined ||
this._resultMin === undefined ||
this._resultMax === undefined) {
this._resultMin = Number.MAX_VALUE;
this._resultMax = -Number.MAX_VALUE;
this._resultAverage = 0;
let actualAnswerCount = 0;
this.data.forEach((rowData) => {
if (rowData[this.question.name] !== undefined) {
const questionValue = +rowData[this.question.name];
actualAnswerCount++;
this._resultAverage += questionValue;
if (this._resultMin > questionValue) {
this._resultMin = questionValue;
}
if (this._resultMax < questionValue) {
this._resultMax = questionValue;
}
}
});
if (actualAnswerCount > 0) {
this._resultAverage = this._resultAverage / actualAnswerCount;
}
this._resultAverage = Math.ceil(this._resultAverage * 100) / 100;
}
return [this._resultAverage, this._resultMin, this._resultMax];
}
}
NumberModel.stepsCount = 5;
NumberModel.showAsPercentage = false;
function hideEmptyAnswersInData(answersData) {
const result = {
datasets: [],
labels: [],
colors: [],
texts: [],
seriesLabels: [],
};
if (answersData.seriesLabels.length === 0) {
result.datasets.push([]);
result.texts.push([]);
for (var i = 0; i < answersData.datasets[0].length; i++) {
if (answersData.datasets[0][i] != 0) {
result.datasets[0].push(answersData.datasets[0][i]);
result.labels.push(answersData.labels[i]);
result.colors.push(answersData.colors[i]);
result.texts[0].push(answersData.texts[0][i]);
}
}
return result;
}
const seriesDataExistence = [];
seriesDataExistence.length = answersData.seriesLabels.length;
const valuesDataExistence = [];
valuesDataExistence.length = answersData.labels.length;
for (var valueIndex = 0; valueIndex < answersData.labels.length; valueIndex++) {
for (var seriesIndex = 0; seriesIndex < answersData.seriesLabels.length; seriesIndex++) {
if (answersData.datasets[valueIndex][seriesIndex] != 0) {
seriesDataExistence[seriesIndex] = true;
valuesDataExistence[valueIndex] = true;
}
}
}
for (var valueIndex = 0; valueIndex < valuesDataExistence.length; valueIndex++) {
if (valuesDataExistence[valueIndex]) {
result.labels.push(answersData.labels[valueIndex]);
result.colors.push(answersData.colors[valueIndex]);
}
}
for (var seriesIndex = 0; seriesIndex < answersData.seriesLabels.length; seriesIndex++) {
if (seriesDataExistence[seriesIndex]) {
result.seriesLabels.push(answersData.seriesLabels[seriesIndex]);
}
}
for (var valueIndex = 0; valueIndex < answersData.labels.length; valueIndex++) {
if (valuesDataExistence[valueIndex]) {
const dataset = [];
const texts = [];
for (var seriesIndex = 0; seriesIndex < answersData.datasets.length; seriesIndex++) {
if (seriesDataExistence[seriesIndex]) {
dataset.push(answersData.datasets[valueIndex][seriesIndex]);
texts.push(answersData.texts[valueIndex][seriesIndex]);
}
}
result.datasets.push(dataset);
result.texts.push(texts);
}
}
return result;
}
class SelectBase extends VisualizerBase {
constructor(question, data, options, name) {
super(question, data, options, name || "selectBase");
this.selectedItem = undefined;
this.choicesOrderSelector = undefined;
this.showPercentageBtn = undefined;
this.emptyAnswersBtn = undefined;
this.transposeDataBtn = undefined;
this.topNSelector = undefined;
this._showPercentages = false;
this._showOnlyPercentages = false;
this._percentagePrecision = 0;
this._answersOrder = "default";
this._supportSelection = true;
this._hideEmptyAnswers = false;
this._topN = -1;
this.topNValues = [].concat(SelectBase.topNValuesDefaults);
this._transposeData = false;
this._showMissingAnswers = false;
this.missingAnswersBtn = undefined;
this.chartTypes = [];
this._chartType = "bar";
/**
* Fires when answer data has been combined before they passed to draw graph.
* options - the answers data object containing: datasets, labels, colors, additional texts (percentage).
* options fields can be modified
*/
this.onAnswersDataReady = new Event();
question.visibleChoicesChangedCallback = () => {
this.dataProvider.raiseDataChanged();
};
this._showPercentages = this.options.showPercentages === true;
this._showOnlyPercentages = this.options.showOnlyPercentages === true;
if (this.options.percentagePrecision) {
this._percentagePrecision = this.options.percentagePrecision;
}
this._hideEmptyAnswers = this.options.hideEmptyAnswers === true;
this._answersOrder = this.options.answersOrder || "default";
this._showMissingAnswers = this.isSupportMissingAnswers() && this.options.showMissingAnswers === true;
this.registerToolbarItem("changeChartType", () => {
if (this.chartTypes.length > 1) {
return DocumentHelper.createSelector(this.chartTypes.map((chartType) => {
return {
value: chartType,
text: localization.getString("chartType_" + chartType),
};
}), (option) => this.chartType === option.value, (e) => {
this.setChartType(e.target.value);
});
}
return null;
});
this.registerToolbarItem("changeAnswersOrder", () => {
if (this.isSupportAnswersOrder()) {
this.choicesOrderSelector = DocumentHelper.createSelector([
{ text: localization.getString("defaultOrder"), value: "default" },
{ text: localization.getString("ascOrder"), value: "asc" },
{ text: localization.getString("descOrder"), value: "desc" },
], (option) => false, (e) => {
this.answersOrder = e.target.value;
});
this.updateOrderSelector();
}
return this.choicesOrderSelector;
});
this.registerToolbarItem("showPercentages", () => {
if (this.options.allowShowPercentages &&
(this.chartTypes.indexOf("bar") !== -1 ||
this.chartTypes.indexOf("stackedbar") !== -1)) {
this.showPercentageBtn = DocumentHelper.createButton(() => {
this.showPercentages = !this.showPercentages;
});
this.updateShowPercentageBtn();
return this.showPercentageBtn;
}
});
this.registerToolbarItem("hideEmptyAnswers", () => {
if (this.options.allowHideEmptyAnswers) {
this.emptyAnswersBtn = DocumentHelper.createButton(() => {
this.hideEmptyAnswers = !this._hideEmptyAnswers;
});
this.updateEmptyAnswersBtn();
}
return this.emptyAnswersBtn;
});
this.registerToolbarItem("topNAnswers", () => {
if (this.options.allowTopNAnswers &&
this.getSeriesValues().length === 0) {
this.topNSelector = DocumentHelper.createSelector(this.topNValues.map((value) => {
return {
text: localization.getString("topNValueText" + value),
value: value,
};
}), (option) => false, (e) => {
this.topN = parseInt(e.target.value);
});
this.updateTopNSelector();
}
return this.topNSelector;
});
this.registerToolbarItem("transposeData", () => {
if (this.options.allowTransposeData) {
this.transposeDataBtn = DocumentHelper.createButton(() => {
this.transposeData = !this.transposeData;
});
this.updateTransposeDataBtn();
}
return this.transposeDataBtn;
});
this.registerToolbarItem("showMissingAnswers", () => {
if (this.isSupportMissingAnswers() && this.options.allowShowMissingAnswers) {
this.missingAnswersBtn = DocumentHelper.createButton(() => {
this.showMissingAnswers = !this._showMissingAnswers;
});
this.updateMissingAnswersBtn();
}
return this.missingAnswersBtn;
});
}
/**
* Chart type - current chart type.
*/
get chartType() {
return this._chartType;
}
set chartType(newChartType) {
this.setChartType(newChartType);
}
updateEmptyAnswersBtn() {
if (!!this.emptyAnswersBtn) {
this.emptyAnswersBtn.innerText = this._hideEmptyAnswers
? localization.getString("showEmptyAnswers")
: localization.getString("hideEmptyAnswers");
if (this.chartType == "bar" || this.chartType == "vbar" || this.chartType == "line" || this.chartType == "scatter") {
this.emptyAnswersBtn.style.display = "inline";
}
else {
this.emptyAnswersBtn.style.display = "none";
}
}
}
updateTransposeDataBtn() {
if (!!this.transposeDataBtn) {
this.transposeDataBtn.innerText = this.transposeData
? localization.getString("showPerColumns")
: localization.getString("showPerValues");
if (this.getSeriesValues().length > 0) {
this.transposeDataBtn.style.display = "inline";
}
else {
this.transposeDataBtn.style.display = "none";
}
}
}
updateOrderSelector() {
if (!!this.choicesOrderSelector) {
if (this.chartType == "bar" ||
this.chartType == "vbar" ||
this.chartType == "line" ||
this.chartType == "scatter" ||
((this.chartType == "pie" || this.chartType == "doughnut") &&
this.topN > 0)) {
this.choicesOrderSelector.style.display = "inline-flex";
}
else {
this.choicesOrderSelector.style.display = "none";
}
this.choicesOrderSelector.getElementsByTagName("select")[0].value = this.answersOrder;
}
}
updateShowPercentageBtn() {
if (!!this.showPercentageBtn) {
this.showPercentageBtn.innerText = this._showPercentages
? localization.getString("hidePercentages")
: localization.getString("showPercentages");
if (this.chartType == "bar" || this.chartType == "vbar" || this.chartType == "stackedbar") {
this.showPercentageBtn.style.display = "inline";
}
else {
this.showPercentageBtn.style.display = "none";
}
}
}
updateTopNSelector() {
if (!!this.topNSelector) {
this.topNSelector.getElementsByTagName("select")[0].value = (this._topN);
}
}
updateMissingAnswersBtn() {
if (!!this.missingAnswersBtn) {
this.missingAnswersBtn.innerText = this._showMissingAnswers
? localization.getString("hideMissingAnswers")
: localization.getString("showMissingAnswers");
}
}
onChartTypeChanged() {
this.updateOrderSelector();
this.updateShowPercentageBtn();
this.updateEmptyAnswersBtn();
this.updateTopNSelector();
this.stateChanged("chartType", this._chartType);
}
setChartType(chartType) {
if (this.chartTypes.indexOf(chartType) !== -1 &&
this._chartType !== chartType) {
this._chartType = chartType;
this.onChartTypeChanged();
this.refreshContent();
}
}
getCorrectAnswerText() {
const correctAnswerValue = super.getCorrectAnswerText();
const resultValues = Array.isArray(correctAnswerValue) ? correctAnswerValue : [correctAnswerValue];
const selectBaseQuestion = this.question;
return resultValues.map((value) => ItemValue.getTextOrHtmlByValue(selectBaseQuestion.choices, value)).join(", ");
}
getSelectedItemByText(itemText) {
const selBase = this.question;
if (this.question.hasOther && itemText == selBase.otherText) {
return selBase.otherItem;
}
else {
return selBase.choices.filter((choice) => choice.text === itemText)[0];
}
}
setSelection(item) {
var _a;
if (this.selectedItem !== item) {
this.selectedItem = item;
if (this.onDataItemSelected !== undefined) {
this.onDataItemSelected(item !== undefined ? item.value : undefined, item !== undefined ? item.text : "");
}
this.stateChanged("filter", (_a = this.selectedItem) === null || _a === void 0 ? void 0 : _a.value);
}
}
get selection() {
return this.selectedItem;
}
/**
* Gets and sets whether chart should show percentages only.
*/
get showOnlyPercentages() {
return this._showOnlyPercentages;
}
set showOnlyPercentages(val) {
this._showOnlyPercentages = val;
this.refreshContent();
this.stateChanged("showOnlyPercentages", val);
}
/**
* Gets and sets whether chart should show values and percentages.
*/
get showPercentages() {
return this._showPercentages;
}
set showPercentages(val) {
this._showPercentages = val;
this.updateShowPercentageBtn();
this.refreshContent();
this.stateChanged("showPercentages", val);
}
/**
* Gets and sets chart elements order.
*/
get answersOrder() {
return this._answersOrder;
}
set answersOrder(value) {
this._answersOrder = value;
this.updateOrderSelector();
this.refreshContent();
this.stateChanged("answersOrder", value);
}
/**
* Set to true if need to hide empty chart elements (e.g. bars vith zero value).
*/
get hideEmptyAnswers() {
return this._hideEmptyAnswers;
}
set hideEmptyAnswers(value) {
this._hideEmptyAnswers = value;
this.updateEmptyAnswersBtn();
this.refreshContent();
this.stateChanged("hideEmptyAnswers", value);
}
get transposeData() {
return this._transposeData;
}
set transposeData(value) {
this._transposeData = value;
this.updateTransposeDataBtn();
this.refreshContent();
this.stateChanged("transposeData", value);
}
/**
* Set to some value if need to show top N chart elements.
*/
get topN() {
return this._topN;
}
set topN(value) {
this._topN = value;
this.updateTopNSelector();
this.updateOrderSelector();
this.refreshContent();
this.stateChanged("topN", value);
}
isSupportAnswersOrder() {
return this.options.allowSortAnswers !== false &&
this.options.allowChangeAnswersOrder !== false &&
this.getSeriesValues().length === 0;
}
isSupportMissingAnswers() {
return true;
}
/**
* Set to true if you want to see chart elements for missing answers (e.g. radiogroup items never been selected by surveyee).
*/
get showMissingAnswers() {
return this._showMissingAnswers;
}
set showMissingAnswers(value) {
this._showMissingAnswers = this.isSupportMissingAnswers() && value;
this.updateMissingAnswersBtn();
this.dataProvider.raiseDataChanged(this.name);
this.refreshContent();
this.stateChanged("showMissingAnsewrs", value);
}
refreshContent() {
if (!!this.contentContainer) {
this.destroyContent(this.contentContainer);
this.renderContent(this.contentContainer);
}
this.invokeOnUpdate();
}
get showValuesInOriginalOrder() {
return this.options.showValuesInOriginalOrder !== false;
}
valuesSource() {
let question = this.question;
if (!!question.choicesFromQuestion && !!question.survey) {
question = question.survey.getQuestionByName(question.choicesFromQuestion);
}
return question["activeChoices"];
}
getValues() {
const values = this.valuesSource().map((choice) => choice.value);
if (this.question.hasNone) {
values.push(this.question.noneItem.value);
}
if (this.question.hasOther) {
values.push("other");
}
if (this.showMissingAnswers) {
values.unshift(undefined);
}
if (this.showValuesInOriginalOrder) {
return values.reverse();
}
return values;
}
getLabels() {
if (this.options.useValuesAsLabels) {
return this.getValues();
}
const labels = this.valuesSource().map((choice) => ItemValue.getTextOrHtmlByValue(this.valuesSource(), choice.value));
const selBase = this.question;
if (selBase.hasNone) {
labels.push(selBase.noneText);
}
if (selBase.hasOther) {
labels.push(selBase.otherText);
}
if (this.showMissingAnswers) {
labels.unshift(localization.getString("missingAnswersLabel"));
}
if (this.showValuesInOriginalOrder) {
return labels.reverse();
}
return labels;
}
getPercentages(data) {
var percentages = [];
var percentagePrecision = this._percentagePrecision;
if (data.length < 2) {
data.forEach((res, index) => {
var sum = res.reduce((sum, val) => sum + val, 0);
percentages[index] = res.map((val) => {
var value = percentagePrecision ? +(val / sum).toFixed(percentagePrecision) : Math.round((val / sum) * 10000);
return sum && (value / 100);
});
});
}
else {
for (var i = 0; i < data[0].length; i++) {
var sum = 0;
for (var j = 0; j < data.length; j++) {
sum += data[j][i];
}
for (var j = 0; j < data.length; j++) {
if (!Array.isArray(percentages[j]))
percentages[j] = [];
var value = percentagePrecision ? +((data[j][i] / sum) * 100).toFixed(percentagePrecision) : Math.round((data[j][i] / sum) * 100);
percentages[j][i] = sum && value;
}
}
}
return percentages;
}
answersDataReady(answersData) {
let result = {};
if (this.hideEmptyAnswers) {
result = hideEmptyAnswersInData(answersData);
}
else {
result = answersData;
}
if (this.topN > 0) {
result.datasets[0] = result.datasets[0].slice(-this.topN);
result.labels = result.labels.slice(-this.topN);
result.colors = result.colors.slice(-this.topN);
result.texts[0] = result.texts[0].slice(-this.topN);
}
return result;
}
/**
* Returns object with all infotmation for data visualization: datasets, labels, colors, additional texts (percentage).
*/
getAnswersData() {
return __awaiter(this, void 0, void 0, function* () {
let seriesLabels = this.getSeriesLabels();
let datasets = (yield this.getCalculatedValues());
let labels = this.getLabels();
let colors = this.getColors();
var texts = this.showPercentages ? this.getPercentages(datasets) : datasets;
if (this.transposeData) {
datasets = this.transpose(datasets);
texts = this.transpose(texts);
const temp = seriesLabels;
seriesLabels = labels;
labels = temp;
}
if (this.answersOrder == "asc" || this.answersOrder == "desc") {
var zippedArray = this.showPercentages
? DataHelper.zipArrays(labels, colors, texts[0])
: DataHelper.zipArrays(labels, colors);
let dict = DataHelper.sortDictionary(zippedArray, datasets[0], this.answersOrder == "desc");
let unzippedArray = DataHelper.unzipArrays(dict.keys);
labels = unzippedArray[0];
colors = unzippedArray[1];
if (this.showPercentages)
texts[0] = unzippedArray[2];
datasets[0] = dict.values;
}
let answersData = {
datasets,
labels,
colors,
texts,
seriesLabels,
};
answersData = this.answersDataReady(answersData);
this.onAnswersDataReady.fire(this, answersData);
return answersData;
});
}
convertFromExternalData(externalCalculatedData) {
const values = this.getValues();
const series = this.getSeriesValues();
const innerCalculatedData = [];
if (series.length > 0) {
for (let i = 0; i < values.length; i++) {
const seriesData = [];
for (let j = 0; j < series.length; j++) {
if (!!externalCalculatedData[series[j]]) {
seriesData.push(externalCalculatedData[series[j]][values[i]] || 0);
}
else {
seriesData.push(0);
}
}
innerCalculatedData.push(seriesData);
}
}
else {
const seriesData = [];
for (let i = 0; i < values.length; i++) {
seriesData.push(externalCalculatedData[values[i]] || 0);
}
innerCalculatedData.push(seriesData);
}
return innerCalculatedData;
}
transpose(data) {
const dim2 = data[0].length;
const result = new Array(dim2);
for (let i = 0; i < dim2; ++i)
result[i] = new Array(data.length);
for (let i = 0; i < data.length; ++i)
for (let j = 0; j < dim2; ++j) {
result[j][i] = data[i][j];
}
return result;
}
getState() {
let state = {};
SelectBase._stateProperties.forEach(propertyName => {
state[propertyName] = this[propertyName];
});
if (!!this.selectedItem) {
state.filter = this.selectedItem.value;
}
return state;
}
setState(state) {
SelectBase._stateProperties.forEach(propertyName => {
if (state[propertyName] !== undefined) {
this[propertyName] = state[propertyName];
}
});
const selectedItem = ItemValue.getItemByValue(this.question.visibleChoices, state.filter);
this.setSelection(selectedItem !== null && selectedItem !== void 0 ? selectedItem : undefined);
}
}
SelectBase.topNValuesDefaults = [-1, 5, 10, 20];
SelectBase._stateProperties = ["chartType", "answersOrder", "hideEmptyAnswers", "topN"];
class Matrix extends SelectBase {
constructor(question, data, options, name) {
super(question, data, options, name || "matrix");
// this.getAnswersData();
}
get matrixQuestion() {
return this.question;
}
isSupportMissingAnswers() {
return false;
}
getSeriesValues() {
return this.matrixQuestion.rows.map((row) => "" + row.value);
}
getSeriesLabels() {
return this.matrixQuestion.rows.map((row) => ItemValue.getTextOrHtmlByValue(this.matrixQuestion.rows, row.value));
}
getSelectedItemByText(itemText) {
return this.matrixQuestion.columns.filter((column) => column.text === itemText)[0];
}
valuesSource() {
return this.matrixQuestion.columns;
}
getHasAnswersInAllSeriesArray(datasets) {
let result = Array();
for (let i = 0; i < datasets[0].length; i++) {
for (let j = 0; j < datasets.length; j++) {
if (datasets[j][i] != 0) {
result[i] = true;
}
}
}
return result;
}
getHasAnswersInSeries(dataset) {
for (let i = 0; i < dataset.length; i++) {
if (dataset[i] != 0) {
return true;
}
}
return false;
}
hideEmptyAnswersInData(answersData) {
const result = {
datasets: [],
labels: [],
colors: [],
texts: [],
seriesLabels: [],
};
const hasAnswersInAllSeriesArr = this.getHasAnswersInAllSeriesArray(answersData.datasets);
for (let i = 0; i < answersData.datasets.length; i++) {
const hasAnswersInSeries = this.getHasAnswersInSeries(answersData.datasets[i]);
if (hasAnswersInSeries) {
result.labels.push(answersData.labels[i]);
result.colors.push(answersData.colors[i]);
}
else {
continue;
}
const datasets = [];
const texts = [];
for (let j = 0; j < answersData.datasets[0].length; j++) {
if (hasAnswersInAllSeriesArr[j]) {
datasets.push(answersData.datasets[i][j]);
texts.push(answersData.texts[i][j]);
}
}
result.datasets.push(datasets);
result.texts.push(texts);
}
for (let i = 0; i < answersData.datasets[0].length; i++) {
if (hasAnswersInAllSeriesArr[i]) {
result.seriesLabels.push(answersData.seriesLabels[i]);
}
}
return result;
}
getCalculatedValuesCore() {
const statistics = super.getCalculatedValuesCore();
const series = this.getSeriesValues();
const values = this.getValues();
const preparedData = [];
values.forEach((val, valueIndex) => {
const seriesData = series.map((seriesName, seriesIndex) => statistics[seriesIndex][valueIndex]);
preparedData.push(seriesData);
});
return preparedData;
}
}
class BooleanModel extends SelectBase {
constructor(question, data, options, name) {
super(question, data, options, name || "boolean");
}
getCorrectAnswerText() {
const correctAnswerValue = this.booleanQuestion.correctAnswer;
if (this.booleanQuestion.valueTrue !== undefined && this.booleanQuestion.valueTrue === correctAnswerValue || !!correctAnswerValue) {
return this.booleanQuestion.locLabelTrue.textOrHtml;
}
if (this.booleanQuestion.valueFalse !== undefined && this.booleanQuestion.valueFalse === correctAnswerValue || !correctAnswerValue) {
return this.booleanQuestion.locLabelFalse.textOrHtml;
}
return correctAnswerValue;
}
get booleanQuestion() {
return this.question;
}
getSelectedItemByText(itemText) {
const labels = this.getLabels();
const values = this.getValues();
return new ItemValue(values[labels.indexOf(itemText)], itemText);
}
getValues() {
const values = [
this.booleanQuestion.valueTrue !== undefined
? this.booleanQuestion.valueTrue
: true,
this.booleanQuestion.valueFalse !== undefined
? this.booleanQuestion.valueFalse
: false,
];
if (this.showMissingAnswers) {
values.push(undefined);
}
return values;
}
getLabels() {
var labels = [].concat(this.getValues());
if (this.booleanQuestion.labelTrue !== undefined) {
labels[0] = this.booleanQuestion.locLabelTrue.textOrHtml;
}
if (this.booleanQuestion.labelFalse !== undefined) {
labels[1] = this.booleanQuestion.locLabelFalse.textOrHtml;
}
if (this.showMissingAnswers) {
labels[2] = localization.getString("missingAnswersLabel");
}
return labels;
}
}
BooleanModel.trueColor = "";
BooleanModel.falseColor = "";
class HistogramModel extends SelectBase {
constructor(question, data, options, name) {
super(question, data, options, name || "histogram");
this.valueType = "number";
this._cachedValues = undefined;
this._continiousData = undefined;
this._cachedIntervals = undefined;
this._intervalPrecision = 2;
if (this.options.intervalPrecision !== undefined) {
this._intervalPrecision = this.options.intervalPrecision;
}
const questionType = question.getType();
if (questionType === "text" && (question["inputType"] === "date" || question["inputType"] === "datetime")) {
this.valueType = "date";
}
else {
this.valueType = "number";
}
}
reset() {
this._continiousData = undefined;
this._cachedValues = undefined;
this._cachedIntervals = undefined;
}
getContiniousValue(value) {
if (this.valueType === "date") {
return Date.parse(value);
}
return parseFloat(value);
}
getString(value) {
if (this.valueType === "date") {
return new Date(value).toLocaleDateString();
}
return "" + value;
}
toPrecision(value) {
const base = Math.pow(10, this._intervalPrecision);
return Math.round(base * value) / base;
}
getSelectedItemByText(itemText) {
if (this.hasCustomIntervals || this.getContiniousValues().length > HistogramModel.UseIntervalsFrom) {
const interval = this.intervals.filter(interval => interval.label === itemText)[0];
return new ItemValue(interval, interval !== undefined ? interval.label : "");
}
const labels = this.getLabels();
const labelIndex = labels.indexOf(itemText);
return new ItemValue(this.getValues()[labelIndex], labels[labelIndex]);
}
/**
* Updates visualizer data.
*/
updateData(data) {
this.reset();
super.updateData(data);
}
onDataChanged() {
this.reset();
super.onDataChanged();
}
getContiniousValues() {
if (this._cachedValues === undefined) {
const series = this.getSeriesValues();
if (series.length === 0) {
series.push("");
}
this._continiousData = {};
series.forEach(seriesValue => this._continiousData[seriesValue] = []);
const hash = {};
this.data.forEach(dataItem => {
const answerData = dataItem[this.name];
if (answerData !== undefined) {
const seriesValue = dataItem[DataProvider.seriesMarkerKey] || "";
// TODO: _continiousData should be sorted in order to speed-up statistics calculation in the getData function
this._continiousData[seriesValue].push(this.getContiniousValue(answerData));
hash[answerData] = answerData;
}
});
this._cachedValues = Object.keys(hash).map(key => ({ original: hash[key], continious: this.getContiniousValue(key) }));
this._cachedValues.sort((a, b) => a.continious - b.continious);
}
return this._cachedValues;
}
isSupportMissingAnswers() {
return false;
}
get needUseRateValues() {
return this.question.getType() == "rating" && Array.isArray(this.question["rateValues"]) && this.question["rateValues"].length > 0;
}
getValues() {
return this.intervals.map(interval => interval.start);
}
getLabels() {
return this.intervals.map(interval => interval.label);
}
get hasCustomIntervals() {
return !!this.questionOptions && Array.isArray(this.questionOptions.intervals);
}
get intervals() {
if (this.hasCustomIntervals) {
return this.questionOptions.intervals;
}
if (this.question.getType() == "rating") {
if (this.needUseRateValues) {
const rateValues = this.question["rateValues"];
rateValues.sort((iv1, iv2) => iv1.value - iv2.value);
return rateValues.map((rateValue, i) => ({
start: rateValue.value,
end: i < rateValues.length - 1 ? rateValues[i + 1].value : rateValue.value + 1,
label: rateValue.text
}));
}
else {
const rateIntervals = [];
for (let i = (this.question["rateMin"] || 0); i <= (this.question["rateMax"] || (HistogramModel.IntervalsCount - 1)); i += (this.question["rateStep"] || 1)) {
rateIntervals.push({
start: i,
end: i + 1,
label: "" + (!!this.question["rateMin"] && !!this.question["rateMax"] ? i : (i + "-" + (i + 1)))
});
}
return rateIntervals;
}
}
if (this._cachedIntervals === undefined) {
const continiousValues = this.getContiniousValues();
this._cachedIntervals = [];
if (continiousValues.length) {
let start = continiousValues[0].continious;
const end = continiousValues[continiousValues.length - 1].continious;
const intervalsCount = HistogramModel.IntervalsCount;
const delta = (end - start) / intervalsCount;
for (let i = 0; i < intervalsCount; ++i) {
const next = start + delta;
const istart = this.toPrecision(start);
const inext = this.toPrecision(next);
this._cachedIntervals.push({
start: istart,
end: i < intervalsCount - 1 ? inext : inext + delta / 100,
label: "" + this.getString(istart) + "-" + this.getString(inext)
});
start = next;
}
}
}
return this._cachedIntervals;
}
convertFromExternalData(externalCalculatedData) {
return [externalCalculatedData];
}
getCalculatedValuesCore() {
this.getContiniousValues();
const intervals = this.intervals;
const statistics = [];
const series = this.getSeriesValues();
if (series.length === 0) {
series.push("");
}
for (var i = 0; i < series.length; ++i) {
statistics.push(intervals.map(i => 0));
this._continiousData[series[i]].forEach(dataValue => {
for (let j = 0; j < intervals.length; ++j) {
if (intervals[j].start <= dataValue && (dataValue < intervals[j].end || j == intervals.length - 1)) {
statistics[i][j]++;
break;
}
}
});
}
return statistics;
}
}
HistogramModel.IntervalsCount = 10;
HistogramModel.UseIntervalsFrom = 10;
class AlternativeVisualizersWrapper extends VisualizerBase {
updateVisualizerSelector() {
if (!!this.visualizerSelector) {
this.visualizerSelector.getElementsByTagName("select")[0].value = this.visualizer.type;
}
}
constructor(visualizers, question, data, options) {
super(question, data, options);
this.visualizers = visualizers;
this.visualizersWithSelection = [];
this.onAfterVisualizerRenderCallback = () => {
this.afterRender(this.contentContainer);
};
this.onVisualizerStateChangedCallback = (s, options) => {
this.stateChanged("visualizer", options);
};
/**
* The event is fired right after AlternativeVisualizersWrapper content type has been changed.
**/
this.onVisualizerChanged = new Event();
this.loadingData = false;
if (!visualizers || visualizers.length < 2) {
throw new Error("VisualizerArrayWrapper works with visualizers collection only.");
}
this.visualizers.forEach((visualizer) => {
visualizer.onUpdate = () => this.invokeOnUpdate();
if (visualizer.supportSelection) {
this._supportSelection = true;
this.visualizersWithSelection.push(visualizer);
}
});
this.registerToolbarItem("changeVisualizer", () => this.visualizerSelector = DocumentHelper.createSelector(this.visualizers.map((visualizer) => {
return {
value: visualizer.type,
text: localization.getString("visualizer_" + visualizer.type),
};
}), (option) => this.visualizer.type === option.value, (e) => this.setVisualizer(e.target.value)));
this.visualizer = visualizers[0];
this.visualizer.onAfterRender.add(this.onAfterVisualizerRenderCallback);
this.visualizer.onStateChanged.add(this.onVisualizerStateChangedCallback);
}
get hasFooter() {
return false;
}
getVisualizers() {
return this.visualizers;
}
/**
* This method selects visualizer to show by it name.
*
* parameters:
* name - the name of visualizer to show,
* quiet - set it to true if you don't want to rise a notification event
*
**/
setVisualizer(type, quiet = false) {
const visualizerCandidate = this.visualizers.filter((v) => v.type === type)[0];
if (!!visualizerCandidate && visualizerCandidate !== this.visualizer) {
if (!!this.visualizer) {
this.visualizer.onStateChanged.remove(this.onVisualizerStateChangedCallback);
this.visualizer.onAfterRender.remove(this.onAfterVisualizerRenderCallback);
this.visualizer.destroy();
}
this.visualizer = visualizerCandidate;
this.refresh();
this.visualizer.onAfterRender.add(this.onAfterVisualizerRenderCallback);
this.visualizer.onStateChanged.add(this.onVisualizerStateChangedCallback);
if (!quiet) {
this.onVisualizerChanged.fire(this, { visualizer: this.visualizer });
this.stateChanged("visualizer", type);
}
this.updateVisualizerSelector();
}
}
updateData(data) {
super.updateData(data);
this.visualizers.forEach((visualizer) => {
visualizer.updateData(data);
});
}
set onDataItemSelected(val) {
this.visualizersWithSelection.forEach((visualizer) => {
visualizer.onDataItemSelected = val;
});
}
setSelection(item) {
this.visualizersWithSelection.forEach((visualizer) => {
visualizer.setSelection(item);
});
}
get selection() {
return this.visualizer.selection || this.selectedItem;
}
renderContent(container) {
this.visualizerContainer = container;
this.visualizer.render(this.visualizerContainer);
}
setBackgroundColorCore(color) {
super.setBackgroundColorCore(color);
this.visualizers.forEach(visualizer => visualizer.backgroundColor = color);
}
/**
* Returns an object with properties that describe a current visualizer state. The properties are different for each individual visualizer.
*
* > This method is overriden in descendant classes.
* @see setState
*/
getState() {
const currentVisualizerState = this.visualizer.getState();
const state = {
visualizer: this.visualizer.type,
};
if (Object.keys(currentVisualizerState).length > 0) {
state.state = currentVisualizerState;
}
return state;
}
/**
* Sets the visualizer's state.
*
* > This method is overriden in descendant classes.
* @see getState
*/
setState(state) {
if (!!state.visualizer) {
this.setVisualizer(state.visualizer, true);
}
if (!!state.state) {
this.visualizer.setState(state.state);
}
}
destroy() {
this.visualizers.forEach((visualizer) => {
visualizer.onAfterRender.remove(this.onAfterVisualizerRenderCallback);
visualizer.onStateChanged.remove(this.onVisualizerStateChangedCallback);
visualizer.onUpdate = undefined;
});
this.visualizer.destroy();
super.destroy();
}
}
VisualizationManager.registerAltVisualizerSelector(AlternativeVisualizersWrapper);
class FilterInfo {
constructor(visualizer) {
this._htmlElement = undefined;
this.text = undefined;
this._htmlElement = (DocumentHelper.createElement("div", "sa-question__filter"));
this.text = DocumentHelper.createElement("span", "sa-question__filter-text");
this._htmlElement.appendChild(this.text);
const filterClear = DocumentHelper.createButton(() => {
visualizer.setSelection(undefined);
}, localization.getString("clearButton"));
this._htmlElement.appendChild(filterClear);
}
get htmlElement() {
return this._htmlElement;
}
update(selection) {
if (selection !== undefined && selection.value !== undefined) {
this._htmlElement.style.display = "inline-flex";
this.text.innerText = localization.getString("filter") + ": [" + selection.text + "]";
}
else {
this._htmlElement.style.display = "none";
this.text.innerText = "";
}
}
}
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var muuri$1 = {exports: {}};
/**
* Muuri v0.8.0
* https://github.com/haltu/muuri
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
* @license MIT
*
* Muuri Packer
* Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*
* Muuri Ticker / Muuri Emitter / Muuri Queue
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*/
var muuri = muuri$1.exports;
var hasRequiredMuuri;
function requireMuuri () {
if (hasRequiredMuuri) return muuri$1.exports;
hasRequiredMuuri = 1;
(function (module, exports) {
(function (global, factory) {
module.exports = factory() ;
}(muuri, function () {
var namespace = 'Muuri';
var gridInstances = {};
var actionSwap = 'swap';
var actionMove = 'move';
var eventSynchronize = 'synchronize';
var eventLayoutStart = 'layoutStart';
var eventLayoutEnd = 'layoutEnd';
var eventAdd = 'add';
var eventRemove = 'remove';
var eventShowStart = 'showStart';
var eventShowEnd = 'showEnd';
var eventHideStart = 'hideStart';
var eventHideEnd = 'hideEnd';
var eventFilter = 'filter';
var eventSort = 'sort';
var eventMove = 'move';
var eventSend = 'send';
var eventBeforeSend = 'beforeSend';
var eventReceive = 'receive';
var eventBeforeReceive = 'beforeReceive';
var eventDragInit = 'dragInit';
var eventDragStart = 'dragStart';
var eventDragMove = 'dragMove';
var eventDragScroll = 'dragScroll';
var eventDragEnd = 'dragEnd';
var eventDragReleaseStart = 'dragReleaseStart';
var eventDragReleaseEnd = 'dragReleaseEnd';
var eventDestroy = 'destroy';
/**
* Event emitter constructor.
*
* @class
*/
function Emitter() {
this._events = {};
this._queue = [];
this._counter = 0;
this._isDestroyed = false;
}
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @memberof Emitter.prototype
* @param {String} event
* @param {Function} listener
* @returns {Emitter}
*/
Emitter.prototype.on = function(event, listener) {
if (this._isDestroyed) return this;
// Get listeners queue and create it if it does not exist.
var listeners = this._events[event];
if (!listeners) listeners = this._events[event] = [];
// Add the listener to the queue.
listeners.push(listener);
return this;
};
/**
* Unbind all event listeners that match the provided listener function.
*
* @public
* @memberof Emitter.prototype
* @param {String} event
* @param {Function} [listener]
* @returns {Emitter}
*/
Emitter.prototype.off = function(event, listener) {
if (this._isDestroyed) return this;
// Get listeners and return immediately if none is found.
var listeners = this._events[event];
if (!listeners || !listeners.length) return this;
// If no specific listener is provided remove all listeners.
if (!listener) {
listeners.length = 0;
return this;
}
// Remove all matching listeners.
var i = listeners.length;
while (i--) {
if (listener === listeners[i]) listeners.splice(i, 1);
}
return this;
};
/**
* Emit all listeners in a specified event with the provided arguments.
*
* @public
* @memberof Emitter.prototype
* @param {String} event
* @param {*} [arg1]
* @param {*} [arg2]
* @param {*} [arg3]
* @returns {Emitter}
*/
Emitter.prototype.emit = function(event, arg1, arg2, arg3) {
if (this._isDestroyed) return this;
// Get event listeners and quit early if there's no listeners.
var listeners = this._events[event];
if (!listeners || !listeners.length) return this;
var queue = this._queue;
var qLength = queue.length;
var aLength = arguments.length - 1;
var i;
// Add the current listeners to the callback queue before we process them.
// This is necessary to guarantee that all of the listeners are called in
// correct order even if new event listeners are removed/added during
// processing and/or events are emitted during processing.
for (i = 0; i < listeners.length; i++) {
queue.push(listeners[i]);
}
// Increment queue counter. This is needed for the scenarios where emit is
// triggered while the queue is already processing. We need to keep track of
// how many "queue processors" there are active so that we can safely reset
// the queue in the end when the last queue processor is finished.
++this._counter;
// Process the queue (the specific part of it for this emit).
for (i = qLength, qLength = queue.length; i < qLength; i++) {
// prettier-ignore
aLength === 0 ? queue[i]() :
aLength === 1 ? queue[i](arg1) :
aLength === 2 ? queue[i](arg1, arg2) :
queue[i](arg1, arg2, arg3);
// Stop processing if the emitter is destroyed.
if (this._isDestroyed) return this;
}
// Decrement queue process counter.
--this._counter;
// Reset the queue if there are no more queue processes running.
if (!this._counter) queue.length = 0;
return this;
};
/**
* Destroy emitter instance. Basically just removes all bound listeners.
*
* @public
* @memberof Emitter.prototype
* @returns {Emitter}
*/
Emitter.prototype.destroy = function() {
if (this._isDestroyed) return this;
var events = this._events;
var event;
// Flag as destroyed.
this._isDestroyed = true;
// Reset queue (if queue is currently processing this will also stop that).
this._queue.length = this._counter = 0;
// Remove all listeners.
for (event in events) {
if (events[event]) {
events[event].length = 0;
events[event] = undefined;
}
}
return this;
};
// Set up the default export values.
var transformStyle = 'transform';
var transformProp = 'transform';
// Find the supported transform prop and style names.
var docElemStyle = window.document.documentElement.style;
var style = 'transform';
var styleCap = 'Transform';
var found = false;
['', 'Webkit', 'Moz', 'O', 'ms'].forEach(function(prefix) {
if (found) return;
var propName = prefix ? prefix + styleCap : style;
if (docElemStyle[propName] !== undefined) {
prefix = prefix.toLowerCase();
transformStyle = prefix ? '-' + prefix + '-' + style : style;
transformProp = propName;
found = true;
}
});
var stylesCache = typeof WeakMap === 'function' ? new WeakMap() : null;
/**
* Returns the computed value of an element's style property as a string.
*
* @param {HTMLElement} element
* @param {String} style
* @returns {String}
*/
function getStyle(element, style) {
var styles = stylesCache && stylesCache.get(element);
if (!styles) {
styles = window.getComputedStyle(element, null);
if (stylesCache) stylesCache.set(element, styles);
}
return styles.getPropertyValue(style === 'transform' ? transformStyle : style);
}
var styleNameRegEx = /([A-Z])/g;
/**
* Transforms a camel case style property to kebab case style property.
*
* @param {String} string
* @returns {String}
*/
function getStyleName(string) {
return string.replace(styleNameRegEx, '-$1').toLowerCase();
}
var strFunction = 'function';
/**
* Check if a value is a function.
*
* @param {*} val
* @returns {Boolean}
*/
function isFunction(val) {
return typeof val === strFunction;
}
var transformStyle$1 = 'transform';
/**
* Set inline styles to an element.
*
* @param {HTMLElement} element
* @param {Object} styles
*/
function setStyles(element, styles) {
for (var prop in styles) {
element.style[prop === transformStyle$1 ? transformProp : prop] = styles[prop];
}
}
/**
* Item animation handler powered by Web Animations API.
*
* @class
* @param {HTMLElement} element
*/
function ItemAnimate(element) {
this._element = element;
this._animation = null;
this._callback = null;
this._props = [];
this._values = [];
this._keyframes = [];
this._options = {};
this._isDestroyed = false;
this._onFinish = this._onFinish.bind(this);
}
/**
* Public prototype methods
* ************************
*/
/**
* Start instance's animation. Automatically stops current animation if it is
* running.
*
* @public
* @memberof ItemAnimate.prototype
* @param {Object} propsFrom
* @param {Object} propsTo
* @param {Object} [options]
* @param {Number} [options.duration=300]
* @param {String} [options.easing='ease']
* @param {Function} [options.onFinish]
*/
ItemAnimate.prototype.start = function(propsFrom, propsTo, options) {
if (this._isDestroyed) return;
var animation = this._animation;
var currentProps = this._props;
var currentValues = this._values;
var opts = options || 0;
var cancelAnimation = false;
// If we have an existing animation running, let's check if it needs to be
// cancelled or if it can continue running.
if (animation) {
var propCount = 0;
var propIndex;
// Check if the requested animation target props and values match with the
// current props and values.
for (var propName in propsTo) {
++propCount;
propIndex = currentProps.indexOf(propName);
if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) {
cancelAnimation = true;
break;
}
}
// Check if the target props count matches current props count. This is
// needed for the edge case scenario where target props contain the same
// styles as current props, but the current props have some additional
// props.
if (!cancelAnimation && propCount !== currentProps.length) {
cancelAnimation = true;
}
}
// Cancel animation (if required).
if (cancelAnimation) animation.cancel();
// Store animation callback.
this._callback = isFunction(opts.onFinish) ? opts.onFinish : null;
// If we have a running animation that does not need to be cancelled, let's
// call it a day here and let it run.
if (animation && !cancelAnimation) return;
// Store target props and values to instance.
currentProps.length = currentValues.length = 0;
for (propName in propsTo) {
currentProps.push(propName);
currentValues.push(propsTo[propName]);
}
// Set up keyframes.
var animKeyframes = this._keyframes;
animKeyframes[0] = propsFrom;
animKeyframes[1] = propsTo;
// Set up options.
var animOptions = this._options;
animOptions.duration = opts.duration || 300;
animOptions.easing = opts.easing || 'ease';
// Start the animation
var element = this._element;
animation = element.animate(animKeyframes, animOptions);
animation.onfinish = this._onFinish;
this._animation = animation;
// Set the end styles. This makes sure that the element stays at the end
// values after animation is finished.
setStyles(element, propsTo);
};
/**
* Stop instance's current animation if running.
*
* @public
* @memberof ItemAnimate.prototype
* @param {Object} [styles]
*/
ItemAnimate.prototype.stop = function(styles) {
if (this._isDestroyed || !this._animation) return;
var element = this._element;
var currentProps = this._props;
var currentValues = this._values;
var propName;
var propValue;
var i;
// Calculate (if not provided) and set styles.
if (!styles) {
for (i = 0; i < currentProps.length; i++) {
propName = currentProps[i];
propValue = getStyle(element, getStyleName(propName));
element.style[propName === 'transform' ? transformProp : propName] = propValue;
}
} else {
setStyles(element, styles);
}
// Cancel animation.
this._animation.cancel();
this._animation = this._callback = null;
// Reset current props and values.
currentProps.length = currentValues.length = 0;
};
/**
* Check if the item is being animated currently.
*
* @public
* @memberof ItemAnimate.prototype
* @return {Boolean}
*/
ItemAnimate.prototype.isAnimating = function() {
return !!this._animation;
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
* @memberof ItemAnimate.prototype
*/
ItemAnimate.prototype.destroy = function() {
if (this._isDestroyed) return;
this.stop();
this._element = this._options = this._keyframes = null;
this._isDestroyed = true;
};
/**
* Private prototype methods
* *************************
*/
/**
* Animation end handler.
*
* @private
* @memberof ItemAnimate.prototype
*/
ItemAnimate.prototype._onFinish = function() {
var callback = this._callback;
this._animation = this._callback = null;
this._props.length = this._values.length = 0;
callback && callback();
};
var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O'];
/**
* Get prefixed CSS property name when given a non-prefixed CSS property name.
* @param {Object} elemStyle
* @param {String} propName
* @returns {!String}
*/
function getPrefixedPropName(elemStyle, propName) {
var camelPropName = propName[0].toUpperCase() + propName.slice(1);
var i = 0;
var prefix;
var prefixedPropName;
while (i < vendorPrefixes.length) {
prefix = vendorPrefixes[i];
prefixedPropName = prefix ? prefix + camelPropName : propName;
if (prefixedPropName in elemStyle) return prefixedPropName;
++i;
}
return null;
}
var dt = 1000 / 60;
var raf = (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return this.setTimeout(function() {
callback(dt);
}, dt);
}
).bind(window);
// Detect support for passive events:
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
var isPassiveEventsSupported = false;
try {
var passiveOpts = Object.defineProperty({}, 'passive', {
get: function() {
isPassiveEventsSupported = true;
}
});
window.addEventListener('testPassive', null, passiveOpts);
window.removeEventListener('testPassive', null, passiveOpts);
} catch (e) {}
// Dragger events.
var events = {
start: 'start',
move: 'move',
end: 'end',
cancel: 'cancel'
};
var hasTouchEvents = !!('ontouchstart' in window || window.TouchEvent);
var hasPointerEvents = !!window.PointerEvent;
var hasMsPointerEvents = !!window.navigator.msPointerEnabled;
var isAndroid = /(android)/i.test(window.navigator.userAgent);
var listenerOptions = isPassiveEventsSupported ? { passive: true } : false;
var taProp = 'touchAction';
var taPropPrefixed = getPrefixedPropName(window.document.documentElement.style, taProp);
var taDefaultValue = 'auto';
/**
* Creates a new Dragger instance for an element.
*
* @public
* @class
* @param {HTMLElement} element
* @param {Object} [cssProps]
*/
function Dragger(element, cssProps) {
this._element = element;
this._emitter = new Emitter();
this._isDestroyed = false;
this._cssProps = {};
this._touchAction = '';
this._startEvent = null;
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._preStartCheck = this._preStartCheck.bind(this);
this._abortNonCancelable = this._abortNonCancelable.bind(this);
this._onStart = this._onStart.bind(this);
this._onMove = this._onMove.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onEnd = this._onEnd.bind(this);
// Apply initial css props.
this.setCssProps(cssProps);
// If touch action was not provided with initial css props let's assume it's
// auto.
if (!this._touchAction) {
this.setTouchAction(taDefaultValue);
}
// Prevent native link/image dragging for the item and it's ancestors.
element.addEventListener('dragstart', Dragger._preventDefault, false);
// Listen to start event.
element.addEventListener(Dragger._events.start, this._preStartCheck, listenerOptions);
// If we have touch events, but no pointer events we need to also listen for
// mouse events in addition to touch events for devices which support both
// mouse and touch interaction.
if (hasTouchEvents && !hasPointerEvents && !hasMsPointerEvents) {
element.addEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions);
}
}
/**
* Protected properties
* ********************
*/
Dragger._pointerEvents = {
start: 'pointerdown',
move: 'pointermove',
cancel: 'pointercancel',
end: 'pointerup'
};
Dragger._msPointerEvents = {
start: 'MSPointerDown',
move: 'MSPointerMove',
cancel: 'MSPointerCancel',
end: 'MSPointerUp'
};
Dragger._touchEvents = {
start: 'touchstart',
move: 'touchmove',
cancel: 'touchcancel',
end: 'touchend'
};
Dragger._mouseEvents = {
start: 'mousedown',
move: 'mousemove',
cancel: '',
end: 'mouseup'
};
Dragger._events = (function() {
if (hasPointerEvents) return Dragger._pointerEvents;
if (hasMsPointerEvents) return Dragger._msPointerEvents;
if (hasTouchEvents) return Dragger._touchEvents;
return Dragger._mouseEvents;
})();
Dragger._emitter = new Emitter();
Dragger._activeInstances = [];
/**
* Protected static methods
* ************************
*/
Dragger._preventDefault = function(e) {
if (e.preventDefault && e.cancelable !== false) e.preventDefault();
};
Dragger._activateInstance = function(instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index > -1) return;
Dragger._activeInstances.push(instance);
Dragger._emitter.on(events.move, instance._onMove);
Dragger._emitter.on(events.cancel, instance._onCancel);
Dragger._emitter.on(events.end, instance._onEnd);
if (Dragger._activeInstances.length === 1) {
Dragger._bindListeners();
}
};
Dragger._deactivateInstance = function(instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index === -1) return;
Dragger._activeInstances.splice(index, 1);
Dragger._emitter.off(events.move, instance._onMove);
Dragger._emitter.off(events.cancel, instance._onCancel);
Dragger._emitter.off(events.end, instance._onEnd);
if (!Dragger._activeInstances.length) {
Dragger._unbindListeners();
}
};
Dragger._bindListeners = function() {
var events = Dragger._events;
window.addEventListener(events.move, Dragger._onMove, listenerOptions);
window.addEventListener(events.end, Dragger._onEnd, listenerOptions);
events.cancel && window.addEventListener(events.cancel, Dragger._onCancel, listenerOptions);
};
Dragger._unbindListeners = function() {
var events = Dragger._events;
window.removeEventListener(events.move, Dragger._onMove, listenerOptions);
window.removeEventListener(events.end, Dragger._onEnd, listenerOptions);
events.cancel && window.removeEventListener(events.cancel, Dragger._onCancel, listenerOptions);
};
Dragger._getEventPointerId = function(event) {
// If we have pointer id available let's use it.
if (typeof event.pointerId === 'number') {
return event.pointerId;
}
// For touch events let's get the first changed touch's identifier.
if (event.changedTouches) {
return event.changedTouches[0] ? event.changedTouches[0].identifier : null;
}
// For mouse/other events let's provide a static id.
return 1;
};
Dragger._getTouchById = function(event, id) {
// If we have a pointer event return the whole event if there's a match, and
// null otherwise.
if (typeof event.pointerId === 'number') {
return event.pointerId === id ? event : null;
}
// For touch events let's check if there's a changed touch object that matches
// the pointerId in which case return the touch object.
if (event.changedTouches) {
for (var i = 0; i < event.changedTouches.length; i++) {
if (event.changedTouches[i].identifier === id) {
return event.changedTouches[i];
}
}
return null;
}
// For mouse/other events let's assume there's only one pointer and just
// return the event.
return event;
};
Dragger._onMove = function(e) {
Dragger._emitter.emit(events.move, e);
};
Dragger._onCancel = function(e) {
Dragger._emitter.emit(events.cancel, e);
};
Dragger._onEnd = function(e) {
Dragger._emitter.emit(events.end, e);
};
/**
* Private prototype methods
* *************************
*/
/**
* Reset current drag operation (if any).
*
* @private
* @memberof Dragger.prototype
*/
Dragger.prototype._reset = function() {
if (this._isDestroyed) return;
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._startEvent = null;
this._element.removeEventListener(
Dragger._touchEvents.start,
this._abortNonCancelable,
listenerOptions
);
Dragger._deactivateInstance(this);
};
/**
* Create a custom dragger event from a raw event.
*
* @private
* @memberof Dragger.prototype
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
* @returns {DraggerEvent}
*/
Dragger.prototype._createEvent = function(type, e) {
var touch = this._getTrackedTouch(e);
return {
// Hammer.js compatibility interface.
type: type,
srcEvent: e,
distance: this.getDistance(),
deltaX: this.getDeltaX(),
deltaY: this.getDeltaY(),
deltaTime: type === events.start ? 0 : this.getDeltaTime(),
isFirst: type === events.start,
isFinal: type === events.end || type === events.cancel,
// Partial Touch API interface.
identifier: this._pointerId,
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
target: touch.target
};
};
/**
* Emit a raw event as dragger event internally.
*
* @private
* @memberof Dragger.prototype
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._emit = function(type, e) {
this._emitter.emit(type, this._createEvent(type, e));
};
/**
* If the provided event is a PointerEvent this method will return it if it has
* the same pointerId as the instance. If the provided event is a TouchEvent
* this method will try to look for a Touch instance in the changedTouches that
* has an identifier matching this instance's pointerId. If the provided event
* is a MouseEvent (or just any other event than PointerEvent or TouchEvent)
* it will be returned immediately.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)}
* @returns {?(Touch|PointerEvent|MouseEvent)}
*/
Dragger.prototype._getTrackedTouch = function(e) {
if (this._pointerId === null) {
return null;
} else {
return Dragger._getTouchById(e, this._pointerId);
}
};
/**
* A pre-handler for start event that checks if we can start dragging.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._preStartCheck = function(e) {
if (this._isDestroyed) return;
// Make sure the element is not being dragged currently.
if (this.isDragging()) return;
// Special cancelable check for Android to prevent drag procedure from
// starting if native scrolling is in progress. Part 1.
if (isAndroid && e.cancelable === false) return;
// Make sure left button is pressed on mouse.
if (e.button) return;
// Get (and set) pointer id.
this._pointerId = Dragger._getEventPointerId(e);
if (this._pointerId === null) return;
// Store the start event and trigger start (async or sync). Pointer events
// are emitted before touch events if the browser supports both of them. And
// if you try to move an element before `touchstart` is emitted the pointer
// events for that element will be canceled. The fix is to delay the emitted
// pointer events in such a scenario by one frame so that `touchstart` has
// time to be emitted before the element is (potentially) moved.
this._startEvent = e;
if (hasTouchEvents && (hasPointerEvents || hasMsPointerEvents)) {
// Special cancelable check for Android to prevent drag procedure from
// starting if native scrolling is in progress. Part 2.
if (isAndroid) {
this._element.addEventListener(
Dragger._touchEvents.start,
this._abortNonCancelable,
listenerOptions
);
}
raf(this._onStart);
} else {
this._onStart();
}
};
/**
* Abort start event if it turns out to be non-cancelable.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._abortNonCancelable = function(e) {
this._element.removeEventListener(
Dragger._touchEvents.start,
this._abortNonCancelable,
listenerOptions
);
if (this._startEvent && e.cancelable === false) {
this._pointerId = null;
this._startEvent = null;
}
};
/**
* Start the drag procedure if possible.
*
* @private
* @memberof Dragger.prototype
*/
Dragger.prototype._onStart = function() {
var e = this._startEvent;
if (!e) return;
this._startEvent = null;
var touch = this._getTrackedTouch(e);
if (!touch) return;
// Set up init data and emit start event.
this._startX = this._currentX = touch.clientX;
this._startY = this._currentY = touch.clientY;
this._startTime = Date.now();
this._emit(events.start, e);
Dragger._activateInstance(this);
};
/**
* Handler for move event.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onMove = function(e) {
var touch = this._getTrackedTouch(e);
if (!touch) return;
this._currentX = touch.clientX;
this._currentY = touch.clientY;
this._emit(events.move, e);
};
/**
* Handler for move cancel event.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onCancel = function(e) {
if (!this._getTrackedTouch(e)) return;
this._emit(events.cancel, e);
this._reset();
};
/**
* Handler for end event.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onEnd = function(e) {
if (!this._getTrackedTouch(e)) return;
this._emit(events.end, e);
this._reset();
};
/**
* Public prototype methods
* ************************
*/
/**
* Check if the element is being dragged at the moment.
*
* @public
* @memberof Dragger.prototype
* @returns {Boolean}
*/
Dragger.prototype.isDragging = function() {
return this._pointerId !== null;
};
/**
* Set element's touch-action CSS property.
*
* @public
* @memberof Dragger.prototype
* @param {String} value
*/
Dragger.prototype.setTouchAction = function(value) {
// Store unmodified touch action value (we trust user input here).
this._touchAction = value;
// Set touch-action style.
if (taPropPrefixed) {
this._cssProps[taPropPrefixed] = '';
this._element.style[taPropPrefixed] = value;
}
// If we have an unsupported touch-action value let's add a special listener
// that prevents default action on touch start event. A dirty hack, but best
// we can do for now. The other options would be to somehow polyfill the
// unsupported touch action behavior with custom heuristics which sounds like
// a can of worms.
if (hasTouchEvents) {
this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false);
if (this._element.style[taPropPrefixed] !== value) {
this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false);
}
}
};
/**
* Update element's CSS properties. Accepts an object with camel cased style
* props with value pairs as it's first argument.
*
* @public
* @memberof Dragger.prototype
* @param {Object} [newProps]
*/
Dragger.prototype.setCssProps = function(newProps) {
if (!newProps) return;
var currentProps = this._cssProps;
var element = this._element;
var prop;
var prefixedProp;
// Reset current props.
for (prop in currentProps) {
element.style[prop] = currentProps[prop];
delete currentProps[prop];
}
// Set new props.
for (prop in newProps) {
// Make sure we have a value for the prop.
if (!newProps[prop]) continue;
// Special handling for touch-action.
if (prop === taProp) {
this.setTouchAction(newProps[prop]);
continue;
}
// Get prefixed prop and skip if it does not exist.
prefixedProp = getPrefixedPropName(element.style, prop);
if (!prefixedProp) continue;
// Store the prop and add the style.
currentProps[prefixedProp] = '';
element.style[prefixedProp] = newProps[prop];
}
};
/**
* How much the pointer has moved on x-axis from start position, in pixels.
* Positive value indicates movement from left to right.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDeltaX = function() {
return this._currentX - this._startX;
};
/**
* How much the pointer has moved on y-axis from start position, in pixels.
* Positive value indicates movement from top to bottom.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDeltaY = function() {
return this._currentY - this._startY;
};
/**
* How far (in pixels) has pointer moved from start position.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDistance = function() {
var x = this.getDeltaX();
var y = this.getDeltaY();
return Math.sqrt(x * x + y * y);
};
/**
* How long has pointer been dragged.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDeltaTime = function() {
return this._startTime ? Date.now() - this._startTime : 0;
};
/**
* Bind drag event listeners.
*
* @public
* @memberof Dragger.prototype
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.on = function(eventName, listener) {
this._emitter.on(eventName, listener);
};
/**
* Unbind drag event listeners.
*
* @public
* @memberof Dragger.prototype
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.off = function(events, listener) {
this._emitter.off(eventName, listener);
};
/**
* Destroy the instance and unbind all drag event listeners.
*
* @public
* @memberof Dragger.prototype
*/
Dragger.prototype.destroy = function() {
if (this._isDestroyed) return;
var element = this._element;
var events = Dragger._events;
// Reset data and deactivate the instance.
this._reset();
// Destroy emitter.
this._emitter.destroy();
// Unbind event handlers.
element.removeEventListener(events.start, this._preStartCheck, listenerOptions);
element.removeEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions);
element.removeEventListener('dragstart', Dragger._preventDefault, false);
element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false);
// Reset styles.
for (var prop in this._cssProps) {
element.style[prop] = this._cssProps[prop];
delete this._cssProps[prop];
}
// Reset data.
this._element = null;
// Mark as destroyed.
this._isDestroyed = true;
};
/**
* A ticker system for handling DOM reads and writes in an efficient way.
* Contains a read queue and a write queue that are processed on the next
* animation frame when needed.
*
* @class
*/
function Ticker() {
this._nextStep = null;
this._queue = [];
this._reads = {};
this._writes = {};
this._batch = [];
this._batchReads = {};
this._batchWrites = {};
this._step = this._step.bind(this);
}
Ticker.prototype.add = function(id, readOperation, writeOperation, isPrioritized) {
// First, let's check if an item has been added to the queues with the same id
// and if so -> remove it.
var currentIndex = this._queue.indexOf(id);
if (currentIndex > -1) this._queue[currentIndex] = undefined;
// Add entry.
isPrioritized ? this._queue.unshift(id) : this._queue.push(id);
this._reads[id] = readOperation;
this._writes[id] = writeOperation;
// Finally, let's kick-start the next tick if it is not running yet.
if (!this._nextStep) this._nextStep = raf(this._step);
};
Ticker.prototype.cancel = function(id) {
var currentIndex = this._queue.indexOf(id);
if (currentIndex > -1) {
this._queue[currentIndex] = undefined;
delete this._reads[id];
delete this._writes[id];
}
};
Ticker.prototype._step = function() {
var queue = this._queue;
var reads = this._reads;
var writes = this._writes;
var batch = this._batch;
var batchReads = this._batchReads;
var batchWrites = this._batchWrites;
var length = queue.length;
var id;
var i;
// Reset ticker.
this._nextStep = null;
// Setup queues and callback placeholders.
for (i = 0; i < length; i++) {
id = queue[i];
if (!id) continue;
batch.push(id);
batchReads[id] = reads[id];
delete reads[id];
batchWrites[id] = writes[id];
delete writes[id];
}
// Reset queue.
queue.length = 0;
// Process read callbacks.
for (i = 0; i < length; i++) {
id = batch[i];
if (batchReads[id]) {
batchReads[id]();
delete batchReads[id];
}
}
// Process write callbacks.
for (i = 0; i < length; i++) {
id = batch[i];
if (batchWrites[id]) {
batchWrites[id]();
delete batchWrites[id];
}
}
// Reset batch.
batch.length = 0;
// Restart the ticker if needed.
if (!this._nextStep && queue.length) {
this._nextStep = raf(this._step);
}
};
var ticker = new Ticker();
var layoutTick = 'layout';
var visibilityTick = 'visibility';
var moveTick = 'move';
var scrollTick = 'scroll';
var placeholderTick = 'placeholder';
function addLayoutTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + layoutTick, readCallback, writeCallback);
}
function cancelLayoutTick(itemId) {
return ticker.cancel(itemId + layoutTick);
}
function addVisibilityTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + visibilityTick, readCallback, writeCallback);
}
function cancelVisibilityTick(itemId) {
return ticker.cancel(itemId + visibilityTick);
}
function addMoveTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + moveTick, readCallback, writeCallback, true);
}
function cancelMoveTick(itemId) {
return ticker.cancel(itemId + moveTick);
}
function addScrollTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + scrollTick, readCallback, writeCallback, true);
}
function cancelScrollTick(itemId) {
return ticker.cancel(itemId + scrollTick);
}
function addPlaceholderTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + placeholderTick, readCallback, writeCallback);
}
function cancelPlaceholderTick(itemId) {
return ticker.cancel(itemId + placeholderTick);
}
var ElProto = window.Element.prototype;
var matchesFn =
ElProto.matches ||
ElProto.matchesSelector ||
ElProto.webkitMatchesSelector ||
ElProto.mozMatchesSelector ||
ElProto.msMatchesSelector ||
ElProto.oMatchesSelector ||
function() {
return false;
};
/**
* Check if element matches a CSS selector.
*
* @param {Element} el
* @param {String} selector
* @returns {Boolean}
*/
function elementMatches(el, selector) {
return matchesFn.call(el, selector);
}
/**
* Add class to an element.
*
* @param {HTMLElement} element
* @param {String} className
*/
function addClass(element, className) {
if (element.classList) {
element.classList.add(className);
} else {
if (!elementMatches(element, '.' + className)) {
element.className += ' ' + className;
}
}
}
var tempArray = [];
var numberType = 'number';
/**
* Insert an item or an array of items to array to a specified index. Mutates
* the array. The index can be negative in which case the items will be added
* to the end of the array.
*
* @param {Array} array
* @param {*} items
* @param {Number} [index=-1]
*/
function arrayInsert(array, items, index) {
var startIndex = typeof index === numberType ? index : -1;
if (startIndex < 0) startIndex = array.length - startIndex + 1;
array.splice.apply(array, tempArray.concat(startIndex, 0, items));
tempArray.length = 0;
}
/**
* Normalize array index. Basically this function makes sure that the provided
* array index is within the bounds of the provided array and also transforms
* negative index to the matching positive index.
*
* @param {Array} array
* @param {Number} index
* @param {Boolean} isMigration
*/
function normalizeArrayIndex(array, index, isMigration) {
var length = array.length;
var maxIndex = Math.max(0, isMigration ? length : length - 1);
return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index;
}
/**
* Move array item to another index.
*
* @param {Array} array
* @param {Number} fromIndex
* - Index (positive or negative) of the item that will be moved.
* @param {Number} toIndex
* - Index (positive or negative) where the item should be moved to.
*/
function arrayMove(array, fromIndex, toIndex) {
// Make sure the array has two or more items.
if (array.length < 2) return;
// Normalize the indices.
var from = normalizeArrayIndex(array, fromIndex);
var to = normalizeArrayIndex(array, toIndex);
// Add target item to the new position.
if (from !== to) {
array.splice(to, 0, array.splice(from, 1)[0]);
}
}
/**
* Swap array items.
*
* @param {Array} array
* @param {Number} index
* - Index (positive or negative) of the item that will be swapped.
* @param {Number} withIndex
* - Index (positive or negative) of the other item that will be swapped.
*/
function arraySwap(array, index, withIndex) {
// Make sure the array has two or more items.
if (array.length < 2) return;
// Normalize the indices.
var indexA = normalizeArrayIndex(array, index);
var indexB = normalizeArrayIndex(array, withIndex);
var temp;
// Swap the items.
if (indexA !== indexB) {
temp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = temp;
}
}
var actionCancel = 'cancel';
var actionFinish = 'finish';
var debounceTick = 'debounce';
var debounceId = 0;
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. The returned function accepts one argument which, when
* being "finish", calls the debounce function immediately if it is currently
* waiting to be called, and when being "cancel" cancels the currently queued
* function call.
*
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
function debounce(fn, wait) {
var timeout;
var tickerId = ++debounceId + debounceTick;
if (wait > 0) {
return function(action) {
if (timeout !== undefined) {
timeout = window.clearTimeout(timeout);
ticker.cancel(tickerId);
if (action === actionFinish) fn();
}
if (action !== actionCancel && action !== actionFinish) {
timeout = window.setTimeout(function() {
timeout = undefined;
ticker.add(tickerId, fn, null, true);
}, wait);
}
};
}
return function(action) {
if (action !== actionCancel) fn();
};
}
/**
* Returns true if element is transformed, false if not. In practice the
* element's display value must be anything else than "none" or "inline" as
* well as have a valid transform value applied in order to be counted as a
* transformed element.
*
* Borrowed from Mezr (v0.6.1):
* https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661
*
* @param {HTMLElement} element
* @returns {Boolean}
*/
function isTransformed(element) {
var transform = getStyle(element, 'transform');
if (!transform || transform === 'none') return false;
var display = getStyle(element, 'display');
if (display === 'inline' || display === 'none') return false;
return true;
}
/**
* Returns an absolute positioned element's containing block, which is
* considered to be the closest ancestor element that the target element's
* positioning is relative to. Disclaimer: this only works as intended for
* absolute positioned elements.
*
* @param {HTMLElement} element
* @param {Boolean} [includeSelf=false]
* - When this is set to true the containing block checking is started from
* the provided element. Otherwise the checking is started from the
* provided element's parent element.
* @returns {(Document|Element)}
*/
function getContainingBlock(element, includeSelf) {
// As long as the containing block is an element, static and not
// transformed, try to get the element's parent element and fallback to
// document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339
var document = window.document;
var ret = (element ) || document;
while (ret && ret !== document && getStyle(ret, 'position') === 'static' && !isTransformed(ret)) {
ret = ret.parentElement || document;
}
return ret;
}
/**
* Returns the computed value of an element's style property transformed into
* a float value.
*
* @param {HTMLElement} el
* @param {String} style
* @returns {Number}
*/
function getStyleAsFloat(el, style) {
return parseFloat(getStyle(el, style)) || 0;
}
var offsetA = {};
var offsetB = {};
var offsetDiff = {};
/**
* Returns the element's document offset, which in practice means the vertical
* and horizontal distance between the element's northwest corner and the
* document's northwest corner. Note that this function always returns the same
* object so be sure to read the data from it instead using it as a reference.
*
* @param {(Document|Element|Window)} element
* @param {Object} [offsetData]
* - Optional data object where the offset data will be inserted to. If not
* provided a new object will be created for the return data.
* @returns {Object}
*/
function getOffset(element, offsetData) {
var ret = offsetData || {};
var rect;
// Set up return data.
ret.left = 0;
ret.top = 0;
// Document's offsets are always 0.
if (element === document) return ret;
// Add viewport scroll left/top to the respective offsets.
ret.left = window.pageXOffset || 0;
ret.top = window.pageYOffset || 0;
// Window's offsets are the viewport scroll left/top values.
if (element.self === window.self) return ret;
// Add element's client rects to the offsets.
rect = element.getBoundingClientRect();
ret.left += rect.left;
ret.top += rect.top;
// Exclude element's borders from the offset.
ret.left += getStyleAsFloat(element, 'border-left-width');
ret.top += getStyleAsFloat(element, 'border-top-width');
return ret;
}
/**
* Calculate the offset difference two elements.
*
* @param {HTMLElement} elemA
* @param {HTMLElement} elemB
* @param {Boolean} [compareContainingBlocks=false]
* - When this is set to true the containing blocks of the provided elements
* will be used for calculating the difference. Otherwise the provided
* elements will be compared directly.
* @returns {Object}
*/
function getOffsetDiff(elemA, elemB, compareContainingBlocks) {
offsetDiff.left = 0;
offsetDiff.top = 0;
// If elements are same let's return early.
if (elemA === elemB) return offsetDiff;
// Compare containing blocks if necessary.
if (compareContainingBlocks) {
elemA = getContainingBlock(elemA);
elemB = getContainingBlock(elemB);
// If containing blocks are identical, let's return early.
if (elemA === elemB) return offsetDiff;
}
// Finally, let's calculate the offset diff.
getOffset(elemA, offsetA);
getOffset(elemB, offsetB);
offsetDiff.left = offsetB.left - offsetA.left;
offsetDiff.top = offsetB.top - offsetA.top;
return offsetDiff;
}
var styleOverflow = 'overflow';
var styleOverflowX = 'overflow-x';
var styleOverflowY = 'overflow-y';
var overflowAuto = 'auto';
var overflowScroll = 'scroll';
/**
* Check if an element is scrollable.
*
* @param {HTMLElement} element
* @returns {Boolean}
*/
function isScrollable(element) {
var overflow = getStyle(element, styleOverflow);
if (overflow === overflowAuto || overflow === overflowScroll) return true;
overflow = getStyle(element, styleOverflowX);
if (overflow === overflowAuto || overflow === overflowScroll) return true;
overflow = getStyle(element, styleOverflowY);
if (overflow === overflowAuto || overflow === overflowScroll) return true;
return false;
}
/**
* Collect element's ancestors that are potentially scrollable elements.
*
* @param {HTMLElement} element
* @param {Boolean} [includeSelf=false]
* @param {Array} [data]
* @returns {Array}
*/
function getScrollableAncestors(element, includeSelf, data) {
var ret = data || [];
var parent = includeSelf ? element : element.parentNode;
// Find scroll parents.
while (parent && parent !== document) {
// If element is inside ShadowDOM let's get it's host node from the real
// DOM and continue looping.
if (parent.getRootNode && parent instanceof DocumentFragment) {
parent = parent.getRootNode().host;
continue;
}
// If element is scrollable let's add it to the scrollable list.
if (isScrollable(parent)) {
ret.push(parent);
}
parent = parent.parentNode;
}
// Always add window to the results.
ret.push(window);
return ret;
}
var translateValue = {};
var transformStyle$2 = 'transform';
var transformNone = 'none';
var rxMat3d = /^matrix3d/;
var rxMatTx = /([^,]*,){4}/;
var rxMat3dTx = /([^,]*,){12}/;
var rxNextItem = /[^,]*,/;
/**
* Returns the element's computed translateX and translateY values as a floats.
* The returned object is always the same object and updated every time this
* function is called.
*
* @param {HTMLElement} element
* @returns {Object}
*/
function getTranslate(element) {
translateValue.x = 0;
translateValue.y = 0;
var transform = getStyle(element, transformStyle$2);
if (!transform || transform === transformNone) {
return translateValue;
}
// Transform style can be in either matrix3d(...) or matrix(...).
var isMat3d = rxMat3d.test(transform);
var tX = transform.replace(isMat3d ? rxMat3dTx : rxMatTx, '');
var tY = tX.replace(rxNextItem, '');
translateValue.x = parseFloat(tX) || 0;
translateValue.y = parseFloat(tY) || 0;
return translateValue;
}
/**
* Transform translateX and translateY value into CSS transform style
* property's value.
*
* @param {Number} x
* @param {Number} y
* @returns {String}
*/
function getTranslateString(x, y) {
return 'translateX(' + x + 'px) translateY(' + y + 'px)';
}
/**
* Remove class from an element.
*
* @param {HTMLElement} element
* @param {String} className
*/
function removeClass(element, className) {
if (element.classList) {
element.classList.remove(className);
} else {
if (elementMatches(element, '.' + className)) {
element.className = (' ' + element.className + ' ')
.replace(' ' + className + ' ', ' ')
.trim();
}
}
}
// Drag start predicate states.
var startPredicateInactive = 0;
var startPredicatePending = 1;
var startPredicateResolved = 2;
var startPredicateRejected = 3;
/**
* Bind touch interaction to an item.
*
* @class
* @param {Item} item
*/
function ItemDrag(item) {
var element = item._element;
var grid = item.getGrid();
var settings = grid._settings;
this._item = item;
this._gridId = grid._id;
this._isDestroyed = false;
this._isMigrating = false;
// Start predicate data.
this._startPredicate = isFunction(settings.dragStartPredicate)
? settings.dragStartPredicate
: ItemDrag.defaultStartPredicate;
this._startPredicateState = startPredicateInactive;
this._startPredicateResult = undefined;
// Data for drag sort predicate heuristics.
this._hBlockedIndex = null;
this._hX1 = 0;
this._hX2 = 0;
this._hY1 = 0;
this._hY2 = 0;
// Setup item's initial drag data.
this._reset();
// Bind the methods that needs binding.
this._preStartCheck = this._preStartCheck.bind(this);
this._preEndCheck = this._preEndCheck.bind(this);
this._onScroll = this._onScroll.bind(this);
this._prepareMove = this._prepareMove.bind(this);
this._applyMove = this._applyMove.bind(this);
this._prepareScroll = this._prepareScroll.bind(this);
this._applyScroll = this._applyScroll.bind(this);
this._checkOverlap = this._checkOverlap.bind(this);
// Create debounce overlap checker function.
var sortInterval = settings.dragSortHeuristics.sortInterval;
this._checkOverlapDebounce = debounce(this._checkOverlap, sortInterval);
// Init dragger.
this._dragger = new Dragger(element, settings.dragCssProps);
this._dragger.on('start', this._preStartCheck);
this._dragger.on('move', this._preStartCheck);
this._dragger.on('cancel', this._preEndCheck);
this._dragger.on('end', this._preEndCheck);
}
/**
* Public static methods
* *********************
*/
/**
* Default drag start predicate handler that handles anchor elements
* gracefully. The return value of this function defines if the drag is
* started, rejected or pending. When true is returned the dragging is started
* and when false is returned the dragging is rejected. If nothing is returned
* the predicate will be called again on the next drag movement.
*
* @public
* @memberof ItemDrag
* @param {Item} item
* @param {DraggerEvent} event
* @param {Object} [options]
* - An optional options object which can be used to pass the predicate
* it's options manually. By default the predicate retrieves the options
* from the grid's settings.
* @returns {Boolean}
*/
ItemDrag.defaultStartPredicate = function(item, event, options) {
var drag = item._drag;
var predicate = drag._startPredicateData || drag._setupStartPredicate(options);
// Final event logic. At this stage return value does not matter anymore,
// the predicate is either resolved or it's not and there's nothing to do
// about it. Here we just reset data and if the item element is a link
// we follow it (if there has only been slight movement).
if (event.isFinal) {
drag._finishStartPredicate(event);
return;
}
// Find and store the handle element so we can check later on if the
// cursor is within the handle. If we have a handle selector let's find
// the corresponding element. Otherwise let's use the item element as the
// handle.
if (!predicate.handleElement) {
predicate.handleElement = drag._getStartPredicateHandle(event);
if (!predicate.handleElement) return false;
}
// If delay is defined let's keep track of the latest event and initiate
// delay if it has not been done yet.
if (predicate.delay) {
predicate.event = event;
if (!predicate.delayTimer) {
predicate.delayTimer = window.setTimeout(function() {
predicate.delay = 0;
if (drag._resolveStartPredicate(predicate.event)) {
drag._forceResolveStartPredicate(predicate.event);
drag._resetStartPredicate();
}
}, predicate.delay);
}
}
return drag._resolveStartPredicate(event);
};
/**
* Default drag sort predicate.
*
* @public
* @memberof ItemDrag
* @param {Item} item
* @param {Object} [options]
* @param {Number} [options.threshold=50]
* @param {String} [options.action='move']
* @returns {(Boolean|DragSortCommand)}
* - Returns false if no valid index was found. Otherwise returns drag sort
* command.
*/
ItemDrag.defaultSortPredicate = (function() {
var itemRect = {};
var targetRect = {};
var returnData = {};
var rootGridArray = [];
function getTargetGrid(item, rootGrid, threshold) {
var target = null;
var dragSort = rootGrid._settings.dragSort;
var bestScore = -1;
var gridScore;
var grids;
var grid;
var i;
// Get potential target grids.
if (dragSort === true) {
rootGridArray[0] = rootGrid;
grids = rootGridArray;
} else {
grids = dragSort.call(rootGrid, item);
}
// Return immediately if there are no grids.
if (!Array.isArray(grids)) return target;
// Loop through the grids and get the best match.
for (i = 0; i < grids.length; i++) {
grid = grids[i];
// Filter out all destroyed grids.
if (grid._isDestroyed) continue;
// We need to update the grid's offsets and dimensions since they might
// have changed (e.g during scrolling).
grid._updateBoundingRect();
// Check how much dragged element overlaps the container element.
targetRect.width = grid._width;
targetRect.height = grid._height;
targetRect.left = grid._left;
targetRect.top = grid._top;
gridScore = getRectOverlapScore(itemRect, targetRect);
// Check if this grid is the best match so far.
if (gridScore > threshold && gridScore > bestScore) {
bestScore = gridScore;
target = grid;
}
}
// Always reset root grid array.
rootGridArray.length = 0;
return target;
}
return function(item, options) {
var drag = item._drag;
var rootGrid = drag._getGrid();
// Get drag sort predicate settings.
var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50;
var sortAction = options && options.action === actionSwap ? actionSwap : actionMove;
// Populate item rect data.
itemRect.width = item._width;
itemRect.height = item._height;
itemRect.left = drag._elementClientX;
itemRect.top = drag._elementClientY;
// Calculate the target grid.
var grid = getTargetGrid(item, rootGrid, sortThreshold);
// Return early if we found no grid container element that overlaps the
// dragged item enough.
if (!grid) return false;
var gridOffsetLeft = 0;
var gridOffsetTop = 0;
var matchScore = -1;
var matchIndex;
var hasValidTargets;
var target;
var score;
var i;
// If item is moved within it's originating grid adjust item's left and
// top props. Otherwise if item is moved to/within another grid get the
// container element's offset (from the element's content edge).
if (grid === rootGrid) {
itemRect.left = drag._gridX + item._marginLeft;
itemRect.top = drag._gridY + item._marginTop;
} else {
grid._updateBorders(1, 0, 1, 0);
gridOffsetLeft = grid._left + grid._borderLeft;
gridOffsetTop = grid._top + grid._borderTop;
}
// Loop through the target grid items and try to find the best match.
for (i = 0; i < grid._items.length; i++) {
target = grid._items[i];
// If the target item is not active or the target item is the dragged
// item let's skip to the next item.
if (!target._isActive || target === item) {
continue;
}
// Mark the grid as having valid target items.
hasValidTargets = true;
// Calculate the target's overlap score with the dragged item.
targetRect.width = target._width;
targetRect.height = target._height;
targetRect.left = target._left + target._marginLeft + gridOffsetLeft;
targetRect.top = target._top + target._marginTop + gridOffsetTop;
score = getRectOverlapScore(itemRect, targetRect);
// Update best match index and score if the target's overlap score with
// the dragged item is higher than the current best match score.
if (score > matchScore) {
matchIndex = i;
matchScore = score;
}
}
// If there is no valid match and the item is being moved into another
// grid.
if (matchScore < sortThreshold && item.getGrid() !== grid) {
matchIndex = hasValidTargets ? -1 : 0;
matchScore = Infinity;
}
// Check if the best match overlaps enough to justify a placement switch.
if (matchScore >= sortThreshold) {
returnData.grid = grid;
returnData.index = matchIndex;
returnData.action = sortAction;
return returnData;
}
return false;
};
})();
/**
* Public prototype methods
* ************************
*/
/**
* Abort dragging and reset drag data.
*
* @public
* @memberof ItemDrag.prototype
* @returns {ItemDrag}
*/
ItemDrag.prototype.stop = function() {
var item = this._item;
var element = item._element;
var grid = this._getGrid();
if (!this._isActive) return this;
// If the item is being dropped into another grid, finish it up and return
// immediately.
if (this._isMigrating) {
this._finishMigration();
return this;
}
// Cancel queued move and scroll ticks.
cancelMoveTick(item._id);
cancelScrollTick(item._id);
// Remove scroll listeners.
this._unbindScrollListeners();
// Cancel overlap check.
this._checkOverlapDebounce('cancel');
// Append item element to the container if it's not it's child. Also make
// sure the translate values are adjusted to account for the DOM shift.
if (element.parentNode !== grid._element) {
grid._element.appendChild(element);
element.style[transformProp] = getTranslateString(this._gridX, this._gridY);
}
// Remove dragging class.
removeClass(element, grid._settings.itemDraggingClass);
// Reset drag data.
this._reset();
return this;
};
/**
* Destroy instance.
*
* @public
* @memberof ItemDrag.prototype
* @returns {ItemDrag}
*/
ItemDrag.prototype.destroy = function() {
if (this._isDestroyed) return this;
this.stop();
this._dragger.destroy();
this._isDestroyed = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* Get Grid instance.
*
* @private
* @memberof ItemDrag.prototype
* @returns {?Grid}
*/
ItemDrag.prototype._getGrid = function() {
return gridInstances[this._gridId] || null;
};
/**
* Setup/reset drag data.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._reset = function() {
// Is item being dragged?
this._isActive = false;
// The dragged item's container element.
this._container = null;
// The dragged item's containing block.
this._containingBlock = null;
// Drag/scroll event data.
this._dragEvent = null;
this._scrollEvent = null;
// All the elements which need to be listened for scroll events during
// dragging.
this._scrollers = [];
// The current translateX/translateY position.
this._left = 0;
this._top = 0;
// Dragged element's current position within the grid.
this._gridX = 0;
this._gridY = 0;
// Dragged element's current offset from window's northwest corner. Does
// not account for element's margins.
this._elementClientX = 0;
this._elementClientY = 0;
// Offset difference between the dragged element's temporary drag
// container and it's original container.
this._containerDiffX = 0;
this._containerDiffY = 0;
};
/**
* Bind drag scroll handlers to all scrollable ancestor elements of the
* dragged element and the drag container element.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._bindScrollListeners = function() {
var gridContainer = this._getGrid()._element;
var dragContainer = this._container;
var scrollers = this._scrollers;
var gridScrollers;
var i;
// Get dragged element's scrolling parents.
scrollers.length = 0;
getScrollableAncestors(this._item._element, false, scrollers);
// If drag container is defined and it's not the same element as grid
// container then we need to add the grid container and it's scroll parents
// to the elements which are going to be listener for scroll events.
if (dragContainer !== gridContainer) {
gridScrollers = [];
getScrollableAncestors(gridContainer, true, gridScrollers);
for (i = 0; i < gridScrollers.length; i++) {
if (scrollers.indexOf(gridScrollers[i]) < 0) {
scrollers.push(gridScrollers[i]);
}
}
}
// Bind scroll listeners.
for (i = 0; i < scrollers.length; i++) {
scrollers[i].addEventListener('scroll', this._onScroll);
}
};
/**
* Unbind currently bound drag scroll handlers from all scrollable ancestor
* elements of the dragged element and the drag container element.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._unbindScrollListeners = function() {
var scrollers = this._scrollers;
var i;
for (i = 0; i < scrollers.length; i++) {
scrollers[i].removeEventListener('scroll', this._onScroll);
}
scrollers.length = 0;
};
/**
* Setup default start predicate.
*
* @private
* @memberof ItemDrag.prototype
* @param {Object} [options]
* @returns {Object}
*/
ItemDrag.prototype._setupStartPredicate = function(options) {
var config = options || this._getGrid()._settings.dragStartPredicate || 0;
return (this._startPredicateData = {
distance: Math.abs(config.distance) || 0,
delay: Math.max(config.delay, 0) || 0,
handle: typeof config.handle === 'string' ? config.handle : false
});
};
/**
* Setup default start predicate handle.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
* @returns {?HTMLElement}
*/
ItemDrag.prototype._getStartPredicateHandle = function(event) {
var predicate = this._startPredicateData;
var element = this._item._element;
var handleElement = element;
// No handle, no hassle -> let's use the item element as the handle.
if (!predicate.handle) return handleElement;
// If there is a specific predicate handle defined, let's try to get it.
handleElement = event.target;
while (handleElement && !elementMatches(handleElement, predicate.handle)) {
handleElement = handleElement !== element ? handleElement.parentElement : null;
}
return handleElement || null;
};
/**
* Unbind currently bound drag scroll handlers from all scrollable ancestor
* elements of the dragged element and the drag container element.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
* @returns {Boolean}
*/
ItemDrag.prototype._resolveStartPredicate = function(event) {
var predicate = this._startPredicateData;
// If the moved distance is smaller than the threshold distance or there is
// some delay left, ignore this predicate cycle.
if (event.distance < predicate.distance || predicate.delay) return;
// Get handle rect data.
var handleRect = predicate.handleElement.getBoundingClientRect();
var handleLeft = handleRect.left + (window.pageXOffset || 0);
var handleTop = handleRect.top + (window.pageYOffset || 0);
var handleWidth = handleRect.width;
var handleHeight = handleRect.height;
// Reset predicate data.
this._resetStartPredicate();
// If the cursor is still within the handle let's start the drag.
return (
handleWidth &&
handleHeight &&
event.pageX >= handleLeft &&
event.pageX < handleLeft + handleWidth &&
event.pageY >= handleTop &&
event.pageY < handleTop + handleHeight
);
};
/**
* Forcefully resolve drag start predicate.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._forceResolveStartPredicate = function(event) {
if (!this._isDestroyed && this._startPredicateState === startPredicatePending) {
this._startPredicateState = startPredicateResolved;
this._onStart(event);
}
};
/**
* Finalize start predicate.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._finishStartPredicate = function(event) {
var element = this._item._element;
// Check if this is a click (very subjective heuristics).
var isClick = Math.abs(event.deltaX) < 2 && Math.abs(event.deltaY) < 2 && event.deltaTime < 200;
// Reset predicate.
this._resetStartPredicate();
// If the gesture can be interpreted as click let's try to open the element's
// href url (if it is an anchor element).
if (isClick) openAnchorHref(element);
};
/**
* Reset drag sort heuristics.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._resetHeuristics = function(event) {
this._hBlockedIndex = null;
this._hX1 = this._hX2 = event.clientX;
this._hY1 = this._hY2 = event.clientY;
};
/**
* Run heuristics and return true if overlap check can be performed, and false
* if it can not.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
* @returns {Boolean}
*/
ItemDrag.prototype._checkHeuristics = function(event) {
var settings = this._getGrid()._settings.dragSortHeuristics;
var minDist = settings.minDragDistance;
// Skip heuristics if not needed.
if (minDist <= 0) {
this._hBlockedIndex = null;
return true;
}
var x = event.clientX;
var y = event.clientY;
var diffX = x - this._hX2;
var diffY = y - this._hY2;
// If we can't do proper bounce back check make sure that the blocked index
// is not set.
var canCheckBounceBack = minDist > 3 && settings.minBounceBackAngle > 0;
if (!canCheckBounceBack) {
this._hBlockedIndex = null;
}
if (Math.abs(diffX) > minDist || Math.abs(diffY) > minDist) {
// Reset blocked index if angle changed enough. This check requires a
// minimum value of 3 for minDragDistance to function properly.
if (canCheckBounceBack) {
var angle = Math.atan2(diffX, diffY);
var prevAngle = Math.atan2(this._hX2 - this._hX1, this._hY2 - this._hY1);
var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle));
if (Math.abs(deltaAngle) > settings.minBounceBackAngle) {
this._hBlockedIndex = null;
}
}
// Update points.
this._hX1 = this._hX2;
this._hY1 = this._hY2;
this._hX2 = x;
this._hY2 = y;
return true;
}
return false;
};
/**
* Reset for default drag start predicate function.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._resetStartPredicate = function() {
var predicate = this._startPredicateData;
if (predicate) {
if (predicate.delayTimer) {
predicate.delayTimer = window.clearTimeout(predicate.delayTimer);
}
this._startPredicateData = null;
}
};
/**
* Check (during drag) if an item is overlapping other items and based on
* the configuration layout the items.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._checkOverlap = function() {
if (!this._isActive) return;
var item = this._item;
var settings = this._getGrid()._settings;
var result;
var currentGrid;
var currentIndex;
var targetGrid;
var targetIndex;
var sortAction;
var isMigration;
// Get overlap check result.
if (isFunction(settings.dragSortPredicate)) {
result = settings.dragSortPredicate(item, this._dragEvent);
} else {
result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate);
}
// Let's make sure the result object has a valid index before going further.
if (!result || typeof result.index !== 'number') return;
currentGrid = item.getGrid();
targetGrid = result.grid || currentGrid;
isMigration = currentGrid !== targetGrid;
currentIndex = currentGrid._items.indexOf(item);
targetIndex = normalizeArrayIndex(targetGrid._items, result.index, isMigration);
sortAction = result.action === actionSwap ? actionSwap : actionMove;
// Prevent position bounce.
if (!isMigration && targetIndex === this._hBlockedIndex) {
return;
}
// If the item was moved within it's current grid.
if (!isMigration) {
// Make sure the target index is not the current index.
if (currentIndex !== targetIndex) {
this._hBlockedIndex = currentIndex;
// Do the sort.
(sortAction === actionSwap ? arraySwap : arrayMove)(
currentGrid._items,
currentIndex,
targetIndex
);
// Emit move event.
if (currentGrid._hasListeners(eventMove)) {
currentGrid._emit(eventMove, {
item: item,
fromIndex: currentIndex,
toIndex: targetIndex,
action: sortAction
});
}
// Layout the grid.
currentGrid.layout();
}
}
// If the item was moved to another grid.
else {
this._hBlockedIndex = null;
// Emit beforeSend event.
if (currentGrid._hasListeners(eventBeforeSend)) {
currentGrid._emit(eventBeforeSend, {
item: item,
fromGrid: currentGrid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Emit beforeReceive event.
if (targetGrid._hasListeners(eventBeforeReceive)) {
targetGrid._emit(eventBeforeReceive, {
item: item,
fromGrid: currentGrid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Update item's grid id reference.
item._gridId = targetGrid._id;
// Update drag instance's migrating indicator.
this._isMigrating = item._gridId !== this._gridId;
// Move item instance from current grid to target grid.
currentGrid._items.splice(currentIndex, 1);
arrayInsert(targetGrid._items, item, targetIndex);
// Set sort data as null, which is an indicator for the item comparison
// function that the sort data of this specific item should be fetched
// lazily.
item._sortData = null;
// Emit send event.
if (currentGrid._hasListeners(eventSend)) {
currentGrid._emit(eventSend, {
item: item,
fromGrid: currentGrid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Emit receive event.
if (targetGrid._hasListeners(eventReceive)) {
targetGrid._emit(eventReceive, {
item: item,
fromGrid: currentGrid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Layout both grids.
currentGrid.layout();
targetGrid.layout();
}
};
/**
* If item is dragged into another grid, finish the migration process
* gracefully.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._finishMigration = function() {
var item = this._item;
var release = item._release;
var element = item._element;
var isActive = item._isActive;
var targetGrid = item.getGrid();
var targetGridElement = targetGrid._element;
var targetSettings = targetGrid._settings;
var targetContainer = targetSettings.dragContainer || targetGridElement;
var currentSettings = this._getGrid()._settings;
var currentContainer = element.parentNode;
var translate;
var offsetDiff;
// Destroy current drag. Note that we need to set the migrating flag to
// false first, because otherwise we create an infinite loop between this
// and the drag.stop() method.
this._isMigrating = false;
this.destroy();
// Remove current classnames.
removeClass(element, currentSettings.itemClass);
removeClass(element, currentSettings.itemVisibleClass);
removeClass(element, currentSettings.itemHiddenClass);
// Add new classnames.
addClass(element, targetSettings.itemClass);
addClass(element, isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass);
// Move the item inside the target container if it's different than the
// current container.
if (targetContainer !== currentContainer) {
targetContainer.appendChild(element);
offsetDiff = getOffsetDiff(currentContainer, targetContainer, true);
translate = getTranslate(element);
translate.x -= offsetDiff.left;
translate.y -= offsetDiff.top;
}
// Update item's cached dimensions and sort data.
item._refreshDimensions();
item._refreshSortData();
// Calculate the offset difference between target's drag container (if any)
// and actual grid container element. We save it later for the release
// process.
offsetDiff = getOffsetDiff(targetContainer, targetGridElement, true);
release._containerDiffX = offsetDiff.left;
release._containerDiffY = offsetDiff.top;
// Recreate item's drag handler.
item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null;
// Adjust the position of the item element if it was moved from a container
// to another.
if (targetContainer !== currentContainer) {
element.style[transformProp] = getTranslateString(translate.x, translate.y);
}
// Update child element's styles to reflect the current visibility state.
item._child.removeAttribute('style');
setStyles(item._child, isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles);
// Start the release.
release.start();
};
/**
* Drag pre-start handler.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._preStartCheck = function(event) {
// Let's activate drag start predicate state.
if (this._startPredicateState === startPredicateInactive) {
this._startPredicateState = startPredicatePending;
}
// If predicate is pending try to resolve it.
if (this._startPredicateState === startPredicatePending) {
this._startPredicateResult = this._startPredicate(this._item, event);
if (this._startPredicateResult === true) {
this._startPredicateState = startPredicateResolved;
this._onStart(event);
} else if (this._startPredicateResult === false) {
this._startPredicateState = startPredicateRejected;
}
}
// Otherwise if predicate is resolved and drag is active, move the item.
else if (this._startPredicateState === startPredicateResolved && this._isActive) {
this._onMove(event);
}
};
/**
* Drag pre-end handler.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._preEndCheck = function(event) {
// Check if the start predicate was resolved during drag.
var isResolved = this._startPredicateState === startPredicateResolved;
// Do final predicate check to allow user to unbind stuff for the current
// drag procedure within the predicate callback. The return value of this
// check will have no effect to the state of the predicate.
this._startPredicate(this._item, event);
// Reset start predicate state.
this._startPredicateState = startPredicateInactive;
// If predicate is resolved and dragging is active, call the end handler.
if (isResolved && this._isActive) this._onEnd(event);
};
/**
* Drag start handler.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._onStart = function(event) {
var item = this._item;
// If item is not active, don't start the drag.
if (!item._isActive) return;
var element = item._element;
var grid = this._getGrid();
var settings = grid._settings;
var release = item._release;
var migrate = item._migrate;
var gridContainer = grid._element;
var dragContainer = settings.dragContainer || gridContainer;
var containingBlock = getContainingBlock(dragContainer);
var translate = getTranslate(element);
var currentLeft = translate.x;
var currentTop = translate.y;
var elementRect = element.getBoundingClientRect();
var hasDragContainer = dragContainer !== gridContainer;
var offsetDiff;
// Reset heuristics data.
this._resetHeuristics(event);
// If grid container is not the drag container, we need to calculate the
// offset difference between grid container and drag container's containing
// element.
if (hasDragContainer) {
offsetDiff = getOffsetDiff(containingBlock, gridContainer);
}
// Stop current positioning animation.
if (item.isPositioning()) {
item._layout.stop(true, { transform: getTranslateString(currentLeft, currentTop) });
}
// Stop current migration animation.
if (migrate._isActive) {
currentLeft -= migrate._containerDiffX;
currentTop -= migrate._containerDiffY;
migrate.stop(true, { transform: getTranslateString(currentLeft, currentTop) });
}
// If item is being released reset release data.
if (item.isReleasing()) release._reset();
// Setup drag data.
this._isActive = true;
this._dragEvent = event;
this._container = dragContainer;
this._containingBlock = containingBlock;
this._elementClientX = elementRect.left;
this._elementClientY = elementRect.top;
this._left = this._gridX = currentLeft;
this._top = this._gridY = currentTop;
// Create placeholder (if necessary).
if (settings.dragPlaceholder.enabled) {
item._dragPlaceholder.create();
}
// Emit dragInit event.
grid._emit(eventDragInit, item, event);
// If a specific drag container is set and it is different from the
// grid's container element we need to cast some extra spells.
if (hasDragContainer) {
// Store the container offset diffs to drag data.
this._containerDiffX = offsetDiff.left;
this._containerDiffY = offsetDiff.top;
// If the dragged element is a child of the drag container all we need to
// do is setup the relative drag position data.
if (element.parentNode === dragContainer) {
this._gridX = currentLeft - this._containerDiffX;
this._gridY = currentTop - this._containerDiffY;
}
// Otherwise we need to append the element inside the correct container,
// setup the actual drag position data and adjust the element's translate
// values to account for the DOM position shift.
else {
this._left = currentLeft + this._containerDiffX;
this._top = currentTop + this._containerDiffY;
dragContainer.appendChild(element);
element.style[transformProp] = getTranslateString(this._left, this._top);
}
}
// Set drag class and bind scrollers.
addClass(element, settings.itemDraggingClass);
this._bindScrollListeners();
// Emit dragStart event.
grid._emit(eventDragStart, item, event);
};
/**
* Drag move handler.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._onMove = function(event) {
var item = this._item;
// If item is not active, reset drag.
if (!item._isActive) {
this.stop();
return;
}
var settings = this._getGrid()._settings;
var axis = settings.dragAxis;
// Update horizontal position data.
if (axis !== 'y') {
var xDiff = event.clientX - this._dragEvent.clientX;
this._left += xDiff;
this._gridX += xDiff;
this._elementClientX += xDiff;
}
// Update vertical position data.
if (axis !== 'x') {
var yDiff = event.clientY - this._dragEvent.clientY;
this._top += yDiff;
this._gridY += yDiff;
this._elementClientY += yDiff;
}
// Update event data.
this._dragEvent = event;
// Do move prepare/apply handling in the next tick.
addMoveTick(item._id, this._prepareMove, this._applyMove);
};
/**
* Prepare dragged item for moving.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._prepareMove = function() {
// Do nothing if item is not active.
if (!this._item._isActive) return;
// If drag sort is enabled -> check overlap.
if (this._getGrid()._settings.dragSort) {
if (this._checkHeuristics(this._dragEvent)) {
this._checkOverlapDebounce();
}
}
};
/**
* Apply movement to dragged item.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._applyMove = function() {
var item = this._item;
// Do nothing if item is not active.
if (!item._isActive) return;
// Update element's translateX/Y values.
item._element.style[transformProp] = getTranslateString(this._left, this._top);
// Emit dragMove event.
this._getGrid()._emit(eventDragMove, item, this._dragEvent);
};
/**
* Drag scroll handler.
*
* @private
* @memberof ItemDrag.prototype
* @param {Event} event
*/
ItemDrag.prototype._onScroll = function(event) {
var item = this._item;
// If item is not active, reset drag.
if (!item._isActive) {
this.stop();
return;
}
// Update last scroll event.
this._scrollEvent = event;
// Do scroll prepare/apply handling in the next tick.
addScrollTick(item._id, this._prepareScroll, this._applyScroll);
};
/**
* Prepare dragged item for scrolling.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._prepareScroll = function() {
var item = this._item;
// If item is not active do nothing.
if (!item._isActive) return;
var element = item._element;
var grid = this._getGrid();
var settings = grid._settings;
var axis = settings.dragAxis;
var gridContainer = grid._element;
var offsetDiff;
// Calculate element's rect and x/y diff.
var rect = element.getBoundingClientRect();
var xDiff = this._elementClientX - rect.left;
var yDiff = this._elementClientY - rect.top;
// Update container diff.
if (this._container !== gridContainer) {
offsetDiff = getOffsetDiff(this._containingBlock, gridContainer);
this._containerDiffX = offsetDiff.left;
this._containerDiffY = offsetDiff.top;
}
// Update horizontal position data.
if (axis !== 'y') {
this._left += xDiff;
this._gridX = this._left - this._containerDiffX;
}
// Update vertical position data.
if (axis !== 'x') {
this._top += yDiff;
this._gridY = this._top - this._containerDiffY;
}
// Overlap handling.
if (settings.dragSort) this._checkOverlapDebounce();
};
/**
* Apply scroll to dragged item.
*
* @private
* @memberof ItemDrag.prototype
*/
ItemDrag.prototype._applyScroll = function() {
var item = this._item;
// If item is not active do nothing.
if (!item._isActive) return;
// Update element's translateX/Y values.
item._element.style[transformProp] = getTranslateString(this._left, this._top);
// Emit dragScroll event.
this._getGrid()._emit(eventDragScroll, item, this._scrollEvent);
};
/**
* Drag end handler.
*
* @private
* @memberof ItemDrag.prototype
* @param {DraggerEvent} event
*/
ItemDrag.prototype._onEnd = function(event) {
var item = this._item;
var element = item._element;
var grid = this._getGrid();
var settings = grid._settings;
var release = item._release;
// If item is not active, reset drag.
if (!item._isActive) {
this.stop();
return;
}
// Cancel queued move and scroll ticks.
cancelMoveTick(item._id);
cancelScrollTick(item._id);
// Finish currently queued overlap check.
settings.dragSort && this._checkOverlapDebounce('finish');
// Remove scroll listeners.
this._unbindScrollListeners();
// Setup release data.
release._containerDiffX = this._containerDiffX;
release._containerDiffY = this._containerDiffY;
// Reset drag data.
this._reset();
// Remove drag class name from element.
removeClass(element, settings.itemDraggingClass);
// Emit dragEnd event.
grid._emit(eventDragEnd, item, event);
// Finish up the migration process or start the release process.
this._isMigrating ? this._finishMigration() : release.start();
};
/**
* Private helpers
* ***************
*/
/**
* Calculate how many percent the intersection area of two rectangles is from
* the maximum potential intersection area between the rectangles.
*
* @param {Rectangle} a
* @param {Rectangle} b
* @returns {Number}
* - A number between 0-100.
*/
function getRectOverlapScore(a, b) {
// Return 0 immediately if the rectangles do not overlap.
if (
a.left + a.width <= b.left ||
b.left + b.width <= a.left ||
a.top + a.height <= b.top ||
b.top + b.height <= a.top
) {
return 0;
}
// Calculate intersection area's width, height, max height and max width.
var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left);
var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
var maxWidth = Math.min(a.width, b.width);
var maxHeight = Math.min(a.height, b.height);
return ((width * height) / (maxWidth * maxHeight)) * 100;
}
/**
* Check if an element is an anchor element and open the href url if possible.
*
* @param {HTMLElement} element
*/
function openAnchorHref(element) {
// Make sure the element is anchor element.
if (element.tagName.toLowerCase() !== 'a') return;
// Get href and make sure it exists.
var href = element.getAttribute('href');
if (!href) return;
// Finally let's navigate to the link href.
var target = element.getAttribute('target');
if (target && target !== '_self') {
window.open(href, target);
} else {
window.location.href = href;
}
}
/**
* Drag placeholder.
*
* @class
* @param {Item} item
*/
function ItemDragPlaceholder(item) {
this._item = item;
this._animate = new ItemAnimate();
this._element = null;
this._className = '';
this._didMigrate = false;
this._resetAfterLayout = false;
this._currentLeft = 0;
this._currentTop = 0;
this._nextLeft = 0;
this._nextTop = 0;
// Bind animation handlers.
this._setupAnimation = this._setupAnimation.bind(this);
this._startAnimation = this._startAnimation.bind(this);
// Bind event handlers.
this._onLayoutStart = this._onLayoutStart.bind(this);
this._onLayoutEnd = this._onLayoutEnd.bind(this);
this._onReleaseEnd = this._onReleaseEnd.bind(this);
this._onMigrate = this._onMigrate.bind(this);
}
/**
* Private prototype methods
* *************************
*/
/**
* Move placeholder to a new position.
*
* @private
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype._onLayoutStart = function() {
var item = this._item;
var grid = item.getGrid();
// Find out the item's new (unapplied) left and top position.
var itemIndex = grid._items.indexOf(item);
var nextLeft = grid._layout.slots[itemIndex * 2];
var nextTop = grid._layout.slots[itemIndex * 2 + 1];
// If item's position did not change and the item did not migrate we can
// safely skip layout.
if (!this._didMigrate && item._left === nextLeft && item._top === nextTop) {
return;
}
// Slots data is calculated with item margins added to them so we need to add
// item's left and top margin to the slot data to get the placeholder's
// next position.
nextLeft += item._marginLeft;
nextTop += item._marginTop;
// Just snap to new position without any animations if no animation is
// required or if placeholder moves between grids.
var animEnabled = grid._settings.dragPlaceholder.duration > 0;
if (!animEnabled || this._didMigrate) {
// Cancel potential (queued) layout tick.
cancelPlaceholderTick(item._id);
// Snap placeholder to correct position.
var targetStyles = { transform: getTranslateString(nextLeft, nextTop) };
if (this._animate.isAnimating()) {
this._animate.stop(targetStyles);
} else {
setStyles(this._element, targetStyles);
}
// Move placeholder inside correct container after migration.
if (this._didMigrate) {
grid.getElement().appendChild(this._element);
this._didMigrate = false;
}
return;
}
// Start the placeholder's layout animation in the next tick. We do this to
// avoid layout thrashing.
this._nextLeft = nextLeft;
this._nextTop = nextTop;
addPlaceholderTick(item._id, this._setupAnimation, this._startAnimation);
};
/**
* Prepare placeholder for layout animation.
*
* @private
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype._setupAnimation = function() {
if (!this.isActive()) return;
var translate = getTranslate(this._element);
this._currentLeft = translate.x;
this._currentTop = translate.y;
};
/**
* Start layout animation.
*
* @private
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype._startAnimation = function() {
if (!this.isActive()) return;
var animation = this._animate;
var currentLeft = this._currentLeft;
var currentTop = this._currentTop;
var nextLeft = this._nextLeft;
var nextTop = this._nextTop;
var targetStyles = { transform: getTranslateString(nextLeft, nextTop) };
// If placeholder is already in correct position let's just stop animation
// and be done with it.
if (currentLeft === nextLeft && currentTop === nextTop) {
if (animation.isAnimating()) animation.stop(targetStyles);
return;
}
// Otherwise let's start the animation.
var settings = this._item.getGrid()._settings.dragPlaceholder;
var currentStyles = { transform: getTranslateString(currentLeft, currentTop) };
animation.start(currentStyles, targetStyles, {
duration: settings.duration,
easing: settings.easing,
onFinish: this._onLayoutEnd
});
};
/**
* Layout end handler.
*
* @private
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype._onLayoutEnd = function() {
if (this._resetAfterLayout) {
this.reset();
}
};
/**
* Drag end handler. This handler is called when dragReleaseEnd event is
* emitted and receives the event data as it's argument.
*
* @private
* @memberof ItemDragPlaceholder.prototype
* @param {Item} item
*/
ItemDragPlaceholder.prototype._onReleaseEnd = function(item) {
if (item._id === this._item._id) {
// If the placeholder is not animating anymore we can safely reset it.
if (!this._animate.isAnimating()) {
this.reset();
return;
}
// If the placeholder item is still animating here, let's wait for it to
// finish it's animation.
this._resetAfterLayout = true;
}
};
/**
* Migration start handler. This handler is called when beforeSend event is
* emitted and receives the event data as it's argument.
*
* @private
* @memberof ItemDragPlaceholder.prototype
* @param {Object} data
* @param {Item} data.item
* @param {Grid} data.fromGrid
* @param {Number} data.fromIndex
* @param {Grid} data.toGrid
* @param {Number} data.toIndex
*/
ItemDragPlaceholder.prototype._onMigrate = function(data) {
// Make sure we have a matching item.
if (data.item !== this._item) return;
var grid = this._item.getGrid();
var nextGrid = data.toGrid;
// Unbind listeners from current grid.
grid.off(eventDragReleaseEnd, this._onReleaseEnd);
grid.off(eventLayoutStart, this._onLayoutStart);
grid.off(eventBeforeSend, this._onMigrate);
// Bind listeners to the next grid.
nextGrid.on(eventDragReleaseEnd, this._onReleaseEnd);
nextGrid.on(eventLayoutStart, this._onLayoutStart);
nextGrid.on(eventBeforeSend, this._onMigrate);
// Mark the item as migrated.
this._didMigrate = true;
};
/**
* Public prototype methods
* ************************
*/
/**
* Create placeholder. Note that this method only writes to DOM and does not
* read anything from DOM so it should not cause any additional layout
* thrashing when it's called at the end of the drag start procedure.
*
* @public
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype.create = function() {
// If we already have placeholder set up we can skip the initiation logic.
if (this.isActive()) {
this._resetAfterLayout = false;
return;
}
var item = this._item;
var grid = item.getGrid();
var settings = grid._settings;
var animation = this._animate;
// Create placeholder element.
var element;
if (isFunction(settings.dragPlaceholder.createElement)) {
element = settings.dragPlaceholder.createElement(item);
} else {
element = window.document.createElement('div');
}
this._element = element;
// Update element to animation instance.
animation._element = element;
// Add placeholder class to the placeholder element.
this._className = settings.itemPlaceholderClass || '';
if (this._className) {
addClass(element, this._className);
}
// Position the placeholder item correctly.
var left = item._left + item._marginLeft;
var top = item._top + item._marginTop;
setStyles(element, {
display: 'block',
position: 'absolute',
left: '0',
top: '0',
width: item._width + 'px',
height: item._height + 'px',
transform: getTranslateString(left, top)
});
// Bind event listeners.
grid.on(eventLayoutStart, this._onLayoutStart);
grid.on(eventDragReleaseEnd, this._onReleaseEnd);
grid.on(eventBeforeSend, this._onMigrate);
// onCreate hook.
if (isFunction(settings.dragPlaceholder.onCreate)) {
settings.dragPlaceholder.onCreate(item, element);
}
// Insert the placeholder element to the grid.
grid.getElement().appendChild(element);
};
/**
* Reset placeholder data.
*
* @public
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype.reset = function() {
if (!this.isActive()) return;
var element = this._element;
var item = this._item;
var grid = item.getGrid();
var settings = grid._settings;
var animation = this._animate;
// Reset flag.
this._resetAfterLayout = false;
// Cancel potential (queued) layout tick.
cancelPlaceholderTick(item._id);
// Reset animation instance.
animation.stop();
animation._element = null;
// Unbind event listeners.
grid.off(eventDragReleaseEnd, this._onReleaseEnd);
grid.off(eventLayoutStart, this._onLayoutStart);
grid.off(eventBeforeSend, this._onMigrate);
// Remove placeholder class from the placeholder element.
if (this._className) {
removeClass(element, this._className);
this._className = '';
}
// Remove element.
element.parentNode.removeChild(element);
this._element = null;
// onRemove hook. Note that here we use the current grid's onRemove callback
// so if the item has migrated during drag the onRemove method will not be
// the originating grid's method.
if (isFunction(settings.dragPlaceholder.onRemove)) {
settings.dragPlaceholder.onRemove(item, element);
}
};
/**
* Update placeholder's dimensions.
*
* @public
* @memberof ItemDragPlaceholder.prototype
* @param {Number} width
* @param {height} height
*/
ItemDragPlaceholder.prototype.updateDimensions = function(width, height) {
if (this.isActive()) {
setStyles(this._element, {
width: width + 'px',
height: height + 'px'
});
}
};
/**
* Check if placeholder is currently active (visible).
*
* @public
* @memberof ItemDragPlaceholder.prototype
* @returns {Boolean}
*/
ItemDragPlaceholder.prototype.isActive = function() {
return !!this._element;
};
/**
* Destroy placeholder instance.
*
* @public
* @memberof ItemDragPlaceholder.prototype
*/
ItemDragPlaceholder.prototype.destroy = function() {
this.reset();
this._animate.destroy();
this._item = this._animate = null;
};
/**
* Queue constructor.
*
* @class
*/
function Queue() {
this._queue = [];
this._isDestroyed = false;
}
/**
* Public prototype methods
* ************************
*/
/**
* Add callback to the queue.
*
* @public
* @memberof Queue.prototype
* @param {Function} callback
* @returns {Queue}
*/
Queue.prototype.add = function(callback) {
if (this._isDestroyed) return this;
this._queue.push(callback);
return this;
};
/**
* Process queue callbacks and reset the queue.
*
* @public
* @memberof Queue.prototype
* @param {*} arg1
* @param {*} arg2
* @returns {Queue}
*/
Queue.prototype.flush = function(arg1, arg2) {
if (this._isDestroyed) return this;
var queue = this._queue;
var length = queue.length;
var i;
// Quit early if the queue is empty.
if (!length) return this;
var singleCallback = length === 1;
var snapshot = singleCallback ? queue[0] : queue.slice(0);
// Reset queue.
queue.length = 0;
// If we only have a single callback let's just call it.
if (singleCallback) {
snapshot(arg1, arg2);
return this;
}
// If we have multiple callbacks, let's process them.
for (i = 0; i < length; i++) {
snapshot[i](arg1, arg2);
if (this._isDestroyed) break;
}
return this;
};
/**
* Destroy Queue instance.
*
* @public
* @memberof Queue.prototype
* @returns {Queue}
*/
Queue.prototype.destroy = function() {
if (this._isDestroyed) return this;
this._isDestroyed = true;
this._queue.length = 0;
return this;
};
/**
* Layout manager for Item instance.
*
* @class
* @param {Item} item
*/
function ItemLayout(item) {
this._item = item;
this._isActive = false;
this._isDestroyed = false;
this._isInterrupted = false;
this._currentStyles = {};
this._targetStyles = {};
this._currentLeft = 0;
this._currentTop = 0;
this._offsetLeft = 0;
this._offsetTop = 0;
this._skipNextAnimation = false;
this._animateOptions = {
onFinish: this._finish.bind(this)
};
this._queue = new Queue();
// Bind animation handlers and finish method.
this._setupAnimation = this._setupAnimation.bind(this);
this._startAnimation = this._startAnimation.bind(this);
}
/**
* Public prototype methods
* ************************
*/
/**
* Start item layout based on it's current data.
*
* @public
* @memberof ItemLayout.prototype
* @param {Boolean} [instant=false]
* @param {Function} [onFinish]
* @returns {ItemLayout}
*/
ItemLayout.prototype.start = function(instant, onFinish) {
if (this._isDestroyed) return;
var item = this._item;
var element = item._element;
var release = item._release;
var gridSettings = item.getGrid()._settings;
var isPositioning = this._isActive;
var isJustReleased = release._isActive && release._isPositioningStarted === false;
var animDuration = isJustReleased
? gridSettings.dragReleaseDuration
: gridSettings.layoutDuration;
var animEasing = isJustReleased ? gridSettings.dragReleaseEasing : gridSettings.layoutEasing;
var animEnabled = !instant && !this._skipNextAnimation && animDuration > 0;
var isAnimating;
// If the item is currently positioning process current layout callback
// queue with interrupted flag on.
if (isPositioning) this._queue.flush(true, item);
// Mark release positioning as started.
if (isJustReleased) release._isPositioningStarted = true;
// Push the callback to the callback queue.
if (isFunction(onFinish)) this._queue.add(onFinish);
// If no animations are needed, easy peasy!
if (!animEnabled) {
this._updateOffsets();
this._updateTargetStyles();
isAnimating = item._animate.isAnimating();
this.stop(false, this._targetStyles);
!isAnimating && setStyles(element, this._targetStyles);
this._skipNextAnimation = false;
return this._finish();
}
// Set item active and store some data for the animation that is about to be
// triggered.
this._isActive = true;
this._animateOptions.easing = animEasing;
this._animateOptions.duration = animDuration;
this._isInterrupted = isPositioning;
// Start the item's layout animation in the next tick.
addLayoutTick(item._id, this._setupAnimation, this._startAnimation);
return this;
};
/**
* Stop item's position animation if it is currently animating.
*
* @public
* @memberof ItemLayout.prototype
* @param {Boolean} [processCallbackQueue=false]
* @param {Object} [targetStyles]
* @returns {ItemLayout}
*/
ItemLayout.prototype.stop = function(processCallbackQueue, targetStyles) {
if (this._isDestroyed || !this._isActive) return this;
var item = this._item;
// Cancel animation init.
cancelLayoutTick(item._id);
// Stop animation.
item._animate.stop(targetStyles);
// Remove positioning class.
removeClass(item._element, item.getGrid()._settings.itemPositioningClass);
// Reset active state.
this._isActive = false;
// Process callback queue if needed.
if (processCallbackQueue) this._queue.flush(true, item);
return this;
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
* @memberof ItemLayout.prototype
* @returns {ItemLayout}
*/
ItemLayout.prototype.destroy = function() {
if (this._isDestroyed) return this;
this.stop(true, {});
this._queue.destroy();
this._item = this._currentStyles = this._targetStyles = this._animateOptions = null;
this._isDestroyed = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* Calculate and update item's current layout offset data.
*
* @private
* @memberof ItemLayout.prototype
*/
ItemLayout.prototype._updateOffsets = function() {
if (this._isDestroyed) return;
var item = this._item;
var migrate = item._migrate;
var release = item._release;
this._offsetLeft = release._isActive
? release._containerDiffX
: migrate._isActive
? migrate._containerDiffX
: 0;
this._offsetTop = release._isActive
? release._containerDiffY
: migrate._isActive
? migrate._containerDiffY
: 0;
};
/**
* Calculate and update item's layout target styles.
*
* @private
* @memberof ItemLayout.prototype
*/
ItemLayout.prototype._updateTargetStyles = function() {
if (this._isDestroyed) return;
this._targetStyles.transform = getTranslateString(
this._item._left + this._offsetLeft,
this._item._top + this._offsetTop
);
};
/**
* Finish item layout procedure.
*
* @private
* @memberof ItemLayout.prototype
*/
ItemLayout.prototype._finish = function() {
if (this._isDestroyed) return;
var item = this._item;
var migrate = item._migrate;
var release = item._release;
// Mark the item as inactive and remove positioning classes.
if (this._isActive) {
this._isActive = false;
removeClass(item._element, item.getGrid()._settings.itemPositioningClass);
}
// Finish up release and migration.
if (release._isActive) release.stop();
if (migrate._isActive) migrate.stop();
// Process the callback queue.
this._queue.flush(false, item);
};
/**
* Prepare item for layout animation.
*
* @private
* @memberof ItemLayout.prototype
*/
ItemLayout.prototype._setupAnimation = function() {
var translate = getTranslate(this._item._element);
this._currentLeft = translate.x;
this._currentTop = translate.y;
};
/**
* Start layout animation.
*
* @private
* @memberof ItemLayout.prototype
*/
ItemLayout.prototype._startAnimation = function() {
var item = this._item;
var settings = item.getGrid()._settings;
// Let's update the offset data and target styles.
this._updateOffsets();
this._updateTargetStyles();
// If the item is already in correct position let's quit early.
if (
item._left === this._currentLeft - this._offsetLeft &&
item._top === this._currentTop - this._offsetTop
) {
if (this._isInterrupted) this.stop(false, this._targetStyles);
this._isActive = false;
this._finish();
return;
}
// Set item's positioning class if needed.
if (!this._isInterrupted) {
addClass(item._element, settings.itemPositioningClass);
}
// Get current styles for animation.
this._currentStyles.transform = getTranslateString(this._currentLeft, this._currentTop);
// Animate.
item._animate.start(this._currentStyles, this._targetStyles, this._animateOptions);
};
var tempStyles = {};
/**
* The migrate process handler constructor.
*
* @class
* @param {Item} item
*/
function ItemMigrate(item) {
// Private props.
this._item = item;
this._isActive = false;
this._isDestroyed = false;
this._container = false;
this._containerDiffX = 0;
this._containerDiffY = 0;
}
/**
* Public prototype methods
* ************************
*/
/**
* Start the migrate process of an item.
*
* @public
* @memberof ItemMigrate.prototype
* @param {Grid} targetGrid
* @param {GridSingleItemQuery} position
* @param {HTMLElement} [container]
* @returns {ItemMigrate}
*/
ItemMigrate.prototype.start = function(targetGrid, position, container) {
if (this._isDestroyed) return this;
var item = this._item;
var element = item._element;
var isVisible = item.isVisible();
var grid = item.getGrid();
var settings = grid._settings;
var targetSettings = targetGrid._settings;
var targetElement = targetGrid._element;
var targetItems = targetGrid._items;
var currentIndex = grid._items.indexOf(item);
var targetContainer = container || window.document.body;
var targetIndex;
var targetItem;
var currentContainer;
var offsetDiff;
var containerDiff;
var translate;
var translateX;
var translateY;
// Get target index.
if (typeof position === 'number') {
targetIndex = normalizeArrayIndex(targetItems, position, true);
} else {
targetItem = targetGrid._getItem(position);
/** @todo Consider throwing an error here instead of silently failing. */
if (!targetItem) return this;
targetIndex = targetItems.indexOf(targetItem);
}
// Get current translateX and translateY values if needed.
if (item.isPositioning() || this._isActive || item.isReleasing()) {
translate = getTranslate(element);
translateX = translate.x;
translateY = translate.y;
}
// Abort current positioning.
if (item.isPositioning()) {
item._layout.stop(true, { transform: getTranslateString(translateX, translateY) });
}
// Abort current migration.
if (this._isActive) {
translateX -= this._containerDiffX;
translateY -= this._containerDiffY;
this.stop(true, { transform: getTranslateString(translateX, translateY) });
}
// Abort current release.
if (item.isReleasing()) {
translateX -= item._release._containerDiffX;
translateY -= item._release._containerDiffY;
item._release.stop(true, { transform: getTranslateString(translateX, translateY) });
}
// Stop current visibility animations.
item._visibility._stopAnimation();
// Destroy current drag.
if (item._drag) item._drag.destroy();
// Process current visibility animation queue.
item._visibility._queue.flush(true, item);
// Emit beforeSend event.
if (grid._hasListeners(eventBeforeSend)) {
grid._emit(eventBeforeSend, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Emit beforeReceive event.
if (targetGrid._hasListeners(eventBeforeReceive)) {
targetGrid._emit(eventBeforeReceive, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Remove current classnames.
removeClass(element, settings.itemClass);
removeClass(element, settings.itemVisibleClass);
removeClass(element, settings.itemHiddenClass);
// Add new classnames.
addClass(element, targetSettings.itemClass);
addClass(element, isVisible ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass);
// Move item instance from current grid to target grid.
grid._items.splice(currentIndex, 1);
arrayInsert(targetItems, item, targetIndex);
// Update item's grid id reference.
item._gridId = targetGrid._id;
// Get current container.
currentContainer = element.parentNode;
// Move the item inside the target container if it's different than the
// current container.
if (targetContainer !== currentContainer) {
targetContainer.appendChild(element);
offsetDiff = getOffsetDiff(targetContainer, currentContainer, true);
if (!translate) {
translate = getTranslate(element);
translateX = translate.x;
translateY = translate.y;
}
element.style[transformProp] = getTranslateString(
translateX + offsetDiff.left,
translateY + offsetDiff.top
);
}
// Update child element's styles to reflect the current visibility state.
item._child.removeAttribute('style');
setStyles(item._child, isVisible ? targetSettings.visibleStyles : targetSettings.hiddenStyles);
// Update display style.
element.style.display = isVisible ? 'block' : 'hidden';
// Get offset diff for the migration data.
containerDiff = getOffsetDiff(targetContainer, targetElement, true);
// Update item's cached dimensions and sort data.
item._refreshDimensions();
item._refreshSortData();
// Create new drag handler.
item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null;
// Setup migration data.
this._isActive = true;
this._container = targetContainer;
this._containerDiffX = containerDiff.left;
this._containerDiffY = containerDiff.top;
// Emit send event.
if (grid._hasListeners(eventSend)) {
grid._emit(eventSend, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
// Emit receive event.
if (targetGrid._hasListeners(eventReceive)) {
targetGrid._emit(eventReceive, {
item: item,
fromGrid: grid,
fromIndex: currentIndex,
toGrid: targetGrid,
toIndex: targetIndex
});
}
return this;
};
/**
* End the migrate process of an item. This method can be used to abort an
* ongoing migrate process (animation) or finish the migrate process.
*
* @public
* @memberof ItemMigrate.prototype
* @param {Boolean} [abort=false]
* - Should the migration be aborted?
* @param {Object} [currentStyles]
* - Optional current translateX and translateY styles.
* @returns {ItemMigrate}
*/
ItemMigrate.prototype.stop = function(abort, currentStyles) {
if (this._isDestroyed || !this._isActive) return this;
var item = this._item;
var element = item._element;
var grid = item.getGrid();
var gridElement = grid._element;
var translate;
if (this._container !== gridElement) {
if (!currentStyles) {
if (abort) {
translate = getTranslate(element);
tempStyles.transform = getTranslateString(
translate.x - this._containerDiffX,
translate.y - this._containerDiffY
);
} else {
tempStyles.transform = getTranslateString(item._left, item._top);
}
currentStyles = tempStyles;
}
gridElement.appendChild(element);
setStyles(element, currentStyles);
}
this._isActive = false;
this._container = null;
this._containerDiffX = 0;
this._containerDiffY = 0;
return this;
};
/**
* Destroy instance.
*
* @public
* @memberof ItemMigrate.prototype
* @returns {ItemMigrate}
*/
ItemMigrate.prototype.destroy = function() {
if (this._isDestroyed) return this;
this.stop(true);
this._item = null;
this._isDestroyed = true;
return this;
};
var tempStyles$1 = {};
/**
* The release process handler constructor. Although this might seem as proper
* fit for the drag process this needs to be separated into it's own logic
* because there might be a scenario where drag is disabled, but the release
* process still needs to be implemented (dragging from a grid to another).
*
* @class
* @param {Item} item
*/
function ItemRelease(item) {
this._item = item;
this._isActive = false;
this._isDestroyed = false;
this._isPositioningStarted = false;
this._containerDiffX = 0;
this._containerDiffY = 0;
}
/**
* Public prototype methods
* ************************
*/
/**
* Start the release process of an item.
*
* @public
* @memberof ItemRelease.prototype
* @returns {ItemRelease}
*/
ItemRelease.prototype.start = function() {
if (this._isDestroyed || this._isActive) return this;
var item = this._item;
var grid = item.getGrid();
// Flag release as active.
this._isActive = true;
// Add release class name to the released element.
addClass(item._element, grid._settings.itemReleasingClass);
// Emit dragReleaseStart event.
grid._emit(eventDragReleaseStart, item);
// Position the released item.
item._layout.start(false);
return this;
};
/**
* End the release process of an item. This method can be used to abort an
* ongoing release process (animation) or finish the release process.
*
* @public
* @memberof ItemRelease.prototype
* @param {Boolean} [abort=false]
* - Should the release be aborted? When true, the release end event won't be
* emitted. Set to true only when you need to abort the release process
* while the item is animating to it's position.
* @param {Object} [currentStyles]
* - Optional current translateX and translateY styles.
* @returns {ItemRelease}
*/
ItemRelease.prototype.stop = function(abort, currentStyles) {
if (this._isDestroyed || !this._isActive) return this;
var item = this._item;
var element = item._element;
var grid = item.getGrid();
var container = grid._element;
var translate;
// Reset data and remove releasing class name from the element.
this._reset();
// If the released element is outside the grid's container element put it
// back there and adjust position accordingly.
if (element.parentNode !== container) {
if (!currentStyles) {
if (abort) {
translate = getTranslate(element);
tempStyles$1.transform = getTranslateString(
translate.x - this._containerDiffX,
translate.y - this._containerDiffY
);
} else {
tempStyles$1.transform = getTranslateString(item._left, item._top);
}
currentStyles = tempStyles$1;
}
container.appendChild(element);
setStyles(element, currentStyles);
}
// Emit dragReleaseEnd event.
if (!abort) grid._emit(eventDragReleaseEnd, item);
return this;
};
/**
* Destroy instance.
*
* @public
* @memberof ItemRelease.prototype
* @returns {ItemRelease}
*/
ItemRelease.prototype.destroy = function() {
if (this._isDestroyed) return this;
this.stop(true);
this._item = null;
this._isDestroyed = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* Reset public data and remove releasing class.
*
* @private
* @memberof ItemRelease.prototype
*/
ItemRelease.prototype._reset = function() {
if (this._isDestroyed) return;
var item = this._item;
this._isActive = false;
this._isPositioningStarted = false;
this._containerDiffX = 0;
this._containerDiffY = 0;
removeClass(item._element, item.getGrid()._settings.itemReleasingClass);
};
/**
* Get current values of the provided styles definition object.
*
* @param {HTMLElement} element
* @param {Object} styles
* @return {Object}
*/
function getCurrentStyles(element, styles) {
var current = {};
for (var prop in styles) {
current[prop] = getStyle(element, getStyleName(prop));
}
return current;
}
/**
* Visibility manager for Item instance.
*
* @class
* @param {Item} item
*/
function ItemVisibility(item) {
var isActive = item._isActive;
var element = item._element;
var settings = item.getGrid()._settings;
this._item = item;
this._isDestroyed = false;
// Set up visibility states.
this._isHidden = !isActive;
this._isHiding = false;
this._isShowing = false;
// Callback queue.
this._queue = new Queue();
// Bind show/hide finishers.
this._finishShow = this._finishShow.bind(this);
this._finishHide = this._finishHide.bind(this);
// Force item to be either visible or hidden on init.
element.style.display = isActive ? 'block' : 'none';
// Set visible/hidden class.
addClass(element, isActive ? settings.itemVisibleClass : settings.itemHiddenClass);
// Set initial styles for the child element.
setStyles(item._child, isActive ? settings.visibleStyles : settings.hiddenStyles);
}
/**
* Public prototype methods
* ************************
*/
/**
* Show item.
*
* @public
* @memberof ItemVisibility.prototype
* @param {Boolean} instant
* @param {Function} [onFinish]
* @returns {ItemVisibility}
*/
ItemVisibility.prototype.show = function(instant, onFinish) {
if (this._isDestroyed) return this;
var item = this._item;
var element = item._element;
var queue = this._queue;
var callback = isFunction(onFinish) ? onFinish : null;
var grid = item.getGrid();
var settings = grid._settings;
// If item is visible call the callback and be done with it.
if (!this._isShowing && !this._isHidden) {
callback && callback(false, item);
return this;
}
// If item is showing and does not need to be shown instantly, let's just
// push callback to the callback queue and be done with it.
if (this._isShowing && !instant) {
callback && queue.add(callback);
return this;
}
// If the item is hiding or hidden process the current visibility callback
// queue with the interrupted flag active, update classes and set display
// to block if necessary.
if (!this._isShowing) {
queue.flush(true, item);
removeClass(element, settings.itemHiddenClass);
addClass(element, settings.itemVisibleClass);
if (!this._isHiding) element.style.display = 'block';
}
// Push callback to the callback queue.
callback && queue.add(callback);
// Update visibility states.
item._isActive = this._isShowing = true;
this._isHiding = this._isHidden = false;
// Finally let's start show animation.
this._startAnimation(true, instant, this._finishShow);
return this;
};
/**
* Hide item.
*
* @public
* @memberof ItemVisibility.prototype
* @param {Boolean} instant
* @param {Function} [onFinish]
* @returns {ItemVisibility}
*/
ItemVisibility.prototype.hide = function(instant, onFinish) {
if (this._isDestroyed) return this;
var item = this._item;
var element = item._element;
var queue = this._queue;
var callback = isFunction(onFinish) ? onFinish : null;
var grid = item.getGrid();
var settings = grid._settings;
// If item is already hidden call the callback and be done with it.
if (!this._isHiding && this._isHidden) {
callback && callback(false, item);
return this;
}
// If item is hiding and does not need to be hidden instantly, let's just
// push callback to the callback queue and be done with it.
if (this._isHiding && !instant) {
callback && queue.add(callback);
return this;
}
// If the item is showing or visible process the current visibility callback
// queue with the interrupted flag active, update classes and set display
// to block if necessary.
if (!this._isHiding) {
queue.flush(true, item);
addClass(element, settings.itemHiddenClass);
removeClass(element, settings.itemVisibleClass);
}
// Push callback to the callback queue.
callback && queue.add(callback);
// Update visibility states.
this._isHidden = this._isHiding = true;
item._isActive = this._isShowing = false;
// Finally let's start hide animation.
this._startAnimation(false, instant, this._finishHide);
return this;
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
* @memberof ItemVisibility.prototype
* @returns {ItemVisibility}
*/
ItemVisibility.prototype.destroy = function() {
if (this._isDestroyed) return this;
var item = this._item;
var element = item._element;
var grid = item.getGrid();
var queue = this._queue;
var settings = grid._settings;
// Stop visibility animation.
this._stopAnimation({});
// Fire all uncompleted callbacks with interrupted flag and destroy the queue.
queue.flush(true, item).destroy();
// Remove visible/hidden classes.
removeClass(element, settings.itemVisibleClass);
removeClass(element, settings.itemHiddenClass);
// Reset state.
this._item = null;
this._isHiding = this._isShowing = false;
this._isDestroyed = this._isHidden = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* Start visibility animation.
*
* @private
* @memberof ItemVisibility.prototype
* @param {Boolean} toVisible
* @param {Boolean} [instant]
* @param {Function} [onFinish]
*/
ItemVisibility.prototype._startAnimation = function(toVisible, instant, onFinish) {
if (this._isDestroyed) return;
var item = this._item;
var settings = item.getGrid()._settings;
var targetStyles = toVisible ? settings.visibleStyles : settings.hiddenStyles;
var duration = parseInt(toVisible ? settings.showDuration : settings.hideDuration) || 0;
var easing = (toVisible ? settings.showEasing : settings.hideEasing) || 'ease';
var isInstant = instant || duration <= 0;
var currentStyles;
// No target styles? Let's quit early.
if (!targetStyles) {
onFinish && onFinish();
return;
}
// Cancel queued visibility tick.
cancelVisibilityTick(item._id);
// If we need to apply the styles instantly without animation.
if (isInstant) {
if (item._animateChild.isAnimating()) {
item._animateChild.stop(targetStyles);
} else {
setStyles(item._child, targetStyles);
}
onFinish && onFinish();
return;
}
// Start the animation in the next tick (to avoid layout thrashing).
addVisibilityTick(
item._id,
function() {
currentStyles = getCurrentStyles(item._child, targetStyles);
},
function() {
item._animateChild.start(currentStyles, targetStyles, {
duration: duration,
easing: easing,
onFinish: onFinish
});
}
);
};
/**
* Stop visibility animation.
*
* @private
* @memberof ItemVisibility.prototype
* @param {Object} [targetStyles]
*/
ItemVisibility.prototype._stopAnimation = function(targetStyles) {
if (this._isDestroyed) return;
var item = this._item;
cancelVisibilityTick(item._id);
item._animateChild.stop(targetStyles);
};
/**
* Finish show procedure.
*
* @private
* @memberof ItemVisibility.prototype
*/
ItemVisibility.prototype._finishShow = function() {
if (this._isHidden) return;
this._isShowing = false;
this._queue.flush(false, this._item);
};
/**
* Finish hide procedure.
*
* @private
* @memberof ItemVisibility.prototype
*/
var finishStyles = {};
ItemVisibility.prototype._finishHide = function() {
if (!this._isHidden) return;
var item = this._item;
this._isHiding = false;
finishStyles.transform = getTranslateString(0, 0);
item._layout.stop(true, finishStyles);
item._element.style.display = 'none';
this._queue.flush(false, item);
};
var id = 0;
/**
* Returns a unique numeric id (increments a base value on every call).
* @returns {Number}
*/
function createUid() {
return ++id;
}
/**
* Creates a new Item instance for a Grid instance.
*
* @class
* @param {Grid} grid
* @param {HTMLElement} element
* @param {Boolean} [isActive]
*/
function Item(grid, element, isActive) {
var settings = grid._settings;
// Create instance id.
this._id = createUid();
// Reference to connected Grid instance's id.
this._gridId = grid._id;
// Destroyed flag.
this._isDestroyed = false;
// Set up initial positions.
this._left = 0;
this._top = 0;
// The elements.
this._element = element;
this._child = element.children[0];
// If the provided item element is not a direct child of the grid container
// element, append it to the grid container.
if (element.parentNode !== grid._element) {
grid._element.appendChild(element);
}
// Set item class.
addClass(element, settings.itemClass);
// If isActive is not defined, let's try to auto-detect it.
if (typeof isActive !== 'boolean') {
isActive = getStyle(element, 'display') !== 'none';
}
// Set up active state (defines if the item is considered part of the layout
// or not).
this._isActive = isActive;
// Set element's initial position styles.
element.style.left = '0';
element.style.top = '0';
element.style[transformProp] = getTranslateString(0, 0);
// Initiate item's animation controllers.
this._animate = new ItemAnimate(element);
this._animateChild = new ItemAnimate(this._child);
// Setup visibility handler.
this._visibility = new ItemVisibility(this);
// Set up layout handler.
this._layout = new ItemLayout(this);
// Set up migration handler data.
this._migrate = new ItemMigrate(this);
// Set up release handler. Note that although this is fully linked to dragging
// this still needs to be always instantiated to handle migration scenarios
// correctly.
this._release = new ItemRelease(this);
// Set up drag placeholder handler. Note that although this is fully linked to
// dragging this still needs to be always instantiated to handle migration
// scenarios correctly.
this._dragPlaceholder = new ItemDragPlaceholder(this);
// Set up drag handler.
this._drag = settings.dragEnabled ? new ItemDrag(this) : null;
// Set up the initial dimensions and sort data.
this._refreshDimensions();
this._refreshSortData();
}
/**
* Public prototype methods
* ************************
*/
/**
* Get the instance grid reference.
*
* @public
* @memberof Item.prototype
* @returns {Grid}
*/
Item.prototype.getGrid = function() {
return gridInstances[this._gridId];
};
/**
* Get the instance element.
*
* @public
* @memberof Item.prototype
* @returns {HTMLElement}
*/
Item.prototype.getElement = function() {
return this._element;
};
/**
* Get instance element's cached width.
*
* @public
* @memberof Item.prototype
* @returns {Number}
*/
Item.prototype.getWidth = function() {
return this._width;
};
/**
* Get instance element's cached height.
*
* @public
* @memberof Item.prototype
* @returns {Number}
*/
Item.prototype.getHeight = function() {
return this._height;
};
/**
* Get instance element's cached margins.
*
* @public
* @memberof Item.prototype
* @returns {Object}
* - The returned object contains left, right, top and bottom properties
* which indicate the item element's cached margins.
*/
Item.prototype.getMargin = function() {
return {
left: this._marginLeft,
right: this._marginRight,
top: this._marginTop,
bottom: this._marginBottom
};
};
/**
* Get instance element's cached position.
*
* @public
* @memberof Item.prototype
* @returns {Object}
* - The returned object contains left and top properties which indicate the
* item element's cached position in the grid.
*/
Item.prototype.getPosition = function() {
return {
left: this._left,
top: this._top
};
};
/**
* Is the item active?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isActive = function() {
return this._isActive;
};
/**
* Is the item visible?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isVisible = function() {
return !!this._visibility && !this._visibility._isHidden;
};
/**
* Is the item being animated to visible?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isShowing = function() {
return !!(this._visibility && this._visibility._isShowing);
};
/**
* Is the item being animated to hidden?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isHiding = function() {
return !!(this._visibility && this._visibility._isHiding);
};
/**
* Is the item positioning?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isPositioning = function() {
return !!(this._layout && this._layout._isActive);
};
/**
* Is the item being dragged?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isDragging = function() {
return !!(this._drag && this._drag._isActive);
};
/**
* Is the item being released?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isReleasing = function() {
return !!(this._release && this._release._isActive);
};
/**
* Is the item destroyed?
*
* @public
* @memberof Item.prototype
* @returns {Boolean}
*/
Item.prototype.isDestroyed = function() {
return this._isDestroyed;
};
/**
* Private prototype methods
* *************************
*/
/**
* Recalculate item's dimensions.
*
* @private
* @memberof Item.prototype
*/
Item.prototype._refreshDimensions = function() {
if (this._isDestroyed || this._visibility._isHidden) return;
var element = this._element;
var dragPlaceholder = this._dragPlaceholder;
var rect = element.getBoundingClientRect();
// Calculate width and height.
this._width = rect.width;
this._height = rect.height;
// Calculate margins (ignore negative margins).
this._marginLeft = Math.max(0, getStyleAsFloat(element, 'margin-left'));
this._marginRight = Math.max(0, getStyleAsFloat(element, 'margin-right'));
this._marginTop = Math.max(0, getStyleAsFloat(element, 'margin-top'));
this._marginBottom = Math.max(0, getStyleAsFloat(element, 'margin-bottom'));
// Keep drag placeholder's dimensions synced with the item's.
if (dragPlaceholder) {
dragPlaceholder.updateDimensions(this._width, this._height);
}
};
/**
* Fetch and store item's sort data.
*
* @private
* @memberof Item.prototype
*/
Item.prototype._refreshSortData = function() {
if (this._isDestroyed) return;
var data = (this._sortData = {});
var getters = this.getGrid()._settings.sortData;
var prop;
for (prop in getters) {
data[prop] = getters[prop](this, this._element);
}
};
/**
* Destroy item instance.
*
* @private
* @memberof Item.prototype
* @param {Boolean} [removeElement=false]
*/
Item.prototype._destroy = function(removeElement) {
if (this._isDestroyed) return;
var element = this._element;
var grid = this.getGrid();
var settings = grid._settings;
var index = grid._items.indexOf(this);
// Destroy handlers.
this._release.destroy();
this._migrate.destroy();
this._layout.destroy();
this._visibility.destroy();
this._animate.destroy();
this._animateChild.destroy();
this._dragPlaceholder.destroy();
this._drag && this._drag.destroy();
// Remove all inline styles.
element.removeAttribute('style');
this._child.removeAttribute('style');
// Remove item class.
removeClass(element, settings.itemClass);
// Remove item from Grid instance if it still exists there.
index > -1 && grid._items.splice(index, 1);
// Remove element from DOM.
removeElement && element.parentNode.removeChild(element);
// Reset state.
this._isActive = false;
this._isDestroyed = true;
};
/**
* This is the default layout algorithm for Muuri. Based on MAXRECTS approach
* as described by Jukka Jylänki in his survey: "A Thousand Ways to Pack the
* Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing.".
*
* @class
*/
function Packer() {
this._slots = [];
this._slotSizes = [];
this._freeSlots = [];
this._newSlots = [];
this._rectItem = {};
this._rectStore = [];
this._rectId = 0;
// The layout return data, which will be populated in getLayout.
this._layout = {
slots: null,
setWidth: false,
setHeight: false,
width: false,
height: false
};
// Bind sort handlers.
this._sortRectsLeftTop = this._sortRectsLeftTop.bind(this);
this._sortRectsTopLeft = this._sortRectsTopLeft.bind(this);
}
/**
* @public
* @memberof Packer.prototype
* @param {Item[]} items
* @param {Number} width
* @param {Number} height
* @param {Number[]} [slots]
* @param {Object} [options]
* @param {Boolean} [options.fillGaps=false]
* @param {Boolean} [options.horizontal=false]
* @param {Boolean} [options.alignRight=false]
* @param {Boolean} [options.alignBottom=false]
* @returns {LayoutData}
*/
Packer.prototype.getLayout = function(items, width, height, slots, options) {
var layout = this._layout;
var fillGaps = !!(options && options.fillGaps);
var isHorizontal = !!(options && options.horizontal);
var alignRight = !!(options && options.alignRight);
var alignBottom = !!(options && options.alignBottom);
var rounding = !!(options && options.rounding);
var slotSizes = this._slotSizes;
var i;
// Reset layout data.
layout.slots = slots ? slots : this._slots;
layout.width = isHorizontal ? 0 : rounding ? Math.round(width) : width;
layout.height = !isHorizontal ? 0 : rounding ? Math.round(height) : height;
layout.setWidth = isHorizontal;
layout.setHeight = !isHorizontal;
// Make sure slots and slot size arrays are reset.
layout.slots.length = 0;
slotSizes.length = 0;
// No need to go further if items do not exist.
if (!items.length) return layout;
// Find slots for items.
for (i = 0; i < items.length; i++) {
this._addSlot(items[i], isHorizontal, fillGaps, rounding, alignRight || alignBottom);
}
// If the alignment is set to right we need to adjust the results.
if (alignRight) {
for (i = 0; i < layout.slots.length; i = i + 2) {
layout.slots[i] = layout.width - (layout.slots[i] + slotSizes[i]);
}
}
// If the alignment is set to bottom we need to adjust the results.
if (alignBottom) {
for (i = 1; i < layout.slots.length; i = i + 2) {
layout.slots[i] = layout.height - (layout.slots[i] + slotSizes[i]);
}
}
// Reset slots arrays and rect id.
slotSizes.length = 0;
this._freeSlots.length = 0;
this._newSlots.length = 0;
this._rectId = 0;
return layout;
};
/**
* Calculate position for the layout item. Returns the left and top position
* of the item in pixels.
*
* @private
* @memberof Packer.prototype
* @param {Item} item
* @param {Boolean} isHorizontal
* @param {Boolean} fillGaps
* @param {Boolean} rounding
* @returns {Array}
*/
Packer.prototype._addSlot = (function() {
var eps = 0.001;
var itemSlot = {};
return function(item, isHorizontal, fillGaps, rounding, trackSize) {
var layout = this._layout;
var freeSlots = this._freeSlots;
var newSlots = this._newSlots;
var rect;
var rectId;
var potentialSlots;
var ignoreCurrentSlots;
var i;
var ii;
// Reset new slots.
newSlots.length = 0;
// Set item slot initial data.
itemSlot.left = null;
itemSlot.top = null;
itemSlot.width = item._width + item._marginLeft + item._marginRight;
itemSlot.height = item._height + item._marginTop + item._marginBottom;
// Round item slot width and height if needed.
if (rounding) {
itemSlot.width = Math.round(itemSlot.width);
itemSlot.height = Math.round(itemSlot.height);
}
// Try to find a slot for the item.
for (i = 0; i < freeSlots.length; i++) {
rectId = freeSlots[i];
if (!rectId) continue;
rect = this._getRect(rectId);
if (itemSlot.width <= rect.width + eps && itemSlot.height <= rect.height + eps) {
itemSlot.left = rect.left;
itemSlot.top = rect.top;
break;
}
}
// If no slot was found for the item.
if (itemSlot.left === null) {
// Position the item in to the bottom left (vertical mode) or top right
// (horizontal mode) of the grid.
itemSlot.left = !isHorizontal ? 0 : layout.width;
itemSlot.top = !isHorizontal ? layout.height : 0;
// If gaps don't need filling do not add any current slots to the new
// slots array.
if (!fillGaps) {
ignoreCurrentSlots = true;
}
}
// In vertical mode, if the item's bottom overlaps the grid's bottom.
if (!isHorizontal && itemSlot.top + itemSlot.height > layout.height) {
// If item is not aligned to the left edge, create a new slot.
if (itemSlot.left > 0) {
newSlots.push(this._addRect(0, layout.height, itemSlot.left, Infinity));
}
// If item is not aligned to the right edge, create a new slot.
if (itemSlot.left + itemSlot.width < layout.width) {
newSlots.push(
this._addRect(
itemSlot.left + itemSlot.width,
layout.height,
layout.width - itemSlot.left - itemSlot.width,
Infinity
)
);
}
// Update grid height.
layout.height = itemSlot.top + itemSlot.height;
}
// In horizontal mode, if the item's right overlaps the grid's right edge.
if (isHorizontal && itemSlot.left + itemSlot.width > layout.width) {
// If item is not aligned to the top, create a new slot.
if (itemSlot.top > 0) {
newSlots.push(this._addRect(layout.width, 0, Infinity, itemSlot.top));
}
// If item is not aligned to the bottom, create a new slot.
if (itemSlot.top + itemSlot.height < layout.height) {
newSlots.push(
this._addRect(
layout.width,
itemSlot.top + itemSlot.height,
Infinity,
layout.height - itemSlot.top - itemSlot.height
)
);
}
// Update grid width.
layout.width = itemSlot.left + itemSlot.width;
}
// Clean up the current slots making sure there are no old slots that
// overlap with the item. If an old slot overlaps with the item, split it
// into smaller slots if necessary.
for (i = fillGaps ? 0 : ignoreCurrentSlots ? freeSlots.length : i; i < freeSlots.length; i++) {
rectId = freeSlots[i];
if (!rectId) continue;
rect = this._getRect(rectId);
potentialSlots = this._splitRect(rect, itemSlot);
for (ii = 0; ii < potentialSlots.length; ii++) {
rectId = potentialSlots[ii];
rect = this._getRect(rectId);
// Let's make sure here that we have a big enough slot
// (width/height > 0.49px) and also let's make sure that the slot is
// within the boundaries of the grid.
if (
rect.width > 0.49 &&
rect.height > 0.49 &&
((!isHorizontal && rect.top < layout.height) ||
(isHorizontal && rect.left < layout.width))
) {
newSlots.push(rectId);
}
}
}
// Sanitize new slots.
if (newSlots.length) {
this._purgeRects(newSlots).sort(
isHorizontal ? this._sortRectsLeftTop : this._sortRectsTopLeft
);
}
// Update layout width/height.
if (isHorizontal) {
layout.width = Math.max(layout.width, itemSlot.left + itemSlot.width);
} else {
layout.height = Math.max(layout.height, itemSlot.top + itemSlot.height);
}
// Add item slot data to layout slots (and store the slot size for later
// usage too if necessary).
layout.slots.push(itemSlot.left, itemSlot.top);
if (trackSize) this._slotSizes.push(itemSlot.width, itemSlot.height);
// Free/new slots switcheroo!
this._freeSlots = newSlots;
this._newSlots = freeSlots;
};
})();
/**
* Add a new rectangle to the rectangle store. Returns the id of the new
* rectangle.
*
* @private
* @memberof Packer.prototype
* @param {Number} left
* @param {Number} top
* @param {Number} width
* @param {Number} height
* @returns {RectId}
*/
Packer.prototype._addRect = function(left, top, width, height) {
var rectId = ++this._rectId;
var rectStore = this._rectStore;
rectStore[rectId] = left || 0;
rectStore[++this._rectId] = top || 0;
rectStore[++this._rectId] = width || 0;
rectStore[++this._rectId] = height || 0;
return rectId;
};
/**
* Get rectangle data from the rectangle store by id. Optionally you can
* provide a target object where the rectangle data will be written in. By
* default an internal object is reused as a target object.
*
* @private
* @memberof Packer.prototype
* @param {RectId} id
* @param {Object} [target]
* @returns {Object}
*/
Packer.prototype._getRect = function(id, target) {
var rectItem = target ? target : this._rectItem;
var rectStore = this._rectStore;
rectItem.left = rectStore[id] || 0;
rectItem.top = rectStore[++id] || 0;
rectItem.width = rectStore[++id] || 0;
rectItem.height = rectStore[++id] || 0;
return rectItem;
};
/**
* Punch a hole into a rectangle and split the remaining area into smaller
* rectangles (4 at max).
*
* @private
* @memberof Packer.prototype
* @param {Rectangle} rect
* @param {Rectangle} hole
* @returns {RectId[]}
*/
Packer.prototype._splitRect = (function() {
var results = [];
return function(rect, hole) {
// Reset old results.
results.length = 0;
// If the rect does not overlap with the hole add rect to the return data
// as is.
if (!this._doRectsOverlap(rect, hole)) {
results.push(this._addRect(rect.left, rect.top, rect.width, rect.height));
return results;
}
// Left split.
if (rect.left < hole.left) {
results.push(this._addRect(rect.left, rect.top, hole.left - rect.left, rect.height));
}
// Right split.
if (rect.left + rect.width > hole.left + hole.width) {
results.push(
this._addRect(
hole.left + hole.width,
rect.top,
rect.left + rect.width - (hole.left + hole.width),
rect.height
)
);
}
// Top split.
if (rect.top < hole.top) {
results.push(this._addRect(rect.left, rect.top, rect.width, hole.top - rect.top));
}
// Bottom split.
if (rect.top + rect.height > hole.top + hole.height) {
results.push(
this._addRect(
rect.left,
hole.top + hole.height,
rect.width,
rect.top + rect.height - (hole.top + hole.height)
)
);
}
return results;
};
})();
/**
* Check if two rectangles overlap.
*
* @private
* @memberof Packer.prototype
* @param {Rectangle} a
* @param {Rectangle} b
* @returns {Boolean}
*/
Packer.prototype._doRectsOverlap = function(a, b) {
return !(
a.left + a.width <= b.left ||
b.left + b.width <= a.left ||
a.top + a.height <= b.top ||
b.top + b.height <= a.top
);
};
/**
* Check if a rectangle is fully within another rectangle.
*
* @private
* @memberof Packer.prototype
* @param {Rectangle} a
* @param {Rectangle} b
* @returns {Boolean}
*/
Packer.prototype._isRectWithinRect = function(a, b) {
return (
a.left >= b.left &&
a.top >= b.top &&
a.left + a.width <= b.left + b.width &&
a.top + a.height <= b.top + b.height
);
};
/**
* Loops through an array of rectangle ids and resets all that are fully
* within another rectangle in the array. Resetting in this case means that
* the rectangle id value is replaced with zero.
*
* @private
* @memberof Packer.prototype
* @param {RectId[]} rectIds
* @returns {RectId[]}
*/
Packer.prototype._purgeRects = (function() {
var rectA = {};
var rectB = {};
return function(rectIds) {
var i = rectIds.length;
var ii;
while (i--) {
ii = rectIds.length;
if (!rectIds[i]) continue;
this._getRect(rectIds[i], rectA);
while (ii--) {
if (!rectIds[ii] || i === ii) continue;
if (this._isRectWithinRect(rectA, this._getRect(rectIds[ii], rectB))) {
rectIds[i] = 0;
break;
}
}
}
return rectIds;
};
})();
/**
* Sort rectangles with top-left gravity.
*
* @private
* @memberof Packer.prototype
* @param {RectId} aId
* @param {RectId} bId
* @returns {Number}
*/
Packer.prototype._sortRectsTopLeft = (function() {
var rectA = {};
var rectB = {};
return function(aId, bId) {
this._getRect(aId, rectA);
this._getRect(bId, rectB);
// prettier-ignore
return rectA.top < rectB.top ? -1 :
rectA.top > rectB.top ? 1 :
rectA.left < rectB.left ? -1 :
rectA.left > rectB.left ? 1 : 0;
};
})();
/**
* Sort rectangles with left-top gravity.
*
* @private
* @memberof Packer.prototype
* @param {RectId} aId
* @param {RectId} bId
* @returns {Number}
*/
Packer.prototype._sortRectsLeftTop = (function() {
var rectA = {};
var rectB = {};
return function(aId, bId) {
this._getRect(aId, rectA);
this._getRect(bId, rectB);
// prettier-ignore
return rectA.left < rectB.left ? -1 :
rectA.left > rectB.left ? 1 :
rectA.top < rectB.top ? -1 :
rectA.top > rectB.top ? 1 : 0;
};
})();
var htmlCollectionType = '[object HTMLCollection]';
var nodeListType = '[object NodeList]';
/**
* Check if a value is a node list
*
* @param {*} val
* @returns {Boolean}
*/
function isNodeList(val) {
var type = Object.prototype.toString.call(val);
return type === htmlCollectionType || type === nodeListType;
}
var objectType = 'object';
var objectToStringType = '[object Object]';
var toString = Object.prototype.toString;
/**
* Check if a value is a plain object.
*
* @param {*} val
* @returns {Boolean}
*/
function isPlainObject(val) {
return typeof val === objectType && toString.call(val) === objectToStringType;
}
/**
* Converts a value to an array or clones an array.
*
* @param {*} target
* @returns {Array}
*/
function toArray(target) {
return isNodeList(target) ? Array.prototype.slice.call(target) : Array.prototype.concat(target);
}
var packer = new Packer();
var noop = function() {};
var numberType$1 = 'number';
var stringType = 'string';
var instantLayout = 'instant';
/**
* Creates a new Grid instance.
*
* @class
* @param {(HTMLElement|String)} element
* @param {Object} [options]
* @param {?(HTMLElement[]|NodeList|String)} [options.items]
* @param {Number} [options.showDuration=300]
* @param {String} [options.showEasing="ease"]
* @param {Object} [options.visibleStyles]
* @param {Number} [options.hideDuration=300]
* @param {String} [options.hideEasing="ease"]
* @param {Object} [options.hiddenStyles]
* @param {(Function|Object)} [options.layout]
* @param {Boolean} [options.layout.fillGaps=false]
* @param {Boolean} [options.layout.horizontal=false]
* @param {Boolean} [options.layout.alignRight=false]
* @param {Boolean} [options.layout.alignBottom=false]
* @param {Boolean} [options.layout.rounding=true]
* @param {(Boolean|Number)} [options.layoutOnResize=100]
* @param {Boolean} [options.layoutOnInit=true]
* @param {Number} [options.layoutDuration=300]
* @param {String} [options.layoutEasing="ease"]
* @param {?Object} [options.sortData=null]
* @param {Boolean} [options.dragEnabled=false]
* @param {?HtmlElement} [options.dragContainer=null]
* @param {?Function} [options.dragStartPredicate]
* @param {Number} [options.dragStartPredicate.distance=0]
* @param {Number} [options.dragStartPredicate.delay=0]
* @param {(Boolean|String)} [options.dragStartPredicate.handle=false]
* @param {?String} [options.dragAxis]
* @param {(Boolean|Function)} [options.dragSort=true]
* @param {Object} [options.dragSortHeuristics]
* @param {Number} [options.dragSortHeuristics.sortInterval=100]
* @param {Number} [options.dragSortHeuristics.minDragDistance=10]
* @param {Number} [options.dragSortHeuristics.minBounceBackAngle=1]
* @param {(Function|Object)} [options.dragSortPredicate]
* @param {Number} [options.dragSortPredicate.threshold=50]
* @param {String} [options.dragSortPredicate.action="move"]
* @param {Number} [options.dragReleaseDuration=300]
* @param {String} [options.dragReleaseEasing="ease"]
* @param {Object} [options.dragCssProps]
* @param {Object} [options.dragPlaceholder]
* @param {Boolean} [options.dragPlaceholder.enabled=false]
* @param {Number} [options.dragPlaceholder.duration=300]
* @param {String} [options.dragPlaceholder.easing="ease"]
* @param {?Function} [options.dragPlaceholder.createElement=null]
* @param {?Function} [options.dragPlaceholder.onCreate=null]
* @param {?Function} [options.dragPlaceholder.onRemove=null]
* @param {String} [options.containerClass="muuri"]
* @param {String} [options.itemClass="muuri-item"]
* @param {String} [options.itemVisibleClass="muuri-item-visible"]
* @param {String} [options.itemHiddenClass="muuri-item-hidden"]
* @param {String} [options.itemPositioningClass="muuri-item-positioning"]
* @param {String} [options.itemDraggingClass="muuri-item-dragging"]
* @param {String} [options.itemReleasingClass="muuri-item-releasing"]
* @param {String} [options.itemPlaceholderClass="muuri-item-placeholder"]
*/
function Grid(element, options) {
var inst = this;
var settings;
var items;
var layoutOnResize;
// Allow passing element as selector string. Store element for instance.
element = this._element =
typeof element === stringType ? window.document.querySelector(element) : element;
// Throw an error if the container element is not body element or does not
// exist within the body element.
var isElementInDom = element.getRootNode
? element.getRootNode({ composed: true }) === document
: window.document.body.contains(element);
if (!isElementInDom || element === window.document.documentElement) {
throw new Error('Container element must be an existing DOM element');
}
// Create instance settings by merging the options with default options.
settings = this._settings = mergeSettings(Grid.defaultOptions, options);
// Sanitize dragSort setting.
if (!isFunction(settings.dragSort)) {
settings.dragSort = !!settings.dragSort;
}
// Create instance id and store it to the grid instances collection.
this._id = createUid();
gridInstances[this._id] = inst;
// Destroyed flag.
this._isDestroyed = false;
// The layout object (mutated on every layout).
this._layout = {
id: 0,
items: [],
slots: [],
setWidth: false,
setHeight: false,
width: 0,
height: 0
};
// Create private Emitter instance.
this._emitter = new Emitter();
// Add container element's class name.
addClass(element, settings.containerClass);
// Create initial items.
this._items = [];
items = settings.items;
if (typeof items === stringType) {
toArray(element.children).forEach(function(itemElement) {
if (items === '*' || elementMatches(itemElement, items)) {
inst._items.push(new Item(inst, itemElement));
}
});
} else if (Array.isArray(items) || isNodeList(items)) {
this._items = toArray(items).map(function(itemElement) {
return new Item(inst, itemElement);
});
}
// If layoutOnResize option is a valid number sanitize it and bind the resize
// handler.
layoutOnResize = settings.layoutOnResize;
if (typeof layoutOnResize !== numberType$1) {
layoutOnResize = layoutOnResize === true ? 0 : -1;
}
if (layoutOnResize >= 0) {
window.addEventListener(
'resize',
(inst._resizeHandler = debounce(function() {
inst.refreshItems().layout();
}, layoutOnResize))
);
}
// Layout on init if necessary.
if (settings.layoutOnInit) {
this.layout(true);
}
}
/**
* Public properties
* *****************
*/
/**
* @see Item
*/
Grid.Item = Item;
/**
* @see ItemLayout
*/
Grid.ItemLayout = ItemLayout;
/**
* @see ItemVisibility
*/
Grid.ItemVisibility = ItemVisibility;
/**
* @see ItemMigrate
*/
Grid.ItemMigrate = ItemMigrate;
/**
* @see ItemAnimate
*/
Grid.ItemAnimate = ItemAnimate;
/**
* @see ItemDrag
*/
Grid.ItemDrag = ItemDrag;
/**
* @see ItemRelease
*/
Grid.ItemRelease = ItemRelease;
/**
* @see ItemDragPlaceholder
*/
Grid.ItemDragPlaceholder = ItemDragPlaceholder;
/**
* @see Emitter
*/
Grid.Emitter = Emitter;
/**
* @see Dragger
*/
Grid.Dragger = Dragger;
/**
* @see Packer
*/
Grid.Packer = Packer;
/**
* Default options for Grid instance.
*
* @public
* @memberof Grid
*/
Grid.defaultOptions = {
// Item elements
items: '*',
// Default show animation
showDuration: 300,
showEasing: 'ease',
// Default hide animation
hideDuration: 300,
hideEasing: 'ease',
// Item's visible/hidden state styles
visibleStyles: {
opacity: '1',
transform: 'scale(1)'
},
hiddenStyles: {
opacity: '0',
transform: 'scale(0.5)'
},
// Layout
layout: {
fillGaps: false,
horizontal: false,
alignRight: false,
alignBottom: false,
rounding: true
},
layoutOnResize: 100,
layoutOnInit: true,
layoutDuration: 300,
layoutEasing: 'ease',
// Sorting
sortData: null,
// Drag & Drop
dragEnabled: false,
dragContainer: null,
dragStartPredicate: {
distance: 0,
delay: 0,
handle: false
},
dragAxis: null,
dragSort: true,
dragSortHeuristics: {
sortInterval: 100,
minDragDistance: 10,
minBounceBackAngle: 1
},
dragSortPredicate: {
threshold: 50,
action: actionMove
},
dragReleaseDuration: 300,
dragReleaseEasing: 'ease',
dragCssProps: {
touchAction: 'none',
userSelect: 'none',
userDrag: 'none',
tapHighlightColor: 'rgba(0, 0, 0, 0)',
touchCallout: 'none',
contentZooming: 'none'
},
dragPlaceholder: {
enabled: false,
duration: 300,
easing: 'ease',
createElement: null,
onCreate: null,
onRemove: null
},
// Classnames
containerClass: 'muuri',
itemClass: 'muuri-item',
itemVisibleClass: 'muuri-item-shown',
itemHiddenClass: 'muuri-item-hidden',
itemPositioningClass: 'muuri-item-positioning',
itemDraggingClass: 'muuri-item-dragging',
itemReleasingClass: 'muuri-item-releasing',
itemPlaceholderClass: 'muuri-item-placeholder'
};
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @memberof Grid.prototype
* @param {String} event
* @param {Function} listener
* @returns {Grid}
*/
Grid.prototype.on = function(event, listener) {
this._emitter.on(event, listener);
return this;
};
/**
* Unbind an event listener.
*
* @public
* @memberof Grid.prototype
* @param {String} event
* @param {Function} listener
* @returns {Grid}
*/
Grid.prototype.off = function(event, listener) {
this._emitter.off(event, listener);
return this;
};
/**
* Get the container element.
*
* @public
* @memberof Grid.prototype
* @returns {HTMLElement}
*/
Grid.prototype.getElement = function() {
return this._element;
};
/**
* Get all items. Optionally you can provide specific targets (elements and
* indices). Note that the returned array is not the same object used by the
* instance so modifying it will not affect instance's items. All items that
* are not found are omitted from the returned array.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} [targets]
* @returns {Item[]}
*/
Grid.prototype.getItems = function(targets) {
// Return all items immediately if no targets were provided or if the
// instance is destroyed.
if (this._isDestroyed || (!targets && targets !== 0)) {
return this._items.slice(0);
}
var ret = [];
var targetItems = toArray(targets);
var item;
var i;
// If target items are defined return filtered results.
for (i = 0; i < targetItems.length; i++) {
item = this._getItem(targetItems[i]);
item && ret.push(item);
}
return ret;
};
/**
* Update the cached dimensions of the instance's items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} [items]
* @returns {Grid}
*/
Grid.prototype.refreshItems = function(items) {
if (this._isDestroyed) return this;
var targets = this.getItems(items);
var i;
for (i = 0; i < targets.length; i++) {
targets[i]._refreshDimensions();
}
return this;
};
/**
* Update the sort data of the instance's items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} [items]
* @returns {Grid}
*/
Grid.prototype.refreshSortData = function(items) {
if (this._isDestroyed) return this;
var targetItems = this.getItems(items);
var i;
for (i = 0; i < targetItems.length; i++) {
targetItems[i]._refreshSortData();
}
return this;
};
/**
* Synchronize the item elements to match the order of the items in the DOM.
* This comes handy if you need to keep the DOM structure matched with the
* order of the items. Note that if an item's element is not currently a child
* of the container element (if it is dragged for example) it is ignored and
* left untouched.
*
* @public
* @memberof Grid.prototype
* @returns {Grid}
*/
Grid.prototype.synchronize = function() {
if (this._isDestroyed) return this;
var container = this._element;
var items = this._items;
var fragment;
var element;
var i;
// Append all elements in order to the container element.
if (items.length) {
for (i = 0; i < items.length; i++) {
element = items[i]._element;
if (element.parentNode === container) {
fragment = fragment || window.document.createDocumentFragment();
fragment.appendChild(element);
}
}
if (fragment) container.appendChild(fragment);
}
// Emit synchronize event.
this._emit(eventSynchronize);
return this;
};
/**
* Calculate and apply item positions.
*
* @public
* @memberof Grid.prototype
* @param {Boolean} [instant=false]
* @param {LayoutCallback} [onFinish]
* @returns {Grid}
*/
Grid.prototype.layout = function(instant, onFinish) {
if (this._isDestroyed) return this;
var inst = this;
var element = this._element;
var layout = this._updateLayout();
var layoutId = layout.id;
var itemsLength = layout.items.length;
var counter = itemsLength;
var isBorderBox;
var item;
var i;
// The finish function, which will be used for checking if all the items
// have laid out yet. After all items have finished their animations call
// callback and emit layoutEnd event. Only emit layoutEnd event if there
// hasn't been a new layout call during this layout.
function tryFinish() {
if (--counter > 0) return;
var hasLayoutChanged = inst._layout.id !== layoutId;
var callback = isFunction(instant) ? instant : onFinish;
if (isFunction(callback)) {
callback(hasLayoutChanged, layout.items.slice(0));
}
if (!hasLayoutChanged && inst._hasListeners(eventLayoutEnd)) {
inst._emit(eventLayoutEnd, layout.items.slice(0));
}
}
// If grid's width or height was modified, we need to update it's cached
// dimensions. Also keep in mind that grid's cached width/height should
// always equal to what elem.getBoundingClientRect() would return, so
// therefore we need to add the grid element's borders to the dimensions if
// it's box-sizing is border-box. Note that we support providing the
// dimensions as a string here too so that one can define the unit of the
// dimensions, in which case we don't do the border-box check.
if (
(layout.setHeight && typeof layout.height === numberType$1) ||
(layout.setWidth && typeof layout.width === numberType$1)
) {
isBorderBox = getStyle(element, 'box-sizing') === 'border-box';
}
if (layout.setHeight) {
if (typeof layout.height === numberType$1) {
element.style.height =
(isBorderBox ? layout.height + this._borderTop + this._borderBottom : layout.height) + 'px';
} else {
element.style.height = layout.height;
}
}
if (layout.setWidth) {
if (typeof layout.width === numberType$1) {
element.style.width =
(isBorderBox ? layout.width + this._borderLeft + this._borderRight : layout.width) + 'px';
} else {
element.style.width = layout.width;
}
}
// Emit layoutStart event. Note that this is intentionally emitted after the
// container element's dimensions are set, because otherwise there would be
// no hook for reacting to container dimension changes.
if (this._hasListeners(eventLayoutStart)) {
this._emit(eventLayoutStart, layout.items.slice(0));
}
// If there are no items let's finish quickly.
if (!itemsLength) {
tryFinish();
return this;
}
// If there are items let's position them.
for (i = 0; i < itemsLength; i++) {
item = layout.items[i];
if (!item) continue;
// Update item's position.
item._left = layout.slots[i * 2];
item._top = layout.slots[i * 2 + 1];
// Layout item if it is not dragged.
item.isDragging() ? tryFinish() : item._layout.start(instant === true, tryFinish);
}
return this;
};
/**
* Add new items by providing the elements you wish to add to the instance and
* optionally provide the index where you want the items to be inserted into.
* All elements that are not already children of the container element will be
* automatically appended to the container element. If an element has it's CSS
* display property set to "none" it will be marked as inactive during the
* initiation process. As long as the item is inactive it will not be part of
* the layout, but it will retain it's index. You can activate items at any
* point with grid.show() method. This method will automatically call
* grid.layout() if one or more of the added elements are visible. If only
* hidden items are added no layout will be called. All the new visible items
* are positioned without animation during their first layout.
*
* @public
* @memberof Grid.prototype
* @param {(HTMLElement|HTMLElement[])} elements
* @param {Object} [options]
* @param {Number} [options.index=-1]
* @param {Boolean} [options.isActive]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Item[]}
*/
Grid.prototype.add = function(elements, options) {
if (this._isDestroyed || !elements) return [];
var newItems = toArray(elements);
if (!newItems.length) return newItems;
var opts = options || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var items = this._items;
var needsLayout = false;
var item;
var i;
// Map provided elements into new grid items.
for (i = 0; i < newItems.length; i++) {
item = new Item(this, newItems[i], opts.isActive);
newItems[i] = item;
// If the item to be added is active, we need to do a layout. Also, we
// need to mark the item with the skipNextAnimation flag to make it
// position instantly (without animation) during the next layout. Without
// the hack the item would animate to it's new position from the northwest
// corner of the grid, which feels a bit buggy (imho).
if (item._isActive) {
needsLayout = true;
item._layout._skipNextAnimation = true;
}
}
// Add the new items to the items collection to correct index.
arrayInsert(items, newItems, opts.index);
// Emit add event.
if (this._hasListeners(eventAdd)) {
this._emit(eventAdd, newItems.slice(0));
}
// If layout is needed.
if (needsLayout && layout) {
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined);
}
return newItems;
};
/**
* Remove items from the instance.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Object} [options]
* @param {Boolean} [options.removeElements=false]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Item[]}
*/
Grid.prototype.remove = function(items, options) {
if (this._isDestroyed) return this;
var opts = options || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var needsLayout = false;
var allItems = this.getItems();
var targetItems = this.getItems(items);
var indices = [];
var item;
var i;
// Remove the individual items.
for (i = 0; i < targetItems.length; i++) {
item = targetItems[i];
indices.push(allItems.indexOf(item));
if (item._isActive) needsLayout = true;
item._destroy(opts.removeElements);
}
// Emit remove event.
if (this._hasListeners(eventRemove)) {
this._emit(eventRemove, targetItems.slice(0), indices);
}
// If layout is needed.
if (needsLayout && layout) {
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined);
}
return targetItems;
};
/**
* Show instance items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {ShowCallback} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.show = function(items, options) {
if (this._isDestroyed) return this;
this._setItemsVisibility(items, true, options);
return this;
};
/**
* Hide instance items.
*
* @public
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {HideCallback} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.hide = function(items, options) {
if (this._isDestroyed) return this;
this._setItemsVisibility(items, false, options);
return this;
};
/**
* Filter items. Expects at least one argument, a predicate, which should be
* either a function or a string. The predicate callback is executed for every
* item in the instance. If the return value of the predicate is truthy the
* item in question will be shown and otherwise hidden. The predicate callback
* receives the item instance as it's argument. If the predicate is a string
* it is considered to be a selector and it is checked against every item
* element in the instance with the native element.matches() method. All the
* matching items will be shown and others hidden.
*
* @public
* @memberof Grid.prototype
* @param {(Function|String)} predicate
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {FilterCallback} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.filter = function(predicate, options) {
if (this._isDestroyed || !this._items.length) return this;
var itemsToShow = [];
var itemsToHide = [];
var isPredicateString = typeof predicate === stringType;
var isPredicateFn = isFunction(predicate);
var opts = options || 0;
var isInstant = opts.instant === true;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var onFinish = isFunction(opts.onFinish) ? opts.onFinish : null;
var tryFinishCounter = -1;
var tryFinish = noop;
var item;
var i;
// If we have onFinish callback, let's create proper tryFinish callback.
if (onFinish) {
tryFinish = function() {
++tryFinishCounter && onFinish(itemsToShow.slice(0), itemsToHide.slice(0));
};
}
// Check which items need to be shown and which hidden.
if (isPredicateFn || isPredicateString) {
for (i = 0; i < this._items.length; i++) {
item = this._items[i];
if (isPredicateFn ? predicate(item) : elementMatches(item._element, predicate)) {
itemsToShow.push(item);
} else {
itemsToHide.push(item);
}
}
}
// Show items that need to be shown.
if (itemsToShow.length) {
this.show(itemsToShow, {
instant: isInstant,
onFinish: tryFinish,
layout: false
});
} else {
tryFinish();
}
// Hide items that need to be hidden.
if (itemsToHide.length) {
this.hide(itemsToHide, {
instant: isInstant,
onFinish: tryFinish,
layout: false
});
} else {
tryFinish();
}
// If there are any items to filter.
if (itemsToShow.length || itemsToHide.length) {
// Emit filter event.
if (this._hasListeners(eventFilter)) {
this._emit(eventFilter, itemsToShow.slice(0), itemsToHide.slice(0));
}
// If layout is needed.
if (layout) {
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined);
}
}
return this;
};
/**
* Sort items. There are three ways to sort the items. The first is simply by
* providing a function as the comparer which works identically to native
* array sort. Alternatively you can sort by the sort data you have provided
* in the instance's options. Just provide the sort data key(s) as a string
* (separated by space) and the items will be sorted based on the provided
* sort data keys. Lastly you have the opportunity to provide a presorted
* array of items which will be used to sync the internal items array in the
* same order.
*
* @public
* @memberof Grid.prototype
* @param {(Function|Item[]|String|String[])} comparer
* @param {Object} [options]
* @param {Boolean} [options.descending=false]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.sort = (function() {
var sortComparer;
var isDescending;
var origItems;
var indexMap;
function parseCriteria(data) {
return data
.trim()
.split(' ')
.map(function(val) {
return val.split(':');
});
}
function getIndexMap(items) {
var ret = {};
for (var i = 0; i < items.length; i++) {
ret[items[i]._id] = i;
}
return ret;
}
function compareIndices(itemA, itemB) {
var indexA = indexMap[itemA._id];
var indexB = indexMap[itemB._id];
return isDescending ? indexB - indexA : indexA - indexB;
}
function defaultComparer(a, b) {
var result = 0;
var criteriaName;
var criteriaOrder;
var valA;
var valB;
// Loop through the list of sort criteria.
for (var i = 0; i < sortComparer.length; i++) {
// Get the criteria name, which should match an item's sort data key.
criteriaName = sortComparer[i][0];
criteriaOrder = sortComparer[i][1];
// Get items' cached sort values for the criteria. If the item has no sort
// data let's update the items sort data (this is a lazy load mechanism).
valA = (a._sortData ? a : a._refreshSortData())._sortData[criteriaName];
valB = (b._sortData ? b : b._refreshSortData())._sortData[criteriaName];
// Sort the items in descending order if defined so explicitly. Otherwise
// sort items in ascending order.
if (criteriaOrder === 'desc' || (!criteriaOrder && isDescending)) {
result = valB < valA ? -1 : valB > valA ? 1 : 0;
} else {
result = valA < valB ? -1 : valA > valB ? 1 : 0;
}
// If we have -1 or 1 as the return value, let's return it immediately.
if (result) return result;
}
// If values are equal let's compare the item indices to make sure we
// have a stable sort.
if (!result) {
if (!indexMap) indexMap = getIndexMap(origItems);
result = compareIndices(a, b);
}
return result;
}
function customComparer(a, b) {
var result = sortComparer(a, b);
// If descending let's invert the result value.
if (isDescending && result) result = -result;
// If we have a valid result (not zero) let's return it right away.
if (result) return result;
// If result is zero let's compare the item indices to make sure we have a
// stable sort.
if (!indexMap) indexMap = getIndexMap(origItems);
return compareIndices(a, b);
}
return function(comparer, options) {
if (this._isDestroyed || this._items.length < 2) return this;
var items = this._items;
var opts = options || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var i;
// Setup parent scope data.
sortComparer = comparer;
isDescending = !!opts.descending;
origItems = items.slice(0);
indexMap = null;
// If function is provided do a native array sort.
if (isFunction(sortComparer)) {
items.sort(customComparer);
}
// Otherwise if we got a string, let's sort by the sort data as provided in
// the instance's options.
else if (typeof sortComparer === stringType) {
sortComparer = parseCriteria(comparer);
items.sort(defaultComparer);
}
// Otherwise if we got an array, let's assume it's a presorted array of the
// items and order the items based on it.
else if (Array.isArray(sortComparer)) {
if (sortComparer.length !== items.length) {
throw new Error('[' + namespace + '] sort reference items do not match with grid items.');
}
for (i = 0; i < items.length; i++) {
if (sortComparer.indexOf(items[i]) < 0) {
throw new Error('[' + namespace + '] sort reference items do not match with grid items.');
}
items[i] = sortComparer[i];
}
if (isDescending) items.reverse();
}
// Otherwise let's just skip it, nothing we can do here.
else {
/** @todo Maybe throw an error here? */
return this;
}
// Emit sort event.
if (this._hasListeners(eventSort)) {
this._emit(eventSort, items.slice(0), origItems);
}
// If layout is needed.
if (layout) {
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined);
}
return this;
};
})();
/**
* Move item to another index or in place of another item.
*
* @public
* @memberof Grid.prototype
* @param {GridSingleItemQuery} item
* @param {GridSingleItemQuery} position
* @param {Object} [options]
* @param {String} [options.action="move"]
* - Accepts either "move" or "swap".
* - "move" moves the item in place of the other item.
* - "swap" swaps the position of the items.
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
* @returns {Grid}
*/
Grid.prototype.move = function(item, position, options) {
if (this._isDestroyed || this._items.length < 2) return this;
var items = this._items;
var opts = options || 0;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var isSwap = opts.action === actionSwap;
var action = isSwap ? actionSwap : actionMove;
var fromItem = this._getItem(item);
var toItem = this._getItem(position);
var fromIndex;
var toIndex;
// Make sure the items exist and are not the same.
if (fromItem && toItem && fromItem !== toItem) {
// Get the indices of the items.
fromIndex = items.indexOf(fromItem);
toIndex = items.indexOf(toItem);
// Do the move/swap.
if (isSwap) {
arraySwap(items, fromIndex, toIndex);
} else {
arrayMove(items, fromIndex, toIndex);
}
// Emit move event.
if (this._hasListeners(eventMove)) {
this._emit(eventMove, {
item: fromItem,
fromIndex: fromIndex,
toIndex: toIndex,
action: action
});
}
// If layout is needed.
if (layout) {
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined);
}
}
return this;
};
/**
* Send item to another Grid instance.
*
* @public
* @memberof Grid.prototype
* @param {GridSingleItemQuery} item
* @param {Grid} grid
* @param {GridSingleItemQuery} position
* @param {Object} [options]
* @param {HTMLElement} [options.appendTo=document.body]
* @param {(Boolean|LayoutCallback|String)} [options.layoutSender=true]
* @param {(Boolean|LayoutCallback|String)} [options.layoutReceiver=true]
* @returns {Grid}
*/
Grid.prototype.send = function(item, grid, position, options) {
if (this._isDestroyed || grid._isDestroyed || this === grid) return this;
// Make sure we have a valid target item.
item = this._getItem(item);
if (!item) return this;
var opts = options || 0;
var container = opts.appendTo || window.document.body;
var layoutSender = opts.layoutSender ? opts.layoutSender : opts.layoutSender === undefined;
var layoutReceiver = opts.layoutReceiver
? opts.layoutReceiver
: opts.layoutReceiver === undefined;
// Start the migration process.
item._migrate.start(grid, position, container);
// If migration was started successfully and the item is active, let's layout
// the grids.
if (item._migrate._isActive && item._isActive) {
if (layoutSender) {
this.layout(
layoutSender === instantLayout,
isFunction(layoutSender) ? layoutSender : undefined
);
}
if (layoutReceiver) {
grid.layout(
layoutReceiver === instantLayout,
isFunction(layoutReceiver) ? layoutReceiver : undefined
);
}
}
return this;
};
/**
* Destroy the instance.
*
* @public
* @memberof Grid.prototype
* @param {Boolean} [removeElements=false]
* @returns {Grid}
*/
Grid.prototype.destroy = function(removeElements) {
if (this._isDestroyed) return this;
var container = this._element;
var items = this._items.slice(0);
var i;
// Unbind window resize event listener.
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler);
}
// Destroy items.
for (i = 0; i < items.length; i++) {
items[i]._destroy(removeElements);
}
// Restore container.
removeClass(container, this._settings.containerClass);
container.style.height = '';
container.style.width = '';
// Emit destroy event and unbind all events.
this._emit(eventDestroy);
this._emitter.destroy();
// Remove reference from the grid instances collection.
gridInstances[this._id] = undefined;
// Flag instance as destroyed.
this._isDestroyed = true;
return this;
};
/**
* Private prototype methods
* *************************
*/
/**
* Get instance's item by element or by index. Target can also be an Item
* instance in which case the function returns the item if it exists within
* related Grid instance. If nothing is found with the provided target, null
* is returned.
*
* @private
* @memberof Grid.prototype
* @param {GridSingleItemQuery} [target]
* @returns {?Item}
*/
Grid.prototype._getItem = function(target) {
// If no target is specified or the instance is destroyed, return null.
if (this._isDestroyed || (!target && target !== 0)) {
return null;
}
// If target is number return the item in that index. If the number is lower
// than zero look for the item starting from the end of the items array. For
// example -1 for the last item, -2 for the second last item, etc.
if (typeof target === numberType$1) {
return this._items[target > -1 ? target : this._items.length + target] || null;
}
// If the target is an instance of Item return it if it is attached to this
// Grid instance, otherwise return null.
if (target instanceof Item) {
return target._gridId === this._id ? target : null;
}
// In other cases let's assume that the target is an element, so let's try
// to find an item that matches the element and return it. If item is not
// found return null.
/** @todo This could be made a lot faster by using Map/WeakMap of elements. */
for (var i = 0; i < this._items.length; i++) {
if (this._items[i]._element === target) {
return this._items[i];
}
}
return null;
};
/**
* Recalculates and updates instance's layout data.
*
* @private
* @memberof Grid.prototype
* @returns {LayoutData}
*/
Grid.prototype._updateLayout = function() {
var layout = this._layout;
var settings = this._settings.layout;
var width;
var height;
var newLayout;
var i;
// Let's increment layout id.
++layout.id;
// Let's update layout items
layout.items.length = 0;
for (i = 0; i < this._items.length; i++) {
if (this._items[i]._isActive) layout.items.push(this._items[i]);
}
// Let's make sure we have the correct container dimensions.
this._refreshDimensions();
// Calculate container width and height (without borders).
width = this._width - this._borderLeft - this._borderRight;
height = this._height - this._borderTop - this._borderBottom;
// Calculate new layout.
if (isFunction(settings)) {
newLayout = settings(layout.items, width, height);
} else {
newLayout = packer.getLayout(layout.items, width, height, layout.slots, settings);
}
// Let's update the grid's layout.
layout.slots = newLayout.slots;
layout.setWidth = Boolean(newLayout.setWidth);
layout.setHeight = Boolean(newLayout.setHeight);
layout.width = newLayout.width;
layout.height = newLayout.height;
return layout;
};
/**
* Emit a grid event.
*
* @private
* @memberof Grid.prototype
* @param {String} event
* @param {...*} [arg]
*/
Grid.prototype._emit = function() {
if (this._isDestroyed) return;
this._emitter.emit.apply(this._emitter, arguments);
};
/**
* Check if there are any events listeners for an event.
*
* @private
* @memberof Grid.prototype
* @param {String} event
* @returns {Boolean}
*/
Grid.prototype._hasListeners = function(event) {
var listeners = this._emitter._events[event];
return !!(listeners && listeners.length);
};
/**
* Update container's width, height and offsets.
*
* @private
* @memberof Grid.prototype
*/
Grid.prototype._updateBoundingRect = function() {
var element = this._element;
var rect = element.getBoundingClientRect();
this._width = rect.width;
this._height = rect.height;
this._left = rect.left;
this._top = rect.top;
};
/**
* Update container's border sizes.
*
* @private
* @memberof Grid.prototype
* @param {Boolean} left
* @param {Boolean} right
* @param {Boolean} top
* @param {Boolean} bottom
*/
Grid.prototype._updateBorders = function(left, right, top, bottom) {
var element = this._element;
if (left) this._borderLeft = getStyleAsFloat(element, 'border-left-width');
if (right) this._borderRight = getStyleAsFloat(element, 'border-right-width');
if (top) this._borderTop = getStyleAsFloat(element, 'border-top-width');
if (bottom) this._borderBottom = getStyleAsFloat(element, 'border-bottom-width');
};
/**
* Refresh all of container's internal dimensions and offsets.
*
* @private
* @memberof Grid.prototype
*/
Grid.prototype._refreshDimensions = function() {
this._updateBoundingRect();
this._updateBorders(1, 1, 1, 1);
};
/**
* Show or hide Grid instance's items.
*
* @private
* @memberof Grid.prototype
* @param {GridMultiItemQuery} items
* @param {Boolean} toVisible
* @param {Object} [options]
* @param {Boolean} [options.instant=false]
* @param {(ShowCallback|HideCallback)} [options.onFinish]
* @param {(Boolean|LayoutCallback|String)} [options.layout=true]
*/
Grid.prototype._setItemsVisibility = function(items, toVisible, options) {
var grid = this;
var targetItems = this.getItems(items);
var opts = options || 0;
var isInstant = opts.instant === true;
var callback = opts.onFinish;
var layout = opts.layout ? opts.layout : opts.layout === undefined;
var counter = targetItems.length;
var startEvent = toVisible ? eventShowStart : eventHideStart;
var endEvent = toVisible ? eventShowEnd : eventHideEnd;
var method = toVisible ? 'show' : 'hide';
var needsLayout = false;
var completedItems = [];
var hiddenItems = [];
var item;
var i;
// If there are no items call the callback, but don't emit any events.
if (!counter) {
if (isFunction(callback)) callback(targetItems);
return;
}
// Emit showStart/hideStart event.
if (this._hasListeners(startEvent)) {
this._emit(startEvent, targetItems.slice(0));
}
// Show/hide items.
for (i = 0; i < targetItems.length; i++) {
item = targetItems[i];
// If inactive item is shown or active item is hidden we need to do
// layout.
if ((toVisible && !item._isActive) || (!toVisible && item._isActive)) {
needsLayout = true;
}
// If inactive item is shown we also need to do a little hack to make the
// item not animate it's next positioning (layout).
if (toVisible && !item._isActive) {
item._layout._skipNextAnimation = true;
}
// If a hidden item is being shown we need to refresh the item's
// dimensions.
if (toVisible && item._visibility._isHidden) {
hiddenItems.push(item);
}
// Show/hide the item.
item._visibility[method](isInstant, function(interrupted, item) {
// If the current item's animation was not interrupted add it to the
// completedItems array.
if (!interrupted) completedItems.push(item);
// If all items have finished their animations call the callback
// and emit showEnd/hideEnd event.
if (--counter < 1) {
if (isFunction(callback)) callback(completedItems.slice(0));
if (grid._hasListeners(endEvent)) grid._emit(endEvent, completedItems.slice(0));
}
});
}
// Refresh hidden items.
if (hiddenItems.length) this.refreshItems(hiddenItems);
// Layout if needed.
if (needsLayout && layout) {
this.layout(layout === instantLayout, isFunction(layout) ? layout : undefined);
}
};
/**
* Private helpers
* ***************
*/
/**
* Merge default settings with user settings. The returned object is a new
* object with merged values. The merging is a deep merge meaning that all
* objects and arrays within the provided settings objects will be also merged
* so that modifying the values of the settings object will have no effect on
* the returned object.
*
* @param {Object} defaultSettings
* @param {Object} [userSettings]
* @returns {Object} Returns a new object.
*/
function mergeSettings(defaultSettings, userSettings) {
// Create a fresh copy of default settings.
var ret = mergeObjects({}, defaultSettings);
// Merge user settings to default settings.
if (userSettings) {
ret = mergeObjects(ret, userSettings);
}
// Handle visible/hidden styles manually so that the whole object is
// overridden instead of the props.
ret.visibleStyles = (userSettings || 0).visibleStyles || (defaultSettings || 0).visibleStyles;
ret.hiddenStyles = (userSettings || 0).hiddenStyles || (defaultSettings || 0).hiddenStyles;
return ret;
}
/**
* Merge two objects recursively (deep merge). The source object's properties
* are merged to the target object.
*
* @param {Object} target
* - The target object.
* @param {Object} source
* - The source object.
* @returns {Object} Returns the target object.
*/
function mergeObjects(target, source) {
var sourceKeys = Object.keys(source);
var length = sourceKeys.length;
var isSourceObject;
var propName;
var i;
for (i = 0; i < length; i++) {
propName = sourceKeys[i];
isSourceObject = isPlainObject(source[propName]);
// If target and source values are both objects, merge the objects and
// assign the merged value to the target property.
if (isPlainObject(target[propName]) && isSourceObject) {
target[propName] = mergeObjects(mergeObjects({}, target[propName]), source[propName]);
continue;
}
// If source's value is object and target's is not let's clone the object as
// the target's value.
if (isSourceObject) {
target[propName] = mergeObjects({}, source[propName]);
continue;
}
// If source's value is an array let's clone the array as the target's
// value.
if (Array.isArray(source[propName])) {
target[propName] = source[propName].slice(0);
continue;
}
// In all other cases let's just directly assign the source's value as the
// target's value.
target[propName] = source[propName];
}
return target;
}
return Grid;
}));
} (muuri$1));
return muuri$1.exports;
}
var muuriExports = requireMuuri();
var Muuri = /*@__PURE__*/getDefaultExportFromCjs(muuriExports);
/**
* A base class used to implement custom layout engines or integrate third-party layout engines with SurveyJS Dashboard.
*/
class LayoutEngine {
constructor(_allowed) {
this._allowed = _allowed;
}
startCore(container) { }
stopCore() { }
updateCore() { }
get allowed() {
return this._allowed;
}
/**
* Enables the dynamic layout in a given HTML element.
*
* This method should arrange visualization items based on the available screen space and allow users to reorder them via drag and drop.
*/
start(container) {
if (this._allowed) {
this.startCore(container);
}
}
/**
* Disables the dynamic layout.
*/
stop() {
if (this._allowed) {
this.stopCore();
}
}
/**
* Updates the dynamic layout.
*/
update() {
if (this._allowed) {
this.updateCore();
}
}
add(elements, options) { }
remove(elements, options) { }
destroy() {
this.stop();
}
}
class MuuriLayoutEngine extends LayoutEngine {
constructor(allowed, _selector, dragEnabled = true) {
super(allowed);
this._selector = _selector;
this.dragEnabled = dragEnabled;
this._muuri = undefined;
this._layoutingTimer = undefined;
}
startCore(container) {
this._muuri = new Muuri(container, {
dragStartPredicate: {
handle: ".sa-question__title--draggable",
},
items: this._selector,
dragEnabled: this.dragEnabled,
});
this._muuri.on("dragReleaseEnd", (item) => {
const newOrder = item.getGrid().getItems().map(gridItem => gridItem.getElement().dataset.question);
this.onMoveCallback && this.onMoveCallback(newOrder);
});
}
stopCore() {
this._muuri.off("dragReleaseEnd");
this._muuri.destroy();
this._muuri = undefined;
}
updateCore() {
if (!this._muuri)
return;
if (this._layoutingTimer !== undefined) {
clearTimeout(this._layoutingTimer);
}
this._layoutingTimer = setTimeout(() => {
this._layoutingTimer = undefined;
if (!this._muuri)
return;
this._muuri.refreshItems();
this._muuri.layout();
}, 10);
}
add(elements, options) {
if (this._allowed)
this._muuri.add(elements, options);
}
remove(elements, options) {
if (this._allowed)
this._muuri.remove(elements, options);
}
}
const questionElementClassName = "sa-question";
const questionLayoutedElementClassName = "sa-question-layouted";
if (!!document) {
const templateHolder = document.createElement("div");
templateHolder.style.display = "none";
templateHolder.innerHTML = svgTemplate;
document.head.appendChild(templateHolder);
}
/**
* An object that visualizes survey results and allows users to analyze them.
*
* Constructor parameters:
*
* - `questions`: Array\<[`Question`](https://surveyjs.io/form-library/documentation/api-reference/question)\>\
* Survey questions to visualize. Call `SurveyModel`'s [`getAllQuestions()`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#getQuestionByName) method to access all survey questions and pass its result as the `questions` parameter.
* - `data`: `Array<any>`\
* Survey results.
* - `vizPanelOptions`: [`IVisualizationPanelOptions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions)\
* Visualization Panel configuration.
*
* [View Demo](https://surveyjs.io/dashboard/examples/interactive-survey-data-dashboard/ (linkStyle))
*/
class VisualizationPanel extends VisualizerBase {
constructor(questions, data, options = {}, _elements = undefined) {
super(null, data, options, "panel");
this.questions = questions;
this._elements = _elements;
this.visualizers = [];
this.haveCommercialLicense = false;
this.renderedQuestionsCount = 0;
this.onAfterRenderQuestionCallback = (sender, options) => {
this.renderedQuestionsCount++;
if (this.renderedQuestionsCount == this.questions.length) {
this.renderedQuestionsCount = 0;
this.layoutEngine.update();
this.afterRender(this.contentContainer);
}
};
this.onStateChangedCallback = (sender, options) => {
var _a;
this.stateChanged((_a = sender.question) === null || _a === void 0 ? void 0 : _a.name, options);
};
/**
* An event that is raised when a user selects a different visualizer type from the Type drop-down menu.
*
* Parameters:
*
* - `sender`: `AlternativeVisualizersWrapper`\
* An object that controls altenative visualizers.
*
* - `options.visualizer`: `VisualizerBase`\
* An applied visualizer.
**/
this.onAlternativeVisualizerChanged = new Event();
this.onAlternativeVisualizerChangedCallback = (sender, options) => {
this.onAlternativeVisualizerChanged.fire(sender, options);
};
/**
* @deprecated Use [`onElementShown`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#onElementShown), [`onElementHidden`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#onElementHidden), or [`onElementMoved`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#onElementMoved) instead.
*/
this.onVisibleElementsChanged = new Event();
/**
* An event that is raised when users [show a visualization item](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowHideQuestions).
*
* Parameters:
*
* - `sender`: [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel)\
* A `VisualizationPanel` that raised the event.
*
* - `options.elements`: Array\<[`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement)\>\
* Information about all visualization items rendered by this `VisualizationPanel`.
*
* - `options.element`: [`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement)\
* A visualization item that has been shown.
*/
this.onElementShown = new Event();
/**
* An event that is raised when users [hide a visualization item](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowHideQuestions).
*
* Parameters:
*
* - `sender`: [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel)\
* A `VisualizationPanel` that raised the event.
*
* - `options.elements`: Array\<[`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement)\>\
* Information about all visualization items rendered by this `VisualizationPanel`.
*
* - `options.element`: [`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement)\
* A visualization item that has been hidden.
*/
this.onElementHidden = new Event();
/**
* An event that is raised when users [move a visualization item](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowDynamicLayout).
*
* Parameters:
*
* - `sender`: [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel)\
* A `VisualizationPanel` that raised the event.
*
* - `options.elements`: Array\<[`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement)\>\
* Information about all visualization items rendered by this `VisualizationPanel`.
*
* - `options.element`: [`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement)\
* A visualization item that has been moved.
*/
this.onElementMoved = new Event();
this.loadingData = false;
const f = hasLicense;
this.haveCommercialLicense = (!!f && f(4)) ||
VisualizationPanel.haveCommercialLicense ||
(typeof options.haveCommercialLicense !== "undefined"
? options.haveCommercialLicense
: false);
this._layoutEngine =
options.layoutEngine ||
new MuuriLayoutEngine(this.allowDynamicLayout, "." + questionLayoutedElementClassName, this.allowDragDrop);
this._layoutEngine.onMoveCallback = (order) => this.reorderVisibleElements(order);
this.showToolbar = true;
if (this.options.survey) {
localization.currentLocale = this.options.survey.locale;
}
if (_elements === undefined) {
this._elements = this.buildElements(questions);
}
this.buildVisualizers(questions);
if (!this.haveCommercialLicense) {
this.registerToolbarItem("commercialLicense", () => {
return createCommercialLicenseLink();
});
}
this.registerToolbarItem("resetFilter", () => {
return DocumentHelper.createButton(() => {
this.visualizers.forEach((visualizer) => {
if (visualizer instanceof SelectBase || visualizer instanceof AlternativeVisualizersWrapper) {
visualizer.setSelection(undefined);
}
});
}, localization.getString("resetFilter"));
});
this.registerToolbarItem("addElement", (toolbar) => {
if (this.allowHideQuestions) {
let addElementSelector = undefined;
const addElementSelectorUpdater = (panel, options) => {
const hiddenElements = this.hiddenElements;
if (hiddenElements.length > 0) {
const selectWrapper = DocumentHelper.createSelector([
{
name: undefined,
displayName: localization.getString("addElement"),
},
]
.concat(hiddenElements)
.map((element) => {
return {
value: element.name,
text: element.displayName,
};
}), (option) => false, (e) => {
this.showElement(e.target.value);
});
(addElementSelector &&
toolbar.replaceChild(selectWrapper, addElementSelector)) ||
toolbar.appendChild(selectWrapper);
addElementSelector = selectWrapper;
}
else {
addElementSelector && toolbar.removeChild(addElementSelector);
addElementSelector = undefined;
}
};
addElementSelectorUpdater();
this.onVisibleElementsChanged.add(addElementSelectorUpdater);
}
return undefined;
});
if (!this.options.disableLocaleSwitch && this.locales.length > 1) {
const localeChoices = this.locales.map((element) => {
return {
value: element,
text: localization.localeNames[element] || localization.getString(element) || element
};
});
// localeChoices.unshift({
// value: "",
// text: localization.getString("changeLocale"),
// });
this.registerToolbarItem("changeLocale", () => {
return DocumentHelper.createSelector(localeChoices, (option) => !!option.value && (this.locale || surveyLocalization.defaultLocale) === option.value, (e) => {
var newLocale = e.target.value;
this.locale = newLocale;
});
});
}
}
reorderVisibleElements(order) {
const newElements = [];
order.forEach(name => {
newElements.push(this._elements.filter(el => el.name === name)[0]);
});
this._elements.forEach(el => {
if (order.indexOf(el.name) == -1) {
newElements.push(el);
}
});
this._elements = newElements;
this.visibleElementsChanged(undefined, "REORDERED");
}
onDataChanged() {
}
showElementCore(element, elementIndex = -1) {
element.isVisible = true;
const questionElement = this.renderPanelElement(element, this.contentContainer);
let options = undefined;
if (elementIndex >= 0) {
options = { index: elementIndex };
}
this.layoutEngine.add([questionElement], options);
}
showElement(elementName) {
const element = this.getElement(elementName);
const elementIndex = this._elements.indexOf(element);
this.showElementCore(element, elementIndex);
this.visibleElementsChanged(element, "ADDED");
}
hideElementCore(element) {
element.isVisible = false;
if (!!element.renderedElement) {
this.layoutEngine.remove([element.renderedElement]);
this.contentContainer.removeChild(element.renderedElement);
element.renderedElement = undefined;
}
}
hideElement(elementName) {
const element = this.getElement(elementName);
this.hideElementCore(element);
this.visibleElementsChanged(element, "REMOVED");
}
/**
* Hides all panel elements. Users can select the elements they want to show from a drop-down menu.
* @see showAllElements
* @see allowHideQuestions
*/
hideAllElements() {
this._elements.forEach(element => {
if (element.isVisible) {
this.hideElementCore(element);
}
});
this.visibleElementsChanged(undefined, "REMOVEDALL");
}
/**
* Shows all panel elements if they are hidden to a drop-down menu.
* @see hideAllElements
* @see allowHideQuestions
*/
showAllElements() {
this._elements.forEach(element => {
if (!element.isVisible) {
this.showElementCore(element);
}
});
this.visibleElementsChanged(undefined, "ADDEDDALL");
}
makeElementPrivate(element) {
element.isPublic = false;
this.stateChanged("isPublic", false);
this.onPermissionsChangedCallback &&
this.onPermissionsChangedCallback(this);
}
makeElementPublic(element) {
element.isPublic = true;
this.stateChanged("isPublic", true);
this.onPermissionsChangedCallback &&
this.onPermissionsChangedCallback(this);
}
moveVisibleElement(fromVisibleIndex, toVisibleIndex) {
let fromIndex, toIndex;
let fromVisibleIndexElement = this.visibleElements[fromVisibleIndex];
let toVisibleIndexElement = this.visibleElements[toVisibleIndex];
fromIndex = this._elements.indexOf(fromVisibleIndexElement);
toIndex = this._elements.indexOf(toVisibleIndexElement);
this.moveElement(fromIndex, toIndex);
}
moveElement(fromIndex, toIndex) {
var elements = this._elements.splice(fromIndex, 1);
this._elements.splice(toIndex, 0, elements[0]);
this.visibleElementsChanged(elements[0], "MOVED");
}
setBackgroundColorCore(color) {
super.setBackgroundColorCore(color);
this.visualizers.forEach(visualizer => visualizer.backgroundColor = color);
}
buildVisualizers(questions) {
questions.forEach((question) => {
const visualizer = this.createVisualizer(question);
if (!visualizer) {
return;
}
if (this.allowHideQuestions) {
visualizer.registerToolbarItem("removeQuestion", () => {
return DocumentHelper.createButton(() => {
setTimeout(() => this.hideElement(question.name), 0);
}, localization.getString("hideButton"));
});
}
if (this.allowMakeQuestionsPrivate) {
visualizer.registerToolbarItem("makePrivatePublic", () => {
const element = this.getElement(question.name);
const state = element.isPublic ? "first" : "second";
const pathMakePrivateSvg = "makeprivate";
const pathMakePublicSvg = "makepublic";
const makePrivateTitle = localization.getString("makePrivateButton");
const makePublicTitle = localization.getString("makePublicButton");
const doPrivate = (e) => {
setTimeout(() => this.makeElementPrivate(element), 0);
};
const doPublic = (e) => {
setTimeout(() => this.makeElementPublic(element), 0);
};
return DocumentHelper.createSvgToggleButton(pathMakePublicSvg, pathMakePrivateSvg, makePrivateTitle, makePublicTitle, doPublic, doPrivate, state);
});
}
if (visualizer.supportSelection) {
const visualizerWithSelection = visualizer;
let filterInfo = new FilterInfo(visualizerWithSelection);
visualizer.registerToolbarItem("questionFilterInfo", () => {
filterInfo.update(visualizerWithSelection.selection);
return filterInfo.htmlElement;
});
visualizerWithSelection.onDataItemSelected = (selectedValue, selectedText) => {
filterInfo.update({ value: selectedValue, text: selectedText });
this.setFilter(question.name, selectedValue);
};
}
visualizer.onUpdate = () => this.layout();
visualizer.onAfterRender.add(this.onAfterRenderQuestionCallback);
visualizer.onStateChanged.add(this.onStateChangedCallback);
if (visualizer instanceof AlternativeVisualizersWrapper) {
visualizer.onVisualizerChanged.add(this.onAlternativeVisualizerChangedCallback);
}
this.visualizers.push(visualizer);
});
}
destroyVisualizers() {
this.visualizers.forEach((visualizer) => {
visualizer.onUpdate = undefined;
if (visualizer instanceof SelectBase) {
visualizer.onDataItemSelected = undefined;
}
if (visualizer instanceof AlternativeVisualizersWrapper) {
visualizer.onVisualizerChanged.remove(this.onAlternativeVisualizerChangedCallback);
}
visualizer.onStateChanged.remove(this.onStateChangedCallback);
visualizer.onAfterRender.remove(this.onAfterRenderQuestionCallback);
visualizer.destroy();
});
this.visualizers = [];
}
setLocale(newLocale) {
super.setLocale(newLocale);
(this.questions || []).forEach((question) => {
const element = this.getElement(question.name);
if (!!element) {
element.displayName = this.processText(question.title);
}
});
this.visualizers.forEach(v => {
v.options.seriesLabels = this.options.seriesLabels;
v.locale = newLocale;
});
this.stateChanged("locale", newLocale);
}
/**
* Returns the [`allowDynamicLayout`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowDynamicLayout) property value of the [`IVisualizationPanelOptions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions) object.
*/
get allowDynamicLayout() {
return (this.options.allowDynamicLayout === undefined ||
this.options.allowDynamicLayout === true);
}
/**
* Returns the [`allowDragDrop`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowDragDrop) property value of the [`IVisualizationPanelOptions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions) object.
*/
get allowDragDrop() {
if (IsTouch) {
return false;
}
return (this.options.allowDragDrop === undefined ||
this.options.allowDragDrop === true);
}
/**
* Returns the [`allowHideQuestions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowHideQuestions) property value of the [`IVisualizationPanelOptions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions) object.
*/
get allowHideQuestions() {
return (this.options.allowHideQuestions === undefined ||
this.options.allowHideQuestions === true);
}
get allowMakeQuestionsPrivate() {
return this.options.allowMakeQuestionsPrivate === true;
}
/**
* Returns a [`LayoutEngine`](https://surveyjs.io/dashboard/documentation/api-reference/layoutengine) instance used to arrange visualization items on `VisualizationPanel`.
*/
get layoutEngine() {
return this._layoutEngine;
}
buildElements(questions) {
return (questions || []).map((question) => {
return {
name: question.name,
displayName: this.processText(question.title),
isVisible: true,
isPublic: true,
};
});
}
/**
* Returns an array of [`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement) objects with information about visualization items.
* @param questionNames Question [names](https://surveyjs.io/form-library/documentation/api-reference/question#name). Do not specify this parameter to get an array of all visualization items.
* @see visibleElements
* @see hiddenElements
*/
getElements(questionNames) {
const result = [];
(this._elements || []).forEach((element) => {
if (!questionNames || questionNames.indexOf(element.name) !== -1) {
result.push({
name: element.name,
displayName: element.displayName,
isVisible: element.isVisible,
isPublic: element.isPublic,
});
}
});
return result;
}
/**
* Returns an array of [`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement) objects with information about currently visible visualization items.
*
* If you want to disallow users to hide visualization items, set the [`allowHideQuestions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowHideQuestions) property to `false`.
* @see hiddenElements
* @see getElements
*/
get visibleElements() {
return this._elements.filter((el) => el.isVisible);
}
/**
* Returns an array of [`IVisualizerPanelElement`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizerpanelelement) objects with information about currently hidden visualization items.
*
* If you want to disallow users to hide visualization items, set the [`allowHideQuestions`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowHideQuestions) property to `false`.
* @see visibleElements
* @see getElements
*/
get hiddenElements() {
return this._elements.filter((el) => !el.isVisible);
}
get publicElements() {
return this._elements.filter((el) => el.isPublic);
}
get privateElements() {
return this._elements.filter((el) => !el.isPublic);
}
get locales() {
if (this.options.survey)
return this.options.survey.getUsedLocales();
return [];
}
/**
* Returns a visualization item with a specified question name.
* @param name A question [name](https://surveyjs.io/form-library/documentation/api-reference/question#name).
*/
getElement(questionName) {
return this._elements.filter((el) => el.name === questionName)[0];
}
/**
* Returns a [visualizer](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase) that visualizes a specified survey question.
* @param questionName A question [name](https://surveyjs.io/form-library/documentation/api-reference/question#name).
*/
getVisualizer(questionName) {
return this.visualizers.filter((v) => v.question.name === questionName)[0];
}
visibleElementsChanged(element, reason) {
if (reason === "SHOWN" && !this.onElementShown.isEmpty) {
this.onElementShown.fire(this, {
elements: this._elements,
element: element
});
}
if (reason === "REMOVED" && !this.onElementHidden.isEmpty) {
this.onElementHidden.fire(this, {
elements: this._elements,
element: element
});
}
if (reason === "MOVED" && !this.onElementMoved.isEmpty) {
this.onElementMoved.fire(this, {
elements: this._elements,
element: element
});
}
if (!this.onVisibleElementsChanged.isEmpty) {
this.onVisibleElementsChanged.fire(this, {
elements: this._elements,
changed: element,
reason: reason,
});
}
this.stateChanged("visibleElements", reason);
this.layout();
}
renderPanelElement(element, container) {
const visualizer = this.getVisualizer(element.name);
if (!visualizer) {
return;
}
const questionElement = DocumentHelper.createElement("div");
questionElement.dataset.question = element.name;
!!container && container.appendChild(questionElement);
const questionContent = DocumentHelper.createElement("div");
const titleElement = DocumentHelper.createElement("h3");
const vizualizerElement = DocumentHelper.createElement("div");
titleElement.innerText = element.displayName;
questionElement.className = this.allowDynamicLayout
? questionElementClassName + " " + questionLayoutedElementClassName
: questionElementClassName;
titleElement.className = questionElementClassName + "__title";
if (this.allowDynamicLayout && this.allowDragDrop) {
titleElement.className =
titleElement.className +
" " +
questionElementClassName +
"__title--draggable";
}
questionContent.className = questionElementClassName + "__content";
questionContent.style.backgroundColor = this.backgroundColor;
questionContent.appendChild(titleElement);
questionContent.appendChild(vizualizerElement);
questionElement.appendChild(questionContent);
visualizer.render(vizualizerElement);
element.renderedElement = questionElement;
return questionElement;
}
renderToolbar(container) {
container.className += " sa-panel__header";
super.renderToolbar(container);
}
renderContent(container) {
container.className += " sa-panel__content sa-grid";
this.visibleElements.forEach((element) => {
this.renderPanelElement(element, container);
});
this.layoutEngine.start(container);
// !!window && window.dispatchEvent(new UIEvent("resize"));
}
destroyContent(container) {
this.layoutEngine.stop();
super.destroyContent(container);
}
/**
* Redraws the `VisualizationPanel` and all its content.
*/
refresh() {
if (!!this.toolbarContainer) {
this.destroyToolbar(this.toolbarContainer);
this.renderToolbar(this.toolbarContainer);
}
super.refresh();
}
layout() {
this.layoutEngine.update();
}
/**
* Filters visualized data based on a specified question name and value. This method is called when a user clicks a chart point.
* @param questionName A question [name](https://surveyjs.io/form-library/documentation/api-reference/question#name).
* @param selectedValue
* @see IVisualizationPanelOptions.allowSelection
*/
setFilter(questionName, selectedValue) {
this.dataProvider.setFilter(questionName, selectedValue);
}
getState() {
return {
locale: this.locale,
elements: [].concat(this._elements.map(element => {
const visualizer = this.getVisualizer(element.name);
const elementState = Object.assign(Object.assign({}, element), visualizer === null || visualizer === void 0 ? void 0 : visualizer.getState());
if (elementState.renderedElement !== undefined) {
delete elementState.renderedElement;
}
return elementState;
})),
};
}
/**
* The state of `VisualizationPanel`. Includes information about the visualized elements and current locale.
* @see onStateChanged
*/
get state() {
return this.getState();
}
set state(newState) {
if (!newState)
return;
this._settingState = true;
try {
if (Array.isArray(newState.elements)) {
const questionNames = this.questions.map(q => q.name);
this._elements = [].concat(newState.elements.filter(e => (questionNames.indexOf(e.name) !== -1)));
}
if (typeof newState.locale !== "undefined")
this.setLocale(newState.locale);
this._elements.forEach(elementState => {
const visualizer = this.getVisualizer(elementState.name);
if (visualizer !== undefined) {
visualizer.setState(elementState);
}
});
}
finally {
this._settingState = false;
}
this.refresh();
}
get permissions() {
return this._elements.map((element) => {
return {
name: element.name,
isPublic: element.isPublic,
};
});
}
set permissions(permissions) {
const updatedElements = this._elements.map((element) => {
permissions.forEach((permission) => {
if (permission.name === element.name)
element.isPublic = permission.isPublic;
});
return Object.assign({}, element);
});
this._elements = [].concat(updatedElements);
this.refresh();
this.onPermissionsChangedCallback &&
this.onPermissionsChangedCallback(this);
}
destroy() {
super.destroy();
this.destroyVisualizers();
}
}
VisualizationPanel.haveCommercialLicense = false;
class VisualizationPanelDynamic extends VisualizerBase {
constructor(question, data, options = {}, name) {
super(question, data, options, name || "panelDynamic");
this._panelVisualizer = undefined;
this.onAfterRenderPanelCallback = () => {
this.afterRender(this.contentContainer);
};
this.loadingData = false;
var options = Object.assign({}, options);
options.allowDynamicLayout = false;
options.dataProvider = undefined;
this._panelVisualizer = new VisualizationPanel(this.getQuestions(), [], options);
this._panelVisualizer.onAfterRender.add(this.onAfterRenderPanelCallback);
this.updateData(data);
}
setLocale(newLocale) {
super.setLocale(newLocale);
this._panelVisualizer.locale = newLocale;
}
get type() {
return "panelDynamic";
}
updatePanelVisualizerData() {
let panelData = [];
this.data.forEach((dataItem) => {
if (dataItem[this.question.name] !== undefined) {
panelData = panelData.concat(dataItem[this.question.name]);
}
});
this._panelVisualizer.updateData(panelData);
}
updateData(data) {
super.updateData(data);
this.updatePanelVisualizerData();
}
onDataChanged() {
this.updatePanelVisualizerData();
super.onDataChanged();
}
getQuestions() {
const paneldynamic = this.question;
return paneldynamic.template.questions;
}
destroyContent(container) {
this._panelVisualizer.clear();
super.destroyContent(this.contentContainer);
}
renderContent(container) {
this._panelVisualizer.render(container);
}
destroy() {
super.destroy();
this._panelVisualizer.onAfterRender.remove(this.onAfterRenderPanelCallback);
}
}
VisualizationManager.registerVisualizer("paneldynamic", VisualizationPanelDynamic);
class VisualizationMatrixDynamic extends VisualizationPanelDynamic {
constructor(question, data, options) {
super(question, data, options);
}
get type() {
return "matrixDynamic";
}
getQuestions() {
const matrixdynamic = this.question;
const visibleRows = matrixdynamic.visibleRows;
if (visibleRows.length === 0)
return [];
return visibleRows[0].cells.map(c => c.question);
}
}
VisualizationManager.registerVisualizer("matrixdynamic", VisualizationMatrixDynamic);
class VisualizationMatrixDropdown extends VisualizerBase {
constructor(question, data, options = {}, name) {
super(question, data, options, name || "matrixDropdown");
this._matrixDropdownVisualizer = undefined;
this.onPanelAfterRenderCallback = () => {
this.afterRender(this.contentContainer);
};
this.loadingData = false;
this._childOptions = Object.assign({}, options);
this._childOptions.disableLocaleSwitch = true;
this._childOptions.dataProvider = undefined;
this._childOptions.allowDynamicLayout = false;
this._childOptions.seriesValues = question.rows.map((row) => row.value);
this._childOptions.seriesLabels = question.rows.map((row) => row.text);
const innerQuestions = this.getQuestions();
const canGroupColumns = this._childOptions.seriesValues.length == 1 && innerQuestions.every(innerQuestion => Helpers.isArraysEqual(innerQuestion.choices, this.question.choices));
if (canGroupColumns) {
var creators = VisualizationManager.getVisualizersByType("matrixdropdown-grouped");
this._matrixDropdownVisualizer = new creators[0](this.question, [], this._childOptions);
}
else {
this._matrixDropdownVisualizer = new VisualizationPanel(innerQuestions, [], this._childOptions);
}
this._matrixDropdownVisualizer.onAfterRender.add(this.onPanelAfterRenderCallback);
this.updateData(data);
}
setLocale(newLocale) {
super.setLocale(newLocale);
this._childOptions.seriesLabels = this.question.rows.map((row) => row.text);
this._matrixDropdownVisualizer.locale = newLocale;
}
get matrixDropdownVisualizer() {
return this._matrixDropdownVisualizer;
}
updateDropdownVisualizerData() {
let panelData = [];
this.data.forEach((dataItem) => {
let rawDataItem = dataItem[this.question.name];
if (!!rawDataItem) {
Object.keys(rawDataItem).forEach((key) => {
var nestedDataItem = Object.assign({}, rawDataItem[key]);
nestedDataItem[DataProvider.seriesMarkerKey] = key;
panelData.push(nestedDataItem);
});
}
});
this._matrixDropdownVisualizer.updateData(panelData);
}
updateData(data) {
super.updateData(data);
this.updateDropdownVisualizerData();
}
onDataChanged() {
this.updateDropdownVisualizerData();
super.onDataChanged();
}
getQuestions() {
const matrixdropdown = this.question;
return matrixdropdown.columns.map((column) => {
const cellQuestion = column.templateQuestion;
if (Array.isArray(cellQuestion.choices) && cellQuestion.choices.length === 0) {
cellQuestion.choices = matrixdropdown.choices;
}
return cellQuestion;
});
}
destroyContent(container) {
this._matrixDropdownVisualizer.clear();
super.destroyContent(this.contentContainer);
}
renderContent(container) {
this._matrixDropdownVisualizer.render(container);
}
destroy() {
super.destroy();
this._matrixDropdownVisualizer.onAfterRender.remove(this.onPanelAfterRenderCallback);
}
}
VisualizationManager.registerVisualizer("matrixdropdown", VisualizationMatrixDropdown);
class PlotlySetup {
static setup(charType, model, answersData) {
return this.setups[charType](model, answersData);
}
static setupPie(model, answersData) {
let { datasets, labels, colors, texts, seriesLabels, } = answersData;
let traces = [];
const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix";
const traceConfig = {
type: model.chartType,
y: hasSeries ? seriesLabels : labels,
text: (hasSeries ? seriesLabels : labels).map((label) => {
return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength);
}),
hoverinfo: "x+y",
mode: "markers",
marker: {},
};
traceConfig.hoverinfo = "label+value+percent";
traceConfig.marker.colors = colors;
traceConfig.textposition = "inside";
if (model.chartType === "doughnut") {
traceConfig.type = "pie";
traceConfig.hole = 0.4;
}
if (!hasSeries) {
traceConfig.marker.symbol = "circle";
traceConfig.marker.size = 16;
}
datasets.forEach((dataset, index) => {
traces.push(Object.assign({}, traceConfig, {
values: dataset,
labels: hasSeries ? seriesLabels : labels,
customdata: hasSeries ? seriesLabels : labels,
}));
});
const radius = labels.length < 10 ? labels.length * 50 + 100 : 550;
const height = radius * Math.round(traces.length / 2) + 25;
const layout = {
font: {
family: "Segoe UI, sans-serif",
size: 14,
weight: "normal",
color: "#404040",
},
height: height,
margin: {
l: 0,
t: 25,
b: 0,
r: 10,
},
colorway: colors,
hovermode: "closest",
plot_bgcolor: model.backgroundColor,
paper_bgcolor: model.backgroundColor,
showlegend: false,
};
if (hasSeries) {
layout.annotations = [];
labels.forEach((label, index) => {
traces[index].title = { position: "bottom center", text: label };
});
traces = traces.filter(t => !(t.values.length === 1 && t.values[0] === 0));
traces.forEach((label, index) => {
traces[index].domain = {
column: index % 2,
row: Math.floor(index / 2),
};
});
layout.grid = {
rows: Math.round(traces.length / 2),
columns: 2,
};
layout.height = radius * Math.round(traces.length / 2) + 25;
}
return { traces, layout, hasSeries };
}
static setupBar(model, answersData) {
let lineHeight = 30;
let topMargin = 30;
let bottomMargin = 30;
let { datasets, labels, colors, texts, seriesLabels, } = answersData;
const traces = [];
const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix";
const yFullTexts = hasSeries ? seriesLabels : labels;
const traceConfig = {
type: model.chartType,
y: yFullTexts,
text: yFullTexts,
customdata: hasSeries ? seriesLabels : labels,
hoverinfo: "text",
orientation: "h",
mode: "markers",
textposition: "none",
width: 0.5,
bargap: 0.5,
marker: {},
};
traceConfig.marker.color = colors;
datasets.forEach((dataset, index) => {
var trace = Object.assign({}, traceConfig, {
x: dataset,
text: texts[index],
hovertext: yFullTexts.map((label, labelIndex) => {
return `${texts[index][labelIndex]}, ${label}`;
}),
});
if (model.showPercentages) {
let texttemplate = model.showOnlyPercentages ? "%{text}%" : "%{value} (%{text}%)";
trace.textposition = "inside";
trace.texttemplate = texttemplate;
trace.bargap = 0.1;
}
traces.push(trace);
});
const height = (labels.length + 1) * lineHeight + topMargin + bottomMargin;
const layout = {
font: {
family: "Segoe UI, sans-serif",
size: 14,
weight: "normal",
color: "#404040",
},
height: height,
margin: {
t: topMargin,
b: bottomMargin,
r: 10,
},
colorway: colors,
hovermode: "closest",
yaxis: {
automargin: true,
type: "category",
orientation: "h",
tickmode: "array",
tickvals: yFullTexts,
ticktext: yFullTexts.map((label) => {
return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength) + " ";
}),
},
xaxis: {
rangemode: "nonnegative",
automargin: true,
// dtick: 1
},
plot_bgcolor: model.backgroundColor,
paper_bgcolor: model.backgroundColor,
showlegend: false,
};
if (hasSeries) {
layout.showlegend = true;
if (model.chartType == "stackedbar") {
layout.barmode = "stack";
}
else {
layout.height =
(labels.length + 1) * lineHeight * seriesLabels.length +
topMargin +
bottomMargin;
}
labels.forEach((label, index) => {
traces[index].marker.color = undefined;
traces[index].name = label;
if (model.chartType === "stackedbar") {
traces[index].type = "bar";
traces[index].width = 0.5;
}
else {
traces[index].width =
(model.showPercentages ? 0.7 : 0.5) / traces.length;
}
});
traces.forEach((trace, traceIndex) => {
const percentString = model.showPercentages ? "%" : "";
traces[traceIndex].hovertext = [];
yFullTexts.forEach((yFullText, yFullTextIndex) => {
traces[traceIndex].hovertext.push(`${trace.y[yFullTextIndex]} : ${trace.name}, ${trace.text[yFullTextIndex]}${percentString}`);
});
});
}
return { traces, layout, hasSeries };
}
static setupVBar(model, answersData) {
let topMargin = 30;
let bottomMargin = 30;
let { datasets, labels, colors, texts, seriesLabels, } = answersData;
if (model.type !== "histogram") {
labels = [].concat(labels).reverse();
colors = [].concat(colors.slice(0, labels.length)).reverse();
const ts = [];
texts.forEach(text => {
ts.push([].concat(text).reverse());
});
texts = ts;
const ds = [];
datasets.forEach(dataset => {
ds.push([].concat(dataset).reverse());
});
datasets = ds;
}
const traces = [];
const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix";
const layout = {
font: {
family: "Segoe UI, sans-serif",
size: 14,
weight: "normal",
color: "#404040",
},
margin: {
t: topMargin,
b: bottomMargin,
r: 10,
},
colorway: colors,
hovermode: "closest",
plot_bgcolor: model.backgroundColor,
paper_bgcolor: model.backgroundColor,
showlegend: false,
};
const traceConfig = {
type: model.chartType === "line" ? "line" : "bar",
customdata: hasSeries ? seriesLabels : labels,
hoverinfo: "x+y",
mode: model.chartType === "line" ? "lines+markers" : "markers",
textposition: "none",
width: 0.5,
bargap: 0.5,
marker: {},
};
traceConfig.marker.color = colors;
datasets.forEach((dataset, index) => {
var trace = Object.assign({}, traceConfig, {
x: labels,
y: model.showPercentages ? texts[index].map(y => y / 100) : dataset,
text: texts[index],
});
if (model.showPercentages) {
let texttemplate = model.showOnlyPercentages ? "%{text}%" : "%{value} (%{text}%)";
trace.textposition = "inside";
trace.texttemplate = texttemplate;
trace.width = 0.9;
trace.bargap = 0.1;
}
traces.push(trace);
});
if (model.showPercentages && model.showOnlyPercentages) {
layout.yaxis = {
automargin: true,
tickformat: ".0%",
range: [0, 1],
ticklen: model.showOnlyPercentages ? 25 : 5,
tickcolor: "transparent",
};
}
if (!model.getValueType || model.getValueType() != "date") {
layout.xaxis = {
automargin: true,
type: "category",
};
}
return { traces, layout, hasSeries };
}
static setupScatter(model, answersData) {
let lineHeight = 30;
let topMargin = 30;
let bottomMargin = 30;
let { datasets, labels, colors, texts, seriesLabels, } = answersData;
const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix";
const traces = [];
const traceConfig = {
type: "scatter",
y: (hasSeries ? seriesLabels : labels).map((label) => {
return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength);
}),
customdata: hasSeries ? seriesLabels : labels,
text: hasSeries ? seriesLabels : labels,
hoverinfo: "x+y",
orientation: "h",
mode: "markers",
width: 0.5,
marker: {},
};
if (!hasSeries) {
traceConfig.marker.symbol = "circle";
traceConfig.marker.size = 16;
}
datasets.forEach((dataset) => {
{
var trace = Object.assign({}, traceConfig, {
x: dataset,
});
traces.push(trace);
}
});
const height = (labels.length + 1) * lineHeight + topMargin + bottomMargin;
const layout = {
font: {
family: "Segoe UI, sans-serif",
size: 14,
weight: "normal",
color: "#404040",
},
height: height,
margin: {
t: topMargin,
b: bottomMargin,
r: 10,
},
colorway: colors,
hovermode: "closest",
yaxis: {
automargin: true,
type: "category",
ticklen: 5,
tickcolor: "transparent",
},
xaxis: {
rangemode: "nonnegative",
automargin: true,
},
plot_bgcolor: model.backgroundColor,
paper_bgcolor: model.backgroundColor,
showlegend: false,
};
if (hasSeries) {
layout.showlegend = true;
layout.height = undefined;
labels.forEach((label, index) => {
traces[index].hoverinfo = "x+name";
traces[index].marker.color = undefined;
traces[index].name = label;
});
}
return { traces, layout, hasSeries };
}
}
PlotlySetup.imageExportFormat = "png";
/**
* Fires when end user clicks on the 'save as image' button.
*/
PlotlySetup.onImageSaving = new Event();
/**
* Fires before plot will be created. User can change traces, layout and config of the plot.
* Options is an object with the following fields: traces, layout and config of the plot.
*/
PlotlySetup.onPlotCreating = new Event();
PlotlySetup.setups = {
bar: PlotlySetup.setupBar,
vbar: PlotlySetup.setupVBar,
line: PlotlySetup.setupVBar,
stackedbar: PlotlySetup.setupBar,
doughnut: PlotlySetup.setupPie,
pie: PlotlySetup.setupPie,
scatter: PlotlySetup.setupScatter,
};
PlotlySetup.getTruncatedLabel = (label, labelTruncateLength) => {
const truncateSymbols = "...";
const truncateSymbolsLength = truncateSymbols.length;
if (!labelTruncateLength)
return label;
if (labelTruncateLength === -1)
return label;
if (label.length <= labelTruncateLength + truncateSymbolsLength)
return label;
return label.substring(0, labelTruncateLength) + truncateSymbols;
};
class PlotlyChartAdapter {
constructor(model) {
this.model = model;
this._chart = undefined;
}
patchConfigParameters(chartNode, traces, layout, config) { }
get chart() {
return this._chart;
}
create(chartNode) {
return __awaiter(this, void 0, void 0, function* () {
const [plot, plotlyOptions] = yield this.update(chartNode);
chartNode["on"]("plotly_click", (data) => {
if (data.points.length > 0) {
let itemText = "";
if (!plotlyOptions.hasSeries) {
itemText = Array.isArray(data.points[0].customdata)
? data.points[0].customdata[0]
: data.points[0].customdata;
const item = this.model.getSelectedItemByText(itemText);
this.model.setSelection(item);
}
else {
itemText = data.points[0].data.name;
const propertyLabel = data.points[0].label;
const seriesValues = this.model.getSeriesValues();
const seriesLabels = this.model.getSeriesLabels();
const propertyValue = seriesValues[seriesLabels.indexOf(propertyLabel)];
const selectedItem = this.model.getSelectedItemByText(itemText);
const item = new ItemValue({ [propertyValue]: selectedItem.value }, propertyLabel + ": " + selectedItem.text);
this.model.setSelection(item);
}
// const itemText = plotlyOptions.hasSeries
// ? data.points[0].data.name
// : Array.isArray(data.points[0].customdata)
// ? data.points[0].customdata[0]
// : data.points[0].customdata;
// const item: ItemValue = this.model.getSelectedItemByText(itemText);
// this.model.setSelection(item);
}
});
var getDragLayer = () => chartNode.getElementsByClassName("nsewdrag")[0];
chartNode["on"]("plotly_hover", () => {
const dragLayer = getDragLayer();
dragLayer && (dragLayer.style.cursor = "pointer");
});
chartNode["on"]("plotly_unhover", () => {
const dragLayer = getDragLayer();
dragLayer && (dragLayer.style.cursor = "");
});
// setTimeout(() => Plotly.Plots.resize(chartNode), 10);
this._chart = plot;
return plot;
});
}
update(chartNode) {
return __awaiter(this, void 0, void 0, function* () {
const answersData = yield this.model.getAnswersData();
var plotlyOptions = PlotlySetup.setup(this.model.chartType, this.model, answersData);
let config = {
displaylogo: false,
responsive: true,
locale: localization.currentLocale,
modeBarButtonsToRemove: ["toImage"],
modeBarButtonsToAdd: [
{
name: "toImageSjs",
title: localization.getString("saveDiagramAsPNG"),
icon: Plotly.Icons.camera,
click: (gd) => {
let options = {
format: PlotlySetup.imageExportFormat,
// width: 800,
// height: 600,
filename: this.model.question.name,
};
PlotlySetup.onImageSaving.fire(this.model, options);
Plotly.downloadImage(gd, options);
},
},
],
};
if (SelectBasePlotly.displayModeBar !== undefined) {
config.displayModeBar = SelectBasePlotly.displayModeBar;
}
this.patchConfigParameters(chartNode, plotlyOptions.traces, plotlyOptions.layout, config);
let options = {
traces: plotlyOptions.traces,
layout: plotlyOptions.layout,
config: config,
};
PlotlySetup.onPlotCreating.fire(this.model, options);
const plot = Plotly.react(chartNode, options.traces, options.layout, options.config);
return [plot, plotlyOptions];
});
}
destroy(node) {
if (!!node) {
Plotly.purge(node);
}
this._chart = undefined;
}
}
class SelectBasePlotly extends SelectBase {
constructor(question, data, options = {}, name) {
super(question, data, options, name);
this.chartTypes = [].concat(SelectBasePlotly.types);
if (this.getSeriesValues().length > 0 && this.chartTypes.indexOf("stackedbar") === -1) {
this.chartTypes.push("stackedbar");
}
if (options.allowExperimentalFeatures) ;
this._chartType = this.chartTypes[0];
if (this.chartTypes.indexOf(options.defaultChartType) !== -1) {
this._chartType = options.defaultChartType;
}
this._chartAdapter = new PlotlyChartAdapter(this);
}
destroyContent(container) {
this._chartAdapter.destroy(container.children[0]);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
container.innerHTML = "";
container.appendChild(chartNode);
yield this._chartAdapter.create(chartNode);
return container;
});
}
updateContent() {
var _a;
const chartNode = (_a = this.contentContainer) === null || _a === void 0 ? void 0 : _a.children[0];
if (chartNode) {
this._chartAdapter.update(chartNode);
}
}
getCalculatedValuesCore() {
const statistics = super.getCalculatedValuesCore();
const series = this.getSeriesValues();
const values = this.getValues();
if (series.length > 1) {
const preparedData = [];
values.forEach((val, valueIndex) => {
const seriesData = series.map((seriesValue, seriesIndex) => statistics[seriesIndex][valueIndex]);
preparedData.push(seriesData);
});
return preparedData;
}
return statistics;
}
}
SelectBasePlotly.types = ["bar", "vbar", "pie", "doughnut"];
SelectBasePlotly.displayModeBar = undefined;
VisualizationManager.registerVisualizer("checkbox", SelectBasePlotly);
VisualizationManager.registerVisualizer("radiogroup", SelectBasePlotly);
VisualizationManager.registerVisualizer("dropdown", SelectBasePlotly);
VisualizationManager.registerVisualizer("imagepicker", SelectBasePlotly);
VisualizationManager.registerVisualizer("tagbox", SelectBasePlotly);
class MatrixPlotly extends Matrix {
constructor(question, data, options, name) {
super(question, data, options, name);
this.chartTypes = MatrixPlotly.types;
this._chartType = this.chartTypes[0];
this._chartAdapter = new PlotlyChartAdapter(this);
}
destroyContent(container) {
this._chartAdapter.destroy(container.children[0]);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
container.innerHTML = "";
container.appendChild(chartNode);
yield this._chartAdapter.create(chartNode);
return container;
});
}
}
MatrixPlotly.types = ["bar", "stackedbar", "pie", "doughnut"];
VisualizationManager.registerVisualizer("matrix", MatrixPlotly);
class PlotlyBoolChartAdapter extends PlotlyChartAdapter {
constructor(model) {
super(model);
}
patchConfigParameters(chartNode, traces, layout, config) {
const colors = this.model.getColors();
const boolColors = [
BooleanPlotly.trueColor || colors[0],
BooleanPlotly.falseColor || colors[1],
];
if (this.model.showMissingAnswers) {
boolColors.push(colors[2]);
}
if (this.model.chartType === "pie" || this.model.chartType === "doughnut") {
traces.forEach((trace) => {
trace.marker.colors = boolColors;
});
}
else if (this.model.chartType === "bar") {
traces.forEach((trace) => {
trace.marker.color = boolColors;
});
}
}
}
class BooleanPlotly extends BooleanModel {
constructor(question, data, options, name) {
super(question, data, options, name);
this.chartTypes = BooleanPlotly.types;
this._chartType = this.chartTypes[0];
this._chartAdapter = new PlotlyBoolChartAdapter(this);
}
destroyContent(container) {
this._chartAdapter.destroy(container.children[0]);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
yield this._chartAdapter.create(chartNode);
container.innerHTML = "";
container.appendChild(chartNode);
return container;
});
}
}
BooleanPlotly.types = ["pie", "bar", "doughnut"];
VisualizationManager.registerVisualizer("boolean", BooleanPlotly);
class RankingPlotly extends SelectBasePlotly {
getQuestionResults() {
const name = this.question.name;
return this.data.map((dataItem) => dataItem[name]);
}
getEmptyData() {
const choices = this.getValues();
let data = [];
data.length = choices.length;
data.fill(0);
return data;
}
getCalculatedValuesCore() {
const results = this.getQuestionResults();
const choices = this.getValues();
let plotlyData = this.getEmptyData();
results.forEach((result) => {
this.applyResultToPlotlyData(result, plotlyData, choices);
});
return [plotlyData];
}
applyResultToPlotlyData(result, plotlyData, choices) {
if (!result || !plotlyData || !choices)
return;
result.forEach((resultValue, resultValueIndex, result) => {
let index = choices.indexOf(resultValue);
plotlyData[index] =
+plotlyData[index] + (result.length - resultValueIndex);
});
}
}
VisualizationManager.registerVisualizer("ranking", RankingPlotly);
class MatrixDropdownGrouped extends SelectBase {
constructor(question, data, options, name) {
super(question, data, options, name || "matrixDropdownGrouped");
// this.getAnswersData();
}
get matrixQuestion() {
return this.question;
}
get dataNames() {
return this.matrixQuestion.columns.map(column => column.name);
}
getSeriesValues() {
return this.matrixQuestion.columns.map((column) => column.name);
}
getSeriesLabels() {
return this.matrixQuestion.columns.map((column) => column.title);
}
// public getSelectedItemByText(itemText: string) {
// return this.matrixQuestion.columns.filter(
// (column: ItemValue) => column.text === itemText
// )[0];
// }
valuesSource() {
return this.matrixQuestion.choices;
}
isSupportMissingAnswers() {
return false;
}
getCalculatedValuesCore() {
const values = this.getValues();
const series = this.getSeriesValues();
const rows = this.matrixQuestion.rows.map(row => row.value);
const statistics = defaultStatisticsCalculator(this.surveyData, {
name: this.name,
dataNames: series,
getValues: () => values,
getLabels: () => values,
getSeriesValues: () => rows,
getSeriesLabels: () => rows,
});
const preparedData = [];
values.forEach((val, valueIndex) => {
const seriesData = series.map((seriesName, seriesIndex) => statistics[seriesIndex][0][valueIndex]);
preparedData.push(seriesData);
});
return preparedData;
}
}
class MatrixDropdownGroupedPlotly extends MatrixDropdownGrouped {
constructor(question, data, options, name) {
super(question, data, options, name);
this.chartTypes = MatrixDropdownGroupedPlotly.types;
this._chartType = this.chartTypes[0];
this._chartAdapter = new PlotlyChartAdapter(this);
}
destroyContent(container) {
this._chartAdapter.destroy(container.children[0]);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
container.innerHTML = "";
container.appendChild(chartNode);
yield this._chartAdapter.create(chartNode);
return container;
});
}
}
MatrixDropdownGroupedPlotly.types = ["stackedbar", "bar", "pie", "doughnut"];
VisualizationManager.registerVisualizer("matrixdropdown-grouped", MatrixDropdownGroupedPlotly);
class HistogramPlotly extends HistogramModel {
constructor(question, data, options, name) {
super(question, data, options, name);
this.chartTypes = HistogramPlotly.types;
this._chartType = this.chartTypes[0];
this._chartAdapter = new PlotlyChartAdapter(this);
}
destroyContent(container) {
this._chartAdapter.destroy(container.children[0]);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
container.innerHTML = "";
container.appendChild(chartNode);
yield this._chartAdapter.create(chartNode);
return container;
});
}
getCalculatedValuesCore() {
const statistics = super.getCalculatedValuesCore();
const series = this.getSeriesValues();
const values = this.getValues();
if (series.length > 1) {
const preparedData = [];
values.forEach((val, valueIndex) => {
const seriesData = series.map((seriesValue, seriesIndex) => statistics[seriesIndex][valueIndex]);
preparedData.push(seriesData);
});
return preparedData;
}
return statistics;
}
getValueType() {
return this.valueType;
}
}
HistogramPlotly.types = ["vbar", "bar"];
VisualizationManager.registerVisualizer("date", HistogramPlotly);
VisualizationManager.registerVisualizer("number", HistogramPlotly);
VisualizationManager.registerVisualizer("rating", HistogramPlotly);
class PlotlyGaugeAdapter {
constructor(model) {
this.model = model;
this._chart = undefined;
}
get chart() {
return this._chart;
}
create(chartNode) {
return __awaiter(this, void 0, void 0, function* () {
const question = this.model.question;
let [level, minValue, maxValue] = yield this.model.getCalculatedValues();
if (question.getType() === "rating") {
const rateValues = question.visibleRateValues;
maxValue = rateValues[rateValues.length - 1].value;
minValue = rateValues[0].value;
}
const colors = this.model.generateColors(maxValue, minValue, GaugePlotly.stepsCount);
if (GaugePlotly.showAsPercentage) {
level = DataHelper.toPercentage(level, maxValue);
minValue = DataHelper.toPercentage(minValue, maxValue);
maxValue = DataHelper.toPercentage(maxValue, maxValue);
}
var data = [
{
type: "indicator",
mode: "gauge+number",
gauge: {
axis: { range: [minValue, maxValue] },
shape: this.model.chartType,
bgcolor: "white",
bar: { color: colors[0] },
},
value: level,
text: question.name,
domain: { x: [0, 1], y: [0, 1] },
},
];
const chartMargin = this.model.chartType === "bullet" ? 60 : 30;
var layout = {
height: 250,
margin: {
l: chartMargin,
r: chartMargin,
b: chartMargin,
t: chartMargin,
pad: 5
},
plot_bgcolor: this.model.backgroundColor,
paper_bgcolor: this.model.backgroundColor,
};
const config = {
displayModeBar: true,
locale: localization.currentLocale,
responsive: true,
displaylogo: false,
modeBarButtonsToRemove: ["toImage"],
modeBarButtonsToAdd: [
{
name: "toImageSjs",
title: localization.getString("saveDiagramAsPNG"),
icon: Plotly.Icons.camera,
click: (gd) => {
let options = {
format: PlotlySetup.imageExportFormat,
// width: 800,
// height: 600,
filename: this.model.question.name,
};
PlotlySetup.onImageSaving.fire(this.model, options);
Plotly.downloadImage(gd, options);
},
},
],
};
if (GaugePlotly.displayModeBar !== undefined) {
config.displayModeBar = GaugePlotly.displayModeBar;
}
let options = {
data: data,
layout: layout,
config: config,
};
PlotlySetup.onPlotCreating.fire(this.model, options);
const plot = Plotly.newPlot(chartNode, options.data, options.layout, options.config);
// setTimeout(() => Plotly.Plots.resize(chartNode), 10);
return plot;
});
}
destroy(node) {
if (!!node) {
Plotly.purge(node);
}
this._chart = undefined;
}
}
class GaugePlotly extends NumberModel {
constructor(question, data, options, name) {
super(question, data, options, name);
this.chartTypes = GaugePlotly.types;
this.chartType = this.chartTypes[0];
this._chartAdapter = new PlotlyGaugeAdapter(this);
}
destroyContent(container) {
this._chartAdapter.destroy(container.children[0]);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
container.innerHTML = "";
container.appendChild(chartNode);
yield this._chartAdapter.create(chartNode);
return container;
});
}
}
GaugePlotly.displayModeBar = undefined;
GaugePlotly.types = ["gauge", "bullet"];
VisualizationManager.registerVisualizer("number", GaugePlotly);
VisualizationManager.registerVisualizer("rating", GaugePlotly);
VisualizationManager.registerVisualizer("expression", GaugePlotly);
var stopWords$3 = [
"...",
"a",
"about",
"above",
"across",
"after",
"again",
"against",
"all",
"almost",
"alone",
"along",
"already",
"also",
"although",
"always",
"among",
"an",
"and",
"another",
"any",
"anybody",
"anyone",
"anything",
"anywhere",
"are",
"area",
"areas",
"around",
"as",
"ask",
"asked",
"asking",
"asks",
"at",
"away",
"b",
"back",
"backed",
"backing",
"backs",
"be",
"became",
"because",
"become",
"becomes",
"been",
"before",
"began",
"behind",
"being",
"beings",
"best",
"better",
"between",
"big",
"both",
"but",
"by",
"c",
"came",
"can",
"cannot",
"case",
"cases",
"certain",
"certainly",
"clear",
"clearly",
"come",
"could",
"d",
"did",
"differ",
"different",
"differently",
"do",
"does",
"done",
"down",
"down",
"downed",
"downing",
"downs",
"during",
"e",
"each",
"early",
"either",
"end",
"ended",
"ending",
"ends",
"enough",
"even",
"evenly",
"ever",
"every",
"everybody",
"everyone",
"everything",
"everywhere",
"f",
"face",
"faces",
"fact",
"facts",
"far",
"felt",
"few",
"find",
"finds",
"first",
"for",
"four",
"from",
"full",
"fully",
"further",
"furthered",
"furthering",
"furthers",
"g",
"gave",
"general",
"generally",
"get",
"gets",
"give",
"given",
"gives",
"go",
"going",
"good",
"goods",
"got",
"great",
"greater",
"greatest",
"group",
"grouped",
"grouping",
"groups",
"h",
"had",
"has",
"have",
"having",
"he",
"her",
"here",
"herself",
"high",
"high",
"high",
"higher",
"highest",
"him",
"himself",
"his",
"how",
"however",
"i",
"if",
"important",
"in",
"interest",
"interested",
"interesting",
"interests",
"into",
"is",
"it",
"its",
"itself",
"j",
"just",
"k",
"keep",
"keeps",
"kind",
"knew",
"know",
"known",
"knows",
"l",
"large",
"largely",
"last",
"later",
"latest",
"least",
"less",
"let",
"lets",
"like",
"likely",
"long",
"longer",
"longest",
"m",
"made",
"make",
"making",
"man",
"many",
"may",
"me",
"member",
"members",
"men",
"might",
"more",
"most",
"mostly",
"mr",
"mrs",
"much",
"must",
"my",
"myself",
"n",
"necessary",
"need",
"needed",
"needing",
"needs",
"never",
"new",
"new",
"newer",
"newest",
"next",
"no",
"nobody",
"non",
"noone",
"not",
"nothing",
"now",
"nowhere",
"number",
"numbers",
"o",
"of",
"off",
"often",
"old",
"older",
"oldest",
"on",
"once",
"one",
"only",
"open",
"opened",
"opening",
"opens",
"or",
"order",
"ordered",
"ordering",
"orders",
"other",
"others",
"our",
"out",
"over",
"p",
"part",
"parted",
"parting",
"parts",
"per",
"perhaps",
"place",
"places",
"point",
"pointed",
"pointing",
"points",
"possible",
"present",
"presented",
"presenting",
"presents",
"problem",
"problems",
"put",
"puts",
"q",
"quite",
"r",
"rather",
"really",
"right",
"right",
"room",
"rooms",
"s",
"said",
"same",
"saw",
"say",
"says",
"second",
"seconds",
"see",
"seem",
"seemed",
"seeming",
"seems",
"sees",
"several",
"shall",
"she",
"should",
"show",
"showed",
"showing",
"shows",
"side",
"sides",
"since",
"small",
"smaller",
"smallest",
"so",
"some",
"somebody",
"someone",
"something",
"somewhere",
"state",
"states",
"still",
"still",
"such",
"sure",
"t",
"take",
"taken",
"than",
"that",
"the",
"their",
"them",
"then",
"there",
"therefore",
"these",
"they",
"thing",
"things",
"think",
"thinks",
"this",
"those",
"though",
"thought",
"thoughts",
"three",
"through",
"thus",
"to",
"today",
"together",
"too",
"took",
"toward",
"turn",
"turned",
"turning",
"turns",
"two",
"u",
"under",
"until",
"up",
"upon",
"us",
"use",
"used",
"uses",
"v",
"very",
"w",
"want",
"wanted",
"wanting",
"wants",
"was",
"way",
"ways",
"we",
"well",
"wells",
"went",
"were",
"what",
"when",
"where",
"whether",
"which",
"while",
"who",
"whole",
"whose",
"why",
"will",
"with",
"within",
"without",
"work",
"worked",
"working",
"works",
"would",
"x",
"y",
"year",
"years",
"yet",
"you",
"young",
"younger",
"youngest",
"your",
"yours",
"z"
];
var stopWords$2 = [
"...",
"alle",
"andre",
"arbeid",
"at",
"av",
"bare",
"begge",
"ble",
"blei",
"bli",
"blir",
"blitt",
"bort",
"bruke",
"både",
"båe",
"da",
"de",
"deg",
"dei",
"deim",
"deira",
"deires",
"dem",
"den",
"denne",
"der",
"dere",
"deres",
"det",
"dette",
"di",
"din",
"disse",
"ditt",
"du",
"dykk",
"dykkar",
"då",
"eg",
"ein",
"eit",
"eitt",
"eller",
"elles",
"en",
"ene",
"eneste",
"enhver",
"enn",
"er",
"et",
"ett",
"etter",
"folk",
"for",
"fordi",
"forsøke",
"fra",
"få",
"før",
"før",
"først",
"gjorde",
"gjøre",
"god",
"gå",
"ha",
"hadde",
"han",
"hans",
"har",
"hennar",
"henne",
"hennes",
"her",
"hjå",
"ho",
"hoe",
"honom",
"hoss",
"hossen",
"hun",
"hva",
"hvem",
"hver",
"hvilke",
"hvilken",
"hvis",
"hvor",
"hvordan",
"hvorfor",
"i",
"ikke",
"ikkje",
"ingen",
"ingi",
"inkje",
"inn",
"innen",
"inni",
"ja",
"jeg",
"kan",
"kom",
"korleis",
"korso",
"kun",
"kunne",
"kva",
"kvar",
"kvarhelst",
"kven",
"kvi",
"kvifor",
"lage",
"lang",
"lik",
"like",
"makt",
"man",
"mange",
"me",
"med",
"medan",
"meg",
"meget",
"mellom",
"men",
"mens",
"mer",
"mest",
"mi",
"min",
"mine",
"mitt",
"mot",
"mye",
"mykje",
"må",
"måte",
"navn",
"ned",
"nei",
"no",
"noe",
"noen",
"noka",
"noko",
"nokon",
"nokor",
"nokre",
"ny",
"nå",
"når",
"og",
"også",
"om",
"opp",
"oss",
"over",
"part",
"punkt",
"på",
"samme",
"seg",
"selv",
"si",
"sia",
"sidan",
"siden",
"sin",
"sine",
"sist",
"sitt",
"sjøl",
"skal",
"skulle",
"slik",
"slutt",
"so",
"som",
"somme",
"somt",
"start",
"stille",
"så",
"sånn",
"tid",
"til",
"tilbake",
"tilstand",
"um",
"under",
"upp",
"ut",
"uten",
"var",
"vart",
"varte",
"ved",
"verdi",
"vere",
"verte",
"vi",
"vil",
"ville",
"vite",
"vore",
"vors",
"vort",
"vår",
"være",
"vært",
"vøre",
"vørt",
"æ",
"å"
];
var stopWords$1 = [
"de",
"en",
"van",
"ik",
"te",
"dat",
"die",
"in",
"een",
"hij",
"het",
"niet",
"zijn",
"is",
"was",
"op",
"aan",
"met",
"als",
"voor",
"had",
"er",
"maar",
"om",
"hem",
"dan",
"zou",
"of",
"wat",
"mijn",
"men",
"dit",
"zo",
"door",
"over",
"ze",
"zich",
"bij",
"ook",
"tot",
"je",
"mij",
"uit",
"der",
"daar",
"haar",
"naar",
"heb",
"hoe",
"heeft",
"hebben",
"deze",
"u",
"want",
"nog",
"zal",
"me",
"zij",
"nu",
"ge",
"geen",
"omdat",
"iets",
"worden",
"toch",
"al",
"waren",
"veel",
"meer",
"doen",
"toen",
"moet",
"ben",
"zonder",
"kan",
"hun",
"dus",
"alles",
"onder",
"ja",
"eens",
"hier",
"wie",
"werd",
"altijd",
"doch",
"wordt",
"wezen",
"kunnen",
"ons",
"zelf",
"tegen",
"na",
"reeds",
"wil",
"kon",
"niets",
"uw",
"iemand",
"geweest",
"andere"
];
var stopWords = [
"...",
"a",
"actualmente",
"adelante",
"además",
"afirmó",
"agregó",
"ahora",
"ahí",
"al",
"algo",
"alguna",
"algunas",
"alguno",
"algunos",
"algún",
"alrededor",
"ambos",
"ampleamos",
"ante",
"anterior",
"antes",
"apenas",
"aproximadamente",
"aquel",
"aquellas",
"aquellos",
"aqui",
"aquí",
"arriba",
"aseguró",
"así",
"atras",
"aunque",
"ayer",
"añadió",
"aún",
"bajo",
"bastante",
"bien",
"buen",
"buena",
"buenas",
"bueno",
"buenos",
"cada",
"casi",
"cerca",
"cierta",
"ciertas",
"cierto",
"ciertos",
"cinco",
"comentó",
"como",
"con",
"conocer",
"conseguimos",
"conseguir",
"considera",
"consideró",
"consigo",
"consigue",
"consiguen",
"consigues",
"contra",
"cosas",
"creo",
"cual",
"cuales",
"cualquier",
"cuando",
"cuanto",
"cuatro",
"cuenta",
"cómo",
"da",
"dado",
"dan",
"dar",
"de",
"debe",
"deben",
"debido",
"decir",
"dejó",
"del",
"demás",
"dentro",
"desde",
"después",
"dice",
"dicen",
"dicho",
"dieron",
"diferente",
"diferentes",
"dijeron",
"dijo",
"dio",
"donde",
"dos",
"durante",
"e",
"ejemplo",
"el",
"ella",
"ellas",
"ello",
"ellos",
"embargo",
"empleais",
"emplean",
"emplear",
"empleas",
"empleo",
"en",
"encima",
"encuentra",
"entonces",
"entre",
"era",
"erais",
"eramos",
"eran",
"eras",
"eres",
"es",
"esa",
"esas",
"ese",
"eso",
"esos",
"esta",
"estaba",
"estabais",
"estaban",
"estabas",
"estad",
"estada",
"estadas",
"estado",
"estados",
"estais",
"estamos",
"estan",
"estando",
"estar",
"estaremos",
"estará",
"estarán",
"estarás",
"estaré",
"estaréis",
"estaría",
"estaríais",
"estaríamos",
"estarían",
"estarías",
"estas",
"este",
"estemos",
"esto",
"estos",
"estoy",
"estuve",
"estuviera",
"estuvierais",
"estuvieran",
"estuvieras",
"estuvieron",
"estuviese",
"estuvieseis",
"estuviesen",
"estuvieses",
"estuvimos",
"estuviste",
"estuvisteis",
"estuviéramos",
"estuviésemos",
"estuvo",
"está",
"estábamos",
"estáis",
"están",
"estás",
"esté",
"estéis",
"estén",
"estés",
"ex",
"existe",
"existen",
"explicó",
"expresó",
"fin",
"fue",
"fuera",
"fuerais",
"fueran",
"fueras",
"fueron",
"fuese",
"fueseis",
"fuesen",
"fueses",
"fui",
"fuimos",
"fuiste",
"fuisteis",
"fuéramos",
"fuésemos",
"gran",
"grandes",
"gueno",
"ha",
"haber",
"habida",
"habidas",
"habido",
"habidos",
"habiendo",
"habremos",
"habrá",
"habrán",
"habrás",
"habré",
"habréis",
"habría",
"habríais",
"habríamos",
"habrían",
"habrías",
"habéis",
"había",
"habíais",
"habíamos",
"habían",
"habías",
"hace",
"haceis",
"hacemos",
"hacen",
"hacer",
"hacerlo",
"haces",
"hacia",
"haciendo",
"hago",
"han",
"has",
"hasta",
"hay",
"haya",
"hayamos",
"hayan",
"hayas",
"hayáis",
"he",
"hecho",
"hemos",
"hicieron",
"hizo",
"hoy",
"hube",
"hubiera",
"hubierais",
"hubieran",
"hubieras",
"hubieron",
"hubiese",
"hubieseis",
"hubiesen",
"hubieses",
"hubimos",
"hubiste",
"hubisteis",
"hubiéramos",
"hubiésemos",
"hubo",
"igual",
"incluso",
"indicó",
"informó",
"intenta",
"intentais",
"intentamos",
"intentan",
"intentar",
"intentas",
"intento",
"ir",
"junto",
"la",
"lado",
"largo",
"las",
"le",
"les",
"llegó",
"lleva",
"llevar",
"lo",
"los",
"luego",
"lugar",
"manera",
"manifestó",
"mayor",
"me",
"mediante",
"mejor",
"mencionó",
"menos",
"mi",
"mientras",
"mio",
"mis",
"misma",
"mismas",
"mismo",
"mismos",
"modo",
"momento",
"mucha",
"muchas",
"mucho",
"muchos",
"muy",
"más",
"mí",
"mía",
"mías",
"mío",
"míos",
"nada",
"nadie",
"ni",
"ninguna",
"ningunas",
"ninguno",
"ningunos",
"ningún",
"no",
"nos",
"nosotras",
"nosotros",
"nuestra",
"nuestras",
"nuestro",
"nuestros",
"nueva",
"nuevas",
"nuevo",
"nuevos",
"nunca",
"o",
"ocho",
"os",
"otra",
"otras",
"otro",
"otros",
"para",
"parece",
"parte",
"partir",
"pasada",
"pasado",
"pero",
"pesar",
"poca",
"pocas",
"poco",
"pocos",
"podeis",
"podemos",
"poder",
"podria",
"podriais",
"podriamos",
"podrian",
"podrias",
"podrá",
"podrán",
"podría",
"podrían",
"poner",
"por",
"por qué",
"porque",
"posible",
"primer",
"primera",
"primero",
"primeros",
"principalmente",
"propia",
"propias",
"propio",
"propios",
"próximo",
"próximos",
"pudo",
"pueda",
"puede",
"pueden",
"puedo",
"pues",
"que",
"quedó",
"queremos",
"quien",
"quienes",
"quiere",
"quién",
"qué",
"realizado",
"realizar",
"realizó",
"respecto",
"sabe",
"sabeis",
"sabemos",
"saben",
"saber",
"sabes",
"se",
"sea",
"seamos",
"sean",
"seas",
"segunda",
"segundo",
"según",
"seis",
"ser",
"seremos",
"será",
"serán",
"serás",
"seré",
"seréis",
"sería",
"seríais",
"seríamos",
"serían",
"serías",
"seáis",
"señaló",
"si",
"sido",
"siempre",
"siendo",
"siete",
"sigue",
"siguiente",
"sin",
"sino",
"sobre",
"sois",
"sola",
"solamente",
"solas",
"solo",
"solos",
"somos",
"son",
"soy",
"su",
"sus",
"suya",
"suyas",
"suyo",
"suyos",
"sí",
"sólo",
"tal",
"también",
"tampoco",
"tan",
"tanto",
"te",
"tendremos",
"tendrá",
"tendrán",
"tendrás",
"tendré",
"tendréis",
"tendría",
"tendríais",
"tendríamos",
"tendrían",
"tendrías",
"tened",
"teneis",
"tenemos",
"tener",
"tenga",
"tengamos",
"tengan",
"tengas",
"tengo",
"tengáis",
"tenida",
"tenidas",
"tenido",
"tenidos",
"teniendo",
"tenéis",
"tenía",
"teníais",
"teníamos",
"tenían",
"tenías",
"tercera",
"ti",
"tiempo",
"tiene",
"tienen",
"tienes",
"toda",
"todas",
"todavía",
"todo",
"todos",
"total",
"trabaja",
"trabajais",
"trabajamos",
"trabajan",
"trabajar",
"trabajas",
"trabajo",
"tras",
"trata",
"través",
"tres",
"tu",
"tus",
"tuve",
"tuviera",
"tuvierais",
"tuvieran",
"tuvieras",
"tuvieron",
"tuviese",
"tuvieseis",
"tuviesen",
"tuvieses",
"tuvimos",
"tuviste",
"tuvisteis",
"tuviéramos",
"tuviésemos",
"tuvo",
"tuya",
"tuyas",
"tuyo",
"tuyos",
"tú",
"ultimo",
"un",
"una",
"unas",
"uno",
"unos",
"usa",
"usais",
"usamos",
"usan",
"usar",
"usas",
"uso",
"usted",
"va",
"vais",
"valor",
"vamos",
"van",
"varias",
"varios",
"vaya",
"veces",
"ver",
"verdad",
"verdadera",
"verdadero",
"vez",
"vosotras",
"vosotros",
"voy",
"vuestra",
"vuestras",
"vuestro",
"vuestros",
"y",
"ya",
"yo",
"él",
"éramos",
"ésta",
"éstas",
"éste",
"éstos",
"última",
"últimas",
"último",
"últimos"
];
var stopWordsDictionary = {};
stopWordsDictionary["en"] = stopWords$3;
stopWordsDictionary["no"] = stopWords$2;
stopWordsDictionary["nl"] = stopWords$1;
stopWordsDictionary["es"] = stopWords;
var textHelper = {
getStopWords: (locale = "") => {
return stopWordsDictionary[locale || "en"] || [];
},
};
const defaultOptions = {
spiralResolution: 1,
spiralLimit: 360 * 5,
lineHeight: 0.8,
xWordPadding: 0,
yWordPadding: 3,
weightFactor: 40,
topN: 40,
maxHeight: 0,
padding: 10
};
class WordCloudWidget {
constructor(_options = defaultOptions) {
this._options = _options;
this._words = [];
this._placedWords = [];
this._minWeight = 1;
this._weightFactor = 1;
this._renderedTarget = undefined;
this.colors = ["black"];
}
get words() {
return this._words;
}
set words(w) {
this._words = [].concat(w || []);
this._minWeight = 1;
this._weightFactor = 1;
if (this._words.length > 0) {
this._words.sort((a, b) => -1 * (a[1] - b[1]));
this._minWeight = this._words[this._words.length - 1][1];
this._weightFactor = (this._words[0][1] - this._minWeight + 1) / this._options.weightFactor;
}
}
createWordElement(text, weight, color) {
var element = document.createElement("div");
element.style.position = "absolute";
element.style.fontSize = (weight - this._minWeight + 1) / this._weightFactor + "px";
element.style.lineHeight = this._options.lineHeight + "em";
if (this.colors.length > 0) {
element.style.color = this.colors[color % this.colors.length];
}
element.title = text + " (" + weight + ")";
element.appendChild(document.createTextNode(text));
return element;
}
isIntersectWithPlaced(currentWordRect) {
for (var i = 0; i < this._placedWords.length; i += 1) {
var existingWordRect = this._placedWords[i].rect;
if (!(currentWordRect.right + this._options.xWordPadding < existingWordRect.left - this._options.xWordPadding ||
currentWordRect.left - this._options.xWordPadding > existingWordRect.right + this._options.xWordPadding ||
currentWordRect.bottom + this._options.yWordPadding < existingWordRect.top - this._options.yWordPadding ||
currentWordRect.top - this._options.yWordPadding > existingWordRect.bottom + this._options.yWordPadding)) {
return true;
}
}
return false;
}
arrangeWords(cloudElement, startPoint) {
const currentPoint = {
x: 0,
y: 0
};
let yMin = currentPoint.y;
let yMax = currentPoint.y;
const displayWordsCount = Math.min(this._options.topN, this.words.length);
for (var i = 0; i < displayWordsCount; i += 1) {
var wordElement = this.createWordElement(this.words[i][0], this.words[i][1], i);
cloudElement.appendChild(wordElement);
for (var j = 0; j < this._options.spiralLimit; j++) {
const angle = this._options.spiralResolution * j;
currentPoint.x = (1 + angle) * Math.cos(angle);
currentPoint.y = (1 + angle) * Math.sin(angle);
const left = startPoint.x + currentPoint.x - wordElement.offsetWidth / 2;
const top = startPoint.y + currentPoint.y - wordElement.offsetHeight / 2;
wordElement.style.left = left + "px";
wordElement.style.top = top + "px";
const wordRect = wordElement.getBoundingClientRect();
if (!this.isIntersectWithPlaced(wordRect)) {
this._placedWords.push({
element: wordElement,
rect: wordRect,
left,
top
});
break;
}
}
const yMinProbe = currentPoint.y - wordElement.offsetHeight / 2;
if (yMin > yMinProbe) {
yMin = yMinProbe;
}
const yMaxProbe = currentPoint.y + wordElement.offsetHeight / 2;
if (yMax < yMaxProbe) {
yMax = yMaxProbe;
}
}
return [yMin, yMax];
}
render(target) {
this._renderedTarget = target;
var cloudElement = document.createElement("div");
document.body.appendChild(cloudElement);
cloudElement.style.position = "fixed";
cloudElement.style.top = "-1000px";
cloudElement.style.width = "0";
cloudElement.style.height = "0";
const startPoint = {
x: cloudElement.offsetWidth / 2,
y: cloudElement.offsetHeight / 2
};
const [yMin, yMax] = this.arrangeWords(cloudElement, startPoint);
this._placedWords.forEach(wordInfo => {
wordInfo.element.style.top = wordInfo.top - yMin + this._options.padding + "px";
});
cloudElement.remove();
cloudElement.style.top = "auto";
cloudElement.style.left = "50%";
cloudElement.className = "sa-visualizer-wordcloud";
cloudElement.style.position = "relative";
if (this._options.maxHeight > 0) {
cloudElement.style.height = this._options.maxHeight + "px";
cloudElement.style.overflow = "auto";
}
if (this._options.maxHeight == 0) {
cloudElement.style.height = yMax - yMin + this._options.padding * 2 + "px";
}
target.appendChild(cloudElement);
}
dispose() {
if (!!this._renderedTarget) {
this._renderedTarget.innerHTML = "";
this._renderedTarget = undefined;
}
}
}
class WordCloudAdapter {
constructor(model) {
this.model = model;
}
get wordcloud() {
return this._wordcloud;
}
create(element) {
return __awaiter(this, void 0, void 0, function* () {
const data = yield this.model.getCalculatedValues();
this.model.getColors();
if (data.length === 0) {
const emptyTextNode = DocumentHelper.createElement("p", "", {
innerText: localization.getString("noResults"),
});
element.appendChild(emptyTextNode);
return;
}
const config = JSON.parse(JSON.stringify(defaultOptions));
const options = {
config
};
WordCloudAdapter.onWordcloudCreating.fire(this.model, options);
this._wordcloud = new WordCloudWidget(config);
this._wordcloud.colors = this.model.getColors();
this._wordcloud.words = data;
this._wordcloud.render(element);
return this._wordcloud;
});
}
destroy(node) {
if (this._wordcloud && typeof this._wordcloud.dispose === "function") {
this._wordcloud.dispose();
}
this._wordcloud = undefined;
}
}
WordCloudAdapter.drawOutOfBound = false;
WordCloudAdapter.shrinkToFit = true;
WordCloudAdapter.abortThreshold = undefined;
WordCloudAdapter.weightFactor = 20;
WordCloudAdapter.onWordcloudCreating = new Event();
class WordCloud extends VisualizerBase {
constructor(question, data, options, name) {
super(question, data, options, name || "wordcloud");
this._wordcloudAdapter = new WordCloudAdapter(this);
}
convertFromExternalData(externalCalculatedData) {
const innerCalculatedData = [];
Object.keys(externalCalculatedData || []).forEach(word => {
innerCalculatedData.push([word, externalCalculatedData[word]]);
});
return innerCalculatedData;
}
getCalculatedValuesCore() {
let result = {};
let stopWords = [];
const locale = localization.currentLocale;
if (locale) {
stopWords = textHelper.getStopWords(locale);
}
else {
stopWords = textHelper.getStopWords();
}
const clearWordRegexp = new RegExp("[.,\/#!$%\^\*;:{}=\-_`~()]", "g");
const stopTheWord = (word) => {
if (stopWords.indexOf(word) !== -1) {
return "";
}
return word;
};
const processString = (row) => {
row = "" + row;
// if (row.length > 15) row = row.substring(0, 14) + "...";
if (!!row) {
row.split(" ").forEach((word) => {
let clearedWord = (word || "").toLowerCase().replace(clearWordRegexp, "");
clearedWord = stopTheWord(clearedWord);
if (!!clearedWord) {
if (!result[clearedWord]) {
result[clearedWord] = 1;
}
else {
result[clearedWord]++;
}
}
});
}
};
this.surveyData.forEach((row) => {
const rowValue = row[this.question.name];
if (!!rowValue) {
if (Array.isArray(rowValue)) {
rowValue.forEach(processString);
}
else {
if (typeof rowValue === "object") {
Object.keys(rowValue).forEach((key) => processString(rowValue[key]));
}
else {
processString(rowValue);
}
}
}
});
return Object.keys(result).map((key) => {
return [key, result[key]];
});
}
destroyContent(container) {
this._wordcloudAdapter.destroy(container);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const chartNode = DocumentHelper.createElement("div");
container.appendChild(chartNode);
yield this._wordcloudAdapter.create(chartNode);
container.innerHTML = "";
container.appendChild(chartNode);
return container;
});
}
destroy() {
this._wordcloudAdapter.destroy(this.contentContainer);
super.destroy();
}
}
VisualizationManager.registerVisualizer("text", WordCloud);
VisualizationManager.registerVisualizer("comment", WordCloud);
VisualizationManager.registerVisualizer("multipletext", WordCloud);
class TextTableAdapter {
constructor(model) {
this.model = model;
}
create(container) {
return __awaiter(this, void 0, void 0, function* () {
const { columnCount, data } = (yield this.model.getCalculatedValues());
const emptyTextNode = DocumentHelper.createElement("p", "", {
innerText: localization.getString("noResults"),
});
if (data.length === 0) {
container.appendChild(emptyTextNode);
return;
}
const tableNode = (DocumentHelper.createElement("table", "sa-text-table"));
tableNode.style.backgroundColor = this.model.backgroundColor;
container.appendChild(tableNode);
if (this.model.columns) {
var row = DocumentHelper.createElement("tr");
this.model.columns.forEach(column => {
var td = DocumentHelper.createElement("th", "sa-text-table__cell", {
textContent: column.title,
});
row.appendChild(td);
});
tableNode.appendChild(row);
}
data.forEach((rowData) => {
var row = DocumentHelper.createElement("tr");
for (var i = 0; i < columnCount; i++) {
const column = this.model.columns[i];
var td = DocumentHelper.createElement("td", "sa-text-table__cell" + ((column === null || column === void 0 ? void 0 : column.type) == "number" ? " sa-text-table__cell--number" : ""), {
textContent: rowData[i],
});
row.appendChild(td);
}
tableNode.appendChild(row);
});
container.className = "sa-text-table__container";
container.appendChild(tableNode);
});
}
destroy(node) {
node.innerHTML = "";
}
}
class Text extends VisualizerBase {
constructor(question, data, options, name) {
super(question, data, options, name || "text");
this._textTableAdapter = new TextTableAdapter(this);
}
get columns() {
const columns = [];
if (this.question.getType() == "multipletext") {
this.question.items.forEach(item => {
columns.push({ name: item.name, title: item.title, type: item.inputType });
});
}
return columns;
}
getCalculatedValuesCore() {
let result = [];
let columnCount = 0;
this.surveyData.forEach((row) => {
const rowValue = row[this.question.name];
let dataStrings = [];
if (!!rowValue) {
if (Array.isArray(rowValue)) {
dataStrings = dataStrings.concat(rowValue);
}
else {
if (typeof rowValue === "object") {
Object.keys(rowValue).forEach((key) => dataStrings.push(rowValue[key]));
}
else {
dataStrings.push(rowValue);
}
}
result.push(dataStrings);
if (dataStrings.length > columnCount) {
columnCount = dataStrings.length;
}
}
});
return { columnCount: columnCount, data: result };
}
destroyContent(container) {
this._textTableAdapter.destroy(container);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const tableNode = DocumentHelper.createElement("div");
yield this._textTableAdapter.create(tableNode);
container.innerHTML = "";
container.appendChild(tableNode);
return container;
});
}
destroy() {
this._textTableAdapter.destroy(this.contentContainer);
super.destroy();
}
}
VisualizationManager.registerVisualizer("text", Text);
VisualizationManager.registerVisualizer("comment", Text);
VisualizationManager.registerVisualizer("multipletext", Text);
class StatisticsTableAdapter {
constructor(model) {
this.model = model;
}
create(container) {
return __awaiter(this, void 0, void 0, function* () {
let { datasets, labels, colors, texts, seriesLabels, } = yield this.model.getAnswersData();
seriesLabels.length > 1;
const emptyTextNode = DocumentHelper.createElement("p", "", {
innerText: localization.getString("noResults"),
});
if (datasets.length === 0 || datasets[0].length === 0) {
container.appendChild(emptyTextNode);
return;
}
datasets.forEach((data, idx) => {
const tableNode = (DocumentHelper.createElement("table", "sa-statistics-table"));
tableNode.style.backgroundColor = this.model.backgroundColor;
container.appendChild(tableNode);
var headerRow = DocumentHelper.createElement("tr");
var labelCell = DocumentHelper.createElement("th", "sa-statistics-table__cell-header", {
textContent: localization.getString("answer"),
});
headerRow.appendChild(labelCell);
var sparklineCell = DocumentHelper.createElement("th", "sa-statistics-table__cell-header", {
textContent: localization.getString("statistics_chart"),
});
headerRow.appendChild(sparklineCell);
var percentCell = DocumentHelper.createElement("th", "sa-statistics-table__cell-header", {
textContent: localization.getString("percentage"),
});
headerRow.appendChild(percentCell);
var valueCell = DocumentHelper.createElement("th", "sa-statistics-table__cell-header", {
textContent: localization.getString("responses"),
});
headerRow.appendChild(valueCell);
tableNode.appendChild(headerRow);
for (let index = data.length - 1; index >= 0; index--) {
var row = DocumentHelper.createElement("tr");
var labelCell = DocumentHelper.createElement("td", "sa-statistics-table__cell", {
textContent: labels[index],
});
row.appendChild(labelCell);
var sparklineCell = DocumentHelper.createElement("td", "sa-statistics-table__cell");
var outerBar = DocumentHelper.createElement("div", "sa-choices-sparkline");
var innerBar = DocumentHelper.createElement("div", "sa-choices-sparkline-value");
innerBar.style.width = texts[idx][index] + "%";
outerBar.appendChild(innerBar);
sparklineCell.appendChild(outerBar);
row.appendChild(sparklineCell);
var percentCell = DocumentHelper.createElement("td", "sa-statistics-table__cell sa-statistics-table__cell-value", {
textContent: "" + texts[idx][index] + "%",
});
row.appendChild(percentCell);
var valueCell = DocumentHelper.createElement("td", "sa-statistics-table__cell sa-statistics-table__cell-value", {
textContent: data[index],
});
row.appendChild(valueCell);
tableNode.appendChild(row);
}
container.className = "sa-statistics-table__container";
container.appendChild(tableNode);
});
});
}
destroy(node) {
node.innerHTML = "";
}
}
class StatisticsTable extends SelectBase {
constructor(question, data, options, name) {
super(question, data, options, name || "choices");
this._statisticsTableAdapter = new StatisticsTableAdapter(this);
this.showPercentages = true;
}
destroyContent(container) {
this._statisticsTableAdapter.destroy(container);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const tableNode = DocumentHelper.createElement("div");
yield this._statisticsTableAdapter.create(tableNode);
container.innerHTML = "";
container.appendChild(tableNode);
return container;
});
}
destroy() {
this._statisticsTableAdapter.destroy(this.contentContainer);
super.destroy();
}
}
VisualizationManager.registerVisualizer("radiogroup", StatisticsTable);
VisualizationManager.registerVisualizer("dropdown", StatisticsTable);
VisualizationManager.registerVisualizer("checkbox", StatisticsTable);
class NpsVisualizerWidget {
constructor(_model, _data) {
this._model = _model;
this._data = _data;
this._renderedTarget = undefined;
}
renderScorePart(partId, value, percent) {
const scorePartElement = DocumentHelper.createElement("div", "sa-visualizer-nps__score-part");
const titleElement = DocumentHelper.createElement("div", "sa-visualizer-nps__score-part-title");
titleElement.innerText = localization.getString(partId);
scorePartElement.appendChild(titleElement);
const valuesElement = DocumentHelper.createElement("div", "sa-visualizer-nps__score-part-values");
scorePartElement.appendChild(valuesElement);
const valueElement = DocumentHelper.createElement("div", "sa-visualizer-nps__score-part-value");
valueElement.innerText = "" + value;
valuesElement.appendChild(valueElement);
if (percent) {
const percentElement = DocumentHelper.createElement("div", "sa-visualizer-nps__score-part-percent");
percentElement.innerText = "" + percent + "%";
valuesElement.appendChild(percentElement);
}
return scorePartElement;
}
render(target) {
this._renderedTarget = target;
var npsElement = DocumentHelper.createElement("div", "sa-visualizer-nps");
npsElement.appendChild(this.renderScorePart("npsScore", this.npsScore));
npsElement.appendChild(this.renderScorePart("npsPromoters", this._data.promoters, this.promotersPercent));
npsElement.appendChild(this.renderScorePart("npsPassives", this._data.total - this._data.promoters - this._data.detractors, this.passivePercent));
npsElement.appendChild(this.renderScorePart("npsDetractors", this._data.detractors, this.detractorsPercent));
target.appendChild(npsElement);
}
get npsScore() {
return toPrecision(((this._data.promoters - this._data.detractors) / this._data.total) * 100, this._model.precision);
}
get promotersPercent() {
return toPrecision(this._data.promoters / this._data.total * 100, this._model.precision);
}
get passivePercent() {
return toPrecision((this._data.total - this._data.promoters - this._data.detractors) / this._data.total * 100, this._model.precision);
}
get detractorsPercent() {
return toPrecision(this._data.detractors / this._data.total * 100, this._model.precision);
}
dispose() {
if (!!this._renderedTarget) {
this._renderedTarget.innerHTML = "";
this._renderedTarget = undefined;
}
}
}
class NpsAdapter {
constructor(model) {
this.model = model;
}
get npsVisualizer() {
return this._npsVisualizer;
}
create(element) {
return __awaiter(this, void 0, void 0, function* () {
const data = yield this.model.getCalculatedValues();
this._npsVisualizer = new NpsVisualizerWidget(this.model, data);
this._npsVisualizer.render(element);
return this._npsVisualizer;
});
}
destroy(node) {
if (this._npsVisualizer && typeof this._npsVisualizer.dispose === "function") {
this._npsVisualizer.dispose();
}
this._npsVisualizer = undefined;
}
}
class NpsVisualizer extends VisualizerBase {
constructor(question, data, options, name) {
super(question, data, options, name || "nps");
this.precision = 2;
this._npsAdapter = new NpsAdapter(this);
}
getCalculatedValuesCore() {
let result = {
detractors: 0,
passive: 0,
promoters: 0,
total: 0,
};
this.data.forEach((row) => {
const rowValue = row[this.question.name];
const scoreValue = parseInt(rowValue);
if (!Number.isNaN(scoreValue)) {
if (scoreValue <= NpsVisualizer.DetractorScore) {
result.detractors++;
}
else if (scoreValue >= NpsVisualizer.PromoterScore) {
result.promoters++;
}
else {
result.passive++;
}
result.total++;
}
});
return result;
}
destroyContent(container) {
this._npsAdapter.destroy(container);
super.destroyContent(container);
}
renderContentAsync(container) {
return __awaiter(this, void 0, void 0, function* () {
const npsNode = DocumentHelper.createElement("div");
container.appendChild(npsNode);
yield this._npsAdapter.create(npsNode);
container.innerHTML = "";
container.appendChild(npsNode);
return container;
});
}
destroy() {
this._npsAdapter.destroy(this.contentContainer);
super.destroy();
}
}
NpsVisualizer.DetractorScore = 6;
NpsVisualizer.PromoterScore = 9;
// VisualizationManager.registerVisualizer("rating", NpsVisualizer);
export { AlternativeVisualizersWrapper, BooleanModel, BooleanPlotly, DataProvider, DocumentHelper, GaugePlotly, HistogramModel, HistogramPlotly, Matrix, MatrixDropdownGroupedPlotly, MatrixPlotly, NpsAdapter, NpsVisualizer, NpsVisualizerWidget, NumberModel, PlotlyBoolChartAdapter, PlotlyChartAdapter, PlotlyGaugeAdapter, PlotlySetup, PostponeHelper, RankingPlotly, SelectBase, SelectBasePlotly, StatisticsTable, StatisticsTableAdapter, Text, TextTableAdapter, VisualizationManager, VisualizationMatrixDropdown, VisualizationMatrixDynamic, VisualizationPanel, VisualizationPanelDynamic, VisualizerBase, VisualizerFactory, WordCloud, WordCloudAdapter, defaultStatisticsCalculator, hideEmptyAnswersInData, localization, textHelper };
//# sourceMappingURL=survey.analytics.mjs.map