@odoo/o-spreadsheet 17.2.0-alpha.1 → 17.2.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 17.2.0-alpha.1
6
- * @date 2024-01-17T10:27:26.763Z
7
- * @hash 296f467
5
+ * @version 17.2.0-alpha.2
6
+ * @date 2024-01-29T13:55:57.132Z
7
+ * @hash 03c1335
8
8
  */
9
9
 
10
10
  import { Component, useRef, onMounted, useEffect, onWillPatch, useState, onWillUpdateProps, onPatched, useComponent, useExternalListener, onWillUnmount, onWillStart, xml, useChildSubEnv, useSubEnv, markRaw } from '@odoo/owl';
@@ -2060,6 +2060,7 @@ var CommandResult;
2060
2060
  CommandResult["BlockingValidationRule"] = "BlockingValidationRule";
2061
2061
  CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
2062
2062
  CommandResult["NoChanges"] = "NoChanges";
2063
+ CommandResult["InvalidInputId"] = "InvalidInputId";
2063
2064
  })(CommandResult || (CommandResult = {}));
2064
2065
 
2065
2066
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -2121,34 +2122,34 @@ const CellErrorType = {
2121
2122
  const errorTypes = new Set(Object.values(CellErrorType));
2122
2123
  class EvaluationError extends Error {
2123
2124
  value;
2124
- constructor(message, value = CellErrorType.GenericError) {
2125
- super(message || _t("Error"));
2125
+ constructor(message = _t("Error"), value = CellErrorType.GenericError) {
2126
+ super(message);
2126
2127
  this.value = value;
2127
2128
  }
2128
2129
  }
2129
2130
  class BadExpressionError extends EvaluationError {
2130
- constructor(message) {
2131
- super(message || _t("Invalid expression"), CellErrorType.BadExpression);
2131
+ constructor(message = _t("Invalid expression")) {
2132
+ super(message, CellErrorType.BadExpression);
2132
2133
  }
2133
2134
  }
2134
2135
  class CircularDependencyError extends EvaluationError {
2135
- constructor(message) {
2136
- super(message || _t("Circular reference"), CellErrorType.CircularDependency);
2136
+ constructor(message = _t("Circular reference")) {
2137
+ super(message, CellErrorType.CircularDependency);
2137
2138
  }
2138
2139
  }
2139
2140
  class InvalidReferenceError extends EvaluationError {
2140
- constructor(message) {
2141
- super(message || _t("Invalid reference"), CellErrorType.InvalidReference);
2141
+ constructor(message = _t("Invalid reference")) {
2142
+ super(message, CellErrorType.InvalidReference);
2142
2143
  }
2143
2144
  }
2144
2145
  class NotAvailableError extends EvaluationError {
2145
- constructor(message) {
2146
- super(message || _t("Data not available"), CellErrorType.NotAvailable);
2146
+ constructor(message = _t("Data not available")) {
2147
+ super(message, CellErrorType.NotAvailable);
2147
2148
  }
2148
2149
  }
2149
2150
  class UnknownFunctionError extends EvaluationError {
2150
- constructor(message) {
2151
- super(message || _t("Unknown function"), CellErrorType.UnknownFunction);
2151
+ constructor(message = _t("Unknown function")) {
2152
+ super(message, CellErrorType.UnknownFunction);
2152
2153
  }
2153
2154
  }
2154
2155
 
@@ -3314,27 +3315,30 @@ function roundFormat(format) {
3314
3315
  }
3315
3316
  function createLargeNumberFormat(format, magnitude, postFix, locale) {
3316
3317
  const internalFormat = parseFormat(format || "#,##0");
3317
- const largeNumberFormat = internalFormat
3318
- .map((formatPart) => {
3319
- if (formatPart.type === "NUMBER") {
3320
- return [
3321
- {
3322
- ...formatPart,
3323
- format: {
3324
- ...formatPart.format,
3325
- magnitude,
3326
- decimalPart: undefined,
3327
- },
3328
- },
3329
- {
3330
- type: "STRING",
3331
- format: postFix,
3332
- },
3333
- ];
3318
+ const largeNumberFormat = [];
3319
+ for (let i = 0; i < internalFormat.length; i++) {
3320
+ const formatPart = internalFormat[i];
3321
+ if (formatPart.type !== "NUMBER") {
3322
+ largeNumberFormat.push(formatPart);
3323
+ continue;
3334
3324
  }
3335
- return formatPart;
3336
- })
3337
- .flat();
3325
+ largeNumberFormat.push({
3326
+ ...formatPart,
3327
+ format: {
3328
+ ...formatPart.format,
3329
+ magnitude,
3330
+ decimalPart: undefined,
3331
+ },
3332
+ });
3333
+ largeNumberFormat.push({
3334
+ type: "STRING",
3335
+ format: postFix,
3336
+ });
3337
+ const nextFormatPart = internalFormat[i + 1];
3338
+ if (nextFormatPart?.type === "STRING" && ["k", "m", "b"].includes(nextFormatPart.format)) {
3339
+ i++;
3340
+ }
3341
+ }
3338
3342
  return convertInternalFormatToFormat(largeNumberFormat);
3339
3343
  }
3340
3344
  function changeDecimalPlaces(format, step, locale) {
@@ -6691,166 +6695,848 @@ const FilterMenuPopoverBuilder = {
6691
6695
  },
6692
6696
  };
6693
6697
 
6694
- const macRegex = /Mac/i;
6698
+ const LINK_TOOLTIP_HEIGHT = 32;
6699
+ const LINK_TOOLTIP_WIDTH = 220;
6700
+ css /* scss */ `
6701
+ .o-link-tool {
6702
+ font-size: 13px;
6703
+ background-color: white;
6704
+ box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);
6705
+ padding: 6px 12px;
6706
+ border-radius: 4px;
6707
+ display: flex;
6708
+ justify-content: space-between;
6709
+ height: ${LINK_TOOLTIP_HEIGHT}px;
6710
+ width: ${LINK_TOOLTIP_WIDTH}px;
6711
+ box-sizing: border-box !important;
6712
+
6713
+ img {
6714
+ margin-right: 3px;
6715
+ width: 16px;
6716
+ height: 16px;
6717
+ }
6718
+
6719
+ a.o-link {
6720
+ color: #01666b;
6721
+ text-decoration: none;
6722
+ flex-grow: 2;
6723
+ white-space: nowrap;
6724
+ overflow: hidden;
6725
+ text-overflow: ellipsis;
6726
+ }
6727
+ a.o-link:hover {
6728
+ text-decoration: none;
6729
+ color: #001d1f;
6730
+ cursor: pointer;
6731
+ }
6732
+ }
6733
+ .o-link-icon {
6734
+ float: right;
6735
+ padding-left: 5px;
6736
+ .o-icon {
6737
+ height: 16px;
6738
+ }
6739
+ }
6740
+ .o-link-icon .o-icon {
6741
+ height: 13px;
6742
+ }
6743
+ .o-link-icon:hover {
6744
+ cursor: pointer;
6745
+ color: #000;
6746
+ }
6747
+ `;
6748
+ class LinkDisplay extends Component {
6749
+ static template = "o-spreadsheet-LinkDisplay";
6750
+ static props = {
6751
+ cellPosition: Object,
6752
+ onClosed: { type: Function, optional: true },
6753
+ };
6754
+ get cell() {
6755
+ const { col, row } = this.props.cellPosition;
6756
+ const sheetId = this.env.model.getters.getActiveSheetId();
6757
+ return this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
6758
+ }
6759
+ get link() {
6760
+ if (this.cell.link) {
6761
+ return this.cell.link;
6762
+ }
6763
+ const { col, row } = this.props.cellPosition;
6764
+ throw new Error(`LinkDisplay Component can only be used with link cells. ${toXC(col, row)} is not a link.`);
6765
+ }
6766
+ getUrlRepresentation(link) {
6767
+ return urlRepresentation(link, this.env.model.getters);
6768
+ }
6769
+ openLink() {
6770
+ openLink(this.link, this.env);
6771
+ }
6772
+ edit() {
6773
+ const { col, row } = this.props.cellPosition;
6774
+ this.env.model.dispatch("OPEN_CELL_POPOVER", {
6775
+ col,
6776
+ row,
6777
+ popoverType: "LinkEditor",
6778
+ });
6779
+ }
6780
+ unlink() {
6781
+ const sheetId = this.env.model.getters.getActiveSheetId();
6782
+ const { col, row } = this.props.cellPosition;
6783
+ const style = this.env.model.getters.getCellComputedStyle({ sheetId, col, row });
6784
+ const textColor = style?.textColor === LINK_COLOR ? undefined : style?.textColor;
6785
+ this.env.model.dispatch("UPDATE_CELL", {
6786
+ col,
6787
+ row,
6788
+ sheetId,
6789
+ content: this.link.label,
6790
+ style: { ...style, textColor, underline: undefined },
6791
+ });
6792
+ }
6793
+ }
6794
+ const LinkCellPopoverBuilder = {
6795
+ onHover: (position, getters) => {
6796
+ const cell = getters.getEvaluatedCell(position);
6797
+ const shouldDisplayLink = !getters.isDashboard() && cell.link && getters.isVisibleInViewport(position);
6798
+ if (!shouldDisplayLink)
6799
+ return { isOpen: false };
6800
+ return {
6801
+ isOpen: true,
6802
+ Component: LinkDisplay,
6803
+ props: { cellPosition: position },
6804
+ cellCorner: "BottomLeft",
6805
+ };
6806
+ },
6807
+ };
6808
+
6695
6809
  /**
6696
- * Return true if the event was triggered from
6697
- * a child element.
6810
+ * Tokenizer
6811
+ *
6812
+ * A tokenizer is a piece of code whose job is to transform a string into a list
6813
+ * of "tokens". For example, "(12+" is converted into:
6814
+ * [{type: "LEFT_PAREN", value: "("},
6815
+ * {type: "NUMBER", value: "12"},
6816
+ * {type: "OPERATOR", value: "+"}]
6817
+ *
6818
+ * As the example shows, a tokenizer does not care about the meaning behind those
6819
+ * tokens. It only cares about the structure.
6820
+ *
6821
+ * The tokenizer is usually the first step in a compilation pipeline. Also, it
6822
+ * is useful for the composer, which needs to be able to work with incomplete
6823
+ * formulas.
6698
6824
  */
6699
- function isChildEvent(parent, ev) {
6700
- return !!ev.target && parent.contains(ev.target);
6825
+ const POSTFIX_UNARY_OPERATORS = ["%"];
6826
+ const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
6827
+ function tokenize(str, locale = DEFAULT_LOCALE) {
6828
+ str = replaceSpecialSpaces(str);
6829
+ const chars = new TokenizingChars(str);
6830
+ const result = [];
6831
+ while (!chars.isOver()) {
6832
+ let token = tokenizeSpace(chars) ||
6833
+ tokenizeArgsSeparator(chars, locale) ||
6834
+ tokenizeMisc(chars) ||
6835
+ tokenizeOperator(chars) ||
6836
+ tokenizeString(chars) ||
6837
+ tokenizeDebugger(chars) ||
6838
+ tokenizeInvalidRange(chars) ||
6839
+ tokenizeNumber(chars, locale) ||
6840
+ tokenizeSymbol(chars);
6841
+ if (!token) {
6842
+ token = { type: "UNKNOWN", value: chars.shift() };
6843
+ }
6844
+ result.push(token);
6845
+ }
6846
+ return result;
6701
6847
  }
6702
- function gridOverlayPosition() {
6703
- const spreadsheetElement = document.querySelector(".o-grid-overlay");
6704
- if (spreadsheetElement) {
6705
- const { top, left } = spreadsheetElement?.getBoundingClientRect();
6706
- return { top, left };
6848
+ function tokenizeDebugger(chars) {
6849
+ if (chars.current === "?") {
6850
+ chars.shift();
6851
+ return { type: "DEBUGGER", value: "?" };
6707
6852
  }
6708
- throw new Error("Can't find spreadsheet position");
6853
+ return null;
6709
6854
  }
6710
- function getBoundingRectAsPOJO(el) {
6711
- const rect = el.getBoundingClientRect();
6712
- return {
6713
- x: rect.x,
6714
- y: rect.y,
6715
- width: rect.width,
6716
- height: rect.height,
6717
- };
6855
+ const misc$1 = {
6856
+ "(": "LEFT_PAREN",
6857
+ ")": "RIGHT_PAREN",
6858
+ };
6859
+ function tokenizeMisc(chars) {
6860
+ if (chars.current in misc$1) {
6861
+ const value = chars.shift();
6862
+ const type = misc$1[value];
6863
+ return { type, value };
6864
+ }
6865
+ return null;
6718
6866
  }
6719
- /**
6720
- * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
6721
- */
6722
- function* iterateChildren(el) {
6723
- yield el;
6724
- if (el.hasChildNodes()) {
6725
- for (let child of el.childNodes) {
6726
- yield* iterateChildren(child);
6867
+ function tokenizeArgsSeparator(chars, locale) {
6868
+ if (chars.current === locale.formulaArgSeparator) {
6869
+ const value = chars.shift();
6870
+ const type = "ARG_SEPARATOR";
6871
+ return { type, value };
6872
+ }
6873
+ return null;
6874
+ }
6875
+ function tokenizeOperator(chars) {
6876
+ for (let op of OPERATORS) {
6877
+ if (chars.currentStartsWith(op)) {
6878
+ chars.advanceBy(op.length);
6879
+ return { type: "OPERATOR", value: op };
6727
6880
  }
6728
6881
  }
6882
+ return null;
6729
6883
  }
6730
- function getOpenedMenus() {
6731
- return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
6884
+ function tokenizeNumber(chars, locale) {
6885
+ const match = chars.remaining().match(getFormulaNumberRegex(locale.decimalSeparator));
6886
+ if (match) {
6887
+ chars.advanceBy(match[0].length);
6888
+ return { type: "NUMBER", value: match[0] };
6889
+ }
6890
+ return null;
6732
6891
  }
6733
- const letterRegex = /^[a-zA-Z]$/;
6892
+ function tokenizeString(chars) {
6893
+ if (chars.current === '"') {
6894
+ const startChar = chars.shift();
6895
+ let letters = startChar;
6896
+ while (chars.current && (chars.current !== startChar || letters[letters.length - 1] === "\\")) {
6897
+ letters += chars.shift();
6898
+ }
6899
+ if (chars.current === '"') {
6900
+ letters += chars.shift();
6901
+ }
6902
+ return {
6903
+ type: "STRING",
6904
+ value: letters,
6905
+ };
6906
+ }
6907
+ return null;
6908
+ }
6909
+ const separatorRegexp = /\w|\.|!|\$/;
6734
6910
  /**
6735
- * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.
6736
- *
6737
- * @argument ev - The keyboard event to transform
6738
- * @argument mode - Use either ev.key of ev.code to get the string shortcut
6911
+ * A "Symbol" is just basically any word-like element that can appear in a
6912
+ * formula, which is not a string. So:
6913
+ * A1
6914
+ * SUM
6915
+ * CEILING.MATH
6916
+ * A$1
6917
+ * Sheet2!A2
6918
+ * 'Sheet 2'!A2
6739
6919
  *
6740
- * @example
6741
- * event : { ctrlKey: true, key: "a" } => "Ctrl+A"
6742
- * event : { shift: true, alt: true, key: "Home" } => "Alt+Shift+Home"
6920
+ * are examples of symbols
6743
6921
  */
6744
- function keyboardEventToShortcutString(ev, mode = "key") {
6745
- let keyDownString = "";
6746
- if (isCtrlKey(ev) && ev.key !== "Ctrl")
6747
- keyDownString += "Ctrl+";
6748
- if (ev.metaKey)
6749
- keyDownString += "Ctrl+";
6750
- if (ev.altKey && ev.key !== "Alt")
6751
- keyDownString += "Alt+";
6752
- if (ev.shiftKey && ev.key !== "Shift")
6753
- keyDownString += "Shift+";
6754
- const key = mode === "key" ? ev.key : ev.code;
6755
- keyDownString += letterRegex.test(key) ? key.toUpperCase() : key;
6756
- return keyDownString;
6922
+ function tokenizeSymbol(chars) {
6923
+ let result = "";
6924
+ // there are two main cases to manage: either something which starts with
6925
+ // a ', like 'Sheet 2'A2, or a word-like element.
6926
+ if (chars.current === "'") {
6927
+ let lastChar = chars.shift();
6928
+ result += lastChar;
6929
+ while (chars.current) {
6930
+ lastChar = chars.shift();
6931
+ result += lastChar;
6932
+ if (lastChar === "'") {
6933
+ if (chars.current && chars.current === "'") {
6934
+ lastChar = chars.shift();
6935
+ result += lastChar;
6936
+ }
6937
+ else {
6938
+ break;
6939
+ }
6940
+ }
6941
+ }
6942
+ if (lastChar !== "'") {
6943
+ return {
6944
+ type: "UNKNOWN",
6945
+ value: result,
6946
+ };
6947
+ }
6948
+ }
6949
+ while (chars.current && separatorRegexp.test(chars.current)) {
6950
+ result += chars.shift();
6951
+ }
6952
+ if (result.length) {
6953
+ const value = result;
6954
+ const isReference = rangeReference.test(value);
6955
+ if (isReference) {
6956
+ return { type: "REFERENCE", value };
6957
+ }
6958
+ return { type: "SYMBOL", value };
6959
+ }
6960
+ return null;
6757
6961
  }
6758
- function isMacOS() {
6759
- return Boolean(macRegex.test(navigator.userAgent));
6962
+ function tokenizeSpace(chars) {
6963
+ let length = 0;
6964
+ while (chars.current === NEWLINE) {
6965
+ length++;
6966
+ chars.shift();
6967
+ }
6968
+ if (length) {
6969
+ return { type: "SPACE", value: NEWLINE.repeat(length) };
6970
+ }
6971
+ while (chars.current === " ") {
6972
+ length++;
6973
+ chars.shift();
6974
+ }
6975
+ if (length) {
6976
+ return { type: "SPACE", value: " ".repeat(length) };
6977
+ }
6978
+ return null;
6760
6979
  }
6761
- /**
6762
- * @param {KeyboardEvent | MouseEvent} ev
6763
- * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
6764
- * On Mac, this is the "meta" or "command" key.
6765
- */
6766
- function isCtrlKey(ev) {
6767
- return isMacOS() ? ev.metaKey : ev.ctrlKey;
6980
+ function tokenizeInvalidRange(chars) {
6981
+ if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
6982
+ chars.advanceBy(CellErrorType.InvalidReference.length);
6983
+ return { type: "INVALID_REFERENCE", value: CellErrorType.InvalidReference };
6984
+ }
6985
+ return null;
6768
6986
  }
6769
-
6770
- /**
6771
- * Return the o-spreadsheet element position relative
6772
- * to the browser viewport.
6773
- */
6774
- function useSpreadsheetRect() {
6775
- const position = useState({ x: 0, y: 0, width: 0, height: 0 });
6776
- let spreadsheetElement = document.querySelector(".o-spreadsheet");
6777
- updatePosition();
6778
- function updatePosition() {
6779
- if (!spreadsheetElement) {
6780
- spreadsheetElement = document.querySelector(".o-spreadsheet");
6781
- }
6782
- if (spreadsheetElement) {
6783
- const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
6784
- position.x = left;
6785
- position.y = top;
6786
- position.width = width;
6787
- position.height = height;
6987
+ class TokenizingChars {
6988
+ text;
6989
+ currentIndex = 0;
6990
+ current;
6991
+ constructor(text) {
6992
+ this.text = text;
6993
+ this.current = text[0];
6994
+ }
6995
+ shift() {
6996
+ const current = this.current;
6997
+ const next = this.text[++this.currentIndex];
6998
+ this.current = next;
6999
+ return current;
7000
+ }
7001
+ advanceBy(length) {
7002
+ this.currentIndex += length;
7003
+ this.current = this.text[this.currentIndex];
7004
+ }
7005
+ isOver() {
7006
+ return this.currentIndex >= this.text.length;
7007
+ }
7008
+ remaining() {
7009
+ return this.text.substring(this.currentIndex);
7010
+ }
7011
+ currentStartsWith(str) {
7012
+ for (let j = 0; j < str.length; j++) {
7013
+ if (this.text[this.currentIndex + j] !== str[j]) {
7014
+ return false;
7015
+ }
6788
7016
  }
7017
+ return true;
6789
7018
  }
6790
- onMounted(updatePosition);
6791
- onPatched(updatePosition);
6792
- return position;
7019
+ }
7020
+
7021
+ function isValidLocale(locale) {
7022
+ if (!(locale &&
7023
+ typeof locale === "object" &&
7024
+ typeof locale.name === "string" &&
7025
+ typeof locale.code === "string" &&
7026
+ typeof locale.thousandsSeparator === "string" &&
7027
+ typeof locale.decimalSeparator === "string" &&
7028
+ typeof locale.dateFormat === "string" &&
7029
+ typeof locale.timeFormat === "string" &&
7030
+ typeof locale.formulaArgSeparator === "string")) {
7031
+ return false;
7032
+ }
7033
+ if (!Object.values(locale).every((v) => v)) {
7034
+ return false;
7035
+ }
7036
+ if (locale.formulaArgSeparator === locale.decimalSeparator) {
7037
+ return false;
7038
+ }
7039
+ try {
7040
+ formatValue(1, { locale, format: "#,##0.00" });
7041
+ formatValue(1, { locale, format: locale.dateFormat });
7042
+ formatValue(1, { locale, format: locale.timeFormat });
7043
+ }
7044
+ catch {
7045
+ return false;
7046
+ }
7047
+ return true;
6793
7048
  }
6794
7049
  /**
6795
- * Return the component (or ref's component) BoundingRect, relative
6796
- * to the upper left corner of the screen (<body> element).
7050
+ * Change a content string from the given locale to its canonical form (en_US locale). Don't convert date string.
6797
7051
  *
6798
- * Note: when used with a <Portal/> component, it will
6799
- * return the portal position, not the teleported position.
7052
+ * @example
7053
+ * canonicalizeNumberContent("=SUM(1,5; 02/12/2012)", FR_LOCALE) // "=SUM(1.5, 02/12/2012)"
7054
+ * canonicalizeNumberContent("125,9", FR_LOCALE) // "125.9"
7055
+ * canonicalizeNumberContent("02/12/2012", FR_LOCALE) // "02/12/2012"
6800
7056
  */
6801
- function useAbsoluteBoundingRect(ref) {
6802
- const rect = useState({ x: 0, y: 0, width: 0, height: 0 });
6803
- function updateElRect() {
6804
- const el = ref.el;
6805
- if (el === null) {
6806
- return;
6807
- }
6808
- const { top, left, width, height } = el.getBoundingClientRect();
6809
- rect.x = left;
6810
- rect.y = top;
6811
- rect.width = width;
6812
- rect.height = height;
6813
- }
6814
- onMounted(updateElRect);
6815
- onPatched(updateElRect);
6816
- return rect;
7057
+ function canonicalizeNumberContent(content, locale) {
7058
+ return content.startsWith("=")
7059
+ ? canonicalizeFormula$1(content, locale)
7060
+ : canonicalizeNumberLiteral(content, locale);
6817
7061
  }
6818
7062
  /**
6819
- * Get the rectangle inside which a popover should stay when being displayed.
6820
- * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
6821
- * element by default.
7063
+ * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
7064
+ * This is destructive and won't preserve the original format.
6822
7065
  *
6823
- * Coordinates are expressed expressed as absolute DOM position.
7066
+ * @example
7067
+ * canonicalizeContent("=SUM(1,5; 5)", FR_LOCALE) // "=SUM(1.5, 5)"
7068
+ * canonicalizeContent("125,9", FR_LOCALE) // "125.9"
7069
+ * canonicalizeContent("02/12/2012", FR_LOCALE) // "12/02/2012"
7070
+ * canonicalizeContent("02-12-2012", FR_LOCALE) // "12/02/2012"
6824
7071
  */
6825
- function usePopoverContainer() {
6826
- const container = useState({ x: 0, y: 0, width: 0, height: 0 });
6827
- const component = useComponent();
6828
- const spreadsheetRect = useSpreadsheetRect();
6829
- function updateRect() {
6830
- const env = component.env;
6831
- const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
6832
- container.x = newRect.x;
6833
- container.y = newRect.y;
6834
- container.width = newRect.width;
6835
- container.height = newRect.height;
6836
- }
6837
- updateRect();
6838
- onMounted(updateRect);
6839
- onPatched(updateRect);
6840
- return container;
7072
+ function canonicalizeContent(content, locale) {
7073
+ return content.startsWith("=")
7074
+ ? canonicalizeFormula$1(content, locale)
7075
+ : canonicalizeLiteral(content, locale);
6841
7076
  }
6842
-
6843
7077
  /**
6844
- * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap
7078
+ * Change a content string from its canonical form (en_US locale) to the given locale. Also convert date string.
7079
+ *
7080
+ * @example
7081
+ * localizeContent("=SUM(1.5, 5)", FR_LOCALE) // "=SUM(1,5; 5)"
7082
+ * localizeContent("125.9", FR_LOCALE) // "125,9"
7083
+ * localizeContent("12/02/2012", FR_LOCALE) // "02/12/2012"
6845
7084
  */
6846
- function rectIntersection(rect1, rect2) {
6847
- return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));
7085
+ function localizeContent(content, locale) {
7086
+ return content.startsWith("=")
7087
+ ? localizeFormula(content, locale)
7088
+ : localizeLiteral(content, locale);
6848
7089
  }
6849
- /** Compute the union of the rectangles, ie. the smallest rectangle that contain them all */
6850
- function rectUnion(...rects) {
6851
- return zoneToRect(union(...rects.map(rectToZone)));
7090
+ /** Change a formula to its canonical form (en_US locale) */
7091
+ function canonicalizeFormula$1(formula, locale) {
7092
+ return _localizeFormula$1(formula, locale, DEFAULT_LOCALE);
6852
7093
  }
6853
- function rectToZone(rect) {
7094
+ /** Change a formula from the canonical form to the given locale */
7095
+ function localizeFormula(formula, locale) {
7096
+ return _localizeFormula$1(formula, DEFAULT_LOCALE, locale);
7097
+ }
7098
+ function _localizeFormula$1(formula, fromLocale, toLocale) {
7099
+ if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
7100
+ fromLocale.decimalSeparator === toLocale.decimalSeparator) {
7101
+ return formula;
7102
+ }
7103
+ const tokens = tokenize(formula, fromLocale);
7104
+ let localizedFormula = "";
7105
+ for (const token of tokens) {
7106
+ if (token.type === "NUMBER") {
7107
+ localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
7108
+ }
7109
+ else if (token.type === "ARG_SEPARATOR") {
7110
+ localizedFormula += toLocale.formulaArgSeparator;
7111
+ }
7112
+ else {
7113
+ localizedFormula += token.value;
7114
+ }
7115
+ }
7116
+ return localizedFormula;
7117
+ }
7118
+ /**
7119
+ * Change a literal string from the given locale to its canonical form (en_US locale). Don't convert date string.
7120
+ *
7121
+ * @example
7122
+ * canonicalizeNumberLiteral("125,9", FR_LOCALE) // "125.9"
7123
+ * canonicalizeNumberLiteral("02/12/2012", FR_LOCALE) // "02/12/2012"
7124
+ */
7125
+ function canonicalizeNumberLiteral(content, locale) {
7126
+ if (locale.decimalSeparator === "." || !isNumber(content, locale)) {
7127
+ return content;
7128
+ }
7129
+ return content.replace(locale.decimalSeparator, ".");
7130
+ }
7131
+ /**
7132
+ * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
7133
+ * This is destructive and won't preserve the original format.
7134
+ *
7135
+ * @example
7136
+ * canonicalizeLiteral("125,9", FR_LOCALE) // "125.9"
7137
+ * canonicalizeLiteral("02/12/2012", FR_LOCALE) // "12/02/2012"
7138
+ * canonicalizeLiteral("02-12-2012", FR_LOCALE) // "12/02/2012"
7139
+ */
7140
+ function canonicalizeLiteral(content, locale) {
7141
+ if (isDateTime(content, locale)) {
7142
+ const dateNumber = toNumber(content, locale);
7143
+ let format = DEFAULT_LOCALE.dateFormat;
7144
+ if (!Number.isInteger(dateNumber)) {
7145
+ format += " " + DEFAULT_LOCALE.timeFormat;
7146
+ }
7147
+ return formatValue(dateNumber, { locale: DEFAULT_LOCALE, format });
7148
+ }
7149
+ return canonicalizeNumberLiteral(content, locale);
7150
+ }
7151
+ /**
7152
+ * Change a literal string from its canonical form (en_US locale) to the given locale. Don't convert date string.
7153
+ * This is destructive and won't preserve the original format.
7154
+ *
7155
+ * @example
7156
+ * localizeNumberLiteral("125.9", FR_LOCALE) // "125,9"
7157
+ * localizeNumberLiteral("12/02/2012", FR_LOCALE) // "12/02/2012"
7158
+ * localizeNumberLiteral("12-02-2012", FR_LOCALE) // "12/02/2012"
7159
+ */
7160
+ function localizeNumberLiteral(literal, locale) {
7161
+ if (locale.decimalSeparator === "." || !isNumber(literal, DEFAULT_LOCALE)) {
7162
+ return literal;
7163
+ }
7164
+ const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);
7165
+ const localized = literal.replace(decimalNumberRegex, (match) => {
7166
+ return match.replace(".", locale.decimalSeparator);
7167
+ });
7168
+ return localized;
7169
+ }
7170
+ /**
7171
+ * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.
7172
+ *
7173
+ * @example
7174
+ * localizeLiteral("125.9", FR_LOCALE) // "125,9"
7175
+ * localizeLiteral("12/02/2012", FR_LOCALE) // "02/12/2012"
7176
+ */
7177
+ function localizeLiteral(literal, locale) {
7178
+ if (isDateTime(literal, DEFAULT_LOCALE)) {
7179
+ const dateNumber = toNumber(literal, DEFAULT_LOCALE);
7180
+ let format = locale.dateFormat;
7181
+ if (!Number.isInteger(dateNumber)) {
7182
+ format += " " + locale.timeFormat;
7183
+ }
7184
+ return formatValue(dateNumber, { locale, format });
7185
+ }
7186
+ return localizeNumberLiteral(literal, locale);
7187
+ }
7188
+ function canonicalizeCFRule(cf, locale) {
7189
+ return changeCFRuleLocale(cf, (content) => canonicalizeContent(content, locale));
7190
+ }
7191
+ function localizeCFRule(cf, locale) {
7192
+ return changeCFRuleLocale(cf, (content) => localizeContent(content, locale));
7193
+ }
7194
+ function localizeDataValidationRule(rule, locale) {
7195
+ const localizedDVRule = deepCopy(rule);
7196
+ localizedDVRule.criterion.values = localizedDVRule.criterion.values.map((content) => localizeContent(content, locale));
7197
+ return localizedDVRule;
7198
+ }
7199
+ function changeCFRuleLocale(rule, changeContentLocale) {
7200
+ rule = deepCopy(rule);
7201
+ switch (rule.type) {
7202
+ case "CellIsRule":
7203
+ // Only change value for number operators
7204
+ switch (rule.operator) {
7205
+ case "Between":
7206
+ case "NotBetween":
7207
+ case "Equal":
7208
+ case "NotEqual":
7209
+ case "GreaterThan":
7210
+ case "GreaterThanOrEqual":
7211
+ case "LessThan":
7212
+ case "LessThanOrEqual":
7213
+ rule.values = rule.values.map((v) => changeContentLocale(v));
7214
+ return rule;
7215
+ case "BeginsWith":
7216
+ case "ContainsText":
7217
+ case "EndsWith":
7218
+ case "NotContains":
7219
+ case "IsEmpty":
7220
+ case "IsNotEmpty":
7221
+ return rule;
7222
+ }
7223
+ break;
7224
+ case "ColorScaleRule":
7225
+ rule.minimum = changeCFRuleThresholdLocale(rule.minimum, changeContentLocale);
7226
+ rule.maximum = changeCFRuleThresholdLocale(rule.maximum, changeContentLocale);
7227
+ if (rule.midpoint) {
7228
+ rule.midpoint = changeCFRuleThresholdLocale(rule.midpoint, changeContentLocale);
7229
+ }
7230
+ return rule;
7231
+ case "IconSetRule":
7232
+ rule.lowerInflectionPoint.value = changeContentLocale(rule.lowerInflectionPoint.value);
7233
+ rule.upperInflectionPoint.value = changeContentLocale(rule.upperInflectionPoint.value);
7234
+ return rule;
7235
+ }
7236
+ }
7237
+ function changeCFRuleThresholdLocale(threshold, changeContentLocale) {
7238
+ if (!threshold?.value) {
7239
+ return threshold;
7240
+ }
7241
+ const value = threshold.type === "formula" ? "=" + threshold.value : threshold.value;
7242
+ const modified = changeContentLocale(value);
7243
+ const newValue = threshold.type === "formula" ? modified.slice(1) : modified;
7244
+ return { ...threshold, value: newValue };
7245
+ }
7246
+ function getDateTimeFormat(locale) {
7247
+ return locale.dateFormat + " " + locale.timeFormat;
7248
+ }
7249
+
7250
+ const linkSheet = {
7251
+ name: _t("Link sheet"),
7252
+ children: [
7253
+ (env) => {
7254
+ const sheets = env.model.getters
7255
+ .getSheetIds()
7256
+ .map((sheetId) => env.model.getters.getSheet(sheetId));
7257
+ return sheets.map((sheet) => ({
7258
+ id: sheet.id,
7259
+ name: sheet.name,
7260
+ execute: () => markdownLink(sheet.name, buildSheetLink(sheet.id)),
7261
+ }));
7262
+ },
7263
+ ],
7264
+ };
7265
+ const deleteSheet = {
7266
+ name: _t("Delete"),
7267
+ isVisible: (env) => {
7268
+ return env.model.getters.getSheetIds().length > 1;
7269
+ },
7270
+ execute: (env) => env.askConfirmation(_t("Are you sure you want to delete this sheet?"), () => {
7271
+ env.model.dispatch("DELETE_SHEET", { sheetId: env.model.getters.getActiveSheetId() });
7272
+ }),
7273
+ };
7274
+ const duplicateSheet = {
7275
+ name: _t("Duplicate"),
7276
+ execute: (env) => {
7277
+ const sheetIdFrom = env.model.getters.getActiveSheetId();
7278
+ const sheetIdTo = env.model.uuidGenerator.uuidv4();
7279
+ env.model.dispatch("DUPLICATE_SHEET", {
7280
+ sheetId: sheetIdFrom,
7281
+ sheetIdTo,
7282
+ });
7283
+ env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom, sheetIdTo });
7284
+ },
7285
+ };
7286
+ const renameSheet = (args) => {
7287
+ return {
7288
+ name: _t("Rename"),
7289
+ execute: args.renameSheetCallback,
7290
+ };
7291
+ };
7292
+ const sheetMoveRight = {
7293
+ name: _t("Move right"),
7294
+ isVisible: (env) => {
7295
+ const sheetId = env.model.getters.getActiveSheetId();
7296
+ const sheetIds = env.model.getters.getVisibleSheetIds();
7297
+ return sheetIds.indexOf(sheetId) !== sheetIds.length - 1;
7298
+ },
7299
+ execute: (env) => env.model.dispatch("MOVE_SHEET", {
7300
+ sheetId: env.model.getters.getActiveSheetId(),
7301
+ delta: 1,
7302
+ }),
7303
+ };
7304
+ const sheetMoveLeft = {
7305
+ name: _t("Move left"),
7306
+ isVisible: (env) => {
7307
+ const sheetId = env.model.getters.getActiveSheetId();
7308
+ return env.model.getters.getVisibleSheetIds()[0] !== sheetId;
7309
+ },
7310
+ execute: (env) => env.model.dispatch("MOVE_SHEET", {
7311
+ sheetId: env.model.getters.getActiveSheetId(),
7312
+ delta: -1,
7313
+ }),
7314
+ };
7315
+ const hideSheet = {
7316
+ name: _t("Hide sheet"),
7317
+ isVisible: (env) => env.model.getters.getVisibleSheetIds().length !== 1,
7318
+ execute: (env) => env.model.dispatch("HIDE_SHEET", { sheetId: env.model.getters.getActiveSheetId() }),
7319
+ };
7320
+
7321
+ /**
7322
+ * The class Registry is extended in order to add the function addChild
7323
+ *
7324
+ */
7325
+ class MenuItemRegistry extends Registry {
7326
+ /**
7327
+ * @override
7328
+ */
7329
+ add(key, value) {
7330
+ if (value.id === undefined) {
7331
+ value.id = key;
7332
+ }
7333
+ this.content[key] = value;
7334
+ return this;
7335
+ }
7336
+ /**
7337
+ * Add a subitem to an existing item
7338
+ * @param path Path of items to add this subitem
7339
+ * @param value Subitem to add
7340
+ */
7341
+ addChild(key, path, value) {
7342
+ if (typeof value !== "function" && value.id === undefined) {
7343
+ value.id = key;
7344
+ }
7345
+ const root = path.splice(0, 1)[0];
7346
+ let node = this.content[root];
7347
+ if (!node) {
7348
+ throw new Error(`Path ${root + ":" + path.join(":")} not found`);
7349
+ }
7350
+ for (let p of path) {
7351
+ const children = node.children;
7352
+ if (!children || typeof children === "function") {
7353
+ throw new Error(`${p} is either not a node or it's dynamically computed`);
7354
+ }
7355
+ node = children.find((elt) => elt.id === p);
7356
+ if (!node) {
7357
+ throw new Error(`Path ${root + ":" + path.join(":")} not found`);
7358
+ }
7359
+ }
7360
+ if (!node.children) {
7361
+ node.children = [];
7362
+ }
7363
+ node.children.push(value);
7364
+ return this;
7365
+ }
7366
+ getMenuItems() {
7367
+ return createActions(this.getAll());
7368
+ }
7369
+ }
7370
+
7371
+ //------------------------------------------------------------------------------
7372
+ // Link Menu Registry
7373
+ //------------------------------------------------------------------------------
7374
+ const linkMenuRegistry = new MenuItemRegistry();
7375
+ linkMenuRegistry.add("sheet", {
7376
+ ...linkSheet,
7377
+ sequence: 10,
7378
+ });
7379
+
7380
+ /**
7381
+ * Return the o-spreadsheet element position relative
7382
+ * to the browser viewport.
7383
+ */
7384
+ function useSpreadsheetRect() {
7385
+ const position = useState({ x: 0, y: 0, width: 0, height: 0 });
7386
+ let spreadsheetElement = document.querySelector(".o-spreadsheet");
7387
+ updatePosition();
7388
+ function updatePosition() {
7389
+ if (!spreadsheetElement) {
7390
+ spreadsheetElement = document.querySelector(".o-spreadsheet");
7391
+ }
7392
+ if (spreadsheetElement) {
7393
+ const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
7394
+ position.x = left;
7395
+ position.y = top;
7396
+ position.width = width;
7397
+ position.height = height;
7398
+ }
7399
+ }
7400
+ onMounted(updatePosition);
7401
+ onPatched(updatePosition);
7402
+ return position;
7403
+ }
7404
+ /**
7405
+ * Return the component (or ref's component) BoundingRect, relative
7406
+ * to the upper left corner of the screen (<body> element).
7407
+ *
7408
+ * Note: when used with a <Portal/> component, it will
7409
+ * return the portal position, not the teleported position.
7410
+ */
7411
+ function useAbsoluteBoundingRect(ref) {
7412
+ const rect = useState({ x: 0, y: 0, width: 0, height: 0 });
7413
+ function updateElRect() {
7414
+ const el = ref.el;
7415
+ if (el === null) {
7416
+ return;
7417
+ }
7418
+ const { top, left, width, height } = el.getBoundingClientRect();
7419
+ rect.x = left;
7420
+ rect.y = top;
7421
+ rect.width = width;
7422
+ rect.height = height;
7423
+ }
7424
+ onMounted(updateElRect);
7425
+ onPatched(updateElRect);
7426
+ return rect;
7427
+ }
7428
+ /**
7429
+ * Get the rectangle inside which a popover should stay when being displayed.
7430
+ * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
7431
+ * element by default.
7432
+ *
7433
+ * Coordinates are expressed expressed as absolute DOM position.
7434
+ */
7435
+ function usePopoverContainer() {
7436
+ const container = useState({ x: 0, y: 0, width: 0, height: 0 });
7437
+ const component = useComponent();
7438
+ const spreadsheetRect = useSpreadsheetRect();
7439
+ function updateRect() {
7440
+ const env = component.env;
7441
+ const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
7442
+ container.x = newRect.x;
7443
+ container.y = newRect.y;
7444
+ container.width = newRect.width;
7445
+ container.height = newRect.height;
7446
+ }
7447
+ updateRect();
7448
+ onMounted(updateRect);
7449
+ onPatched(updateRect);
7450
+ return container;
7451
+ }
7452
+
7453
+ const macRegex = /Mac/i;
7454
+ /**
7455
+ * Return true if the event was triggered from
7456
+ * a child element.
7457
+ */
7458
+ function isChildEvent(parent, ev) {
7459
+ return !!ev.target && parent.contains(ev.target);
7460
+ }
7461
+ function gridOverlayPosition() {
7462
+ const spreadsheetElement = document.querySelector(".o-grid-overlay");
7463
+ if (spreadsheetElement) {
7464
+ const { top, left } = spreadsheetElement?.getBoundingClientRect();
7465
+ return { top, left };
7466
+ }
7467
+ throw new Error("Can't find spreadsheet position");
7468
+ }
7469
+ function getBoundingRectAsPOJO(el) {
7470
+ const rect = el.getBoundingClientRect();
7471
+ return {
7472
+ x: rect.x,
7473
+ y: rect.y,
7474
+ width: rect.width,
7475
+ height: rect.height,
7476
+ };
7477
+ }
7478
+ /**
7479
+ * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
7480
+ */
7481
+ function* iterateChildren(el) {
7482
+ yield el;
7483
+ if (el.hasChildNodes()) {
7484
+ for (let child of el.childNodes) {
7485
+ yield* iterateChildren(child);
7486
+ }
7487
+ }
7488
+ }
7489
+ function getOpenedMenus() {
7490
+ return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
7491
+ }
7492
+ const letterRegex = /^[a-zA-Z]$/;
7493
+ /**
7494
+ * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.
7495
+ *
7496
+ * @argument ev - The keyboard event to transform
7497
+ * @argument mode - Use either ev.key of ev.code to get the string shortcut
7498
+ *
7499
+ * @example
7500
+ * event : { ctrlKey: true, key: "a" } => "Ctrl+A"
7501
+ * event : { shift: true, alt: true, key: "Home" } => "Alt+Shift+Home"
7502
+ */
7503
+ function keyboardEventToShortcutString(ev, mode = "key") {
7504
+ let keyDownString = "";
7505
+ if (isCtrlKey(ev) && ev.key !== "Ctrl")
7506
+ keyDownString += "Ctrl+";
7507
+ if (ev.metaKey)
7508
+ keyDownString += "Ctrl+";
7509
+ if (ev.altKey && ev.key !== "Alt")
7510
+ keyDownString += "Alt+";
7511
+ if (ev.shiftKey && ev.key !== "Shift")
7512
+ keyDownString += "Shift+";
7513
+ const key = mode === "key" ? ev.key : ev.code;
7514
+ keyDownString += letterRegex.test(key) ? key.toUpperCase() : key;
7515
+ return keyDownString;
7516
+ }
7517
+ function isMacOS() {
7518
+ return Boolean(macRegex.test(navigator.userAgent));
7519
+ }
7520
+ /**
7521
+ * @param {KeyboardEvent | MouseEvent} ev
7522
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
7523
+ * On Mac, this is the "meta" or "command" key.
7524
+ */
7525
+ function isCtrlKey(ev) {
7526
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
7527
+ }
7528
+
7529
+ /**
7530
+ * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap
7531
+ */
7532
+ function rectIntersection(rect1, rect2) {
7533
+ return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));
7534
+ }
7535
+ /** Compute the union of the rectangles, ie. the smallest rectangle that contain them all */
7536
+ function rectUnion(...rects) {
7537
+ return zoneToRect(union(...rects.map(rectToZone)));
7538
+ }
7539
+ function rectToZone(rect) {
6854
7540
  return {
6855
7541
  left: rect.x,
6856
7542
  top: rect.y,
@@ -7145,841 +7831,158 @@ class Menu extends Component {
7145
7831
  depth: 1,
7146
7832
  };
7147
7833
  subMenu = useState({
7148
- isOpen: false,
7149
- position: null,
7150
- scrollOffset: 0,
7151
- menuItems: [],
7152
- });
7153
- menuRef = useRef("menu");
7154
- position = useAbsoluteBoundingRect(this.menuRef);
7155
- setup() {
7156
- useExternalListener(window, "click", this.onExternalClick, { capture: true });
7157
- useExternalListener(window, "contextmenu", this.onExternalClick, { capture: true });
7158
- onWillUpdateProps((nextProps) => {
7159
- if (nextProps.menuItems !== this.props.menuItems) {
7160
- this.closeSubMenu();
7161
- }
7162
- });
7163
- }
7164
- get menuItemsAndSeparators() {
7165
- const menuItemsAndSeparators = [];
7166
- for (let i = 0; i < this.props.menuItems.length; i++) {
7167
- const menuItem = this.props.menuItems[i];
7168
- if (menuItem.isVisible(this.env)) {
7169
- menuItemsAndSeparators.push(menuItem);
7170
- }
7171
- if (menuItem.separator &&
7172
- i !== this.props.menuItems.length - 1 && // no separator at the end
7173
- menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
7174
- ) {
7175
- menuItemsAndSeparators.push("separator");
7176
- }
7177
- }
7178
- if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") {
7179
- menuItemsAndSeparators.pop();
7180
- }
7181
- if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
7182
- return [];
7183
- }
7184
- return menuItemsAndSeparators;
7185
- }
7186
- get subMenuPosition() {
7187
- const position = Object.assign({}, this.subMenu.position);
7188
- position.y -= this.subMenu.scrollOffset || 0;
7189
- return position;
7190
- }
7191
- get popoverProps() {
7192
- const isRoot = this.props.depth === 1;
7193
- return {
7194
- anchorRect: {
7195
- x: this.props.position.x - MENU_WIDTH * (this.props.depth - 1),
7196
- y: this.props.position.y,
7197
- width: isRoot ? 0 : MENU_WIDTH,
7198
- height: isRoot ? 0 : MENU_ITEM_HEIGHT,
7199
- },
7200
- positioning: "TopRight",
7201
- verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
7202
- onPopoverHidden: () => this.closeSubMenu(),
7203
- onPopoverMoved: () => this.closeSubMenu(),
7204
- };
7205
- }
7206
- get childrenHaveIcon() {
7207
- return this.props.menuItems.some((menuItem) => !!this.getIconName(menuItem));
7208
- }
7209
- getIconName(menu) {
7210
- if (menu.icon(this.env)) {
7211
- return menu.icon(this.env);
7212
- }
7213
- if (menu.isActive?.(this.env)) {
7214
- return "o-spreadsheet-Icon.CHECK";
7215
- }
7216
- return "";
7217
- }
7218
- getColor(menu) {
7219
- return menu.textColor ? `color: ${menu.textColor}` : undefined;
7220
- }
7221
- async activateMenu(menu) {
7222
- const result = await menu.execute?.(this.env);
7223
- this.close();
7224
- this.props.onMenuClicked?.({ detail: result });
7225
- }
7226
- close() {
7227
- this.closeSubMenu();
7228
- this.props.onClose();
7229
- }
7230
- onExternalClick(ev) {
7231
- // Don't close a root menu when clicked to open the submenus.
7232
- const el = this.menuRef.el;
7233
- if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {
7234
- return;
7235
- }
7236
- ev.closedMenuId = this.props.menuId;
7237
- this.close();
7238
- }
7239
- getName(menu) {
7240
- return menu.name(this.env);
7241
- }
7242
- isRoot(menu) {
7243
- return !menu.execute;
7244
- }
7245
- isEnabled(menu) {
7246
- if (menu.isEnabled(this.env)) {
7247
- return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
7248
- }
7249
- return false;
7250
- }
7251
- onScroll(ev) {
7252
- this.subMenu.scrollOffset = ev.target.scrollTop;
7253
- }
7254
- /**
7255
- * If the given menu is not disabled, open it's submenu at the
7256
- * correct position according to available surrounding space.
7257
- */
7258
- openSubMenu(menu, menuIndex, ev) {
7259
- const parentMenuEl = ev.currentTarget;
7260
- if (!parentMenuEl)
7261
- return;
7262
- const y = parentMenuEl.getBoundingClientRect().top;
7263
- this.subMenu.position = {
7264
- x: this.position.x + this.props.depth * MENU_WIDTH,
7265
- y: y - (this.subMenu.scrollOffset || 0),
7266
- };
7267
- this.subMenu.menuItems = menu.children(this.env);
7268
- this.subMenu.isOpen = true;
7269
- this.subMenu.parentMenu = menu;
7270
- }
7271
- isParentMenu(subMenu, menuItem) {
7272
- return subMenu.parentMenu?.id === menuItem.id;
7273
- }
7274
- closeSubMenu() {
7275
- this.subMenu.isOpen = false;
7276
- this.subMenu.parentMenu = undefined;
7277
- }
7278
- onClickMenu(menu, menuIndex, ev) {
7279
- if (this.isEnabled(menu)) {
7280
- if (this.isRoot(menu)) {
7281
- this.openSubMenu(menu, menuIndex, ev);
7282
- }
7283
- else {
7284
- this.activateMenu(menu);
7285
- }
7286
- }
7287
- }
7288
- onMouseOver(menu, position, ev) {
7289
- if (this.isEnabled(menu)) {
7290
- if (this.isRoot(menu)) {
7291
- this.openSubMenu(menu, position, ev);
7292
- }
7293
- else {
7294
- this.closeSubMenu();
7295
- }
7296
- }
7297
- }
7298
- }
7299
-
7300
- const LINK_TOOLTIP_HEIGHT = 32;
7301
- const LINK_TOOLTIP_WIDTH = 220;
7302
- css /* scss */ `
7303
- .o-link-tool {
7304
- font-size: 13px;
7305
- background-color: white;
7306
- box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);
7307
- padding: 6px 12px;
7308
- border-radius: 4px;
7309
- display: flex;
7310
- justify-content: space-between;
7311
- height: ${LINK_TOOLTIP_HEIGHT}px;
7312
- width: ${LINK_TOOLTIP_WIDTH}px;
7313
- box-sizing: border-box !important;
7314
-
7315
- img {
7316
- margin-right: 3px;
7317
- width: 16px;
7318
- height: 16px;
7319
- }
7320
-
7321
- a.o-link {
7322
- color: #01666b;
7323
- text-decoration: none;
7324
- flex-grow: 2;
7325
- white-space: nowrap;
7326
- overflow: hidden;
7327
- text-overflow: ellipsis;
7328
- }
7329
- a.o-link:hover {
7330
- text-decoration: none;
7331
- color: #001d1f;
7332
- cursor: pointer;
7333
- }
7334
- }
7335
- .o-link-icon {
7336
- float: right;
7337
- padding-left: 5px;
7338
- .o-icon {
7339
- height: 16px;
7340
- }
7341
- }
7342
- .o-link-icon .o-icon {
7343
- height: 13px;
7344
- }
7345
- .o-link-icon:hover {
7346
- cursor: pointer;
7347
- color: #000;
7348
- }
7349
- `;
7350
- class LinkDisplay extends Component {
7351
- static template = "o-spreadsheet-LinkDisplay";
7352
- static props = {
7353
- cellPosition: Object,
7354
- onClosed: { type: Function, optional: true },
7355
- };
7356
- static components = { Menu };
7357
- get cell() {
7358
- const { col, row } = this.props.cellPosition;
7359
- const sheetId = this.env.model.getters.getActiveSheetId();
7360
- return this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
7361
- }
7362
- get link() {
7363
- if (this.cell.link) {
7364
- return this.cell.link;
7365
- }
7366
- const { col, row } = this.props.cellPosition;
7367
- throw new Error(`LinkDisplay Component can only be used with link cells. ${toXC(col, row)} is not a link.`);
7368
- }
7369
- getUrlRepresentation(link) {
7370
- return urlRepresentation(link, this.env.model.getters);
7371
- }
7372
- openLink() {
7373
- openLink(this.link, this.env);
7374
- }
7375
- edit() {
7376
- const { col, row } = this.props.cellPosition;
7377
- this.env.model.dispatch("OPEN_CELL_POPOVER", {
7378
- col,
7379
- row,
7380
- popoverType: "LinkEditor",
7381
- });
7382
- }
7383
- unlink() {
7384
- const sheetId = this.env.model.getters.getActiveSheetId();
7385
- const { col, row } = this.props.cellPosition;
7386
- const style = this.env.model.getters.getCellComputedStyle({ sheetId, col, row });
7387
- const textColor = style?.textColor === LINK_COLOR ? undefined : style?.textColor;
7388
- this.env.model.dispatch("UPDATE_CELL", {
7389
- col,
7390
- row,
7391
- sheetId,
7392
- content: this.link.label,
7393
- style: { ...style, textColor, underline: undefined },
7394
- });
7395
- }
7396
- }
7397
- const LinkCellPopoverBuilder = {
7398
- onHover: (position, getters) => {
7399
- const cell = getters.getEvaluatedCell(position);
7400
- const shouldDisplayLink = !getters.isDashboard() && cell.link && getters.isVisibleInViewport(position);
7401
- if (!shouldDisplayLink)
7402
- return { isOpen: false };
7403
- return {
7404
- isOpen: true,
7405
- Component: LinkDisplay,
7406
- props: { cellPosition: position },
7407
- cellCorner: "BottomLeft",
7408
- };
7409
- },
7410
- };
7411
-
7412
- /**
7413
- * Tokenizer
7414
- *
7415
- * A tokenizer is a piece of code whose job is to transform a string into a list
7416
- * of "tokens". For example, "(12+" is converted into:
7417
- * [{type: "LEFT_PAREN", value: "("},
7418
- * {type: "NUMBER", value: "12"},
7419
- * {type: "OPERATOR", value: "+"}]
7420
- *
7421
- * As the example shows, a tokenizer does not care about the meaning behind those
7422
- * tokens. It only cares about the structure.
7423
- *
7424
- * The tokenizer is usually the first step in a compilation pipeline. Also, it
7425
- * is useful for the composer, which needs to be able to work with incomplete
7426
- * formulas.
7427
- */
7428
- const POSTFIX_UNARY_OPERATORS = ["%"];
7429
- const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
7430
- function tokenize(str, locale = DEFAULT_LOCALE) {
7431
- str = replaceSpecialSpaces(str);
7432
- const chars = new TokenizingChars(str);
7433
- const result = [];
7434
- while (!chars.isOver()) {
7435
- let token = tokenizeSpace(chars) ||
7436
- tokenizeArgsSeparator(chars, locale) ||
7437
- tokenizeMisc(chars) ||
7438
- tokenizeOperator(chars) ||
7439
- tokenizeString(chars) ||
7440
- tokenizeDebugger(chars) ||
7441
- tokenizeInvalidRange(chars) ||
7442
- tokenizeNumber(chars, locale) ||
7443
- tokenizeSymbol(chars);
7444
- if (!token) {
7445
- token = { type: "UNKNOWN", value: chars.shift() };
7446
- }
7447
- result.push(token);
7448
- }
7449
- return result;
7450
- }
7451
- function tokenizeDebugger(chars) {
7452
- if (chars.current === "?") {
7453
- chars.shift();
7454
- return { type: "DEBUGGER", value: "?" };
7455
- }
7456
- return null;
7457
- }
7458
- const misc$1 = {
7459
- "(": "LEFT_PAREN",
7460
- ")": "RIGHT_PAREN",
7461
- };
7462
- function tokenizeMisc(chars) {
7463
- if (chars.current in misc$1) {
7464
- const value = chars.shift();
7465
- const type = misc$1[value];
7466
- return { type, value };
7467
- }
7468
- return null;
7469
- }
7470
- function tokenizeArgsSeparator(chars, locale) {
7471
- if (chars.current === locale.formulaArgSeparator) {
7472
- const value = chars.shift();
7473
- const type = "ARG_SEPARATOR";
7474
- return { type, value };
7475
- }
7476
- return null;
7477
- }
7478
- function tokenizeOperator(chars) {
7479
- for (let op of OPERATORS) {
7480
- if (chars.currentStartsWith(op)) {
7481
- chars.advanceBy(op.length);
7482
- return { type: "OPERATOR", value: op };
7483
- }
7484
- }
7485
- return null;
7486
- }
7487
- function tokenizeNumber(chars, locale) {
7488
- const match = chars.remaining().match(getFormulaNumberRegex(locale.decimalSeparator));
7489
- if (match) {
7490
- chars.advanceBy(match[0].length);
7491
- return { type: "NUMBER", value: match[0] };
7492
- }
7493
- return null;
7494
- }
7495
- function tokenizeString(chars) {
7496
- if (chars.current === '"') {
7497
- const startChar = chars.shift();
7498
- let letters = startChar;
7499
- while (chars.current && (chars.current !== startChar || letters[letters.length - 1] === "\\")) {
7500
- letters += chars.shift();
7501
- }
7502
- if (chars.current === '"') {
7503
- letters += chars.shift();
7504
- }
7505
- return {
7506
- type: "STRING",
7507
- value: letters,
7508
- };
7509
- }
7510
- return null;
7511
- }
7512
- const separatorRegexp = /\w|\.|!|\$/;
7513
- /**
7514
- * A "Symbol" is just basically any word-like element that can appear in a
7515
- * formula, which is not a string. So:
7516
- * A1
7517
- * SUM
7518
- * CEILING.MATH
7519
- * A$1
7520
- * Sheet2!A2
7521
- * 'Sheet 2'!A2
7522
- *
7523
- * are examples of symbols
7524
- */
7525
- function tokenizeSymbol(chars) {
7526
- let result = "";
7527
- // there are two main cases to manage: either something which starts with
7528
- // a ', like 'Sheet 2'A2, or a word-like element.
7529
- if (chars.current === "'") {
7530
- let lastChar = chars.shift();
7531
- result += lastChar;
7532
- while (chars.current) {
7533
- lastChar = chars.shift();
7534
- result += lastChar;
7535
- if (lastChar === "'") {
7536
- if (chars.current && chars.current === "'") {
7537
- lastChar = chars.shift();
7538
- result += lastChar;
7539
- }
7540
- else {
7541
- break;
7542
- }
7543
- }
7544
- }
7545
- if (lastChar !== "'") {
7546
- return {
7547
- type: "UNKNOWN",
7548
- value: result,
7549
- };
7550
- }
7551
- }
7552
- while (chars.current && separatorRegexp.test(chars.current)) {
7553
- result += chars.shift();
7554
- }
7555
- if (result.length) {
7556
- const value = result;
7557
- const isReference = rangeReference.test(value);
7558
- if (isReference) {
7559
- return { type: "REFERENCE", value };
7560
- }
7561
- return { type: "SYMBOL", value };
7562
- }
7563
- return null;
7564
- }
7565
- function tokenizeSpace(chars) {
7566
- let length = 0;
7567
- while (chars.current === NEWLINE) {
7568
- length++;
7569
- chars.shift();
7570
- }
7571
- if (length) {
7572
- return { type: "SPACE", value: NEWLINE.repeat(length) };
7573
- }
7574
- while (chars.current === " ") {
7575
- length++;
7576
- chars.shift();
7577
- }
7578
- if (length) {
7579
- return { type: "SPACE", value: " ".repeat(length) };
7580
- }
7581
- return null;
7582
- }
7583
- function tokenizeInvalidRange(chars) {
7584
- if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
7585
- chars.advanceBy(CellErrorType.InvalidReference.length);
7586
- return { type: "INVALID_REFERENCE", value: CellErrorType.InvalidReference };
7587
- }
7588
- return null;
7589
- }
7590
- class TokenizingChars {
7591
- text;
7592
- currentIndex = 0;
7593
- current;
7594
- constructor(text) {
7595
- this.text = text;
7596
- this.current = text[0];
7597
- }
7598
- shift() {
7599
- const current = this.current;
7600
- const next = this.text[++this.currentIndex];
7601
- this.current = next;
7602
- return current;
7603
- }
7604
- advanceBy(length) {
7605
- this.currentIndex += length;
7606
- this.current = this.text[this.currentIndex];
7607
- }
7608
- isOver() {
7609
- return this.currentIndex >= this.text.length;
7610
- }
7611
- remaining() {
7612
- return this.text.substring(this.currentIndex);
7613
- }
7614
- currentStartsWith(str) {
7615
- for (let j = 0; j < str.length; j++) {
7616
- if (this.text[this.currentIndex + j] !== str[j]) {
7617
- return false;
7618
- }
7619
- }
7620
- return true;
7621
- }
7622
- }
7623
-
7624
- function isValidLocale(locale) {
7625
- if (!(locale &&
7626
- typeof locale === "object" &&
7627
- typeof locale.name === "string" &&
7628
- typeof locale.code === "string" &&
7629
- typeof locale.thousandsSeparator === "string" &&
7630
- typeof locale.decimalSeparator === "string" &&
7631
- typeof locale.dateFormat === "string" &&
7632
- typeof locale.timeFormat === "string" &&
7633
- typeof locale.formulaArgSeparator === "string")) {
7634
- return false;
7635
- }
7636
- if (!Object.values(locale).every((v) => v)) {
7637
- return false;
7638
- }
7639
- if (locale.formulaArgSeparator === locale.decimalSeparator) {
7640
- return false;
7641
- }
7642
- try {
7643
- formatValue(1, { locale, format: "#,##0.00" });
7644
- formatValue(1, { locale, format: locale.dateFormat });
7645
- formatValue(1, { locale, format: locale.timeFormat });
7646
- }
7647
- catch {
7648
- return false;
7649
- }
7650
- return true;
7651
- }
7652
- /**
7653
- * Change a content string from the given locale to its canonical form (en_US locale). Don't convert date string.
7654
- *
7655
- * @example
7656
- * canonicalizeNumberContent("=SUM(1,5; 02/12/2012)", FR_LOCALE) // "=SUM(1.5, 02/12/2012)"
7657
- * canonicalizeNumberContent("125,9", FR_LOCALE) // "125.9"
7658
- * canonicalizeNumberContent("02/12/2012", FR_LOCALE) // "02/12/2012"
7659
- */
7660
- function canonicalizeNumberContent(content, locale) {
7661
- return content.startsWith("=")
7662
- ? canonicalizeFormula$1(content, locale)
7663
- : canonicalizeNumberLiteral(content, locale);
7664
- }
7665
- /**
7666
- * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
7667
- * This is destructive and won't preserve the original format.
7668
- *
7669
- * @example
7670
- * canonicalizeContent("=SUM(1,5; 5)", FR_LOCALE) // "=SUM(1.5, 5)"
7671
- * canonicalizeContent("125,9", FR_LOCALE) // "125.9"
7672
- * canonicalizeContent("02/12/2012", FR_LOCALE) // "12/02/2012"
7673
- * canonicalizeContent("02-12-2012", FR_LOCALE) // "12/02/2012"
7674
- */
7675
- function canonicalizeContent(content, locale) {
7676
- return content.startsWith("=")
7677
- ? canonicalizeFormula$1(content, locale)
7678
- : canonicalizeLiteral(content, locale);
7679
- }
7680
- /**
7681
- * Change a content string from its canonical form (en_US locale) to the given locale. Also convert date string.
7682
- *
7683
- * @example
7684
- * localizeContent("=SUM(1.5, 5)", FR_LOCALE) // "=SUM(1,5; 5)"
7685
- * localizeContent("125.9", FR_LOCALE) // "125,9"
7686
- * localizeContent("12/02/2012", FR_LOCALE) // "02/12/2012"
7687
- */
7688
- function localizeContent(content, locale) {
7689
- return content.startsWith("=")
7690
- ? localizeFormula(content, locale)
7691
- : localizeLiteral(content, locale);
7692
- }
7693
- /** Change a formula to its canonical form (en_US locale) */
7694
- function canonicalizeFormula$1(formula, locale) {
7695
- return _localizeFormula$1(formula, locale, DEFAULT_LOCALE);
7696
- }
7697
- /** Change a formula from the canonical form to the given locale */
7698
- function localizeFormula(formula, locale) {
7699
- return _localizeFormula$1(formula, DEFAULT_LOCALE, locale);
7700
- }
7701
- function _localizeFormula$1(formula, fromLocale, toLocale) {
7702
- if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
7703
- fromLocale.decimalSeparator === toLocale.decimalSeparator) {
7704
- return formula;
7834
+ isOpen: false,
7835
+ position: null,
7836
+ scrollOffset: 0,
7837
+ menuItems: [],
7838
+ });
7839
+ menuRef = useRef("menu");
7840
+ position = useAbsoluteBoundingRect(this.menuRef);
7841
+ setup() {
7842
+ useExternalListener(window, "click", this.onExternalClick, { capture: true });
7843
+ useExternalListener(window, "contextmenu", this.onExternalClick, { capture: true });
7844
+ onWillUpdateProps((nextProps) => {
7845
+ if (nextProps.menuItems !== this.props.menuItems) {
7846
+ this.closeSubMenu();
7847
+ }
7848
+ });
7705
7849
  }
7706
- const tokens = tokenize(formula, fromLocale);
7707
- let localizedFormula = "";
7708
- for (const token of tokens) {
7709
- if (token.type === "NUMBER") {
7710
- localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
7850
+ get menuItemsAndSeparators() {
7851
+ const menuItemsAndSeparators = [];
7852
+ for (let i = 0; i < this.props.menuItems.length; i++) {
7853
+ const menuItem = this.props.menuItems[i];
7854
+ if (menuItem.isVisible(this.env)) {
7855
+ menuItemsAndSeparators.push(menuItem);
7856
+ }
7857
+ if (menuItem.separator &&
7858
+ i !== this.props.menuItems.length - 1 && // no separator at the end
7859
+ menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
7860
+ ) {
7861
+ menuItemsAndSeparators.push("separator");
7862
+ }
7711
7863
  }
7712
- else if (token.type === "ARG_SEPARATOR") {
7713
- localizedFormula += toLocale.formulaArgSeparator;
7864
+ if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") {
7865
+ menuItemsAndSeparators.pop();
7714
7866
  }
7715
- else {
7716
- localizedFormula += token.value;
7867
+ if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
7868
+ return [];
7717
7869
  }
7870
+ return menuItemsAndSeparators;
7718
7871
  }
7719
- return localizedFormula;
7720
- }
7721
- /**
7722
- * Change a literal string from the given locale to its canonical form (en_US locale). Don't convert date string.
7723
- *
7724
- * @example
7725
- * canonicalizeNumberLiteral("125,9", FR_LOCALE) // "125.9"
7726
- * canonicalizeNumberLiteral("02/12/2012", FR_LOCALE) // "02/12/2012"
7727
- */
7728
- function canonicalizeNumberLiteral(content, locale) {
7729
- if (locale.decimalSeparator === "." || !isNumber(content, locale)) {
7730
- return content;
7872
+ get subMenuPosition() {
7873
+ const position = Object.assign({}, this.subMenu.position);
7874
+ position.y -= this.subMenu.scrollOffset || 0;
7875
+ return position;
7731
7876
  }
7732
- return content.replace(locale.decimalSeparator, ".");
7733
- }
7734
- /**
7735
- * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
7736
- * This is destructive and won't preserve the original format.
7737
- *
7738
- * @example
7739
- * canonicalizeLiteral("125,9", FR_LOCALE) // "125.9"
7740
- * canonicalizeLiteral("02/12/2012", FR_LOCALE) // "12/02/2012"
7741
- * canonicalizeLiteral("02-12-2012", FR_LOCALE) // "12/02/2012"
7742
- */
7743
- function canonicalizeLiteral(content, locale) {
7744
- if (isDateTime(content, locale)) {
7745
- const dateNumber = toNumber(content, locale);
7746
- let format = DEFAULT_LOCALE.dateFormat;
7747
- if (!Number.isInteger(dateNumber)) {
7748
- format += " " + DEFAULT_LOCALE.timeFormat;
7877
+ get popoverProps() {
7878
+ const isRoot = this.props.depth === 1;
7879
+ return {
7880
+ anchorRect: {
7881
+ x: this.props.position.x - MENU_WIDTH * (this.props.depth - 1),
7882
+ y: this.props.position.y,
7883
+ width: isRoot ? 0 : MENU_WIDTH,
7884
+ height: isRoot ? 0 : MENU_ITEM_HEIGHT,
7885
+ },
7886
+ positioning: "TopRight",
7887
+ verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
7888
+ onPopoverHidden: () => this.closeSubMenu(),
7889
+ onPopoverMoved: () => this.closeSubMenu(),
7890
+ };
7891
+ }
7892
+ get childrenHaveIcon() {
7893
+ return this.props.menuItems.some((menuItem) => !!this.getIconName(menuItem));
7894
+ }
7895
+ getIconName(menu) {
7896
+ if (menu.icon(this.env)) {
7897
+ return menu.icon(this.env);
7749
7898
  }
7750
- return formatValue(dateNumber, { locale: DEFAULT_LOCALE, format });
7899
+ if (menu.isActive?.(this.env)) {
7900
+ return "o-spreadsheet-Icon.CHECK";
7901
+ }
7902
+ return "";
7751
7903
  }
7752
- return canonicalizeNumberLiteral(content, locale);
7753
- }
7754
- /**
7755
- * Change a literal string from its canonical form (en_US locale) to the given locale. Don't convert date string.
7756
- * This is destructive and won't preserve the original format.
7757
- *
7758
- * @example
7759
- * localizeNumberLiteral("125.9", FR_LOCALE) // "125,9"
7760
- * localizeNumberLiteral("12/02/2012", FR_LOCALE) // "12/02/2012"
7761
- * localizeNumberLiteral("12-02-2012", FR_LOCALE) // "12/02/2012"
7762
- */
7763
- function localizeNumberLiteral(literal, locale) {
7764
- if (locale.decimalSeparator === "." || !isNumber(literal, DEFAULT_LOCALE)) {
7765
- return literal;
7904
+ getColor(menu) {
7905
+ return menu.textColor ? `color: ${menu.textColor}` : undefined;
7766
7906
  }
7767
- const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);
7768
- const localized = literal.replace(decimalNumberRegex, (match) => {
7769
- return match.replace(".", locale.decimalSeparator);
7770
- });
7771
- return localized;
7772
- }
7773
- /**
7774
- * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.
7775
- *
7776
- * @example
7777
- * localizeLiteral("125.9", FR_LOCALE) // "125,9"
7778
- * localizeLiteral("12/02/2012", FR_LOCALE) // "02/12/2012"
7779
- */
7780
- function localizeLiteral(literal, locale) {
7781
- if (isDateTime(literal, DEFAULT_LOCALE)) {
7782
- const dateNumber = toNumber(literal, DEFAULT_LOCALE);
7783
- let format = locale.dateFormat;
7784
- if (!Number.isInteger(dateNumber)) {
7785
- format += " " + locale.timeFormat;
7907
+ async activateMenu(menu) {
7908
+ const result = await menu.execute?.(this.env);
7909
+ this.close();
7910
+ this.props.onMenuClicked?.({ detail: result });
7911
+ }
7912
+ close() {
7913
+ this.closeSubMenu();
7914
+ this.props.onClose();
7915
+ }
7916
+ onExternalClick(ev) {
7917
+ // Don't close a root menu when clicked to open the submenus.
7918
+ const el = this.menuRef.el;
7919
+ if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {
7920
+ return;
7786
7921
  }
7787
- return formatValue(dateNumber, { locale, format });
7922
+ ev.closedMenuId = this.props.menuId;
7923
+ this.close();
7788
7924
  }
7789
- return localizeNumberLiteral(literal, locale);
7790
- }
7791
- function canonicalizeCFRule(cf, locale) {
7792
- return changeCFRuleLocale(cf, (content) => canonicalizeContent(content, locale));
7793
- }
7794
- function localizeCFRule(cf, locale) {
7795
- return changeCFRuleLocale(cf, (content) => localizeContent(content, locale));
7796
- }
7797
- function localizeDataValidationRule(rule, locale) {
7798
- const localizedDVRule = deepCopy(rule);
7799
- localizedDVRule.criterion.values = localizedDVRule.criterion.values.map((content) => localizeContent(content, locale));
7800
- return localizedDVRule;
7801
- }
7802
- function changeCFRuleLocale(rule, changeContentLocale) {
7803
- rule = deepCopy(rule);
7804
- switch (rule.type) {
7805
- case "CellIsRule":
7806
- // Only change value for number operators
7807
- switch (rule.operator) {
7808
- case "Between":
7809
- case "NotBetween":
7810
- case "Equal":
7811
- case "NotEqual":
7812
- case "GreaterThan":
7813
- case "GreaterThanOrEqual":
7814
- case "LessThan":
7815
- case "LessThanOrEqual":
7816
- rule.values = rule.values.map((v) => changeContentLocale(v));
7817
- return rule;
7818
- case "BeginsWith":
7819
- case "ContainsText":
7820
- case "EndsWith":
7821
- case "NotContains":
7822
- case "IsEmpty":
7823
- case "IsNotEmpty":
7824
- return rule;
7825
- }
7826
- break;
7827
- case "ColorScaleRule":
7828
- rule.minimum = changeCFRuleThresholdLocale(rule.minimum, changeContentLocale);
7829
- rule.maximum = changeCFRuleThresholdLocale(rule.maximum, changeContentLocale);
7830
- if (rule.midpoint) {
7831
- rule.midpoint = changeCFRuleThresholdLocale(rule.midpoint, changeContentLocale);
7832
- }
7833
- return rule;
7834
- case "IconSetRule":
7835
- rule.lowerInflectionPoint.value = changeContentLocale(rule.lowerInflectionPoint.value);
7836
- rule.upperInflectionPoint.value = changeContentLocale(rule.upperInflectionPoint.value);
7837
- return rule;
7925
+ getName(menu) {
7926
+ return menu.name(this.env);
7838
7927
  }
7839
- }
7840
- function changeCFRuleThresholdLocale(threshold, changeContentLocale) {
7841
- if (!threshold?.value) {
7842
- return threshold;
7928
+ isRoot(menu) {
7929
+ return !menu.execute;
7843
7930
  }
7844
- const value = threshold.type === "formula" ? "=" + threshold.value : threshold.value;
7845
- const modified = changeContentLocale(value);
7846
- const newValue = threshold.type === "formula" ? modified.slice(1) : modified;
7847
- return { ...threshold, value: newValue };
7848
- }
7849
- function getDateTimeFormat(locale) {
7850
- return locale.dateFormat + " " + locale.timeFormat;
7851
- }
7852
-
7853
- const linkSheet = {
7854
- name: _t("Link sheet"),
7855
- children: [
7856
- (env) => {
7857
- const sheets = env.model.getters
7858
- .getSheetIds()
7859
- .map((sheetId) => env.model.getters.getSheet(sheetId));
7860
- return sheets.map((sheet) => ({
7861
- id: sheet.id,
7862
- name: sheet.name,
7863
- execute: () => markdownLink(sheet.name, buildSheetLink(sheet.id)),
7864
- }));
7865
- },
7866
- ],
7867
- };
7868
- const deleteSheet = {
7869
- name: _t("Delete"),
7870
- isVisible: (env) => {
7871
- return env.model.getters.getSheetIds().length > 1;
7872
- },
7873
- execute: (env) => env.askConfirmation(_t("Are you sure you want to delete this sheet?"), () => {
7874
- env.model.dispatch("DELETE_SHEET", { sheetId: env.model.getters.getActiveSheetId() });
7875
- }),
7876
- };
7877
- const duplicateSheet = {
7878
- name: _t("Duplicate"),
7879
- execute: (env) => {
7880
- const sheetIdFrom = env.model.getters.getActiveSheetId();
7881
- const sheetIdTo = env.model.uuidGenerator.uuidv4();
7882
- env.model.dispatch("DUPLICATE_SHEET", {
7883
- sheetId: sheetIdFrom,
7884
- sheetIdTo,
7885
- });
7886
- env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom, sheetIdTo });
7887
- },
7888
- };
7889
- const renameSheet = (args) => {
7890
- return {
7891
- name: _t("Rename"),
7892
- execute: args.renameSheetCallback,
7893
- };
7894
- };
7895
- const sheetMoveRight = {
7896
- name: _t("Move right"),
7897
- isVisible: (env) => {
7898
- const sheetId = env.model.getters.getActiveSheetId();
7899
- const sheetIds = env.model.getters.getVisibleSheetIds();
7900
- return sheetIds.indexOf(sheetId) !== sheetIds.length - 1;
7901
- },
7902
- execute: (env) => env.model.dispatch("MOVE_SHEET", {
7903
- sheetId: env.model.getters.getActiveSheetId(),
7904
- delta: 1,
7905
- }),
7906
- };
7907
- const sheetMoveLeft = {
7908
- name: _t("Move left"),
7909
- isVisible: (env) => {
7910
- const sheetId = env.model.getters.getActiveSheetId();
7911
- return env.model.getters.getVisibleSheetIds()[0] !== sheetId;
7912
- },
7913
- execute: (env) => env.model.dispatch("MOVE_SHEET", {
7914
- sheetId: env.model.getters.getActiveSheetId(),
7915
- delta: -1,
7916
- }),
7917
- };
7918
- const hideSheet = {
7919
- name: _t("Hide sheet"),
7920
- isVisible: (env) => env.model.getters.getVisibleSheetIds().length !== 1,
7921
- execute: (env) => env.model.dispatch("HIDE_SHEET", { sheetId: env.model.getters.getActiveSheetId() }),
7922
- };
7923
-
7924
- /**
7925
- * The class Registry is extended in order to add the function addChild
7926
- *
7927
- */
7928
- class MenuItemRegistry extends Registry {
7929
- /**
7930
- * @override
7931
- */
7932
- add(key, value) {
7933
- if (value.id === undefined) {
7934
- value.id = key;
7931
+ isEnabled(menu) {
7932
+ if (menu.isEnabled(this.env)) {
7933
+ return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
7935
7934
  }
7936
- this.content[key] = value;
7937
- return this;
7935
+ return false;
7936
+ }
7937
+ onScroll(ev) {
7938
+ this.subMenu.scrollOffset = ev.target.scrollTop;
7938
7939
  }
7939
7940
  /**
7940
- * Add a subitem to an existing item
7941
- * @param path Path of items to add this subitem
7942
- * @param value Subitem to add
7941
+ * If the given menu is not disabled, open it's submenu at the
7942
+ * correct position according to available surrounding space.
7943
7943
  */
7944
- addChild(key, path, value) {
7945
- if (typeof value !== "function" && value.id === undefined) {
7946
- value.id = key;
7947
- }
7948
- const root = path.splice(0, 1)[0];
7949
- let node = this.content[root];
7950
- if (!node) {
7951
- throw new Error(`Path ${root + ":" + path.join(":")} not found`);
7952
- }
7953
- for (let p of path) {
7954
- const children = node.children;
7955
- if (!children || typeof children === "function") {
7956
- throw new Error(`${p} is either not a node or it's dynamically computed`);
7944
+ openSubMenu(menu, menuIndex, ev) {
7945
+ const parentMenuEl = ev.currentTarget;
7946
+ if (!parentMenuEl)
7947
+ return;
7948
+ const y = parentMenuEl.getBoundingClientRect().top;
7949
+ this.subMenu.position = {
7950
+ x: this.position.x + this.props.depth * MENU_WIDTH,
7951
+ y: y - (this.subMenu.scrollOffset || 0),
7952
+ };
7953
+ this.subMenu.menuItems = menu.children(this.env);
7954
+ this.subMenu.isOpen = true;
7955
+ this.subMenu.parentMenu = menu;
7956
+ }
7957
+ isParentMenu(subMenu, menuItem) {
7958
+ return subMenu.parentMenu?.id === menuItem.id;
7959
+ }
7960
+ closeSubMenu() {
7961
+ this.subMenu.isOpen = false;
7962
+ this.subMenu.parentMenu = undefined;
7963
+ }
7964
+ onClickMenu(menu, menuIndex, ev) {
7965
+ if (this.isEnabled(menu)) {
7966
+ if (this.isRoot(menu)) {
7967
+ this.openSubMenu(menu, menuIndex, ev);
7957
7968
  }
7958
- node = children.find((elt) => elt.id === p);
7959
- if (!node) {
7960
- throw new Error(`Path ${root + ":" + path.join(":")} not found`);
7969
+ else {
7970
+ this.activateMenu(menu);
7961
7971
  }
7962
7972
  }
7963
- if (!node.children) {
7964
- node.children = [];
7965
- }
7966
- node.children.push(value);
7967
- return this;
7968
7973
  }
7969
- getMenuItems() {
7970
- return createActions(this.getAll());
7974
+ onMouseOver(menu, position, ev) {
7975
+ if (this.isEnabled(menu)) {
7976
+ if (this.isRoot(menu)) {
7977
+ this.openSubMenu(menu, position, ev);
7978
+ }
7979
+ else {
7980
+ this.closeSubMenu();
7981
+ }
7982
+ }
7971
7983
  }
7972
7984
  }
7973
7985
 
7974
- //------------------------------------------------------------------------------
7975
- // Link Menu Registry
7976
- //------------------------------------------------------------------------------
7977
- const linkMenuRegistry = new MenuItemRegistry();
7978
- linkMenuRegistry.add("sheet", {
7979
- ...linkSheet,
7980
- sequence: 10,
7981
- });
7982
-
7983
7986
  const MENU_OFFSET_X = 320;
7984
7987
  const MENU_OFFSET_Y = 100;
7985
7988
  const PADDING = 12;
@@ -9130,119 +9133,6 @@ function getBestTimeUnitForScale(labels, format, locale) {
9130
9133
  return "year";
9131
9134
  }
9132
9135
 
9133
- class LineChart extends AbstractChart {
9134
- dataSets;
9135
- labelRange;
9136
- background;
9137
- verticalAxisPosition;
9138
- legendPosition;
9139
- labelsAsText;
9140
- stacked;
9141
- aggregated;
9142
- type = "line";
9143
- dataSetsHaveTitle;
9144
- cumulative;
9145
- constructor(definition, sheetId, getters) {
9146
- super(definition, sheetId, getters);
9147
- this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
9148
- this.labelRange = createRange(this.getters, sheetId, definition.labelRange);
9149
- this.background = definition.background;
9150
- this.verticalAxisPosition = definition.verticalAxisPosition;
9151
- this.legendPosition = definition.legendPosition;
9152
- this.labelsAsText = definition.labelsAsText;
9153
- this.stacked = definition.stacked;
9154
- this.aggregated = definition.aggregated;
9155
- this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
9156
- this.cumulative = definition.cumulative;
9157
- }
9158
- static validateChartDefinition(validator, definition) {
9159
- return validator.checkValidations(definition, checkDataset, checkLabelRange);
9160
- }
9161
- static transformDefinition(definition, executed) {
9162
- return transformChartDefinitionWithDataSetsWithZone(definition, executed);
9163
- }
9164
- static getDefinitionFromContextCreation(context) {
9165
- return {
9166
- background: context.background,
9167
- dataSets: context.range ? context.range : [],
9168
- dataSetsHaveTitle: false,
9169
- labelsAsText: false,
9170
- legendPosition: "top",
9171
- title: context.title || "",
9172
- type: "line",
9173
- verticalAxisPosition: "left",
9174
- labelRange: context.auxiliaryRange || undefined,
9175
- stacked: false,
9176
- aggregated: false,
9177
- cumulative: false,
9178
- };
9179
- }
9180
- getDefinition() {
9181
- return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
9182
- }
9183
- getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
9184
- return {
9185
- type: "line",
9186
- dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
9187
- background: this.background,
9188
- dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
9189
- legendPosition: this.legendPosition,
9190
- verticalAxisPosition: this.verticalAxisPosition,
9191
- labelRange: labelRange
9192
- ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
9193
- : undefined,
9194
- title: this.title,
9195
- labelsAsText: this.labelsAsText,
9196
- stacked: this.stacked,
9197
- aggregated: this.aggregated,
9198
- cumulative: this.cumulative,
9199
- };
9200
- }
9201
- getContextCreation() {
9202
- return {
9203
- background: this.background,
9204
- title: this.title,
9205
- range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
9206
- auxiliaryRange: this.labelRange
9207
- ? this.getters.getRangeString(this.labelRange, this.sheetId)
9208
- : undefined,
9209
- };
9210
- }
9211
- updateRanges(applyChange) {
9212
- const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
9213
- if (!isStale) {
9214
- return this;
9215
- }
9216
- const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
9217
- return new LineChart(definition, this.sheetId, this.getters);
9218
- }
9219
- getDefinitionForExcel() {
9220
- // Excel does not support aggregating labels
9221
- if (this.aggregated)
9222
- return undefined;
9223
- const dataSets = this.dataSets
9224
- .map((ds) => toExcelDataset(this.getters, ds))
9225
- .filter((ds) => ds.range !== ""); // && range !== CellErrorType.InvalidReference ? show incorrect #ref ?
9226
- const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
9227
- return {
9228
- ...this.getDefinition(),
9229
- backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
9230
- fontColor: toXlsxHexColor(chartFontColor(this.background)),
9231
- dataSets,
9232
- labelRange,
9233
- };
9234
- }
9235
- copyForSheetId(sheetId) {
9236
- const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
9237
- const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
9238
- const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
9239
- return new LineChart(definition, sheetId, this.getters);
9240
- }
9241
- copyInSheetId(sheetId) {
9242
- const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
9243
- return new LineChart(definition, sheetId, this.getters);
9244
- }
9245
- }
9246
9136
  function fixEmptyLabelsForDateCharts(labels, dataSetsValues) {
9247
9137
  if (labels.length === 0 || labels.every((label) => !label)) {
9248
9138
  return { labels, dataSetsValues };
@@ -9301,7 +9191,23 @@ function canBeLinearChart(labelRange, getters) {
9301
9191
  }
9302
9192
  return true;
9303
9193
  }
9304
- function getLineConfiguration(chart, labels, localeFormat) {
9194
+ let missingTimeAdapterAlreadyWarned = false;
9195
+ function isLuxonTimeAdapterInstalled() {
9196
+ // @ts-ignore
9197
+ if (!window.Chart) {
9198
+ return false;
9199
+ }
9200
+ // @ts-ignore
9201
+ const adapter = new window.Chart._adapters._date({});
9202
+ // @ts-ignore
9203
+ const isInstalled = adapter._id === "luxon";
9204
+ if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
9205
+ missingTimeAdapterAlreadyWarned = true;
9206
+ console.warn("'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled.");
9207
+ }
9208
+ return isInstalled;
9209
+ }
9210
+ function getLineOrScatterConfiguration(chart, labels, localeFormat) {
9305
9211
  const fontColor = chartFontColor(chart.background);
9306
9212
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
9307
9213
  const legend = {
@@ -9352,13 +9258,13 @@ function getLineConfiguration(chart, labels, localeFormat) {
9352
9258
  },
9353
9259
  },
9354
9260
  };
9355
- if (chart.stacked && config.options?.scales?.y) {
9261
+ if ("stacked" in chart && chart.stacked && config.options?.scales?.y) {
9356
9262
  // @ts-ignore chart.js type is wrong
9357
9263
  config.options.scales.y.stacked = true;
9358
9264
  }
9359
9265
  return config;
9360
9266
  }
9361
- function createLineChartRuntime(chart, getters) {
9267
+ function createLineOrScatterChartRuntime(chart, getters) {
9362
9268
  const axisType = getChartAxisType(chart, getters);
9363
9269
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
9364
9270
  let labels = axisType === "linear" ? labelValues.values : labelValues.formattedValues;
@@ -9378,7 +9284,7 @@ function createLineChartRuntime(chart, getters) {
9378
9284
  const locale = getters.getLocale();
9379
9285
  const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
9380
9286
  const localeFormat = { format: dataSetFormat, locale };
9381
- const config = getLineConfiguration(chart, labels, localeFormat);
9287
+ const config = getLineOrScatterConfiguration(chart, labels, localeFormat);
9382
9288
  const labelFormat = getChartLabelFormat(getters, chart.labelRange);
9383
9289
  if (axisType === "time") {
9384
9290
  const axis = {
@@ -9398,6 +9304,8 @@ function createLineChartRuntime(chart, getters) {
9398
9304
  });
9399
9305
  };
9400
9306
  }
9307
+ const stacked = "stacked" in chart ? chart.stacked : false;
9308
+ const cumulative = "cumulative" in chart ? chart.cumulative : false;
9401
9309
  const colors = new ChartColors();
9402
9310
  for (let [index, { label, data }] of dataSetsValues.entries()) {
9403
9311
  if (["linear", "time"].includes(axisType)) {
@@ -9406,10 +9314,10 @@ function createLineChartRuntime(chart, getters) {
9406
9314
  }
9407
9315
  const color = colors.next();
9408
9316
  let backgroundRGBA = colorToRGBA(color);
9409
- if (chart.stacked) {
9317
+ if (stacked) {
9410
9318
  backgroundRGBA.a = LINE_FILL_TRANSPARENCY;
9411
9319
  }
9412
- if (chart.cumulative) {
9320
+ if (cumulative) {
9413
9321
  let accumulator = 0;
9414
9322
  data = data.map((value) => {
9415
9323
  if (!isNaN(value)) {
@@ -9427,27 +9335,136 @@ function createLineChartRuntime(chart, getters) {
9427
9335
  borderColor: color,
9428
9336
  backgroundColor,
9429
9337
  pointBackgroundColor: color,
9430
- fill: chart.stacked ? getFillingMode(index) : false,
9338
+ fill: stacked ? getFillingMode(index) : false,
9431
9339
  };
9432
9340
  config.data.datasets.push(dataset);
9433
9341
  }
9434
- return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
9342
+ return {
9343
+ chartJsConfig: config,
9344
+ background: chart.background || BACKGROUND_CHART_COLOR,
9345
+ dataSetsValues,
9346
+ labelValues,
9347
+ dataSetFormat,
9348
+ labelFormat,
9349
+ };
9435
9350
  }
9436
- let missingTimeAdapterAlreadyWarned = false;
9437
- function isLuxonTimeAdapterInstalled() {
9438
- // @ts-ignore
9439
- if (!window.Chart) {
9440
- return false;
9351
+
9352
+ class LineChart extends AbstractChart {
9353
+ dataSets;
9354
+ labelRange;
9355
+ background;
9356
+ verticalAxisPosition;
9357
+ legendPosition;
9358
+ labelsAsText;
9359
+ stacked;
9360
+ aggregated;
9361
+ type = "line";
9362
+ dataSetsHaveTitle;
9363
+ cumulative;
9364
+ constructor(definition, sheetId, getters) {
9365
+ super(definition, sheetId, getters);
9366
+ this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
9367
+ this.labelRange = createRange(this.getters, sheetId, definition.labelRange);
9368
+ this.background = definition.background;
9369
+ this.verticalAxisPosition = definition.verticalAxisPosition;
9370
+ this.legendPosition = definition.legendPosition;
9371
+ this.labelsAsText = definition.labelsAsText;
9372
+ this.stacked = definition.stacked;
9373
+ this.aggregated = definition.aggregated;
9374
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
9375
+ this.cumulative = definition.cumulative;
9441
9376
  }
9442
- // @ts-ignore
9443
- const adapter = new window.Chart._adapters._date({});
9444
- // @ts-ignore
9445
- const isInstalled = adapter._id === "luxon";
9446
- if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
9447
- missingTimeAdapterAlreadyWarned = true;
9448
- console.warn("'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled.");
9377
+ static validateChartDefinition(validator, definition) {
9378
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
9379
+ }
9380
+ static transformDefinition(definition, executed) {
9381
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
9382
+ }
9383
+ static getDefinitionFromContextCreation(context) {
9384
+ return {
9385
+ background: context.background,
9386
+ dataSets: context.range ? context.range : [],
9387
+ dataSetsHaveTitle: false,
9388
+ labelsAsText: false,
9389
+ legendPosition: "top",
9390
+ title: context.title || "",
9391
+ type: "line",
9392
+ verticalAxisPosition: "left",
9393
+ labelRange: context.auxiliaryRange || undefined,
9394
+ stacked: false,
9395
+ aggregated: false,
9396
+ cumulative: false,
9397
+ };
9398
+ }
9399
+ getDefinition() {
9400
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
9401
+ }
9402
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
9403
+ return {
9404
+ type: "line",
9405
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
9406
+ background: this.background,
9407
+ dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
9408
+ legendPosition: this.legendPosition,
9409
+ verticalAxisPosition: this.verticalAxisPosition,
9410
+ labelRange: labelRange
9411
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
9412
+ : undefined,
9413
+ title: this.title,
9414
+ labelsAsText: this.labelsAsText,
9415
+ stacked: this.stacked,
9416
+ aggregated: this.aggregated,
9417
+ cumulative: this.cumulative,
9418
+ };
9419
+ }
9420
+ getContextCreation() {
9421
+ return {
9422
+ background: this.background,
9423
+ title: this.title,
9424
+ range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
9425
+ auxiliaryRange: this.labelRange
9426
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
9427
+ : undefined,
9428
+ };
9429
+ }
9430
+ updateRanges(applyChange) {
9431
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
9432
+ if (!isStale) {
9433
+ return this;
9434
+ }
9435
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
9436
+ return new LineChart(definition, this.sheetId, this.getters);
9437
+ }
9438
+ getDefinitionForExcel() {
9439
+ // Excel does not support aggregating labels
9440
+ if (this.aggregated)
9441
+ return undefined;
9442
+ const dataSets = this.dataSets
9443
+ .map((ds) => toExcelDataset(this.getters, ds))
9444
+ .filter((ds) => ds.range !== ""); // && range !== CellErrorType.InvalidReference ? show incorrect #ref ?
9445
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
9446
+ return {
9447
+ ...this.getDefinition(),
9448
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
9449
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
9450
+ dataSets,
9451
+ labelRange,
9452
+ };
9453
+ }
9454
+ copyForSheetId(sheetId) {
9455
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
9456
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
9457
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
9458
+ return new LineChart(definition, sheetId, this.getters);
9459
+ }
9460
+ copyInSheetId(sheetId) {
9461
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
9462
+ return new LineChart(definition, sheetId, this.getters);
9449
9463
  }
9450
- return isInstalled;
9464
+ }
9465
+ function createLineChartRuntime(chart, getters) {
9466
+ const { chartJsConfig, background } = createLineOrScatterChartRuntime(chart, getters);
9467
+ return { chartJsConfig, background };
9451
9468
  }
9452
9469
 
9453
9470
  class PieChart extends AbstractChart {
@@ -9649,6 +9666,132 @@ function createPieChartRuntime(chart, getters) {
9649
9666
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
9650
9667
  }
9651
9668
 
9669
+ class ScatterChart extends AbstractChart {
9670
+ dataSets;
9671
+ labelRange;
9672
+ background;
9673
+ verticalAxisPosition;
9674
+ legendPosition;
9675
+ labelsAsText;
9676
+ aggregated;
9677
+ type = "scatter";
9678
+ dataSetsHaveTitle;
9679
+ constructor(definition, sheetId, getters) {
9680
+ super(definition, sheetId, getters);
9681
+ this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
9682
+ this.labelRange = createRange(this.getters, sheetId, definition.labelRange);
9683
+ this.background = definition.background;
9684
+ this.verticalAxisPosition = definition.verticalAxisPosition;
9685
+ this.legendPosition = definition.legendPosition;
9686
+ this.labelsAsText = definition.labelsAsText;
9687
+ this.aggregated = definition.aggregated;
9688
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
9689
+ }
9690
+ static validateChartDefinition(validator, definition) {
9691
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
9692
+ }
9693
+ static transformDefinition(definition, executed) {
9694
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
9695
+ }
9696
+ static getDefinitionFromContextCreation(context) {
9697
+ return {
9698
+ background: context.background,
9699
+ dataSets: context.range ? context.range : [],
9700
+ dataSetsHaveTitle: false,
9701
+ labelsAsText: false,
9702
+ legendPosition: "top",
9703
+ title: context.title || "",
9704
+ type: "scatter",
9705
+ verticalAxisPosition: "left",
9706
+ labelRange: context.auxiliaryRange || undefined,
9707
+ aggregated: false,
9708
+ };
9709
+ }
9710
+ getDefinition() {
9711
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
9712
+ }
9713
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
9714
+ return {
9715
+ type: "scatter",
9716
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
9717
+ background: this.background,
9718
+ dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
9719
+ legendPosition: this.legendPosition,
9720
+ verticalAxisPosition: this.verticalAxisPosition,
9721
+ labelRange: labelRange
9722
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
9723
+ : undefined,
9724
+ title: this.title,
9725
+ labelsAsText: this.labelsAsText,
9726
+ aggregated: this.aggregated,
9727
+ };
9728
+ }
9729
+ getContextCreation() {
9730
+ return {
9731
+ background: this.background,
9732
+ title: this.title,
9733
+ range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
9734
+ auxiliaryRange: this.labelRange
9735
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
9736
+ : undefined,
9737
+ };
9738
+ }
9739
+ updateRanges(applyChange) {
9740
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
9741
+ if (!isStale) {
9742
+ return this;
9743
+ }
9744
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
9745
+ return new ScatterChart(definition, this.sheetId, this.getters);
9746
+ }
9747
+ getDefinitionForExcel() {
9748
+ return undefined; // TODO
9749
+ }
9750
+ copyForSheetId(sheetId) {
9751
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
9752
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
9753
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
9754
+ return new ScatterChart(definition, sheetId, this.getters);
9755
+ }
9756
+ copyInSheetId(sheetId) {
9757
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
9758
+ return new ScatterChart(definition, sheetId, this.getters);
9759
+ }
9760
+ }
9761
+ function createScatterChartRuntime(chart, getters) {
9762
+ const { chartJsConfig, background, dataSetsValues, dataSetFormat, labelValues, labelFormat } = createLineOrScatterChartRuntime(chart, getters);
9763
+ // use chartJS line chart and disable the lines instead of chartJS scatter chart. This is because the scatter chart
9764
+ // have less options than the line chart (it only works with linear labels)
9765
+ chartJsConfig.type = "line";
9766
+ const configOptions = chartJsConfig.options;
9767
+ configOptions.elements = {
9768
+ point: {
9769
+ radius: 3,
9770
+ hoverRadius: 3,
9771
+ hitRadius: 8,
9772
+ },
9773
+ };
9774
+ const locale = getters.getLocale();
9775
+ configOptions.plugins.tooltip.callbacks.title = () => "";
9776
+ configOptions.plugins.tooltip.callbacks.label = (tooltipItem) => {
9777
+ const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
9778
+ let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
9779
+ if (isNumber(label, locale)) {
9780
+ label = toNumber(label, locale);
9781
+ }
9782
+ const formattedX = formatValue(label, { locale, format: labelFormat });
9783
+ const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
9784
+ const dataSetTitle = tooltipItem.dataset.label;
9785
+ return formattedX
9786
+ ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
9787
+ : `${dataSetTitle}: ${formattedY}`;
9788
+ };
9789
+ for (const dataSet of chartJsConfig.data.datasets) {
9790
+ dataSet.showLine = false;
9791
+ }
9792
+ return { chartJsConfig, background };
9793
+ }
9794
+
9652
9795
  /**
9653
9796
  * This registry is intended to map a cell content (raw string) to
9654
9797
  * an instance of a cell.
@@ -9704,11 +9847,22 @@ chartRegistry.add("gauge", {
9704
9847
  name: _t("Gauge"),
9705
9848
  sequence: 50,
9706
9849
  });
9850
+ chartRegistry.add("scatter", {
9851
+ match: (type) => type === "scatter",
9852
+ createChart: (definition, sheetId, getters) => new ScatterChart(definition, sheetId, getters),
9853
+ getChartRuntime: createScatterChartRuntime,
9854
+ validateChartDefinition: (validator, definition) => ScatterChart.validateChartDefinition(validator, definition),
9855
+ transformDefinition: (definition, executed) => ScatterChart.transformDefinition(definition, executed),
9856
+ getChartDefinitionFromContextCreation: (context) => ScatterChart.getDefinitionFromContextCreation(context),
9857
+ name: _t("Scatter"),
9858
+ sequence: 60,
9859
+ });
9707
9860
  const chartComponentRegistry = new Registry();
9708
9861
  chartComponentRegistry.add("line", ChartJsComponent);
9709
9862
  chartComponentRegistry.add("bar", ChartJsComponent);
9710
9863
  chartComponentRegistry.add("pie", ChartJsComponent);
9711
9864
  chartComponentRegistry.add("gauge", ChartJsComponent);
9865
+ chartComponentRegistry.add("scatter", ChartJsComponent);
9712
9866
  chartComponentRegistry.add("scorecard", ScorecardChart);
9713
9867
 
9714
9868
  /**
@@ -19647,9 +19801,6 @@ const categories = [
19647
19801
  { name: _t("Web"), functions: web },
19648
19802
  ];
19649
19803
  const functionNameRegex = /^[A-Z0-9\_\.]+$/;
19650
- //------------------------------------------------------------------------------
19651
- // Function registry
19652
- //------------------------------------------------------------------------------
19653
19804
  class FunctionRegistry extends Registry {
19654
19805
  mapping = {};
19655
19806
  add(name, addDescr) {
@@ -19935,6 +20086,58 @@ const insertLink = {
19935
20086
  execute: INSERT_LINK,
19936
20087
  icon: "o-spreadsheet-Icon.INSERT_LINK",
19937
20088
  };
20089
+ const insertCheckbox = {
20090
+ name: _t("Checkbox"),
20091
+ execute: (env) => {
20092
+ const zones = env.model.getters.getSelectedZones();
20093
+ const sheetId = env.model.getters.getActiveSheetId();
20094
+ const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
20095
+ env.model.dispatch("ADD_DATA_VALIDATION_RULE", {
20096
+ ranges,
20097
+ sheetId,
20098
+ rule: {
20099
+ id: env.model.uuidGenerator.uuidv4(),
20100
+ criterion: {
20101
+ type: "isBoolean",
20102
+ values: [],
20103
+ },
20104
+ },
20105
+ });
20106
+ },
20107
+ icon: "o-spreadsheet-Icon.INSERT_CHECKBOX",
20108
+ };
20109
+ const insertDropdown = {
20110
+ name: _t("Dropdown list"),
20111
+ execute: (env) => {
20112
+ const zones = env.model.getters.getSelectedZones();
20113
+ const sheetId = env.model.getters.getActiveSheetId();
20114
+ const ranges = zones.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
20115
+ const ruleID = env.model.uuidGenerator.uuidv4();
20116
+ env.model.dispatch("ADD_DATA_VALIDATION_RULE", {
20117
+ ranges,
20118
+ sheetId,
20119
+ rule: {
20120
+ id: ruleID,
20121
+ criterion: {
20122
+ type: "isValueInList",
20123
+ values: [],
20124
+ displayStyle: "arrow",
20125
+ },
20126
+ },
20127
+ });
20128
+ const rule = env.model.getters.getDataValidationRule(sheetId, ruleID);
20129
+ if (!rule) {
20130
+ return;
20131
+ }
20132
+ env.openSidePanel("DataValidationEditor", {
20133
+ rule: localizeDataValidationRule(rule, env.model.getters.getLocale()),
20134
+ onExit: () => {
20135
+ env.openSidePanel("DataValidation");
20136
+ },
20137
+ });
20138
+ },
20139
+ icon: "o-spreadsheet-Icon.INSERT_DROPDOWN",
20140
+ };
19938
20141
  const insertSheet = {
19939
20142
  name: _t("Insert sheet"),
19940
20143
  execute: (env) => {
@@ -21337,7 +21540,7 @@ topbarMenuRegistry
21337
21540
  })
21338
21541
  .addChild("insert_cell", ["insert"], {
21339
21542
  ...insertCell,
21340
- sequence: 43,
21543
+ sequence: 30,
21341
21544
  })
21342
21545
  .addChild("insert_cell_down", ["insert", "insert_cell"], {
21343
21546
  ...insertCellShiftDown,
@@ -21351,7 +21554,7 @@ topbarMenuRegistry
21351
21554
  })
21352
21555
  .addChild("insert_sheet", ["insert"], {
21353
21556
  ...insertSheet,
21354
- sequence: 80,
21557
+ sequence: 40,
21355
21558
  separator: true,
21356
21559
  })
21357
21560
  .addChild("insert_chart", ["insert"], {
@@ -21396,6 +21599,15 @@ topbarMenuRegistry
21396
21599
  ...insertLink,
21397
21600
  separator: true,
21398
21601
  sequence: 70,
21602
+ })
21603
+ .addChild("insert_checkbox", ["insert"], {
21604
+ ...insertCheckbox,
21605
+ sequence: 80,
21606
+ })
21607
+ .addChild("insert_dropdown", ["insert"], {
21608
+ ...insertDropdown,
21609
+ separator: true,
21610
+ sequence: 90,
21399
21611
  })
21400
21612
  // ---------------------------------------------------------------------
21401
21613
  // FORMAT MENU ITEMS
@@ -22604,7 +22816,7 @@ class ChartColor extends Component {
22604
22816
 
22605
22817
  class ChartTitle extends Component {
22606
22818
  static template = "o-spreadsheet.ChartTitle";
22607
- static components = { ColorPickerWidget, Section };
22819
+ static components = { Section };
22608
22820
  static props = { title: String, update: Function };
22609
22821
  updateTitle(ev) {
22610
22822
  this.props.update(ev.target.value);
@@ -22613,7 +22825,7 @@ class ChartTitle extends Component {
22613
22825
 
22614
22826
  class LineBarPieDesignPanel extends Component {
22615
22827
  static template = "o-spreadsheet-LineBarPieDesignPanel";
22616
- static components = { ChartColor, ColorPickerWidget, ChartTitle, Section };
22828
+ static components = { ChartColor, ChartTitle, Section };
22617
22829
  static props = {
22618
22830
  figureId: String,
22619
22831
  definition: Object,
@@ -22644,7 +22856,7 @@ class BarChartDesignPanel extends LineBarPieDesignPanel {
22644
22856
 
22645
22857
  class GaugeChartConfigPanel extends Component {
22646
22858
  static template = "o-spreadsheet-GaugeChartConfigPanel";
22647
- static components = { SelectionInput, ChartErrorSection, ChartDataSeries };
22859
+ static components = { ChartErrorSection, ChartDataSeries };
22648
22860
  static props = {
22649
22861
  figureId: String,
22650
22862
  definition: Object,
@@ -22843,9 +23055,37 @@ class LineChartDesignPanel extends LineBarPieDesignPanel {
22843
23055
  static template = "o-spreadsheet-LineChartDesignPanel";
22844
23056
  }
22845
23057
 
23058
+ class ScatterConfigPanel extends LineBarPieConfigPanel {
23059
+ static template = "o-spreadsheet-ScatterConfigPanel";
23060
+ get canTreatLabelsAsText() {
23061
+ const chart = this.env.model.getters.getChart(this.props.figureId);
23062
+ if (chart && chart instanceof ScatterChart) {
23063
+ return canChartParseLabels(chart.labelRange, this.env.model.getters);
23064
+ }
23065
+ return false;
23066
+ }
23067
+ onUpdateLabelsAsText(labelsAsText) {
23068
+ this.props.updateChart(this.props.figureId, {
23069
+ labelsAsText,
23070
+ });
23071
+ }
23072
+ getLabelRangeOptions() {
23073
+ const options = super.getLabelRangeOptions();
23074
+ if (this.canTreatLabelsAsText) {
23075
+ options.push({
23076
+ name: "labelsAsText",
23077
+ value: this.props.definition.labelsAsText,
23078
+ label: _t("Treat labels as text"),
23079
+ onChange: this.onUpdateLabelsAsText.bind(this),
23080
+ });
23081
+ }
23082
+ return options;
23083
+ }
23084
+ }
23085
+
22846
23086
  class ScorecardChartConfigPanel extends Component {
22847
23087
  static template = "o-spreadsheet-ScorecardChartConfigPanel";
22848
- static components = { SelectionInput, ValidationMessages, ChartErrorSection, Section };
23088
+ static components = { SelectionInput, ChartErrorSection, Section };
22849
23089
  static props = {
22850
23090
  figureId: String,
22851
23091
  definition: Object,
@@ -22963,6 +23203,10 @@ chartSidePanelComponentRegistry
22963
23203
  .add("line", {
22964
23204
  configuration: LineConfigPanel,
22965
23205
  design: LineChartDesignPanel,
23206
+ })
23207
+ .add("scatter", {
23208
+ configuration: ScatterConfigPanel,
23209
+ design: LineChartDesignPanel,
22966
23210
  })
22967
23211
  .add("bar", {
22968
23212
  configuration: BarConfigPanel,
@@ -24296,377 +24540,6 @@ class CustomCurrencyPanel extends Component {
24296
24540
  }
24297
24541
  }
24298
24542
 
24299
- css /* scss */ `
24300
- .o-find-and-replace {
24301
- outline: none;
24302
- height: 100%;
24303
- .o-input-search-container {
24304
- display: flex;
24305
- .o-input-with-count {
24306
- flex-grow: 1;
24307
- width: auto;
24308
- }
24309
- .o-input-without-count {
24310
- width: 100%;
24311
- }
24312
- .o-input-count {
24313
- width: fit-content;
24314
- padding: 4px 0 4px 4px;
24315
- }
24316
- }
24317
-
24318
- .o-matches-count div {
24319
- text-overflow: ellipsis;
24320
- overflow: hidden;
24321
- white-space: nowrap;
24322
- }
24323
- }
24324
- `;
24325
- class FindAndReplacePanel extends Component {
24326
- static template = "o-spreadsheet-FindAndReplacePanel";
24327
- static components = { SelectionInput, Section, Checkbox };
24328
- static props = {
24329
- onCloseSidePanel: Function,
24330
- };
24331
- debounceTimeoutId;
24332
- initialShowFormulaState = false;
24333
- dataRange = "";
24334
- searchInput = useRef("searchInput");
24335
- replaceInput = useRef("replaceInput");
24336
- get hasSearchResult() {
24337
- return this.env.model.getters.getCurrentSelectedMatchIndex() !== null;
24338
- }
24339
- get pendingSearch() {
24340
- return this.debounceTimeoutId !== undefined;
24341
- }
24342
- get searchOptions() {
24343
- return this.env.model.getters.getSearchOptions();
24344
- }
24345
- get toSearch() {
24346
- return this.searchInput.el?.value || "";
24347
- }
24348
- get toReplace() {
24349
- return this.replaceInput.el?.value || "";
24350
- }
24351
- get allSheetsMatchesCount() {
24352
- return _t("%s in all sheets", this.env.model.getters.getAllSheetMatchesCount());
24353
- }
24354
- get currentSheetMatchesCount() {
24355
- return _t("%(matches)s in sheet %(sheetName)s", {
24356
- matches: this.env.model.getters.getActiveSheetMatchesCount(),
24357
- sheetName: this.env.model.getters.getSheetName(this.env.model.getters.getActiveSheetId()),
24358
- });
24359
- }
24360
- get specificRangeMatchesCount() {
24361
- const range = this.searchOptions.specificRange;
24362
- if (!range) {
24363
- return "";
24364
- }
24365
- const { _sheetId, _zone } = range;
24366
- return _t("%(matches)s in range %(range)s of sheet %(sheetName)s", {
24367
- matches: this.env.model.getters.getSpecificRangeMatchesCount().toString(),
24368
- range: zoneToXc(_zone),
24369
- sheetName: this.env.model.getters.getSheetName(_sheetId),
24370
- });
24371
- }
24372
- setup() {
24373
- this.initialShowFormulaState = this.env.model.getters.shouldShowFormulas();
24374
- onMounted(() => this.searchInput.el?.focus());
24375
- onWillUnmount(() => {
24376
- clearTimeout(this.debounceTimeoutId);
24377
- this.env.model.dispatch("CLEAR_SEARCH");
24378
- this.env.model.dispatch("SET_FORMULA_VISIBILITY", { show: this.initialShowFormulaState });
24379
- });
24380
- useEffect(() => {
24381
- const showFormula = this.env.model.getters.shouldShowFormulas();
24382
- this.updateSearch({ searchFormulas: showFormula });
24383
- }, () => [this.env.model.getters.shouldShowFormulas()]);
24384
- }
24385
- onFocusSearch() {
24386
- this.updateDataRange();
24387
- }
24388
- onInput() {
24389
- this.debouncedUpdateSearch();
24390
- }
24391
- onKeydownSearch(ev) {
24392
- if (ev.key === "Enter") {
24393
- ev.preventDefault();
24394
- ev.stopPropagation();
24395
- this.onSelectNextCell();
24396
- }
24397
- }
24398
- onKeydownReplace(ev) {
24399
- if (ev.key === "Enter") {
24400
- ev.preventDefault();
24401
- ev.stopPropagation();
24402
- this.replace();
24403
- }
24404
- }
24405
- searchFormulas(showFormula) {
24406
- this.env.model.dispatch("SET_FORMULA_VISIBILITY", {
24407
- show: showFormula,
24408
- });
24409
- this.updateSearch({ searchFormulas: showFormula });
24410
- }
24411
- searchExactMatch(exactMatch) {
24412
- this.updateSearch({ exactMatch });
24413
- }
24414
- searchMatchCase(matchCase) {
24415
- this.updateSearch({ matchCase });
24416
- }
24417
- changeSearchScope(ev) {
24418
- const searchScope = ev.target.value;
24419
- this.updateSearch({ searchScope });
24420
- }
24421
- onSearchRangeChanged(ranges) {
24422
- this.dataRange = ranges[0];
24423
- }
24424
- updateDataRange() {
24425
- if (!this.dataRange) {
24426
- return;
24427
- }
24428
- if (this.searchOptions.searchScope === "specificRange") {
24429
- const specificRange = this.env.model.getters.getRangeFromSheetXC(this.env.model.getters.getActiveSheetId(), this.dataRange).rangeData;
24430
- this.updateSearch({ specificRange });
24431
- }
24432
- }
24433
- onSelectPreviousCell() {
24434
- this.env.model.dispatch("SELECT_SEARCH_PREVIOUS_MATCH");
24435
- }
24436
- onSelectNextCell() {
24437
- this.env.model.dispatch("SELECT_SEARCH_NEXT_MATCH");
24438
- }
24439
- updateSearch(updateSearchOptions) {
24440
- const searchOptions = {
24441
- ...this.env.model.getters.getSearchOptions(),
24442
- ...updateSearchOptions,
24443
- };
24444
- this.env.model.dispatch("UPDATE_SEARCH", {
24445
- toSearch: this.toSearch,
24446
- searchOptions,
24447
- });
24448
- }
24449
- debouncedUpdateSearch() {
24450
- clearTimeout(this.debounceTimeoutId);
24451
- this.debounceTimeoutId = setTimeout(() => {
24452
- this.updateSearch();
24453
- this.debounceTimeoutId = undefined;
24454
- }, 200);
24455
- }
24456
- replace() {
24457
- this.env.model.dispatch("REPLACE_SEARCH", {
24458
- replaceWith: this.toReplace,
24459
- });
24460
- }
24461
- replaceAll() {
24462
- this.env.model.dispatch("REPLACE_ALL_SEARCH", {
24463
- replaceWith: this.toReplace,
24464
- });
24465
- }
24466
- }
24467
-
24468
- css /* scss */ `
24469
- .o-more-formats-panel {
24470
- .format-preview {
24471
- height: 48px;
24472
- background-color: white;
24473
-
24474
- &:hover {
24475
- background-color: rgba(0, 0, 0, 0.08);
24476
- }
24477
- }
24478
- .check-icon {
24479
- width: 24px;
24480
- }
24481
- }
24482
- `;
24483
- const DATE_FORMAT_ACTIONS = createActions([
24484
- formatNumberFullDateTime,
24485
- formatNumberFullWeekDayAndMonth,
24486
- formatNumberDayAndFullMonth,
24487
- formatNumberShortWeekDay,
24488
- formatNumberDayAndShortMonth,
24489
- formatNumberFullMonth,
24490
- formatNumberShortMonth,
24491
- formatNumberDate,
24492
- formatNumberTime,
24493
- formatNumberDateTime,
24494
- formatNumberDuration,
24495
- ]);
24496
- class MoreFormatsPanel extends Component {
24497
- static template = "o-spreadsheet-MoreFormatsPanel";
24498
- static props = {
24499
- onCloseSidePanel: Function,
24500
- };
24501
- get dateFormatsActions() {
24502
- return DATE_FORMAT_ACTIONS;
24503
- }
24504
- }
24505
-
24506
- css /* scss */ `
24507
- .o-checkbox-selection {
24508
- height: 150px;
24509
- }
24510
- `;
24511
- class RemoveDuplicatesPanel extends Component {
24512
- static template = "o-spreadsheet-RemoveDuplicatesPanel";
24513
- static components = { ValidationMessages, Section, Checkbox };
24514
- state = useState({
24515
- hasHeader: false,
24516
- columns: {},
24517
- });
24518
- setup() {
24519
- onWillUpdateProps(() => this.updateColumns());
24520
- }
24521
- toggleHasHeader() {
24522
- this.state.hasHeader = !this.state.hasHeader;
24523
- }
24524
- toggleAllColumns() {
24525
- const newState = !this.isEveryColumnSelected;
24526
- for (const index in this.state.columns) {
24527
- this.state.columns[index] = newState;
24528
- }
24529
- }
24530
- toggleColumn(colIndex) {
24531
- this.state.columns[colIndex] = !this.state.columns[colIndex];
24532
- }
24533
- onRemoveDuplicates() {
24534
- this.env.model.dispatch("REMOVE_DUPLICATES", {
24535
- hasHeader: this.state.hasHeader,
24536
- columns: this.getColsToAnalyze(),
24537
- });
24538
- }
24539
- getColLabel(colKey) {
24540
- const col = parseInt(colKey);
24541
- let colLabel = _t("Column %s", numberToLetters(col));
24542
- if (this.state.hasHeader) {
24543
- const sheetId = this.env.model.getters.getActiveSheetId();
24544
- const row = this.env.model.getters.getSelectedZone().top;
24545
- const colHeader = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
24546
- if (colHeader.type !== "empty") {
24547
- colLabel += ` - ${colHeader.value}`;
24548
- }
24549
- }
24550
- return colLabel;
24551
- }
24552
- get isEveryColumnSelected() {
24553
- return Object.values(this.state.columns).every((value) => value === true);
24554
- }
24555
- get errorMessages() {
24556
- const cancelledReasons = this.env.model.canDispatch("REMOVE_DUPLICATES", {
24557
- hasHeader: this.state.hasHeader,
24558
- columns: this.getColsToAnalyze(),
24559
- }).reasons;
24560
- const errors = new Set();
24561
- for (const reason of cancelledReasons) {
24562
- errors.add(RemoveDuplicateTerms.Errors[reason] || RemoveDuplicateTerms.Errors.Unexpected);
24563
- }
24564
- return Array.from(errors);
24565
- }
24566
- get selectionStatisticalInformation() {
24567
- const dimension = zoneToDimension(this.env.model.getters.getSelectedZone());
24568
- return _t("%(row_count)s rows and %(column_count)s columns selected", {
24569
- row_count: dimension.numberOfRows,
24570
- column_count: dimension.numberOfCols,
24571
- });
24572
- }
24573
- get canConfirm() {
24574
- return this.errorMessages.length === 0;
24575
- }
24576
- // ---------------------------------------------------------------------------
24577
- // Private
24578
- // ---------------------------------------------------------------------------
24579
- updateColumns() {
24580
- const zone = this.env.model.getters.getSelectedZone();
24581
- const oldColumns = this.state.columns;
24582
- const newColumns = {};
24583
- for (let i = zone.left; i <= zone.right; i++) {
24584
- newColumns[i] = i in oldColumns ? oldColumns[i] : true;
24585
- }
24586
- this.state.columns = newColumns;
24587
- }
24588
- getColsToAnalyze() {
24589
- return Object.keys(this.state.columns)
24590
- .filter((colIndex) => this.state.columns[colIndex])
24591
- .map((colIndex) => parseInt(colIndex));
24592
- }
24593
- }
24594
-
24595
- css /* scss */ `
24596
- .o-locale-preview {
24597
- color: dimgrey;
24598
- }
24599
- `;
24600
- class SettingsPanel extends Component {
24601
- static template = "o-spreadsheet-SettingsPanel";
24602
- static components = { Section };
24603
- static props = { onCloseSidePanel: Function };
24604
- loadedLocales = [];
24605
- setup() {
24606
- onWillStart(() => this.loadLocales());
24607
- }
24608
- onLocaleChange(code) {
24609
- const locale = this.loadedLocales.find((l) => l.code === code);
24610
- if (!locale)
24611
- return;
24612
- this.env.model.dispatch("UPDATE_LOCALE", { locale });
24613
- }
24614
- async loadLocales() {
24615
- this.loadedLocales = (await this.env.loadLocales())
24616
- .filter(isValidLocale)
24617
- .sort((a, b) => a.name.localeCompare(b.name));
24618
- }
24619
- get numberFormatPreview() {
24620
- const locale = this.env.model.getters.getLocale();
24621
- return formatValue(1234567.89, { format: "#,##0.00", locale });
24622
- }
24623
- get dateFormatPreview() {
24624
- const locale = this.env.model.getters.getLocale();
24625
- return formatValue(1.6, { format: locale.dateFormat, locale });
24626
- }
24627
- get dateTimeFormatPreview() {
24628
- const locale = this.env.model.getters.getLocale();
24629
- const dateTimeFormat = getDateTimeFormat(locale);
24630
- return formatValue(1.6, { format: dateTimeFormat, locale });
24631
- }
24632
- get currentLocale() {
24633
- return this.env.model.getters.getLocale();
24634
- }
24635
- get supportedLocales() {
24636
- const currentLocale = this.currentLocale;
24637
- const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);
24638
- if (!localeInLoadedLocales) {
24639
- const locales = [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
24640
- return locales;
24641
- }
24642
- else if (!deepEquals(currentLocale, localeInLoadedLocales)) {
24643
- const index = this.loadedLocales.indexOf(localeInLoadedLocales);
24644
- const locales = [...this.loadedLocales];
24645
- locales[index] = currentLocale;
24646
- locales.sort((a, b) => a.name.localeCompare(b.name));
24647
- return locales;
24648
- }
24649
- return this.loadedLocales;
24650
- }
24651
- }
24652
-
24653
- const SplitToColumnsInteractiveContent = {
24654
- SplitIsDestructive: _t("This will overwrite data in the subsequent columns. Split anyway?"),
24655
- };
24656
- function interactiveSplitToColumns(env, separator, addNewColumns) {
24657
- let result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", { separator, addNewColumns });
24658
- if (result.isCancelledBecause("SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */)) {
24659
- env.askConfirmation(SplitToColumnsInteractiveContent.SplitIsDestructive, () => {
24660
- result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", {
24661
- separator,
24662
- addNewColumns,
24663
- force: true,
24664
- });
24665
- });
24666
- }
24667
- return result;
24668
- }
24669
-
24670
24543
  const dataValidationEvaluatorRegistry = new Registry();
24671
24544
  dataValidationEvaluatorRegistry.add("textContains", {
24672
24545
  type: "textContains",
@@ -25210,111 +25083,6 @@ function getDateCriterionFormattedValues(criterion, getters) {
25210
25083
  });
25211
25084
  }
25212
25085
 
25213
- function interactiveStopEdition(env) {
25214
- const result = env.model.dispatch("STOP_EDITION");
25215
- if (result.isCancelledBecause("BlockingValidationRule" /* CommandResult.BlockingValidationRule */)) {
25216
- const editedCell = env.model.getters.getCurrentEditedCell();
25217
- const cellXc = toXC(editedCell.col, editedCell.row);
25218
- const rule = env.model.getters.getValidationRuleForCell(editedCell);
25219
- if (!rule) {
25220
- return;
25221
- }
25222
- const evaluator = dataValidationEvaluatorRegistry.get(rule.criterion.type);
25223
- const errorStr = evaluator.getErrorString(rule.criterion, env.model.getters, editedCell.sheetId);
25224
- env.raiseError(_t("The data you entered in %s violates the data validation rule set on the cell:\n%s", cellXc, errorStr));
25225
- env.model.dispatch("CANCEL_EDITION");
25226
- }
25227
- }
25228
-
25229
- const SEPARATORS = [
25230
- { name: _t("Detect automatically"), value: "auto" },
25231
- { name: _t("Custom separator"), value: "custom" },
25232
- { name: _t("Space"), value: " " },
25233
- { name: _t("Comma"), value: "," },
25234
- { name: _t("Semicolon"), value: ";" },
25235
- { name: _t("Line Break"), value: NEWLINE },
25236
- ];
25237
- class SplitIntoColumnsPanel extends Component {
25238
- static template = "o-spreadsheet-SplitIntoColumnsPanel";
25239
- static components = { ValidationMessages, Section, Checkbox };
25240
- static props = { onCloseSidePanel: Function };
25241
- state = useState({ separatorValue: "auto", addNewColumns: false, customSeparator: "" });
25242
- setup() {
25243
- onWillUpdateProps(() => {
25244
- // The feature makes no sense if we are editing a cell, because then the selection isn't active
25245
- // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
25246
- if (this.env.model.getters.getEditionMode() !== "inactive") {
25247
- this.props.onCloseSidePanel();
25248
- }
25249
- });
25250
- onMounted(() => {
25251
- interactiveStopEdition(this.env);
25252
- });
25253
- }
25254
- onSeparatorChange(value) {
25255
- this.state.separatorValue = value;
25256
- }
25257
- updateCustomSeparator(ev) {
25258
- if (!ev.target)
25259
- return;
25260
- this.state.customSeparator = ev.target.value;
25261
- }
25262
- updateAddNewColumnsCheckbox(addNewColumns) {
25263
- this.state.addNewColumns = addNewColumns;
25264
- }
25265
- confirm() {
25266
- const result = interactiveSplitToColumns(this.env, this.separatorValue, this.state.addNewColumns);
25267
- if (result.isSuccessful) {
25268
- this.props.onCloseSidePanel();
25269
- }
25270
- }
25271
- get errorMessages() {
25272
- const cancelledReasons = this.env.model.canDispatch("SPLIT_TEXT_INTO_COLUMNS", {
25273
- separator: this.separatorValue,
25274
- addNewColumns: this.state.addNewColumns,
25275
- force: true,
25276
- }).reasons;
25277
- const errors = new Set();
25278
- for (const reason of cancelledReasons) {
25279
- switch (reason) {
25280
- case "SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */:
25281
- case "EmptySplitSeparator" /* CommandResult.EmptySplitSeparator */:
25282
- break;
25283
- default:
25284
- errors.add(SplitToColumnsTerms.Errors[reason] || SplitToColumnsTerms.Errors.Unexpected);
25285
- }
25286
- }
25287
- return Array.from(errors);
25288
- }
25289
- get warningMessages() {
25290
- const warnings = [];
25291
- const cancelledReasons = this.env.model.canDispatch("SPLIT_TEXT_INTO_COLUMNS", {
25292
- separator: this.separatorValue,
25293
- addNewColumns: this.state.addNewColumns,
25294
- force: false,
25295
- }).reasons;
25296
- if (cancelledReasons.includes("SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */)) {
25297
- warnings.push(SplitToColumnsTerms.Errors["SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */]);
25298
- }
25299
- return warnings;
25300
- }
25301
- get separatorValue() {
25302
- if (this.state.separatorValue === "custom") {
25303
- return this.state.customSeparator;
25304
- }
25305
- else if (this.state.separatorValue === "auto") {
25306
- return this.env.model.getters.getAutomaticSeparator();
25307
- }
25308
- return this.state.separatorValue;
25309
- }
25310
- get separators() {
25311
- return SEPARATORS;
25312
- }
25313
- get isConfirmDisabled() {
25314
- return !this.separatorValue || this.errorMessages.length > 0;
25315
- }
25316
- }
25317
-
25318
25086
  /** This component looks like a select input, but on click it opens a Menu with the items given as props instead of a dropdown */
25319
25087
  class SelectMenu extends Component {
25320
25088
  static template = "o-spreadsheet-SelectMenu";
@@ -25343,6 +25111,22 @@ class SelectMenu extends Component {
25343
25111
  }
25344
25112
  }
25345
25113
 
25114
+ function interactiveStopEdition(env) {
25115
+ const result = env.model.dispatch("STOP_EDITION");
25116
+ if (result.isCancelledBecause("BlockingValidationRule" /* CommandResult.BlockingValidationRule */)) {
25117
+ const editedCell = env.model.getters.getCurrentEditedCell();
25118
+ const cellXc = toXC(editedCell.col, editedCell.row);
25119
+ const rule = env.model.getters.getValidationRuleForCell(editedCell);
25120
+ if (!rule) {
25121
+ return;
25122
+ }
25123
+ const evaluator = dataValidationEvaluatorRegistry.get(rule.criterion.type);
25124
+ const errorStr = evaluator.getErrorString(rule.criterion, env.model.getters, editedCell.sheetId);
25125
+ env.raiseError(_t("The data you entered in %s violates the data validation rule set on the cell:\n%s", cellXc, errorStr));
25126
+ env.model.dispatch("CANCEL_EDITION");
25127
+ }
25128
+ }
25129
+
25346
25130
  class DataValidationCriterionForm extends Component {
25347
25131
  static props = {
25348
25132
  criterion: Object,
@@ -25749,92 +25533,552 @@ function getDataValidationCriterionMenuItems(callback) {
25749
25533
  }
25750
25534
 
25751
25535
  css /* scss */ `
25752
- .o-sidePanel .o-sidePanelBody .o-dv-form {
25753
- .o-section {
25754
- padding: 16px 16px 0 16px;
25536
+ .o-sidePanel .o-sidePanelBody .o-dv-form {
25537
+ .o-section {
25538
+ padding: 16px 16px 0 16px;
25539
+ }
25540
+ }
25541
+ `;
25542
+ class DataValidationEditor extends Component {
25543
+ static template = "o-spreadsheet-DataValidationEditor";
25544
+ static components = { SelectionInput, SelectMenu, Section };
25545
+ static props = {
25546
+ rule: { type: Object, optional: true },
25547
+ onExit: Function,
25548
+ };
25549
+ state = useState({ rule: this.defaultDataValidationRule });
25550
+ setup() {
25551
+ if (this.props.rule) {
25552
+ const sheetId = this.env.model.getters.getActiveSheetId();
25553
+ this.state.rule = {
25554
+ ...this.props.rule,
25555
+ ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, sheetId)),
25556
+ };
25557
+ this.state.rule.criterion.type = this.props.rule.criterion.type;
25558
+ }
25559
+ }
25560
+ onCriterionTypeChanged(type) {
25561
+ this.state.rule.criterion.type = type;
25562
+ }
25563
+ onRangesChanged(ranges) {
25564
+ this.state.rule.ranges = ranges;
25565
+ }
25566
+ onCriterionChanged(criterion) {
25567
+ this.state.rule.criterion = criterion;
25568
+ }
25569
+ changeRuleIsBlocking(ev) {
25570
+ const isBlocking = ev.target.value;
25571
+ this.state.rule.isBlocking = isBlocking === "true";
25572
+ }
25573
+ onSave() {
25574
+ if (!this.canSave) {
25575
+ return;
25576
+ }
25577
+ this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
25578
+ this.props.onExit();
25579
+ }
25580
+ get canSave() {
25581
+ return this.env.model.canDispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload)
25582
+ .isSuccessful;
25583
+ }
25584
+ get dispatchPayload() {
25585
+ const rule = { ...this.state.rule, ranges: undefined };
25586
+ const locale = this.env.model.getters.getLocale();
25587
+ const criterion = rule.criterion;
25588
+ const criterionEvaluator = dataValidationEvaluatorRegistry.get(criterion.type);
25589
+ const sheetId = this.env.model.getters.getActiveSheetId();
25590
+ const values = criterion.values
25591
+ .slice(0, criterionEvaluator.numberOfValues(criterion))
25592
+ .map((value) => value?.trim())
25593
+ .filter((value) => value !== "" && value !== undefined)
25594
+ .map((value) => canonicalizeContent(value, locale));
25595
+ rule.criterion = { ...criterion, values };
25596
+ return {
25597
+ sheetId,
25598
+ ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
25599
+ rule,
25600
+ };
25601
+ }
25602
+ get dvCriterionMenuItems() {
25603
+ return getDataValidationCriterionMenuItems((type) => this.onCriterionTypeChanged(type));
25604
+ }
25605
+ get selectedCriterionName() {
25606
+ const selectedType = this.state.rule.criterion.type;
25607
+ return dataValidationEvaluatorRegistry.get(selectedType).name;
25608
+ }
25609
+ get defaultDataValidationRule() {
25610
+ const sheetId = this.env.model.getters.getActiveSheetId();
25611
+ const ranges = this.env.model.getters
25612
+ .getSelectedZones()
25613
+ .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));
25614
+ return {
25615
+ id: this.env.model.uuidGenerator.uuidv4(),
25616
+ criterion: { type: "textContains", values: [""] },
25617
+ ranges,
25618
+ };
25619
+ }
25620
+ get criterionComponent() {
25621
+ return dataValidationPanelCriteriaRegistry.get(this.state.rule.criterion.type).component;
25622
+ }
25623
+ }
25624
+
25625
+ css /* scss */ `
25626
+ .o-find-and-replace {
25627
+ outline: none;
25628
+ height: 100%;
25629
+ .o-input-search-container {
25630
+ display: flex;
25631
+ .o-input-with-count {
25632
+ flex-grow: 1;
25633
+ width: auto;
25634
+ }
25635
+ .o-input-without-count {
25636
+ width: 100%;
25637
+ }
25638
+ .o-input-count {
25639
+ width: fit-content;
25640
+ padding: 4px 0 4px 4px;
25641
+ }
25642
+ }
25643
+
25644
+ .o-matches-count div {
25645
+ text-overflow: ellipsis;
25646
+ overflow: hidden;
25647
+ white-space: nowrap;
25648
+ }
25649
+ }
25650
+ `;
25651
+ class FindAndReplacePanel extends Component {
25652
+ static template = "o-spreadsheet-FindAndReplacePanel";
25653
+ static components = { SelectionInput, Section, Checkbox };
25654
+ static props = {
25655
+ onCloseSidePanel: Function,
25656
+ };
25657
+ debounceTimeoutId;
25658
+ initialShowFormulaState = false;
25659
+ dataRange = "";
25660
+ searchInput = useRef("searchInput");
25661
+ replaceInput = useRef("replaceInput");
25662
+ get hasSearchResult() {
25663
+ return this.env.model.getters.getCurrentSelectedMatchIndex() !== null;
25664
+ }
25665
+ get pendingSearch() {
25666
+ return this.debounceTimeoutId !== undefined;
25667
+ }
25668
+ get searchOptions() {
25669
+ return this.env.model.getters.getSearchOptions();
25670
+ }
25671
+ get toSearch() {
25672
+ return this.searchInput.el?.value || "";
25673
+ }
25674
+ get toReplace() {
25675
+ return this.replaceInput.el?.value || "";
25676
+ }
25677
+ get allSheetsMatchesCount() {
25678
+ return _t("%s in all sheets", this.env.model.getters.getAllSheetMatchesCount());
25679
+ }
25680
+ get currentSheetMatchesCount() {
25681
+ return _t("%(matches)s in sheet %(sheetName)s", {
25682
+ matches: this.env.model.getters.getActiveSheetMatchesCount(),
25683
+ sheetName: this.env.model.getters.getSheetName(this.env.model.getters.getActiveSheetId()),
25684
+ });
25685
+ }
25686
+ get specificRangeMatchesCount() {
25687
+ const range = this.searchOptions.specificRange;
25688
+ if (!range) {
25689
+ return "";
25690
+ }
25691
+ const { _sheetId, _zone } = range;
25692
+ return _t("%(matches)s in range %(range)s of sheet %(sheetName)s", {
25693
+ matches: this.env.model.getters.getSpecificRangeMatchesCount().toString(),
25694
+ range: zoneToXc(_zone),
25695
+ sheetName: this.env.model.getters.getSheetName(_sheetId),
25696
+ });
25697
+ }
25698
+ setup() {
25699
+ this.initialShowFormulaState = this.env.model.getters.shouldShowFormulas();
25700
+ onMounted(() => this.searchInput.el?.focus());
25701
+ onWillUnmount(() => {
25702
+ clearTimeout(this.debounceTimeoutId);
25703
+ this.env.model.dispatch("CLEAR_SEARCH");
25704
+ this.env.model.dispatch("SET_FORMULA_VISIBILITY", { show: this.initialShowFormulaState });
25705
+ });
25706
+ useEffect(() => {
25707
+ const showFormula = this.env.model.getters.shouldShowFormulas();
25708
+ this.updateSearch({ searchFormulas: showFormula });
25709
+ }, () => [this.env.model.getters.shouldShowFormulas()]);
25710
+ }
25711
+ onFocusSearch() {
25712
+ this.updateDataRange();
25713
+ }
25714
+ onInput() {
25715
+ this.debouncedUpdateSearch();
25716
+ }
25717
+ onKeydownSearch(ev) {
25718
+ if (ev.key === "Enter") {
25719
+ ev.preventDefault();
25720
+ ev.stopPropagation();
25721
+ this.onSelectNextCell();
25722
+ }
25723
+ }
25724
+ onKeydownReplace(ev) {
25725
+ if (ev.key === "Enter") {
25726
+ ev.preventDefault();
25727
+ ev.stopPropagation();
25728
+ this.replace();
25729
+ }
25730
+ }
25731
+ searchFormulas(showFormula) {
25732
+ this.env.model.dispatch("SET_FORMULA_VISIBILITY", {
25733
+ show: showFormula,
25734
+ });
25735
+ this.updateSearch({ searchFormulas: showFormula });
25736
+ }
25737
+ searchExactMatch(exactMatch) {
25738
+ this.updateSearch({ exactMatch });
25739
+ }
25740
+ searchMatchCase(matchCase) {
25741
+ this.updateSearch({ matchCase });
25742
+ }
25743
+ changeSearchScope(ev) {
25744
+ const searchScope = ev.target.value;
25745
+ this.updateSearch({ searchScope });
25746
+ }
25747
+ onSearchRangeChanged(ranges) {
25748
+ this.dataRange = ranges[0];
25749
+ }
25750
+ updateDataRange() {
25751
+ if (!this.dataRange) {
25752
+ return;
25753
+ }
25754
+ if (this.searchOptions.searchScope === "specificRange") {
25755
+ const specificRange = this.env.model.getters.getRangeFromSheetXC(this.env.model.getters.getActiveSheetId(), this.dataRange).rangeData;
25756
+ this.updateSearch({ specificRange });
25757
+ }
25758
+ }
25759
+ onSelectPreviousCell() {
25760
+ this.env.model.dispatch("SELECT_SEARCH_PREVIOUS_MATCH");
25761
+ }
25762
+ onSelectNextCell() {
25763
+ this.env.model.dispatch("SELECT_SEARCH_NEXT_MATCH");
25764
+ }
25765
+ updateSearch(updateSearchOptions) {
25766
+ const searchOptions = {
25767
+ ...this.env.model.getters.getSearchOptions(),
25768
+ ...updateSearchOptions,
25769
+ };
25770
+ this.env.model.dispatch("UPDATE_SEARCH", {
25771
+ toSearch: this.toSearch,
25772
+ searchOptions,
25773
+ });
25774
+ }
25775
+ debouncedUpdateSearch() {
25776
+ clearTimeout(this.debounceTimeoutId);
25777
+ this.debounceTimeoutId = setTimeout(() => {
25778
+ this.updateSearch();
25779
+ this.debounceTimeoutId = undefined;
25780
+ }, 200);
25781
+ }
25782
+ replace() {
25783
+ this.env.model.dispatch("REPLACE_SEARCH", {
25784
+ replaceWith: this.toReplace,
25785
+ });
25786
+ }
25787
+ replaceAll() {
25788
+ this.env.model.dispatch("REPLACE_ALL_SEARCH", {
25789
+ replaceWith: this.toReplace,
25790
+ });
25791
+ }
25792
+ }
25793
+
25794
+ css /* scss */ `
25795
+ .o-more-formats-panel {
25796
+ .format-preview {
25797
+ height: 48px;
25798
+ background-color: white;
25799
+
25800
+ &:hover {
25801
+ background-color: rgba(0, 0, 0, 0.08);
25802
+ }
25803
+ }
25804
+ .check-icon {
25805
+ width: 24px;
25755
25806
  }
25756
25807
  }
25757
25808
  `;
25758
- class DataValidationEditor extends Component {
25759
- static template = "o-spreadsheet-DataValidationEditor";
25760
- static components = { SelectionInput, SelectMenu, Section };
25809
+ const DATE_FORMAT_ACTIONS = createActions([
25810
+ formatNumberFullDateTime,
25811
+ formatNumberFullWeekDayAndMonth,
25812
+ formatNumberDayAndFullMonth,
25813
+ formatNumberShortWeekDay,
25814
+ formatNumberDayAndShortMonth,
25815
+ formatNumberFullMonth,
25816
+ formatNumberShortMonth,
25817
+ formatNumberDate,
25818
+ formatNumberTime,
25819
+ formatNumberDateTime,
25820
+ formatNumberDuration,
25821
+ ]);
25822
+ class MoreFormatsPanel extends Component {
25823
+ static template = "o-spreadsheet-MoreFormatsPanel";
25761
25824
  static props = {
25762
- rule: { type: Object, optional: true },
25763
- onExit: Function,
25825
+ onCloseSidePanel: Function,
25764
25826
  };
25765
- state = useState({ rule: this.defaultDataValidationRule });
25827
+ get dateFormatsActions() {
25828
+ return DATE_FORMAT_ACTIONS;
25829
+ }
25830
+ }
25831
+
25832
+ css /* scss */ `
25833
+ .o-checkbox-selection {
25834
+ height: 150px;
25835
+ }
25836
+ `;
25837
+ class RemoveDuplicatesPanel extends Component {
25838
+ static template = "o-spreadsheet-RemoveDuplicatesPanel";
25839
+ static components = { ValidationMessages, Section, Checkbox };
25840
+ state = useState({
25841
+ hasHeader: false,
25842
+ columns: {},
25843
+ });
25766
25844
  setup() {
25767
- if (this.props.rule) {
25845
+ onWillUpdateProps(() => this.updateColumns());
25846
+ }
25847
+ toggleHasHeader() {
25848
+ this.state.hasHeader = !this.state.hasHeader;
25849
+ }
25850
+ toggleAllColumns() {
25851
+ const newState = !this.isEveryColumnSelected;
25852
+ for (const index in this.state.columns) {
25853
+ this.state.columns[index] = newState;
25854
+ }
25855
+ }
25856
+ toggleColumn(colIndex) {
25857
+ this.state.columns[colIndex] = !this.state.columns[colIndex];
25858
+ }
25859
+ onRemoveDuplicates() {
25860
+ this.env.model.dispatch("REMOVE_DUPLICATES", {
25861
+ hasHeader: this.state.hasHeader,
25862
+ columns: this.getColsToAnalyze(),
25863
+ });
25864
+ }
25865
+ getColLabel(colKey) {
25866
+ const col = parseInt(colKey);
25867
+ let colLabel = _t("Column %s", numberToLetters(col));
25868
+ if (this.state.hasHeader) {
25768
25869
  const sheetId = this.env.model.getters.getActiveSheetId();
25769
- this.state.rule = {
25770
- ...this.props.rule,
25771
- ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, sheetId)),
25772
- };
25773
- this.state.rule.criterion.type = this.props.rule.criterion.type;
25870
+ const row = this.env.model.getters.getSelectedZone().top;
25871
+ const colHeader = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
25872
+ if (colHeader.type !== "empty") {
25873
+ colLabel += ` - ${colHeader.value}`;
25874
+ }
25774
25875
  }
25876
+ return colLabel;
25775
25877
  }
25776
- onCriterionTypeChanged(type) {
25777
- this.state.rule.criterion.type = type;
25878
+ get isEveryColumnSelected() {
25879
+ return Object.values(this.state.columns).every((value) => value === true);
25778
25880
  }
25779
- onRangesChanged(ranges) {
25780
- this.state.rule.ranges = ranges;
25881
+ get errorMessages() {
25882
+ const cancelledReasons = this.env.model.canDispatch("REMOVE_DUPLICATES", {
25883
+ hasHeader: this.state.hasHeader,
25884
+ columns: this.getColsToAnalyze(),
25885
+ }).reasons;
25886
+ const errors = new Set();
25887
+ for (const reason of cancelledReasons) {
25888
+ errors.add(RemoveDuplicateTerms.Errors[reason] || RemoveDuplicateTerms.Errors.Unexpected);
25889
+ }
25890
+ return Array.from(errors);
25781
25891
  }
25782
- onCriterionChanged(criterion) {
25783
- this.state.rule.criterion = criterion;
25892
+ get selectionStatisticalInformation() {
25893
+ const dimension = zoneToDimension(this.env.model.getters.getSelectedZone());
25894
+ return _t("%(row_count)s rows and %(column_count)s columns selected", {
25895
+ row_count: dimension.numberOfRows,
25896
+ column_count: dimension.numberOfCols,
25897
+ });
25784
25898
  }
25785
- changeRuleIsBlocking(ev) {
25786
- const isBlocking = ev.target.value;
25787
- this.state.rule.isBlocking = isBlocking === "true";
25899
+ get canConfirm() {
25900
+ return this.errorMessages.length === 0;
25788
25901
  }
25789
- onSave() {
25790
- if (!this.canSave) {
25791
- return;
25902
+ // ---------------------------------------------------------------------------
25903
+ // Private
25904
+ // ---------------------------------------------------------------------------
25905
+ updateColumns() {
25906
+ const zone = this.env.model.getters.getSelectedZone();
25907
+ const oldColumns = this.state.columns;
25908
+ const newColumns = {};
25909
+ for (let i = zone.left; i <= zone.right; i++) {
25910
+ newColumns[i] = i in oldColumns ? oldColumns[i] : true;
25792
25911
  }
25793
- this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
25794
- this.props.onExit();
25912
+ this.state.columns = newColumns;
25795
25913
  }
25796
- get canSave() {
25797
- return this.env.model.canDispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload)
25798
- .isSuccessful;
25914
+ getColsToAnalyze() {
25915
+ return Object.keys(this.state.columns)
25916
+ .filter((colIndex) => this.state.columns[colIndex])
25917
+ .map((colIndex) => parseInt(colIndex));
25799
25918
  }
25800
- get dispatchPayload() {
25801
- const rule = { ...this.state.rule, ranges: undefined };
25919
+ }
25920
+
25921
+ css /* scss */ `
25922
+ .o-locale-preview {
25923
+ color: dimgrey;
25924
+ }
25925
+ `;
25926
+ class SettingsPanel extends Component {
25927
+ static template = "o-spreadsheet-SettingsPanel";
25928
+ static components = { Section };
25929
+ static props = { onCloseSidePanel: Function };
25930
+ loadedLocales = [];
25931
+ setup() {
25932
+ onWillStart(() => this.loadLocales());
25933
+ }
25934
+ onLocaleChange(code) {
25935
+ const locale = this.loadedLocales.find((l) => l.code === code);
25936
+ if (!locale)
25937
+ return;
25938
+ this.env.model.dispatch("UPDATE_LOCALE", { locale });
25939
+ }
25940
+ async loadLocales() {
25941
+ this.loadedLocales = (await this.env.loadLocales())
25942
+ .filter(isValidLocale)
25943
+ .sort((a, b) => a.name.localeCompare(b.name));
25944
+ }
25945
+ get numberFormatPreview() {
25802
25946
  const locale = this.env.model.getters.getLocale();
25803
- const criterion = rule.criterion;
25804
- const criterionEvaluator = dataValidationEvaluatorRegistry.get(criterion.type);
25805
- const sheetId = this.env.model.getters.getActiveSheetId();
25806
- const values = criterion.values
25807
- .slice(0, criterionEvaluator.numberOfValues(criterion))
25808
- .map((value) => value?.trim())
25809
- .filter((value) => value !== "" && value !== undefined)
25810
- .map((value) => canonicalizeContent(value, locale));
25811
- rule.criterion = { ...criterion, values };
25812
- return {
25813
- sheetId,
25814
- ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
25815
- rule,
25816
- };
25947
+ return formatValue(1234567.89, { format: "#,##0.00", locale });
25817
25948
  }
25818
- get dvCriterionMenuItems() {
25819
- return getDataValidationCriterionMenuItems((type) => this.onCriterionTypeChanged(type));
25949
+ get dateFormatPreview() {
25950
+ const locale = this.env.model.getters.getLocale();
25951
+ return formatValue(1.6, { format: locale.dateFormat, locale });
25820
25952
  }
25821
- get selectedCriterionName() {
25822
- const selectedType = this.state.rule.criterion.type;
25823
- return dataValidationEvaluatorRegistry.get(selectedType).name;
25953
+ get dateTimeFormatPreview() {
25954
+ const locale = this.env.model.getters.getLocale();
25955
+ const dateTimeFormat = getDateTimeFormat(locale);
25956
+ return formatValue(1.6, { format: dateTimeFormat, locale });
25824
25957
  }
25825
- get defaultDataValidationRule() {
25826
- const sheetId = this.env.model.getters.getActiveSheetId();
25827
- const ranges = this.env.model.getters
25828
- .getSelectedZones()
25829
- .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));
25830
- return {
25831
- id: this.env.model.uuidGenerator.uuidv4(),
25832
- criterion: { type: "textContains", values: [""] },
25833
- ranges,
25834
- };
25958
+ get currentLocale() {
25959
+ return this.env.model.getters.getLocale();
25835
25960
  }
25836
- get criterionComponent() {
25837
- return dataValidationPanelCriteriaRegistry.get(this.state.rule.criterion.type).component;
25961
+ get supportedLocales() {
25962
+ const currentLocale = this.currentLocale;
25963
+ const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);
25964
+ if (!localeInLoadedLocales) {
25965
+ const locales = [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
25966
+ return locales;
25967
+ }
25968
+ else if (!deepEquals(currentLocale, localeInLoadedLocales)) {
25969
+ const index = this.loadedLocales.indexOf(localeInLoadedLocales);
25970
+ const locales = [...this.loadedLocales];
25971
+ locales[index] = currentLocale;
25972
+ locales.sort((a, b) => a.name.localeCompare(b.name));
25973
+ return locales;
25974
+ }
25975
+ return this.loadedLocales;
25976
+ }
25977
+ }
25978
+
25979
+ const SplitToColumnsInteractiveContent = {
25980
+ SplitIsDestructive: _t("This will overwrite data in the subsequent columns. Split anyway?"),
25981
+ };
25982
+ function interactiveSplitToColumns(env, separator, addNewColumns) {
25983
+ let result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", { separator, addNewColumns });
25984
+ if (result.isCancelledBecause("SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */)) {
25985
+ env.askConfirmation(SplitToColumnsInteractiveContent.SplitIsDestructive, () => {
25986
+ result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", {
25987
+ separator,
25988
+ addNewColumns,
25989
+ force: true,
25990
+ });
25991
+ });
25992
+ }
25993
+ return result;
25994
+ }
25995
+
25996
+ const SEPARATORS = [
25997
+ { name: _t("Detect automatically"), value: "auto" },
25998
+ { name: _t("Custom separator"), value: "custom" },
25999
+ { name: _t("Space"), value: " " },
26000
+ { name: _t("Comma"), value: "," },
26001
+ { name: _t("Semicolon"), value: ";" },
26002
+ { name: _t("Line Break"), value: NEWLINE },
26003
+ ];
26004
+ class SplitIntoColumnsPanel extends Component {
26005
+ static template = "o-spreadsheet-SplitIntoColumnsPanel";
26006
+ static components = { ValidationMessages, Section, Checkbox };
26007
+ static props = { onCloseSidePanel: Function };
26008
+ state = useState({ separatorValue: "auto", addNewColumns: false, customSeparator: "" });
26009
+ setup() {
26010
+ onWillUpdateProps(() => {
26011
+ // The feature makes no sense if we are editing a cell, because then the selection isn't active
26012
+ // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
26013
+ if (this.env.model.getters.getEditionMode() !== "inactive") {
26014
+ this.props.onCloseSidePanel();
26015
+ }
26016
+ });
26017
+ onMounted(() => {
26018
+ interactiveStopEdition(this.env);
26019
+ });
26020
+ }
26021
+ onSeparatorChange(value) {
26022
+ this.state.separatorValue = value;
26023
+ }
26024
+ updateCustomSeparator(ev) {
26025
+ if (!ev.target)
26026
+ return;
26027
+ this.state.customSeparator = ev.target.value;
26028
+ }
26029
+ updateAddNewColumnsCheckbox(addNewColumns) {
26030
+ this.state.addNewColumns = addNewColumns;
26031
+ }
26032
+ confirm() {
26033
+ const result = interactiveSplitToColumns(this.env, this.separatorValue, this.state.addNewColumns);
26034
+ if (result.isSuccessful) {
26035
+ this.props.onCloseSidePanel();
26036
+ }
26037
+ }
26038
+ get errorMessages() {
26039
+ const cancelledReasons = this.env.model.canDispatch("SPLIT_TEXT_INTO_COLUMNS", {
26040
+ separator: this.separatorValue,
26041
+ addNewColumns: this.state.addNewColumns,
26042
+ force: true,
26043
+ }).reasons;
26044
+ const errors = new Set();
26045
+ for (const reason of cancelledReasons) {
26046
+ switch (reason) {
26047
+ case "SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */:
26048
+ case "EmptySplitSeparator" /* CommandResult.EmptySplitSeparator */:
26049
+ break;
26050
+ default:
26051
+ errors.add(SplitToColumnsTerms.Errors[reason] || SplitToColumnsTerms.Errors.Unexpected);
26052
+ }
26053
+ }
26054
+ return Array.from(errors);
26055
+ }
26056
+ get warningMessages() {
26057
+ const warnings = [];
26058
+ const cancelledReasons = this.env.model.canDispatch("SPLIT_TEXT_INTO_COLUMNS", {
26059
+ separator: this.separatorValue,
26060
+ addNewColumns: this.state.addNewColumns,
26061
+ force: false,
26062
+ }).reasons;
26063
+ if (cancelledReasons.includes("SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */)) {
26064
+ warnings.push(SplitToColumnsTerms.Errors["SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */]);
26065
+ }
26066
+ return warnings;
26067
+ }
26068
+ get separatorValue() {
26069
+ if (this.state.separatorValue === "custom") {
26070
+ return this.state.customSeparator;
26071
+ }
26072
+ else if (this.state.separatorValue === "auto") {
26073
+ return this.env.model.getters.getAutomaticSeparator();
26074
+ }
26075
+ return this.state.separatorValue;
26076
+ }
26077
+ get separators() {
26078
+ return SEPARATORS;
26079
+ }
26080
+ get isConfirmDisabled() {
26081
+ return !this.separatorValue || this.errorMessages.length > 0;
25838
26082
  }
25839
26083
  }
25840
26084
 
@@ -25955,6 +26199,10 @@ sidePanelRegistry.add("DataValidation", {
25955
26199
  title: _t("Data validation"),
25956
26200
  Body: DataValidationPanel,
25957
26201
  });
26202
+ sidePanelRegistry.add("DataValidationEditor", {
26203
+ title: _t("Data validation"),
26204
+ Body: DataValidationEditor,
26205
+ });
25958
26206
  sidePanelRegistry.add("MoreFormats", {
25959
26207
  title: _t("More date formats"),
25960
26208
  Body: MoreFormatsPanel,
@@ -27286,6 +27534,9 @@ class Composer extends Component {
27286
27534
  const token = tokens.filter((token) => token.value.includes(currentSelectedText) &&
27287
27535
  token.start <= currentSelection.start &&
27288
27536
  token.end >= currentSelection.end)[0];
27537
+ if (!token) {
27538
+ return;
27539
+ }
27289
27540
  if (token.type === "REFERENCE") {
27290
27541
  this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", {
27291
27542
  start: token.start,
@@ -31179,6 +31430,19 @@ function fixXlsxUnicode(str) {
31179
31430
  return String.fromCharCode(parseInt(code, 16));
31180
31431
  });
31181
31432
  }
31433
+ /** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
31434
+ function getSheetDataHeader(sheetData, dimension, index) {
31435
+ if (dimension === "COL") {
31436
+ if (!sheetData.cols[index]) {
31437
+ sheetData.cols[index] = {};
31438
+ }
31439
+ return sheetData.cols[index];
31440
+ }
31441
+ if (!sheetData.rows[index]) {
31442
+ sheetData.rows[index] = {};
31443
+ }
31444
+ return sheetData.rows[index];
31445
+ }
31182
31446
 
31183
31447
  const XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/m|\s|-|\/|\.|:)+$/i;
31184
31448
  /**
@@ -32035,6 +32299,8 @@ function convertSheets(data, warningManager) {
32035
32299
  convertFormulasContent(sheet, data);
32036
32300
  const sheetDims = getSheetDims(sheet);
32037
32301
  const sheetOptions = sheet.sheetViews[0];
32302
+ const rowHeaderGroups = convertHeaderGroup(sheet, "ROW", sheetDims[1]);
32303
+ const colHeaderGroups = convertHeaderGroup(sheet, "COL", sheetDims[0]);
32038
32304
  return {
32039
32305
  id: sheet.sheetName,
32040
32306
  areGridLinesVisible: sheetOptions ? sheetOptions.showGridLines : true,
@@ -32043,8 +32309,8 @@ function convertSheets(data, warningManager) {
32043
32309
  rowNumber: sheetDims[1],
32044
32310
  cells: convertCells(sheet, data, sheetDims, warningManager),
32045
32311
  merges: sheet.merges,
32046
- cols: convertCols(sheet, sheetDims[0]),
32047
- rows: convertRows(sheet, sheetDims[1]),
32312
+ cols: convertCols(sheet, sheetDims[0], colHeaderGroups),
32313
+ rows: convertRows(sheet, sheetDims[1], rowHeaderGroups),
32048
32314
  conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),
32049
32315
  figures: convertFigures(sheet),
32050
32316
  isVisible: sheet.isVisible,
@@ -32052,10 +32318,11 @@ function convertSheets(data, warningManager) {
32052
32318
  ? { xSplit: sheetOptions.pane.xSplit, ySplit: sheetOptions.pane.ySplit }
32053
32319
  : { xSplit: 0, ySplit: 0 },
32054
32320
  filterTables: [],
32321
+ headerGroups: { COL: colHeaderGroups, ROW: rowHeaderGroups },
32055
32322
  };
32056
32323
  });
32057
32324
  }
32058
- function convertCols(sheet, numberOfCols) {
32325
+ function convertCols(sheet, numberOfCols, headerGroups) {
32059
32326
  const cols = {};
32060
32327
  // Excel begins indexes at 1
32061
32328
  for (let i = 1; i < numberOfCols + 1; i++) {
@@ -32067,11 +32334,18 @@ function convertCols(sheet, numberOfCols) {
32067
32334
  colSize = sheet.sheetFormat.defaultColWidth;
32068
32335
  else
32069
32336
  colSize = EXCEL_DEFAULT_COL_WIDTH;
32070
- cols[i - 1] = { size: convertWidthFromExcel(colSize), isHidden: col?.hidden };
32337
+ // In xlsx there is no difference between hidden columns and columns inside a folded group.
32338
+ // But in o-spreadsheet folded columns are not considered hidden.
32339
+ const colIndex = i - 1;
32340
+ const isColFolded = headerGroups.some((group) => group.isFolded && group.start <= colIndex && colIndex <= group.end);
32341
+ cols[colIndex] = {
32342
+ size: convertWidthFromExcel(colSize),
32343
+ isHidden: !isColFolded && col?.hidden,
32344
+ };
32071
32345
  }
32072
32346
  return cols;
32073
32347
  }
32074
- function convertRows(sheet, numberOfRows) {
32348
+ function convertRows(sheet, numberOfRows, headerGroups) {
32075
32349
  const rows = {};
32076
32350
  // Excel begins indexes at 1
32077
32351
  for (let i = 1; i < numberOfRows + 1; i++) {
@@ -32083,7 +32357,14 @@ function convertRows(sheet, numberOfRows) {
32083
32357
  rowSize = sheet.sheetFormat.defaultRowHeight;
32084
32358
  else
32085
32359
  rowSize = EXCEL_DEFAULT_ROW_HEIGHT;
32086
- rows[i - 1] = { size: convertHeightFromExcel(rowSize), isHidden: row?.hidden };
32360
+ // In xlsx there is no difference between hidden rows and rows inside a folded group.
32361
+ // But in o-spreadsheet folded rows are not considered hidden.
32362
+ const rowIndex = i - 1;
32363
+ const isRowFolded = headerGroups.some((group) => group.isFolded && group.start <= rowIndex && rowIndex <= group.end);
32364
+ rows[rowIndex] = {
32365
+ size: convertHeightFromExcel(rowSize),
32366
+ isHidden: !isRowFolded && row?.hidden,
32367
+ };
32087
32368
  }
32088
32369
  return rows;
32089
32370
  }
@@ -32187,6 +32468,57 @@ function getSheetDims(sheet) {
32187
32468
  dims[1] = Math.max(dims[1], EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS);
32188
32469
  return dims;
32189
32470
  }
32471
+ /**
32472
+ * Get the header groups from the XLS file.
32473
+ *
32474
+ * See ASCII art in HeaderGroupingPlugin.exportForExcel() for details on how the groups are defined in the xlsx.
32475
+ */
32476
+ function convertHeaderGroup(sheet, dim, numberOfHeaders) {
32477
+ const outlineProperties = sheet?.sheetProperties?.outlinePr;
32478
+ const headerGroups = [];
32479
+ let currentLayer = 0;
32480
+ for (let i = 0; i < numberOfHeaders; i++) {
32481
+ const header = getHeader(sheet, dim, i);
32482
+ const headerLayer = header?.outlineLevel || 0;
32483
+ if (headerLayer > currentLayer) {
32484
+ // Whether the flag indicating if the group is collapsed is on the header before or after the group. Default is after.
32485
+ const collapseFlagAfter = (dim === "ROW" ? outlineProperties?.summaryBelow : outlineProperties?.summaryRight) ?? true;
32486
+ const group = computeHeaderGroup(sheet, dim, i, collapseFlagAfter);
32487
+ if (group) {
32488
+ headerGroups.push(group);
32489
+ }
32490
+ }
32491
+ currentLayer = headerLayer;
32492
+ }
32493
+ return headerGroups;
32494
+ }
32495
+ function computeHeaderGroup(sheet, dim, startIndex, collapseFlagAfter) {
32496
+ const startHeader = getHeader(sheet, dim, startIndex);
32497
+ const startLayer = startHeader?.outlineLevel;
32498
+ if (!startLayer || !startLayer) {
32499
+ return undefined;
32500
+ }
32501
+ let currentLayer = startLayer;
32502
+ let currentIndex = startIndex;
32503
+ let currentHeader = startHeader;
32504
+ while (currentHeader && currentLayer >= startLayer) {
32505
+ currentIndex++;
32506
+ currentHeader = getHeader(sheet, dim, currentIndex);
32507
+ currentLayer = currentHeader?.outlineLevel || 0;
32508
+ }
32509
+ const start = startIndex;
32510
+ const end = currentIndex - 1;
32511
+ const collapseFlagHeader = collapseFlagAfter
32512
+ ? getHeader(sheet, dim, end + 1)
32513
+ : getHeader(sheet, dim, start - 1);
32514
+ const isFolded = collapseFlagHeader?.collapsed || false;
32515
+ return { start: start - 1, end: end - 1, isFolded }; // -1 because indices start at 1 in excel and 0 in o-spreadsheet
32516
+ }
32517
+ function getHeader(sheet, dim, index) {
32518
+ return "COL" === dim
32519
+ ? sheet.cols.find((col) => col.min <= index && index <= col.max)
32520
+ : sheet.rows.find((row) => row.index === index);
32521
+ }
32190
32522
 
32191
32523
  const TABLE_HEADER_STYLE = {
32192
32524
  fillColor: "#000000",
@@ -33247,6 +33579,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
33247
33579
  sheetName: this.extractSheetName(),
33248
33580
  sheetViews: this.extractSheetViews(sheetElement),
33249
33581
  sheetFormat: this.extractSheetFormat(sheetElement),
33582
+ sheetProperties: this.extractSheetProperties(sheetElement),
33250
33583
  cols: this.extractCols(sheetElement),
33251
33584
  rows: this.extractRows(sheetElement),
33252
33585
  sharedFormulas: this.extractSharedFormulas(sheetElement),
@@ -33372,6 +33705,23 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
33372
33705
  }).asNum(),
33373
33706
  };
33374
33707
  }
33708
+ extractSheetProperties(worksheet) {
33709
+ const propertiesElement = this.querySelector(worksheet, "sheetPr");
33710
+ if (!propertiesElement)
33711
+ return undefined;
33712
+ return {
33713
+ outlinePr: this.extractSheetOutlineProperties(propertiesElement),
33714
+ };
33715
+ }
33716
+ extractSheetOutlineProperties(sheetProperties) {
33717
+ const properties = this.querySelector(sheetProperties, "outlinePr");
33718
+ if (!properties)
33719
+ return undefined;
33720
+ return {
33721
+ summaryBelow: this.extractAttr(properties, "summaryBelow", { default: true }).asBool(),
33722
+ summaryRight: this.extractAttr(properties, "summaryRight", { default: true }).asBool(),
33723
+ };
33724
+ }
33375
33725
  extractCols(worksheet) {
33376
33726
  return this.mapOnElements({ parent: worksheet, query: "cols col" }, (colElement) => {
33377
33727
  return {
@@ -33382,6 +33732,8 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
33382
33732
  min: this.extractAttr(colElement, "min", { required: true })?.asNum(),
33383
33733
  max: this.extractAttr(colElement, "max", { required: true })?.asNum(),
33384
33734
  styleIndex: this.extractAttr(colElement, "style")?.asNum(),
33735
+ outlineLevel: this.extractAttr(colElement, "outlineLevel")?.asNum(),
33736
+ collapsed: this.extractAttr(colElement, "collapsed")?.asBool(),
33385
33737
  };
33386
33738
  });
33387
33739
  }
@@ -33394,6 +33746,8 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
33394
33746
  customHeight: this.extractAttr(rowElement, "customHeight")?.asBool(),
33395
33747
  hidden: this.extractAttr(rowElement, "hidden")?.asBool(),
33396
33748
  styleIndex: this.extractAttr(rowElement, "s")?.asNum(),
33749
+ outlineLevel: this.extractAttr(rowElement, "outlineLevel")?.asNum(),
33750
+ collapsed: this.extractAttr(rowElement, "collapsed")?.asBool(),
33397
33751
  };
33398
33752
  });
33399
33753
  }
@@ -35927,6 +36281,26 @@ class CellPlugin extends CorePlugin {
35927
36281
  format: "",
35928
36282
  });
35929
36283
  break;
36284
+ case "DELETE_CONTENT":
36285
+ this.clearZones(cmd.sheetId, cmd.target);
36286
+ break;
36287
+ }
36288
+ }
36289
+ clearZones(sheetId, zones) {
36290
+ for (let zone of zones) {
36291
+ for (let col = zone.left; col <= zone.right; col++) {
36292
+ for (let row = zone.top; row <= zone.bottom; row++) {
36293
+ const cell = this.getters.getCell({ sheetId, col, row });
36294
+ if (cell) {
36295
+ this.dispatch("UPDATE_CELL", {
36296
+ sheetId: sheetId,
36297
+ content: "",
36298
+ col,
36299
+ row,
36300
+ });
36301
+ }
36302
+ }
36303
+ }
35930
36304
  }
35931
36305
  }
35932
36306
  /**
@@ -36988,6 +37362,28 @@ class DataValidationPlugin extends CorePlugin {
36988
37362
  this.addDataValidationRule(cmd.sheetId, { ...cmd.rule, ranges });
36989
37363
  break;
36990
37364
  }
37365
+ case "DELETE_CONTENT": {
37366
+ const zones = cmd.target;
37367
+ const sheetId = cmd.sheetId;
37368
+ for (const zone of zones) {
37369
+ for (let row = zone.top; row <= zone.bottom; row++) {
37370
+ for (let col = zone.left; col <= zone.right; col++) {
37371
+ const dataValidation = this.getValidationRuleForCell({ sheetId, col, row });
37372
+ if (!dataValidation) {
37373
+ continue;
37374
+ }
37375
+ if (dataValidation.criterion.type === "isBoolean" ||
37376
+ (dataValidation.criterion.type === "isValueInList" &&
37377
+ !this.getters.getCell({ sheetId, col, row })?.content)) {
37378
+ const rules = this.rules[sheetId];
37379
+ const ranges = [this.getters.getRangeFromSheetXC(sheetId, toXC(col, row))];
37380
+ const adaptedRules = this.removeRangesFromRules(sheetId, ranges, rules);
37381
+ this.history.update("rules", sheetId, adaptedRules);
37382
+ }
37383
+ }
37384
+ }
37385
+ }
37386
+ }
36991
37387
  }
36992
37388
  }
36993
37389
  getDataValidationRules(sheetId) {
@@ -37048,13 +37444,12 @@ class DataValidationPlugin extends CorePlugin {
37048
37444
  setCenterStyleToBooleanCells(rule) {
37049
37445
  for (const position of getCellPositionsInRanges(rule.ranges)) {
37050
37446
  const cell = this.getters.getCell(position);
37051
- const { sheetId, col, row } = position;
37052
37447
  const style = {
37053
37448
  ...cell?.style,
37054
37449
  align: cell?.style?.align ?? "center",
37055
37450
  verticalAlign: cell?.style?.verticalAlign ?? "middle",
37056
37451
  };
37057
- this.dispatch("UPDATE_CELL", { sheetId, col, row, style });
37452
+ this.dispatch("UPDATE_CELL", { ...position, style });
37058
37453
  }
37059
37454
  }
37060
37455
  import(data) {
@@ -37892,7 +38287,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
37892
38287
  if (sheet.rows[row] === undefined) {
37893
38288
  sheet.rows[row] = {};
37894
38289
  }
37895
- sheet.rows[row].isHidden = this.hiddenHeaders[sheet.id]["ROW"][row];
38290
+ sheet.rows[row].isHidden ||= this.hiddenHeaders[sheet.id]["ROW"][row];
37896
38291
  }
37897
38292
  }
37898
38293
  if (sheet.cols === undefined) {
@@ -37903,7 +38298,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
37903
38298
  if (sheet.cols[col] === undefined) {
37904
38299
  sheet.cols[col] = {};
37905
38300
  }
37906
- sheet.cols[col].isHidden = this.hiddenHeaders[sheet.id]["COL"][col];
38301
+ sheet.cols[col].isHidden ||= this.hiddenHeaders[sheet.id]["COL"][col];
37907
38302
  }
37908
38303
  }
37909
38304
  }
@@ -38861,9 +39256,9 @@ class RangeAdapter {
38861
39256
  * Get a Xc string that represent a part of a range
38862
39257
  */
38863
39258
  getRangePartString(range, part) {
38864
- const colFixed = range.parts && range.parts[part].colFixed ? "$" : "";
39259
+ const colFixed = range.parts && range.parts[part]?.colFixed ? "$" : "";
38865
39260
  const col = part === 0 ? numberToLetters(range.zone.left) : numberToLetters(range.zone.right);
38866
- const rowFixed = range.parts && range.parts[part].rowFixed ? "$" : "";
39261
+ const rowFixed = range.parts && range.parts[part]?.rowFixed ? "$" : "";
38867
39262
  const row = part === 0 ? String(range.zone.top + 1) : String(range.zone.bottom + 1);
38868
39263
  let str = "";
38869
39264
  if (range.isFullCol) {
@@ -39003,9 +39398,6 @@ class SheetPlugin extends CorePlugin {
39003
39398
  case "SET_GRID_LINES_VISIBILITY":
39004
39399
  this.setGridLinesVisibility(cmd.sheetId, cmd.areGridLinesVisible);
39005
39400
  break;
39006
- case "DELETE_CONTENT":
39007
- this.clearZones(cmd.sheetId, cmd.target);
39008
- break;
39009
39401
  case "CREATE_SHEET":
39010
39402
  const sheet = this.createSheet(cmd.sheetId, cmd.name || this.getNextSheetName(), cmd.cols || 26, cmd.rows || 100, cmd.position);
39011
39403
  this.history.update("sheetIdsMapName", sheet.name, sheet.id);
@@ -39372,23 +39764,6 @@ class SheetPlugin extends CorePlugin {
39372
39764
  setGridLinesVisibility(sheetId, areGridLinesVisible) {
39373
39765
  this.history.update("sheets", sheetId, "areGridLinesVisible", areGridLinesVisible);
39374
39766
  }
39375
- clearZones(sheetId, zones) {
39376
- for (let zone of zones) {
39377
- for (let col = zone.left; col <= zone.right; col++) {
39378
- for (let row = zone.top; row <= zone.bottom; row++) {
39379
- const cell = this.sheets[sheetId].rows[row].cells[col];
39380
- if (cell) {
39381
- this.dispatch("UPDATE_CELL", {
39382
- sheetId: sheetId,
39383
- content: "",
39384
- col,
39385
- row,
39386
- });
39387
- }
39388
- }
39389
- }
39390
- }
39391
- }
39392
39767
  createSheet(id, name, colNumber, rowNumber, position) {
39393
39768
  const sheet = {
39394
39769
  id,
@@ -40178,6 +40553,41 @@ class HeaderGroupingPlugin extends CorePlugin {
40178
40553
  sheet.headerGroups = this.groups[sheet.id];
40179
40554
  }
40180
40555
  }
40556
+ exportForExcel(data) {
40557
+ /**
40558
+ * Example of header groups in the XLSX file:
40559
+ *
40560
+ * 0. | <row index="1" outlineLevel="1">
40561
+ * 1. | | <row index="2" outlineLevel="2">
40562
+ * 2. | | <row index="3" outlineLevel="2">
40563
+ * 3. | |_ <row index="4" outlineLevel="2">
40564
+ * 4. |_ <row index="5" outlineLevel="1" collapsed="0">
40565
+ * 5. <row index="6" collapsed="0">
40566
+ *
40567
+ * The collapsed flag can be on the header before or after the group (or can be missing). Default is after.
40568
+ */
40569
+ for (const sheet of data.sheets) {
40570
+ for (const dim of ["ROW", "COL"]) {
40571
+ const layers = this.getGroupsLayers(sheet.id, dim);
40572
+ for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {
40573
+ const layer = layers[layerIndex];
40574
+ for (const group of layer) {
40575
+ for (let headerIndex = group.start; headerIndex <= group.end; headerIndex++) {
40576
+ const header = getSheetDataHeader(sheet, dim, headerIndex);
40577
+ header.outlineLevel = layerIndex + 1;
40578
+ if (group.isFolded) {
40579
+ header.isHidden = true;
40580
+ }
40581
+ }
40582
+ if (group.isFolded) {
40583
+ const header = getSheetDataHeader(sheet, dim, group.end + 1);
40584
+ header.collapsed = true;
40585
+ }
40586
+ }
40587
+ }
40588
+ }
40589
+ }
40590
+ }
40181
40591
  }
40182
40592
 
40183
40593
  class SettingsPlugin extends CorePlugin {
@@ -40295,23 +40705,18 @@ class CompilationParametersBuilder {
40295
40705
  * The `compute` of the formula's function must process it completely
40296
40706
  */
40297
40707
  refFn(range, isMeta, functionName, paramNumber) {
40708
+ this.assertRangeValid(range);
40298
40709
  if (isMeta) {
40299
40710
  // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
40300
40711
  const sheetName = this.getters.getSheetName(range.sheetId);
40301
40712
  return { value: getFullReference(sheetName, zoneToXc(range.zone)) };
40302
40713
  }
40303
- if (!isZoneValid(range.zone)) {
40304
- throw new InvalidReferenceError();
40305
- }
40306
40714
  // if the formula definition could have accepted a range, we would pass through the _range function and not here
40307
40715
  if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {
40308
40716
  throw new EvaluationError(paramNumber
40309
40717
  ? _t("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
40310
40718
  : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
40311
40719
  }
40312
- if (range.invalidSheetName) {
40313
- throw new EvaluationError(_t("Invalid sheet name: %s", range.invalidSheetName));
40314
- }
40315
40720
  const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
40316
40721
  return this.readCell(position);
40317
40722
  }
@@ -40343,10 +40748,10 @@ class CompilationParametersBuilder {
40343
40748
  * Note that each col is possibly sparse: it only contain the values of cells
40344
40749
  * that are actually present in the grid.
40345
40750
  */
40346
- range({ sheetId, zone }) {
40347
- if (!isZoneValid(zone)) {
40348
- throw new InvalidReferenceError();
40349
- }
40751
+ range(range) {
40752
+ this.assertRangeValid(range);
40753
+ const sheetId = range.sheetId;
40754
+ const zone = range.zone;
40350
40755
  // Performance issue: Avoid fetching data on positions that are out of the spreadsheet
40351
40756
  // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
40352
40757
  const sheetZone = this.getters.getSheetZone(sheetId);
@@ -40374,6 +40779,14 @@ class CompilationParametersBuilder {
40374
40779
  this.rangeCache[cacheKey] = matrix;
40375
40780
  return matrix;
40376
40781
  }
40782
+ assertRangeValid(range) {
40783
+ if (!isZoneValid(range.zone)) {
40784
+ throw new InvalidReferenceError();
40785
+ }
40786
+ if (range.invalidSheetName) {
40787
+ throw new EvaluationError(_t("Invalid sheet name: %s", range.invalidSheetName));
40788
+ }
40789
+ }
40377
40790
  }
40378
40791
 
40379
40792
  function quickselect(arr, k, left, right, compare) {
@@ -42504,11 +42917,25 @@ class EvaluationDataValidationPlugin extends UIPlugin {
42504
42917
  }
42505
42918
  switch (cmd.type) {
42506
42919
  case "ADD_DATA_VALIDATION_RULE":
42920
+ const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
42921
+ if (cmd.rule.criterion.type === "isBoolean") {
42922
+ this.setContentToBooleanCells({ ...cmd.rule, ranges });
42923
+ }
42924
+ delete this.validationResults[cmd.sheetId];
42925
+ break;
42507
42926
  case "REMOVE_DATA_VALIDATION_RULE":
42508
42927
  delete this.validationResults[cmd.sheetId];
42509
42928
  break;
42510
42929
  }
42511
42930
  }
42931
+ setContentToBooleanCells(rule) {
42932
+ for (const position of getCellPositionsInRanges(rule.ranges)) {
42933
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
42934
+ if (evaluatedCell.type !== CellValueType.boolean) {
42935
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
42936
+ }
42937
+ }
42938
+ }
42512
42939
  isDataValidationInvalid(cellPosition) {
42513
42940
  return !this.getValidationResultForCell(cellPosition).isValid;
42514
42941
  }
@@ -45061,6 +45488,11 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45061
45488
  pasteDataValidation(origin, target) {
45062
45489
  const rule = this.getters.getValidationRuleForCell(origin);
45063
45490
  if (!rule) {
45491
+ const targetRule = this.getters.getValidationRuleForCell(target);
45492
+ if (targetRule) {
45493
+ // Remove the data validation rule on the target cell
45494
+ this.adaptDataValidationRule(target.sheetId, targetRule, [], [toXC(target.col, target.row)]);
45495
+ }
45064
45496
  return;
45065
45497
  }
45066
45498
  const xc = toXC(target.col, target.row);
@@ -45366,12 +45798,8 @@ class FindAndReplacePlugin extends UIPlugin {
45366
45798
  case "ADD_COLUMNS_ROWS":
45367
45799
  case "EVALUATE_CELLS":
45368
45800
  case "UPDATE_CELL":
45369
- this.isSearchDirty = true;
45370
- break;
45371
45801
  case "ACTIVATE_SHEET":
45372
- if (this.searchOptions.searchScope === "activeSheet") {
45373
- this.isSearchDirty = true;
45374
- }
45802
+ this.isSearchDirty = true;
45375
45803
  break;
45376
45804
  }
45377
45805
  }
@@ -46535,27 +46963,6 @@ class SelectionInputPlugin extends UIPlugin {
46535
46963
  // ---------------------------------------------------------------------------
46536
46964
  // Command Handling
46537
46965
  // ---------------------------------------------------------------------------
46538
- allowDispatch(cmd) {
46539
- switch (cmd.type) {
46540
- case "ADD_RANGE":
46541
- case "ADD_EMPTY_RANGE":
46542
- if (this.inputHasSingleRange && this.ranges.length === 1) {
46543
- return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */;
46544
- }
46545
- break;
46546
- case "REMOVE_RANGE":
46547
- if (this.ranges.length === 1) {
46548
- return "MinimumRangesReached" /* CommandResult.MinimumRangesReached */;
46549
- }
46550
- break;
46551
- case "CHANGE_RANGE":
46552
- if (this.inputHasSingleRange && cmd.value.split(",").length > 1) {
46553
- return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */;
46554
- }
46555
- break;
46556
- }
46557
- return "Success" /* CommandResult.Success */;
46558
- }
46559
46966
  handleEvent(event) {
46560
46967
  if (this.focusedRangeIndex === null) {
46561
46968
  return;
@@ -46793,6 +47200,15 @@ class SelectionInputsManagerPlugin extends UIPlugin {
46793
47200
  // Command Handling
46794
47201
  // ---------------------------------------------------------------------------
46795
47202
  allowDispatch(cmd) {
47203
+ switch (cmd.type) {
47204
+ case "FOCUS_RANGE":
47205
+ case "CHANGE_RANGE":
47206
+ case "ADD_EMPTY_RANGE":
47207
+ case "REMOVE_RANGE":
47208
+ if (!this.inputs[cmd.id]) {
47209
+ return "InvalidInputId" /* CommandResult.InvalidInputId */;
47210
+ }
47211
+ }
46796
47212
  switch (cmd.type) {
46797
47213
  case "FOCUS_RANGE":
46798
47214
  const index = this.currentInput?.getIndex(cmd.rangeId);
@@ -46800,9 +47216,25 @@ class SelectionInputsManagerPlugin extends UIPlugin {
46800
47216
  return "InputAlreadyFocused" /* CommandResult.InputAlreadyFocused */;
46801
47217
  }
46802
47218
  break;
46803
- }
46804
- if (this.currentInput) {
46805
- return this.currentInput.allowDispatch(cmd);
47219
+ case "ADD_RANGE":
47220
+ case "ADD_EMPTY_RANGE":
47221
+ const input = this.inputs[cmd.id];
47222
+ if (input.inputHasSingleRange && input.ranges.length === 1) {
47223
+ return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */;
47224
+ }
47225
+ break;
47226
+ case "REMOVE_RANGE":
47227
+ if (this.inputs[cmd.id].ranges.length === 1) {
47228
+ return "MinimumRangesReached" /* CommandResult.MinimumRangesReached */;
47229
+ }
47230
+ break;
47231
+ case "CHANGE_RANGE": {
47232
+ const input = this.inputs[cmd.id];
47233
+ if (input.inputHasSingleRange && cmd.value.split(",").length > 1) {
47234
+ return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */;
47235
+ }
47236
+ break;
47237
+ }
46806
47238
  }
46807
47239
  return "Success" /* CommandResult.Success */;
46808
47240
  }
@@ -48932,9 +49364,10 @@ class EditionPlugin extends UIPlugin {
48932
49364
  */
48933
49365
  getReferencedRanges() {
48934
49366
  const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
48935
- return this.currentTokens
49367
+ const referenceRanges = this.currentTokens
48936
49368
  .filter((token) => token.type === "REFERENCE")
48937
49369
  .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));
49370
+ return referenceRanges.filter((range) => !range.invalidSheetName && !range.invalidXc);
48938
49371
  }
48939
49372
  getAutoCompleteDataValidationValues() {
48940
49373
  if (this.mode === "inactive") {
@@ -50964,6 +51397,7 @@ const corePluginRegistry = new Registry()
50964
51397
  .add("header grouping", HeaderGroupingPlugin)
50965
51398
  .add("header visibility", HeaderVisibilityPlugin)
50966
51399
  .add("filters", FiltersPlugin)
51400
+ .add("dataValidation", DataValidationPlugin)
50967
51401
  .add("cell", CellPlugin)
50968
51402
  .add("merge", MergePlugin)
50969
51403
  .add("headerSize", HeaderSizePlugin)
@@ -50971,8 +51405,7 @@ const corePluginRegistry = new Registry()
50971
51405
  .add("conditional formatting", ConditionalFormatPlugin)
50972
51406
  .add("figures", FigurePlugin)
50973
51407
  .add("chart", ChartPlugin)
50974
- .add("image", ImagePlugin)
50975
- .add("dataValidation", DataValidationPlugin);
51408
+ .add("image", ImagePlugin);
50976
51409
  // Plugins which handle a specific feature, without handling any core commands
50977
51410
  const featurePluginRegistry = new Registry()
50978
51411
  .add("ui_sheet", SheetUIPlugin)
@@ -56005,6 +56438,12 @@ function addColumns(cols) {
56005
56438
  ["customWidth", 1],
56006
56439
  ["hidden", col.isHidden ? 1 : 0],
56007
56440
  ];
56441
+ if (col.outlineLevel) {
56442
+ attributes.push(["outlineLevel", col.outlineLevel]);
56443
+ }
56444
+ if (col.collapsed) {
56445
+ attributes.push(["collapsed", 1]);
56446
+ }
56008
56447
  colNodes.push(escapeXml /*xml*/ `
56009
56448
  <col ${formatAttributes(attributes)}/>
56010
56449
  `);
@@ -56020,8 +56459,18 @@ function addRows(construct, data, sheet) {
56020
56459
  for (let r = 0; r < sheet.rowNumber; r++) {
56021
56460
  const rowAttrs = [["r", r + 1]];
56022
56461
  const row = sheet.rows[r] || {};
56023
- // Always force our own row height
56024
- rowAttrs.push(["ht", convertHeightToExcel(row.size || DEFAULT_CELL_HEIGHT)], ["customHeight", 1], ["hidden", row.isHidden ? 1 : 0]);
56462
+ if (row.size && row.size !== DEFAULT_CELL_HEIGHT) {
56463
+ rowAttrs.push(["ht", convertHeightToExcel(row.size)], ["customHeight", 1]);
56464
+ }
56465
+ if (row.isHidden) {
56466
+ rowAttrs.push(["hidden", 1]);
56467
+ }
56468
+ if (row.outlineLevel) {
56469
+ rowAttrs.push(["outlineLevel", row.outlineLevel]);
56470
+ }
56471
+ if (row.collapsed) {
56472
+ rowAttrs.push(["collapsed", 1]);
56473
+ }
56025
56474
  const cellNodes = [];
56026
56475
  for (let c = 0; c < sheet.colNumber; c++) {
56027
56476
  const xc = toXC(c, r);
@@ -56054,7 +56503,11 @@ function addRows(construct, data, sheet) {
56054
56503
  `);
56055
56504
  }
56056
56505
  }
56057
- if (cellNodes.length || row.size !== DEFAULT_CELL_HEIGHT || row.isHidden) {
56506
+ if (cellNodes.length ||
56507
+ row.size !== DEFAULT_CELL_HEIGHT ||
56508
+ row.isHidden ||
56509
+ row.outlineLevel ||
56510
+ row.collapsed) {
56058
56511
  rowNodes.push(escapeXml /*xml*/ `
56059
56512
  <row ${formatAttributes(rowAttrs)}>
56060
56513
  ${joinXmlNodes(cellNodes)}
@@ -56988,6 +57441,13 @@ const links = {
56988
57441
  urlRepresentation,
56989
57442
  };
56990
57443
  const components = {
57444
+ Checkbox,
57445
+ Section,
57446
+ ChartColor,
57447
+ ChartDataSeries,
57448
+ ChartErrorSection,
57449
+ ChartLabelRange,
57450
+ ChartTitle,
56991
57451
  ChartPanel,
56992
57452
  ChartFigure,
56993
57453
  ChartJsComponent,
@@ -57023,6 +57483,6 @@ const constants = {
57023
57483
  export { AbstractChart, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
57024
57484
 
57025
57485
 
57026
- __info__.version = "17.2.0-alpha.1";
57027
- __info__.date = "2024-01-17T10:27:26.763Z";
57028
- __info__.hash = "296f467";
57486
+ __info__.version = "17.2.0-alpha.2";
57487
+ __info__.date = "2024-01-29T13:55:57.132Z";
57488
+ __info__.hash = "03c1335";