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