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