├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── post-checkout └── pre-commit ├── .prettierignore ├── COPYRIGHT ├── LICENSE ├── demo ├── currencies.js ├── data.js ├── favicon.png ├── file_store.js ├── index.html ├── main.css ├── main.js ├── pivot.js ├── readme.md └── transport.js ├── doc ├── add_function.md ├── add_right_click_item.md ├── extending │ ├── architecture.md │ ├── business_feature.md │ ├── command.md │ ├── plugin.md │ ├── translations.md │ ├── ui_extension.md │ └── xlsx │ │ └── xlsx_import.md ├── integrating │ ├── collaborative │ │ ├── collaborative.md │ │ └── collaborative_choices.md │ └── integration.md ├── o-spreadsheet.png └── o-spreadsheet_terminology.png ├── global.d.ts ├── package-lock.json ├── package.json ├── readme.md ├── rollup.config.js ├── src ├── actions │ ├── action.ts │ ├── data_actions.ts │ ├── edit_actions.ts │ ├── format_actions.ts │ ├── insert_actions.ts │ ├── menu_items_actions.ts │ ├── sheet_actions.ts │ └── view_actions.ts ├── clipboard_handlers │ ├── abstract_cell_clipboard_handler.ts │ ├── abstract_clipboard_handler.ts │ ├── abstract_figure_clipboard_handler.ts │ ├── borders_clipboard.ts │ ├── cell_clipboard.ts │ ├── chart_clipboard.ts │ ├── conditional_format_clipboard.ts │ ├── data_validation_clipboard.ts │ ├── image_clipboard.ts │ ├── index.ts │ ├── merge_clipboard.ts │ ├── references_clipboard.ts │ ├── sheet_clipboard.ts │ └── tables_clipboard.ts ├── collaborative │ ├── local_transport_service.ts │ ├── ot │ │ ├── ot.ts │ │ ├── ot_helpers.ts │ │ └── ot_specific.ts │ ├── revisions.ts │ └── session.ts ├── components │ ├── action_button │ │ ├── action_button.ts │ │ └── action_button.xml │ ├── animation │ │ ├── ripple.ts │ │ └── ripple.xml │ ├── autofill │ │ ├── autofill.ts │ │ └── autofill.xml │ ├── border_editor │ │ ├── border_editor.ts │ │ ├── border_editor.xml │ │ ├── border_editor_widget.ts │ │ └── border_editor_widget.xml │ ├── bottom_bar │ │ ├── bottom_bar.ts │ │ ├── bottom_bar.xml │ │ ├── bottom_bar_sheet │ │ │ ├── bottom_bar_sheet.ts │ │ │ └── bottom_bar_sheet.xml │ │ └── bottom_bar_statistic │ │ │ ├── aggregate_statistics_store.ts │ │ │ ├── bottom_bar_statistic.ts │ │ │ └── bottom_bar_statistic.xml │ ├── collaborative_client_tag │ │ ├── collaborative_client_tag.ts │ │ └── collaborative_client_tag.xml │ ├── color_picker │ │ ├── color_picker.ts │ │ ├── color_picker.xml │ │ ├── color_picker_widget.ts │ │ └── color_picker_widget.xml │ ├── composer │ │ ├── autocomplete_dropdown │ │ │ ├── autocomplete_dropdown.ts │ │ │ ├── autocomplete_dropdown.xml │ │ │ └── autocomplete_dropdown_store.ts │ │ ├── composer │ │ │ ├── abstract_composer_store.ts │ │ │ ├── cell_composer_store.ts │ │ │ ├── composer.ts │ │ │ └── composer.xml │ │ ├── composer_focus_store.ts │ │ ├── content_editable_helper.ts │ │ ├── formula_assistant │ │ │ ├── formula_assistant.ts │ │ │ └── formula_assistant.xml │ │ ├── grid_composer │ │ │ ├── grid_composer.ts │ │ │ └── grid_composer.xml │ │ ├── standalone_composer │ │ │ ├── standalone_composer.ts │ │ │ ├── standalone_composer.xml │ │ │ └── standalone_composer_store.ts │ │ └── top_bar_composer │ │ │ ├── top_bar_composer.ts │ │ │ └── top_bar_composer.xml │ ├── dashboard │ │ ├── clickable_cell_store.ts │ │ ├── dashboard.ts │ │ └── dashboard.xml │ ├── data_validation_overlay │ │ ├── data_validation_overlay.ts │ │ ├── data_validation_overlay.xml │ │ ├── dv_checkbox │ │ │ ├── dv_checkbox.ts │ │ │ └── dv_checkbox.xml │ │ └── dv_list_icon │ │ │ ├── dv_list_icon.ts │ │ │ └── dv_list_icon.xml │ ├── error_tooltip │ │ ├── error_tooltip.ts │ │ └── error_tooltip.xml │ ├── figures │ │ ├── chart │ │ │ ├── chartJs │ │ │ │ ├── chartjs.ts │ │ │ │ ├── chartjs.xml │ │ │ │ ├── chartjs_show_values_plugin.ts │ │ │ │ └── chartjs_waterfall_plugin.ts │ │ │ ├── gauge │ │ │ │ ├── gauge_chart_component.ts │ │ │ │ └── gauge_chart_component.xml │ │ │ └── scorecard │ │ │ │ ├── chart_scorecard.ts │ │ │ │ └── chart_scorecard.xml │ │ ├── figure │ │ │ ├── figure.ts │ │ │ └── figure.xml │ │ ├── figure_chart │ │ │ ├── figure_chart.ts │ │ │ └── figure_chart.xml │ │ ├── figure_container │ │ │ ├── figure_container.ts │ │ │ └── figure_container.xml │ │ └── figure_image │ │ │ ├── figure_image.ts │ │ │ └── figure_image.xml │ ├── filters │ │ ├── filter_icon │ │ │ ├── filter_icon.ts │ │ │ └── filter_icon.xml │ │ ├── filter_icons_overlay │ │ │ ├── filter_icons_overlay.ts │ │ │ └── filter_icons_overlay.xml │ │ ├── filter_menu │ │ │ ├── filter_menu.ts │ │ │ └── filter_menu.xml │ │ └── filter_menu_item │ │ │ ├── filter_menu_value_item.ts │ │ │ └── filter_menu_value_item.xml │ ├── focus_store.ts │ ├── font_size_editor │ │ ├── font_size_editor.ts │ │ └── font_size_editor.xml │ ├── grid │ │ ├── grid.ts │ │ ├── grid.xml │ │ └── hovered_cell_store.ts │ ├── grid_add_rows_footer │ │ ├── grid_add_rows_footer.ts │ │ └── grid_add_rows_footer.xml │ ├── grid_cell_icon │ │ ├── grid_cell_icon.ts │ │ └── grid_cell_icon.xml │ ├── grid_overlay │ │ ├── grid_overlay.ts │ │ └── grid_overlay.xml │ ├── grid_popover │ │ ├── grid_popover.ts │ │ └── grid_popover.xml │ ├── header_group │ │ ├── header_group.ts │ │ ├── header_group.xml │ │ ├── header_group_container.ts │ │ └── header_group_container.xml │ ├── headers_overlay │ │ ├── headers_overlay.ts │ │ └── headers_overlay.xml │ ├── helpers │ │ ├── autofocus_hook.ts │ │ ├── css.ts │ │ ├── dom_helpers.ts │ │ ├── drag_and_drop.ts │ │ ├── drag_and_drop_hook.ts │ │ ├── draw_grid_hook.ts │ │ ├── figure_container_helper.ts │ │ ├── figure_drag_helper.ts │ │ ├── figure_snap_helper.ts │ │ ├── highlight_hook.ts │ │ ├── html_content_helpers.ts │ │ ├── index.ts │ │ ├── listener_hook.ts │ │ ├── position_hook.ts │ │ ├── selection_helpers.ts │ │ ├── time_hooks.ts │ │ ├── touch_scroll_hook.ts │ │ └── wheel_hook.ts │ ├── highlight │ │ ├── border │ │ │ ├── border.ts │ │ │ └── border.xml │ │ ├── corner │ │ │ ├── corner.ts │ │ │ └── corner.xml │ │ └── highlight │ │ │ ├── highlight.ts │ │ │ └── highlight.xml │ ├── icon_picker │ │ ├── icon_picker.ts │ │ └── icon_picker.xml │ ├── icons │ │ ├── icons.ts │ │ └── icons.xml │ ├── index.ts │ ├── link │ │ ├── index.ts │ │ ├── link_display │ │ │ ├── link_display.ts │ │ │ └── link_display.xml │ │ └── link_editor │ │ │ ├── link_editor.ts │ │ │ └── link_editor.xml │ ├── menu │ │ ├── menu.ts │ │ └── menu.xml │ ├── paint_format_button │ │ ├── paint_format_button.ts │ │ ├── paint_format_button.xml │ │ └── paint_format_store.ts │ ├── pivot_html_renderer │ │ ├── pivot_html_renderer.ts │ │ └── pivot_html_renderer.xml │ ├── popover │ │ ├── cell_popover_store.ts │ │ ├── index.ts │ │ ├── popover.ts │ │ ├── popover.xml │ │ └── popover_builders.ts │ ├── scrollbar.ts │ ├── scrollbar │ │ ├── index.ts │ │ ├── scrollbar.ts │ │ ├── scrollbar.xml │ │ ├── scrollbar_horizontal.ts │ │ └── scrollbar_vertical.ts │ ├── selection_input │ │ ├── selection_input.ts │ │ ├── selection_input.xml │ │ └── selection_input_store.ts │ ├── side_panel │ │ ├── chart │ │ │ ├── bar_chart │ │ │ │ ├── bar_chart_config_panel.ts │ │ │ │ └── bar_chart_config_panel.xml │ │ │ ├── building_blocks │ │ │ │ ├── axis_design │ │ │ │ │ ├── axis_design_editor.ts │ │ │ │ │ └── axis_design_editor.xml │ │ │ │ ├── data_series │ │ │ │ │ ├── data_series.ts │ │ │ │ │ └── data_series.xml │ │ │ │ ├── error_section │ │ │ │ │ ├── error_section.ts │ │ │ │ │ └── error_section.xml │ │ │ │ ├── general_design │ │ │ │ │ ├── general_design_editor.ts │ │ │ │ │ └── general_design_editor.xml │ │ │ │ ├── generic_side_panel │ │ │ │ │ ├── config_panel.ts │ │ │ │ │ └── config_panel.xml │ │ │ │ ├── label_range │ │ │ │ │ ├── label_range.ts │ │ │ │ │ └── label_range.xml │ │ │ │ └── title │ │ │ │ │ ├── title.ts │ │ │ │ │ └── title.xml │ │ │ ├── chart_type_picker │ │ │ │ ├── chart_previews.xml │ │ │ │ ├── chart_type_picker.ts │ │ │ │ └── chart_type_picker.xml │ │ │ ├── chart_with_axis │ │ │ │ ├── design_panel.ts │ │ │ │ └── design_panel.xml │ │ │ ├── combo_chart │ │ │ │ ├── combo_chart_config_panel.ts │ │ │ │ ├── combo_chart_config_panel.xml │ │ │ │ ├── combo_chart_design_panel.ts │ │ │ │ └── combo_chart_design_panel.xml │ │ │ ├── gauge_chart_panel │ │ │ │ ├── gauge_chart_config_panel.ts │ │ │ │ ├── gauge_chart_config_panel.xml │ │ │ │ ├── gauge_chart_design_panel.ts │ │ │ │ └── gauge_chart_design_panel.xml │ │ │ ├── index.ts │ │ │ ├── line_chart │ │ │ │ ├── line_chart_config_panel.ts │ │ │ │ └── line_chart_config_panel.xml │ │ │ ├── main_chart_panel │ │ │ │ ├── main_chart_panel.ts │ │ │ │ ├── main_chart_panel.xml │ │ │ │ └── main_chart_panel_store.ts │ │ │ ├── pie_chart │ │ │ │ ├── pie_chart_design_panel.ts │ │ │ │ └── pie_chart_design_panel.xml │ │ │ ├── scatter_chart │ │ │ │ ├── scatter_chart_config_panel.ts │ │ │ │ └── scatter_chart_config_panel.xml │ │ │ ├── scorecard_chart_panel │ │ │ │ ├── scorecard_chart_config_panel.ts │ │ │ │ ├── scorecard_chart_config_panel.xml │ │ │ │ ├── scorecard_chart_design_panel.ts │ │ │ │ └── scorecard_chart_design_panel.xml │ │ │ └── waterfall_chart │ │ │ │ ├── waterfall_chart_design_panel.ts │ │ │ │ └── waterfall_chart_design_panel.xml │ │ ├── components │ │ │ ├── badge_selection │ │ │ │ ├── badge_selection.ts │ │ │ │ └── badge_selection.xml │ │ │ ├── checkbox │ │ │ │ ├── checkbox.ts │ │ │ │ └── checkbox.xml │ │ │ ├── cog_wheel_menu │ │ │ │ ├── cog_wheel_menu.ts │ │ │ │ └── cog_wheel_menu.xml │ │ │ ├── collapsible │ │ │ │ ├── side_panel_collapsible.ts │ │ │ │ └── side_panel_collapsible.xml │ │ │ ├── radio_selection │ │ │ │ ├── radio_selection.ts │ │ │ │ └── radio_selection.xml │ │ │ ├── round_color_picker │ │ │ │ ├── round_color_picker.ts │ │ │ │ └── round_color_picker.xml │ │ │ └── section │ │ │ │ ├── section.ts │ │ │ │ └── section.xml │ │ ├── conditional_formatting │ │ │ ├── cf_editor │ │ │ │ ├── cell_is_rule_editor.xml │ │ │ │ ├── cf_editor.ts │ │ │ │ ├── cf_editor.xml │ │ │ │ ├── color_scale_rule_editor.xml │ │ │ │ ├── data_bar_rule_editor.xml │ │ │ │ └── icon_set_rule_editor.xml │ │ │ ├── cf_preview │ │ │ │ ├── cf_preview.ts │ │ │ │ └── cf_preview.xml │ │ │ ├── cf_preview_list │ │ │ │ ├── cf_preview_list.ts │ │ │ │ └── cf_preview_list.xml │ │ │ ├── conditional_formatting.ts │ │ │ └── conditional_formatting.xml │ │ ├── custom_currency │ │ │ ├── custom_currency.ts │ │ │ └── custom_currency.xml │ │ ├── data_validation │ │ │ ├── data_validation_panel.ts │ │ │ ├── data_validation_panel.xml │ │ │ ├── data_validation_panel_helper.ts │ │ │ ├── dv_criterion_form │ │ │ │ ├── dv_criterion_form.ts │ │ │ │ ├── dv_date_criterion │ │ │ │ │ ├── dv_date_criterion.ts │ │ │ │ │ └── dv_date_criterion.xml │ │ │ │ ├── dv_double_input_criterion │ │ │ │ │ ├── dv_double_input_criterion.ts │ │ │ │ │ └── dv_double_input_criterion.xml │ │ │ │ ├── dv_input │ │ │ │ │ ├── dv_input.ts │ │ │ │ │ └── dv_input.xml │ │ │ │ ├── dv_single_input_criterion │ │ │ │ │ ├── dv_single_input_criterion.ts │ │ │ │ │ └── dv_single_input_criterion.xml │ │ │ │ ├── dv_value_in_list_criterion │ │ │ │ │ ├── dv_value_in_list_criterion.ts │ │ │ │ │ └── dv_value_in_list_criterion.xml │ │ │ │ └── dv_value_in_range_criterion │ │ │ │ │ ├── dv_value_in_range_criterion.ts │ │ │ │ │ └── dv_value_in_range_criterion.xml │ │ │ ├── dv_editor │ │ │ │ ├── dv_editor.ts │ │ │ │ └── dv_editor.xml │ │ │ └── dv_preview │ │ │ │ ├── dv_preview.ts │ │ │ │ └── dv_preview.xml │ │ ├── find_and_replace │ │ │ ├── find_and_replace.ts │ │ │ ├── find_and_replace.xml │ │ │ └── find_and_replace_store.ts │ │ ├── more_formats │ │ │ ├── more_formats.ts │ │ │ └── more_formats.xml │ │ ├── pivot │ │ │ ├── editable_name │ │ │ │ ├── editable_name.ts │ │ │ │ └── editable_name.xml │ │ │ ├── pivot_defer_update │ │ │ │ ├── pivot_defer_update.ts │ │ │ │ └── pivot_defer_update.xml │ │ │ ├── pivot_layout_configurator │ │ │ │ ├── add_dimension_button │ │ │ │ │ ├── add_dimension_button.ts │ │ │ │ │ └── add_dimension_button.xml │ │ │ │ ├── pivot_dimension │ │ │ │ │ ├── pivot_dimension.ts │ │ │ │ │ └── pivot_dimension.xml │ │ │ │ ├── pivot_dimension_granularity │ │ │ │ │ ├── pivot_dimension_granularity.ts │ │ │ │ │ └── pivot_dimension_granularity.xml │ │ │ │ ├── pivot_dimension_order │ │ │ │ │ ├── pivot_dimension_order.ts │ │ │ │ │ └── pivot_dimension_order.xml │ │ │ │ ├── pivot_layout_configurator.ts │ │ │ │ ├── pivot_layout_configurator.xml │ │ │ │ └── pivot_measure │ │ │ │ │ ├── pivot_measure.ts │ │ │ │ │ └── pivot_measure.xml │ │ │ ├── pivot_measure_display_panel │ │ │ │ ├── pivot_measure_display_panel.ts │ │ │ │ ├── pivot_measure_display_panel.xml │ │ │ │ └── pivot_measure_display_panel_store.ts │ │ │ ├── pivot_side_panel │ │ │ │ ├── pivot_side_panel.ts │ │ │ │ ├── pivot_side_panel.xml │ │ │ │ ├── pivot_side_panel_store.ts │ │ │ │ └── pivot_spreadsheet_side_panel │ │ │ │ │ ├── pivot_spreadsheet_side_panel.ts │ │ │ │ │ └── pivot_spreadsheet_side_panel.xml │ │ │ └── pivot_title_section │ │ │ │ ├── pivot_title_section.ts │ │ │ │ └── pivot_title_section.xml │ │ ├── remove_duplicates │ │ │ ├── remove_duplicates.ts │ │ │ └── remove_duplicates.xml │ │ ├── select_menu │ │ │ ├── select_menu.ts │ │ │ └── select_menu.xml │ │ ├── settings │ │ │ ├── settings_panel.ts │ │ │ └── settings_panel.xml │ │ ├── side_panel │ │ │ ├── side_panel.ts │ │ │ ├── side_panel.xml │ │ │ └── side_panel_store.ts │ │ ├── split_to_columns_panel │ │ │ ├── split_to_columns_panel.ts │ │ │ └── split_to_columns_panel.xml │ │ ├── table_panel │ │ │ ├── table_panel.ts │ │ │ └── table_panel.xml │ │ └── table_style_editor_panel │ │ │ ├── table_style_editor_panel.ts │ │ │ └── table_style_editor_panel.xml │ ├── spreadsheet │ │ ├── spreadsheet.ts │ │ └── spreadsheet.xml │ ├── tables │ │ ├── table_dropdown_button │ │ │ ├── table_dropdown_button.ts │ │ │ └── table_dropdown_button.xml │ │ ├── table_resizer │ │ │ ├── table_resizer.ts │ │ │ └── table_resizer.xml │ │ ├── table_style_picker │ │ │ ├── table_style_picker.ts │ │ │ └── table_style_picker.xml │ │ ├── table_style_preview │ │ │ ├── table_canvas_helpers.ts │ │ │ ├── table_style_preview.ts │ │ │ └── table_style_preview.xml │ │ └── table_styles_popover │ │ │ ├── table_styles_popover.ts │ │ │ └── table_styles_popover.xml │ ├── text_input │ │ ├── text_input.ts │ │ └── text_input.xml │ ├── top_bar │ │ ├── top_bar.ts │ │ └── top_bar.xml │ ├── translations_terms.ts │ └── validation_messages │ │ ├── validation_messages.ts │ │ └── validation_messages.xml ├── constants.ts ├── formulas │ ├── code_builder.ts │ ├── compiler.ts │ ├── composer_tokenizer.ts │ ├── helpers.ts │ ├── index.ts │ ├── parser.ts │ ├── range_tokenizer.ts │ └── tokenizer.ts ├── functions │ ├── arguments.ts │ ├── helper_assert.ts │ ├── helper_financial.ts │ ├── helper_logical.ts │ ├── helper_lookup.ts │ ├── helper_math.ts │ ├── helper_matrices.ts │ ├── helper_parser.ts │ ├── helper_statistical.ts │ ├── helpers.ts │ ├── index.ts │ ├── module_array.ts │ ├── module_custom.ts │ ├── module_database.ts │ ├── module_date.ts │ ├── module_engineering.ts │ ├── module_filter.ts │ ├── module_financial.ts │ ├── module_info.ts │ ├── module_logical.ts │ ├── module_lookup.ts │ ├── module_math.ts │ ├── module_operators.ts │ ├── module_parser.ts │ ├── module_statistical.ts │ ├── module_text.ts │ └── module_web.ts ├── helpers │ ├── cells │ │ ├── cell_evaluation.ts │ │ └── index.ts │ ├── chart_date.ts │ ├── clipboard │ │ ├── clipboard_helpers.ts │ │ └── navigator_clipboard_wrapper.ts │ ├── color.ts │ ├── coordinates.ts │ ├── data_normalization.ts │ ├── data_validation_helpers.ts │ ├── dates.ts │ ├── edge_scrolling.ts │ ├── event_bus.ts │ ├── figures │ │ ├── charts │ │ │ ├── abstract_chart.ts │ │ │ ├── bar_chart.ts │ │ │ ├── chart_common.ts │ │ │ ├── chart_common_line_scatter.ts │ │ │ ├── chart_factory.ts │ │ │ ├── chart_ui_common.ts │ │ │ ├── combo_chart.ts │ │ │ ├── gauge_chart.ts │ │ │ ├── gauge_chart_rendering.ts │ │ │ ├── index.ts │ │ │ ├── line_chart.ts │ │ │ ├── pie_chart.ts │ │ │ ├── pyramid_chart.ts │ │ │ ├── scatter_chart.ts │ │ │ ├── scorecard_chart.ts │ │ │ ├── scorecard_chart_config_builder.ts │ │ │ └── waterfall_chart.ts │ │ ├── figure │ │ │ └── figure.ts │ │ └── images │ │ │ └── image_provider.ts │ ├── format │ │ ├── format.ts │ │ ├── format_parser.ts │ │ └── format_tokenizer.ts │ ├── index.ts │ ├── internal_viewport.ts │ ├── inverse_commands.ts │ ├── links.ts │ ├── locale.ts │ ├── misc.ts │ ├── numbers.ts │ ├── pivot │ │ ├── pivot_composer_helpers.ts │ │ ├── pivot_domain_helpers.ts │ │ ├── pivot_helpers.ts │ │ ├── pivot_highlight.ts │ │ ├── pivot_menu_items.ts │ │ ├── pivot_positional_formula_registry.ts │ │ ├── pivot_presence_tracker.ts │ │ ├── pivot_presentation.ts │ │ ├── pivot_registry.ts │ │ ├── pivot_runtime_definition.ts │ │ ├── pivot_side_panel_registry.ts │ │ ├── pivot_time_adapter.ts │ │ ├── spreadsheet_pivot │ │ │ ├── data_entry_spreadsheet_pivot.ts │ │ │ ├── date_spreadsheet_pivot.ts │ │ │ ├── runtime_definition_spreadsheet_pivot.ts │ │ │ └── spreadsheet_pivot.ts │ │ └── table_spreadsheet_pivot.ts │ ├── range.ts │ ├── recompute_zones.ts │ ├── rectangle.ts │ ├── reference_type.ts │ ├── references.ts │ ├── rendering.ts │ ├── search.ts │ ├── sheet.ts │ ├── sort.ts │ ├── state_manager_helpers.ts │ ├── table_helpers.ts │ ├── table_presets.ts │ ├── text_helper.ts │ ├── ui │ │ ├── cut_interactive.ts │ │ ├── freeze_interactive.ts │ │ ├── merge_interactive.ts │ │ ├── paste_interactive.ts │ │ ├── sheet_interactive.ts │ │ ├── split_to_columns_interactive.ts │ │ ├── table_interactive.ts │ │ └── toggle_group_interactive.ts │ ├── uuid.ts │ └── zones.ts ├── history │ ├── branch.ts │ ├── factory.ts │ ├── operation.ts │ ├── operation_sequence.ts │ ├── repeat_commands │ │ ├── repeat_commands_generic.ts │ │ ├── repeat_commands_specific.ts │ │ └── repeat_revision.ts │ ├── selective_history.ts │ └── tree.ts ├── index.ts ├── migrations │ ├── data.ts │ ├── legacy_tools.ts │ ├── locale.ts │ └── migration_steps.ts ├── model.ts ├── plugins │ ├── base_plugin.ts │ ├── core │ │ ├── borders.ts │ │ ├── cell.ts │ │ ├── chart.ts │ │ ├── conditional_format.ts │ │ ├── data_validation.ts │ │ ├── figures.ts │ │ ├── header_grouping.ts │ │ ├── header_size.ts │ │ ├── header_visibility.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── merge.ts │ │ ├── pivot.ts │ │ ├── range.ts │ │ ├── settings.ts │ │ ├── sheet.ts │ │ ├── spreadsheet_pivot.ts │ │ ├── table_style.ts │ │ └── tables.ts │ ├── core_plugin.ts │ ├── index.ts │ ├── ui_core_views │ │ ├── cell_evaluation │ │ │ ├── binary_grid.ts │ │ │ ├── compilation_parameters.ts │ │ │ ├── evaluation_plugin.ts │ │ │ ├── evaluator.ts │ │ │ ├── formula_dependency_graph.ts │ │ │ ├── index.ts │ │ │ ├── position_map.ts │ │ │ ├── position_set.ts │ │ │ ├── r_tree.ts │ │ │ └── spreading_relation.ts │ │ ├── custom_colors.ts │ │ ├── dynamic_tables.ts │ │ ├── evaluation_chart.ts │ │ ├── evaluation_conditional_format.ts │ │ ├── evaluation_data_validation.ts │ │ ├── header_sizes_ui.ts │ │ ├── index.ts │ │ └── pivot_ui.ts │ ├── ui_feature │ │ ├── autofill.ts │ │ ├── automatic_sum.ts │ │ ├── cell_computed_style.ts │ │ ├── collaborative.ts │ │ ├── data_cleanup.ts │ │ ├── datavalidation_insertion.ts │ │ ├── format.ts │ │ ├── header_visibility_ui.ts │ │ ├── index.ts │ │ ├── insert_pivot.ts │ │ ├── local_history.ts │ │ ├── pivot_presence_plugin.ts │ │ ├── sort.ts │ │ ├── split_to_columns.ts │ │ ├── table_autofill.ts │ │ ├── table_computed_style.ts │ │ ├── table_resize_ui.ts │ │ ├── ui_options.ts │ │ └── ui_sheet.ts │ ├── ui_plugin.ts │ └── ui_stateful │ │ ├── clipboard.ts │ │ ├── filter_evaluation.ts │ │ ├── header_positions.ts │ │ ├── index.ts │ │ ├── selection.ts │ │ └── sheetview.ts ├── registries │ ├── auto_completes │ │ ├── auto_complete_registry.ts │ │ ├── data_validation_auto_complete.ts │ │ ├── function_auto_complete.ts │ │ ├── index.ts │ │ ├── pivot_auto_complete.ts │ │ ├── pivot_dimension_auto_complete.ts │ │ └── sheet_name_auto_complete.ts │ ├── autofill_modifiers.ts │ ├── autofill_rules.ts │ ├── cell_clickable_registry.ts │ ├── cell_popovers_registry.ts │ ├── chart_types.ts │ ├── currencies_registry.ts │ ├── data_validation_registry.ts │ ├── evaluation_registry.ts │ ├── figure_registry.ts │ ├── icons_on_cell_registry.ts │ ├── index.ts │ ├── inverse_command_registry.ts │ ├── menu_items_registry.ts │ ├── menus │ │ ├── cell_menu_registry.ts │ │ ├── col_menu_registry.ts │ │ ├── header_group_registry.ts │ │ ├── index.ts │ │ ├── link_menu_registry.ts │ │ ├── number_format_menu_registry.ts │ │ ├── row_menu_registry.ts │ │ ├── sheet_menu_registry.ts │ │ ├── table_style_menu_registry.ts │ │ └── topbar_menu_registry.ts │ ├── ot_registry.ts │ ├── registry.ts │ ├── repeat_commands_registry.ts │ ├── side_panel_registry.ts │ ├── side_panel_registry_entries.ts │ └── topbar_component_registry.ts ├── selection_stream │ ├── event_stream.ts │ └── selection_stream_processor.ts ├── state_observer.ts ├── store_engine │ ├── README.md │ ├── dependency_container.ts │ ├── index.ts │ ├── store.ts │ └── store_hooks.ts ├── stores │ ├── DOM_focus_store.ts │ ├── array_formula_highlight.ts │ ├── grid_renderer_store.ts │ ├── highlight_store.ts │ ├── index.ts │ ├── model_store.ts │ ├── notification_store.ts │ ├── renderer_store.ts │ └── spreadsheet_store.ts ├── translation.ts ├── types │ ├── autofill.ts │ ├── cell_popovers.ts │ ├── cells.ts │ ├── chart │ │ ├── bar_chart.ts │ │ ├── chart.ts │ │ ├── combo_chart.ts │ │ ├── common_bar_combo.ts │ │ ├── common_chart.ts │ │ ├── gauge_chart.ts │ │ ├── index.ts │ │ ├── line_chart.ts │ │ ├── pie_chart.ts │ │ ├── pyramid_chart.ts │ │ ├── scatter_chart.ts │ │ ├── scorecard_chart.ts │ │ └── waterfall_chart.ts │ ├── clipboard.ts │ ├── collaborative │ │ ├── revisions.ts │ │ ├── session.ts │ │ └── transport_service.ts │ ├── commands.ts │ ├── conditional_formatting.ts │ ├── currency.ts │ ├── data_validation.ts │ ├── env.ts │ ├── errors.ts │ ├── event_stream │ │ ├── index.ts │ │ └── selection_events.ts │ ├── figure.ts │ ├── files.ts │ ├── find_and_replace.ts │ ├── format.ts │ ├── functions.ts │ ├── getters.ts │ ├── history.ts │ ├── image.ts │ ├── index.ts │ ├── locale.ts │ ├── misc.ts │ ├── pivot.ts │ ├── pivot_runtime.ts │ ├── range.ts │ ├── rendering.ts │ ├── table.ts │ ├── validator.ts │ ├── workbook_data.ts │ └── xlsx.ts └── xlsx │ ├── constants.ts │ ├── conversion │ ├── cf_conversion.ts │ ├── color_conversion.ts │ ├── conversion_maps.ts │ ├── data_validation_conversion.ts │ ├── figure_conversion.ts │ ├── format_conversion.ts │ ├── formula_conversion.ts │ ├── index.ts │ ├── sheet_conversion.ts │ ├── style_conversion.ts │ └── table_conversion.ts │ ├── extraction │ ├── base_extractor.ts │ ├── cf_extractor.ts │ ├── chart_extractor.ts │ ├── data_validation_extractor.ts │ ├── external_book_extractor.ts │ ├── figure_extractor.ts │ ├── index.ts │ ├── misc_extractor.ts │ ├── pivot_extractor.ts │ ├── sheet_extractor.ts │ ├── style_extractor.ts │ └── table_extractor.ts │ ├── functions │ ├── cells.ts │ ├── charts.ts │ ├── conditional_formatting.ts │ ├── data_validation.ts │ ├── drawings.ts │ ├── styles.ts │ ├── table.ts │ └── worksheet.ts │ ├── helpers │ ├── colors.ts │ ├── content_helpers.ts │ ├── misc.ts │ ├── xlsx_helper.ts │ ├── xlsx_parser_error_manager.ts │ └── xml_helpers.ts │ ├── xlsx_reader.ts │ └── xlsx_writer.ts ├── tests ├── __mocks__ │ ├── content_editable_helper.ts │ ├── dom_helpers.ts │ ├── mock_file_store.ts │ ├── mock_image_provider.ts │ ├── mock_misc_helpers.ts │ └── transport_service.ts ├── __snapshots__ │ ├── cog_wheel_menu.test.ts.snap │ ├── renderer_store.test.ts.snap │ └── top_bar_component.test.ts.snap ├── __xlsx__ │ ├── read_demo_xlsx.ts │ └── xlsx_demo_data.xlsx ├── action_button.test.ts ├── autofill │ ├── autofill_component.test.ts │ └── autofill_plugin.test.ts ├── borders │ ├── border_editor_component.test.ts │ └── border_plugin.test.ts ├── bottom_bar │ ├── __snapshots__ │ │ └── bottom_bar_component.test.ts.snap │ ├── aggregate_statistics_store.test.ts │ ├── automatic_sum_model.test.ts │ └── bottom_bar_component.test.ts ├── cells │ ├── cell_plugin.test.ts │ ├── cell_popovers_store.test.ts │ ├── merges_plugin.test.ts │ └── style_plugin.test.ts ├── clipboard │ ├── clipboard_figure_plugin.test.ts │ └── clipboard_plugin.test.ts ├── cog_wheel_menu.test.ts ├── collaborative │ ├── collaborative.test.ts │ ├── collaborative_clipboard.test.ts │ ├── collaborative_helpers.ts │ ├── collaborative_history.test.ts │ ├── collaborative_monkey_party.test.ts │ ├── collaborative_selection.test.ts │ ├── collaborative_session.test.ts │ ├── collaborative_sheet_manipulations.test.ts │ ├── inverses.test.ts │ └── ot │ │ ├── ot.test.ts │ │ ├── ot_columns_added.test.ts │ │ ├── ot_columns_removed.test.ts │ │ ├── ot_merged.test.ts │ │ ├── ot_rows_added.test.ts │ │ ├── ot_rows_removed.test.ts │ │ └── ot_sheet_deleted.test.ts ├── colors │ ├── __snapshots__ │ │ └── color_picker_component.test.ts.snap │ ├── color_helpers.test.ts │ ├── color_picker_component.test.ts │ └── custom_colors_plugin.test.ts ├── components │ ├── __snapshots__ │ │ ├── pivot_html_renderer.test.ts.snap │ │ └── text_input.test.ts.snap │ ├── pivot_html_renderer.test.ts │ └── text_input.test.ts ├── composer │ ├── __snapshots__ │ │ ├── autocomplete_dropdown_component.test.ts.snap │ │ ├── composer_integration_component.test.ts.snap │ │ ├── content_editable_helpers.test.ts.snap │ │ └── formula_assistant_component.test.ts.snap │ ├── auto_complete │ │ ├── data_validation_auto_complete_store.test.ts │ │ ├── function_auto_complete_store.test.ts │ │ ├── pivot_auto_complete_store.test.ts │ │ └── sheet_name_auto_complete_store.test.ts │ ├── autocomplete_dropdown_component.test.ts │ ├── composer_component.test.ts │ ├── composer_integration_component.test.ts │ ├── composer_sheet_transform_plugin.test.ts │ ├── composer_store.test.ts │ ├── content_editable_helpers.test.ts │ ├── formula_assistant_component.test.ts │ └── standalone_composer_component.test.ts ├── conditional_formatting │ ├── __snapshots__ │ │ └── conditional_formatting_panel_component.test.ts.snap │ ├── conditional_formatting_panel_component.test.ts │ └── conditional_formatting_plugin.test.ts ├── data_validation │ ├── data_validation_blocking_component.test.ts │ ├── data_validation_checkbox_component.test.ts │ ├── data_validation_checkbox_plugin.test.ts │ ├── data_validation_clipboard_plugin.test.ts │ ├── data_validation_core_plugin.test.ts │ ├── data_validation_generics_side_panel_component.test.ts │ ├── data_validation_list_component.test.ts │ ├── data_validation_preview_component.test.ts │ ├── data_validation_registry.test.ts │ └── evaluation_data_validation_plugin.test.ts ├── evaluation │ ├── __snapshots__ │ │ ├── compiler.test.ts.snap │ │ └── composer_tokenizer.test.ts.snap │ ├── compiler.test.ts │ ├── composer_tokenizer.test.ts │ ├── evaluation.test.ts │ ├── evaluation_formula_array.test.ts │ ├── expressions.test.ts │ ├── formulas.test.ts │ ├── parser.test.ts │ ├── range_tokenizer.test.ts │ └── tokenizer.test.ts ├── figures │ ├── __snapshots__ │ │ └── figure_component.test.ts.snap │ ├── chart │ │ ├── __snapshots__ │ │ │ └── chart_plugin.test.ts.snap │ │ ├── bar_chart_plugin.test.ts │ │ ├── chart_plugin.test.ts │ │ ├── chart_type_picker.test.ts │ │ ├── charts_component.test.ts │ │ ├── combo_chart_component.test.ts │ │ ├── combo_chart_plugin.test.ts │ │ ├── common_chart_plugin.test.ts │ │ ├── gauge │ │ │ ├── __snapshots__ │ │ │ │ └── gauge_chart_plugin.test.ts.snap │ │ │ ├── gauge_chart_plugin.test.ts │ │ │ └── gauge_rendering.test.ts │ │ ├── line_chart_plugin.test.ts │ │ ├── menu_item_insert_chart.test.ts │ │ ├── pie_chart_plugin.test.ts │ │ ├── pyramid_chart │ │ │ ├── pyramid_chart_component.test.ts │ │ │ └── pyramid_chart_plugin.test.ts │ │ ├── scorecard │ │ │ ├── scorecard_chart_component.test.ts │ │ │ └── scorecard_chart_plugin.test.ts │ │ └── waterfall │ │ │ ├── waterfall_chart_plugin.test.ts │ │ │ └── waterfall_panel_component.test.ts │ ├── figure_component.test.ts │ ├── figures_plugin.test.ts │ └── image │ │ ├── image_file_store.test.ts │ │ └── image_plugin.test.ts ├── find_and_replace │ ├── find_and_replace_store.test.ts │ └── find_replace_side_panel_component.test.ts ├── formats │ ├── __snapshots__ │ │ └── custom_currency_side_panel_component.test.ts.snap │ ├── custom_currency_side_panel_component.test.ts │ ├── format_helpers.test.ts │ ├── formatting_plugin.test.ts │ ├── more_formats_side_panel.test.ts │ └── plain_text_format.test.ts ├── functions │ ├── arguments.test.ts │ ├── dates.test.ts │ ├── functions.test.ts │ ├── helper.test.ts │ ├── module_array.test.ts │ ├── module_custom.test.ts │ ├── module_database.test.ts │ ├── module_date.test.ts │ ├── module_engineering.test.ts │ ├── module_filter.test.ts │ ├── module_financial.test.ts │ ├── module_info.test.ts │ ├── module_logical.test.ts │ ├── module_lookup.test.ts │ ├── module_math.test.ts │ ├── module_operators.test.ts │ ├── module_parser.test.ts │ ├── module_statistical.test.ts │ ├── module_text.test.ts │ ├── module_web.test.ts │ └── vectorization.test.ts ├── grid │ ├── __snapshots__ │ │ ├── dashboard_grid_component.test.ts.snap │ │ └── grid_component.test.ts.snap │ ├── array_formula_highlights_store.test.ts │ ├── dashboard_grid_component.test.ts │ ├── grid_cell_icon_component.test.ts │ ├── grid_component.test.ts │ ├── grid_drag_and_drop_component.test.ts │ ├── grid_overlay_component.test.ts │ ├── highlight_component.test.ts │ └── highlight_store.test.ts ├── header_group │ ├── __snapshots__ │ │ └── header_group_component.test.ts.snap │ ├── header_group_component.test.ts │ └── header_group_plugin.test.ts ├── headers │ ├── header_visibility_plugin.test.ts │ └── resizing_plugin.test.ts ├── helpers │ ├── coordinates_helpers.test.ts │ ├── css_helpers.test.ts │ ├── locale_helpers.test.ts │ ├── misc_helpers.test.ts │ ├── numbers_helpers.test.ts │ ├── positions_map.test.ts │ ├── positions_set.test.ts │ ├── recompute_zones_helpers.test.ts │ ├── reference_types_helpers.test.ts │ ├── search_helpers.test.ts │ ├── sheet.test.ts │ ├── translation_helpers.test.ts │ ├── ui_helpers.test.ts │ └── zones_helpers.test.ts ├── history │ ├── history_plugin.test.ts │ └── selective_history_plugin.test.ts ├── link │ ├── __snapshots__ │ │ └── link_display_component.test.ts.snap │ ├── link_display_component.test.ts │ └── link_editor_component.test.ts ├── menus │ ├── __snapshots__ │ │ └── context_menu_component.test.ts.snap │ ├── context_menu_component.test.ts │ ├── menu_items_registry.test.ts │ └── menu_items_registry_cross_spreadsheet.test.ts ├── model │ ├── core.test.ts │ ├── data.test.ts │ ├── model.test.ts │ └── model_import_export.test.ts ├── pivots │ ├── add_dimension_button.test.ts │ ├── pivot_calculated_measure.test.ts │ ├── pivot_data.ts │ ├── pivot_helpers.test.ts │ ├── pivot_insert.test.ts │ ├── pivot_measure │ │ ├── pivot_measure_display_model.test.ts │ │ └── pivot_measure_display_panel.test.ts │ ├── pivot_menu_items.test.ts │ ├── pivot_plugin.test.ts │ ├── pivot_side_panel.test.ts │ └── spreadsheet_pivot │ │ ├── __snapshots__ │ │ └── spreadsheet_pivot_side_panel.test.ts.snap │ │ ├── date_spreadsheet_pivot.test.ts │ │ ├── spreadsheet_pivot.test.ts │ │ └── spreadsheet_pivot_side_panel.test.ts ├── popover │ ├── __snapshots__ │ │ └── error_tooltip_component.test.ts.snap │ ├── error_tooltip_component.test.ts │ └── popover_component.test.ts ├── range_plugin.test.ts ├── readme.md ├── remove_duplicates │ ├── remove_duplicates_plugin.test.ts │ └── remove_duplicates_side_panel_component.test.ts ├── renderer_store.test.ts ├── repeat_commands_plugin.test.ts ├── select_menu_component.test.ts ├── selection_input │ ├── selection_input_component.test.ts │ └── selection_input_store.test.ts ├── settings │ ├── settings_plugin.test.ts │ └── settings_side_panel.test.ts ├── setup │ ├── canvas.mock.ts │ ├── jest.setup.ts │ ├── jest_extend.ts │ ├── jest_global_setup.ts │ ├── jest_global_teardown.ts │ ├── polyfill.ts │ └── resize_observer.mock.ts ├── sheet │ ├── __snapshots__ │ │ └── sheet_manipulation_plugin.test.ts.snap │ ├── navigation_plugin.test.ts │ ├── selection_plugin.test.ts │ ├── sheet_manipulation_component.test.ts │ ├── sheet_manipulation_plugin.test.ts │ ├── sheets_plugin.test.ts │ └── sheetview_plugin.test.ts ├── side_panels │ ├── building_blocks │ │ ├── __snapshots__ │ │ │ ├── data_series.test.ts.snap │ │ │ ├── error_section.test.ts.snap │ │ │ ├── label_range.test.ts.snap │ │ │ ├── round_color_picker.test.ts.snap │ │ │ └── title.test.ts.snap │ │ ├── data_series.test.ts │ │ ├── error_section.test.ts │ │ ├── label_range.test.ts │ │ ├── round_color_picker.test.ts │ │ └── title.test.ts │ └── components │ │ ├── __snapshots__ │ │ ├── checkbox.test.ts.snap │ │ └── section.test.ts.snap │ │ ├── checkbox.test.ts │ │ └── section.test.ts ├── sort_plugin.test.ts ├── split_to_column │ ├── split_to_column_plugin.test.ts │ └── split_to_columns_panel.test.ts ├── spreadsheet │ ├── __snapshots__ │ │ └── spreadsheet_component.test.ts.snap │ ├── side_panel_component.test.ts │ └── spreadsheet_component.test.ts ├── table │ ├── __snapshots__ │ │ └── filter_menu_component.test.ts.snap │ ├── dynamic_table_plugin.test.ts │ ├── filter_evaluation_plugin.test.ts │ ├── filter_icon_overlay.test.ts │ ├── filter_menu_component.test.ts │ ├── table_autofill_plugin.test.ts │ ├── table_computed_style_plugin.test.ts │ ├── table_core_style_plugin.test.ts │ ├── table_dropdown_button_component.test.ts │ ├── table_helpers.test.ts │ ├── table_panel_component.test.ts │ ├── table_resize_ui_plugin.test.ts │ ├── table_resizer_component.test.ts │ ├── table_style_editor_panel_component.test.ts │ ├── table_styles_popover_component.test.ts │ └── tables_plugin.test.ts ├── test_helpers │ ├── chart_helpers.ts │ ├── clipboard.ts │ ├── commands_helpers.ts │ ├── constants.ts │ ├── debug_helpers.ts │ ├── dom_helper.ts │ ├── getters_helpers.ts │ ├── helpers.ts │ ├── index.ts │ ├── mock_helpers.ts │ ├── pivot_helpers.ts │ ├── renderer_helpers.ts │ ├── stores.ts │ └── xlsx.ts ├── top_bar_component.test.ts ├── trim_whitespace_plugin.test.ts └── xlsx │ ├── __snapshots__ │ └── xlsx_export.test.ts.snap │ ├── xlsx_export.test.ts │ ├── xlsx_import.test.ts │ └── xlsx_import_export.test.ts ├── tools ├── bundle.cjs ├── bundle_xlsx │ ├── unzip_xlsx_demo.cjs │ ├── unzip_xlsx_demo.sh │ ├── zip_xlsx_demo.cjs │ └── zip_xlsx_demo.sh ├── bundle_xml │ ├── bundle_xml_templates.cjs │ ├── main.cjs │ └── watch_xml_templates.cjs ├── owl_templates │ └── compile_templates.cjs ├── parse_message.cjs └── server │ └── main.cjs ├── tsconfig.base.json ├── tsconfig.jest.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "owl": "readonly", 10 | "o_spreadsheet": "readonly" 11 | }, 12 | "parser": "babel-eslint", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module" 16 | }, 17 | "rules": { 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | **Version (please indicate which version you are using):** 10 | - [ ] 16.0 11 | - [ ] 17.0 12 | - [ ] other: specify 13 | 14 | **Platform (OS and Browser + version):** 15 | example: Windows 10, Chrome 80.0.3987.149 16 | 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **To Reproduce** 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description: 2 | 3 | description of this task, what is implemented and why it is implemented that way. 4 | 5 | Task: [TASK_ID](https://www.odoo.com/odoo/2328/tasks/TASK_ID) 6 | 7 | ## review checklist 8 | 9 | - [ ] feature is organized in plugin, or UI components 10 | - [ ] support of duplicate sheet (deep copy) 11 | - [ ] in model/core: ranges are Range object, and can be adapted (adaptRanges) 12 | - [ ] in model/UI: ranges are strings (to show the user) 13 | - [ ] undo-able commands (uses this.history.update) 14 | - [ ] multiuser-able commands (has inverse commands and transformations where needed) 15 | - [ ] new/updated/removed commands are documented 16 | - [ ] exportable in excel 17 | - [ ] translations (\_t("qmsdf %s", abc)) 18 | - [ ] unit tested 19 | - [ ] clean commented code 20 | - [ ] track breaking changes 21 | - [ ] doc is rebuild (npm run doc) 22 | - [ ] status is correct in Odoo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | dist/ 5 | build/ 6 | .npmrc 7 | /coverage/ 8 | logs/ 9 | 10 | debug.log 11 | .git.cache 12 | tests/__xlsx__/xlsx_demo_data/* 13 | # Excel temp file 14 | tests/__xlsx__/~$xlsx_demo_data.xlsx 15 | 16 | tools/owl_templates/_compiled/* 17 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | if [ "$HUSKY_POST_CHECKOUT" != 0 ]; then 5 | if [ "$1" != "$2" ]; then 6 | npm install >/dev/null 2>&1 7 | fi 8 | fi 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .eslintrc.json 2 | .prettierignore 3 | COPYRIGHT 4 | LICENSE 5 | 6 | .husky/ 7 | .github/ 8 | .idea/ 9 | .vscode/ 10 | dist/ 11 | build/ 12 | .gitignore 13 | .npmrc 14 | coverage/ 15 | logs/ 16 | tests/**/__snapshots__ 17 | tests/**/xlsx_demo_data 18 | # Ignore all PNG files: 19 | **/*.png 20 | **/*.xlsx 21 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | 2 | Most of the files are 3 | 4 | Copyright (c) 2004-2015 Odoo S.A. 5 | 6 | Many files also contain contributions from third 7 | parties. In this case the original copyright of 8 | the contributions can be traced through the 9 | history of the source version control system. 10 | 11 | When that is not the case, the files contain a prominent 12 | notice stating the original copyright and applicable 13 | license, or come with their own dedicated COPYRIGHT 14 | and/or LICENSE file. 15 | 16 | -------------------------------------------------------------------------------- /demo/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/o-spreadsheet/bef1e2bd5da8a72feada65405a2fb2cf782d4ab3/demo/favicon.png -------------------------------------------------------------------------------- /demo/file_store.js: -------------------------------------------------------------------------------- 1 | export class FileStore { 2 | serverUrl = "http://localhost:9090/upload-image"; 3 | 4 | /** 5 | * Upload a file to the server to be saved. Returns the path of the file 6 | */ 7 | async upload(file) { 8 | const fd = new FormData(); 9 | fd.append("image", file /*, optional filename */); 10 | const res = await fetch(this.serverUrl, { 11 | method: "POST", 12 | body: fd, 13 | }); 14 | if (res.ok) { 15 | return await res.text(); 16 | } else { 17 | throw new Error(res.statusText); 18 | } 19 | } 20 | 21 | async delete(path) { 22 | console.warn("cannot delete file. Not implemented"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | O-spreadsheet Dev 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-size: 12px; 3 | } 4 | 5 | body { 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, 7 | "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 8 | "Noto Color Emoji" !important; 9 | } 10 | -------------------------------------------------------------------------------- /demo/readme.md: -------------------------------------------------------------------------------- 1 | This folder contains a demo spreadsheet application. 2 | 3 | **It is not suitable for production use!** 4 | 5 | This is only a simplified implementation for demonstration purposes. 6 | -------------------------------------------------------------------------------- /doc/add_right_click_item.md: -------------------------------------------------------------------------------- 1 | # Add an item on the right click 2 | 3 | ## Of a cell 4 | 5 | TODO 6 | -------------------------------------------------------------------------------- /doc/extending/ui_extension.md: -------------------------------------------------------------------------------- 1 | Here we should explain the UI extension, like Side Panels, menu Items, ... 2 | -------------------------------------------------------------------------------- /doc/o-spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/o-spreadsheet/bef1e2bd5da8a72feada65405a2fb2cf782d4ab3/doc/o-spreadsheet.png -------------------------------------------------------------------------------- /doc/o-spreadsheet_terminology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/o-spreadsheet/bef1e2bd5da8a72feada65405a2fb2cf782d4ab3/doc/o-spreadsheet_terminology.png -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The only aim of this file is to make ts-jest happy, as it does not support 3 | * Object.groupBy and Map.groupBy at the current version (29.1.2) 4 | * This is a workaround to make it work. 5 | */ 6 | 7 | interface ObjectConstructor { 8 | /** 9 | * Groups members of an iterable according to the return value of the passed callback. 10 | * @param items An iterable. 11 | * @param keySelector A callback which will be invoked for each item in items. 12 | */ 13 | groupBy( 14 | items: Iterable, 15 | keySelector: (item: T, index: number) => K 16 | ): Partial>; 17 | } 18 | 19 | interface MapConstructor { 20 | /** 21 | * Groups members of an iterable according to the return value of the passed callback. 22 | * @param items An iterable. 23 | * @param keySelector A callback which will be invoked for each item in items. 24 | */ 25 | groupBy(items: Iterable, keySelector: (item: T, index: number) => K): Map; 26 | } 27 | -------------------------------------------------------------------------------- /src/clipboard_handlers/abstract_clipboard_handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClipboardData, 3 | ClipboardOptions, 4 | ClipboardPasteTarget, 5 | CommandDispatcher, 6 | CommandResult, 7 | Getters, 8 | UID, 9 | Zone, 10 | } from "../types"; 11 | 12 | export class ClipboardHandler { 13 | constructor(protected getters: Getters, protected dispatch: CommandDispatcher["dispatch"]) {} 14 | 15 | copy(data: ClipboardData): T | undefined { 16 | return; 17 | } 18 | 19 | paste(target: ClipboardPasteTarget, clippedContent: T, options: ClipboardOptions) {} 20 | 21 | isPasteAllowed( 22 | sheetId: UID, 23 | target: Zone[], 24 | content: T, 25 | option: ClipboardOptions 26 | ): CommandResult { 27 | return CommandResult.Success; 28 | } 29 | 30 | isCutAllowed(data: ClipboardData): CommandResult { 31 | return CommandResult.Success; 32 | } 33 | 34 | getPasteTarget( 35 | sheetId: UID, 36 | target: Zone[], 37 | content: T, 38 | options: ClipboardOptions 39 | ): ClipboardPasteTarget { 40 | return { zones: [], sheetId }; 41 | } 42 | 43 | convertTextToClipboardData(data: string): T | undefined { 44 | return; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/clipboard_handlers/abstract_figure_clipboard_handler.ts: -------------------------------------------------------------------------------- 1 | import { ClipboardFigureData } from "../types"; 2 | import { ClipboardHandler } from "./abstract_clipboard_handler"; 3 | 4 | export class AbstractFigureClipboardHandler extends ClipboardHandler { 5 | copy(data: ClipboardFigureData): T | undefined { 6 | return; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/clipboard_handlers/references_clipboard.ts: -------------------------------------------------------------------------------- 1 | import { ClipboardCellData, ClipboardOptions, ClipboardPasteTarget, UID, Zone } from "../types"; 2 | import { AbstractCellClipboardHandler } from "./abstract_cell_clipboard_handler"; 3 | 4 | interface ClipboardContent { 5 | zones: Zone[]; 6 | sheetId: UID; 7 | } 8 | 9 | export class ReferenceClipboardHandler extends AbstractCellClipboardHandler { 10 | copy(data: ClipboardCellData): ClipboardContent | undefined { 11 | return { 12 | zones: data.clippedZones, 13 | sheetId: data.sheetId, 14 | }; 15 | } 16 | 17 | paste(target: ClipboardPasteTarget, content: ClipboardContent, options: ClipboardOptions) { 18 | if (options.isCutOperation) { 19 | const selection = target.zones[0]; 20 | this.dispatch("MOVE_RANGES", { 21 | target: content.zones, 22 | sheetId: content.sheetId, 23 | targetSheetId: target.sheetId, 24 | col: selection.left, 25 | row: selection.top, 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/clipboard_handlers/sheet_clipboard.ts: -------------------------------------------------------------------------------- 1 | import { getPasteZones } from "../helpers/clipboard/clipboard_helpers"; 2 | import { ClipboardOptions, CommandResult, UID, Zone } from "../types"; 3 | import { AbstractCellClipboardHandler } from "./abstract_cell_clipboard_handler"; 4 | 5 | type ClipboardContent = { 6 | cells: any[][]; 7 | zones: Zone[]; 8 | sheetId: UID; 9 | }; 10 | 11 | export class SheetClipboardHandler extends AbstractCellClipboardHandler { 12 | isPasteAllowed( 13 | sheetId: UID, 14 | target: Zone[], 15 | content: ClipboardContent, 16 | options: ClipboardOptions 17 | ): CommandResult { 18 | if (!("cells" in content)) { 19 | return CommandResult.Success; 20 | } 21 | const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId); 22 | for (const zone of getPasteZones(target, content.cells)) { 23 | if ( 24 | (zone.left < xSplit && zone.right >= xSplit) || 25 | (zone.top < ySplit && zone.bottom >= ySplit) 26 | ) { 27 | return CommandResult.FrozenPaneOverlap; 28 | } 29 | } 30 | return CommandResult.Success; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/collaborative/local_transport_service.ts: -------------------------------------------------------------------------------- 1 | import { UID } from "../types"; 2 | import { 3 | CollaborationMessage, 4 | NewMessageCallback, 5 | TransportService, 6 | } from "../types/collaborative/transport_service"; 7 | 8 | export class LocalTransportService implements TransportService { 9 | private listeners: { id: UID; callback: NewMessageCallback }[] = []; 10 | 11 | async sendMessage(message: CollaborationMessage) { 12 | for (const { callback } of this.listeners) { 13 | callback(message); 14 | } 15 | } 16 | onNewMessage(id: UID, callback: NewMessageCallback) { 17 | this.listeners.push({ id, callback }); 18 | } 19 | 20 | leave(id: UID) { 21 | this.listeners = this.listeners.filter((listener) => listener.id !== id); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/collaborative/ot/ot_helpers.ts: -------------------------------------------------------------------------------- 1 | import { expandZoneOnInsertion, reduceZoneOnDeletion } from "../../helpers"; 2 | import { CoreCommand, RangeData, UnboundedZone, Zone } from "../../types"; 3 | 4 | export function transformZone( 5 | zone: Z, 6 | executed: CoreCommand 7 | ): Z | undefined { 8 | if (executed.type === "REMOVE_COLUMNS_ROWS") { 9 | return reduceZoneOnDeletion( 10 | zone, 11 | executed.dimension === "COL" ? "left" : "top", 12 | executed.elements 13 | ); 14 | } 15 | if (executed.type === "ADD_COLUMNS_ROWS") { 16 | return expandZoneOnInsertion( 17 | zone, 18 | executed.dimension === "COL" ? "left" : "top", 19 | executed.base, 20 | executed.position, 21 | executed.quantity 22 | ); 23 | } 24 | return zone; 25 | } 26 | 27 | export function transformRangeData(range: RangeData, executed: CoreCommand): RangeData | undefined { 28 | const deletedSheet = executed.type === "DELETE_SHEET" && executed.sheetId; 29 | 30 | if ("sheetId" in executed && range._sheetId !== executed.sheetId) { 31 | return range; 32 | } else { 33 | const newZone = transformZone(range._zone, executed); 34 | if (newZone && deletedSheet !== range._sheetId) { 35 | return { ...range, _zone: newZone }; 36 | } 37 | } 38 | return undefined; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/action_button/action_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/animation/ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/autofill/autofill.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
10 | 11 |
12 | 13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /src/components/border_editor/border_editor_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 10 | 11 | 12 | 13 | 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/components/bottom_bar/bottom_bar_statistic/bottom_bar_statistic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/components/collaborative_client_tag/collaborative_client_tag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/color_picker/color_picker_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 11 | 12 | 13 | 14 | 15 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/components/composer/grid_composer/grid_composer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
9 |
10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/composer/standalone_composer/standalone_composer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/components/composer/top_bar_composer/top_bar_composer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
10 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 11 | 12 | 17 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/data_validation_overlay/data_validation_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/data_validation_overlay/dv_checkbox/dv_checkbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/data_validation_overlay/dv_list_icon/dv_list_icon.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { GRID_ICON_EDGE_LENGTH, TEXT_BODY_MUTED } from "../../../constants"; 3 | import { CellPosition, SpreadsheetChildEnv } from "../../../types"; 4 | import { css } from "../../helpers"; 5 | 6 | const ICON_WIDTH = 13; 7 | 8 | css/* scss */ ` 9 | .o-dv-list-icon { 10 | color: ${TEXT_BODY_MUTED}; 11 | border-radius: 1px; 12 | height: ${GRID_ICON_EDGE_LENGTH}px; 13 | width: ${GRID_ICON_EDGE_LENGTH}px; 14 | 15 | &:hover { 16 | color: #ffffff; 17 | background-color: ${TEXT_BODY_MUTED}; 18 | } 19 | 20 | svg { 21 | width: ${ICON_WIDTH}px; 22 | height: ${ICON_WIDTH}px; 23 | } 24 | } 25 | `; 26 | 27 | interface Props { 28 | cellPosition: CellPosition; 29 | } 30 | 31 | export class DataValidationListIcon extends Component { 32 | static template = "o-spreadsheet-DataValidationListIcon"; 33 | static props = { 34 | cellPosition: Object, 35 | }; 36 | 37 | onClick() { 38 | const { col, row } = this.props.cellPosition; 39 | this.env.model.selection.selectCell(col, row); 40 | this.env.startCellEdition(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/data_validation_overlay/dv_list_icon/dv_list_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /src/components/error_tooltip/error_tooltip.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/components/figures/chart/chartJs/chartjs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/figures/chart/gauge/gauge_chart_component.ts: -------------------------------------------------------------------------------- 1 | import { Component, useEffect, useRef } from "@odoo/owl"; 2 | import { drawGaugeChart } from "../../../../helpers/figures/charts/gauge_chart_rendering"; 3 | import { Figure, SpreadsheetChildEnv } from "../../../../types"; 4 | import { GaugeChartRuntime } from "../../../../types/chart"; 5 | 6 | interface Props { 7 | figure: Figure; 8 | } 9 | 10 | export class GaugeChartComponent extends Component { 11 | static template = "o-spreadsheet-GaugeChartComponent"; 12 | private canvas = useRef("chartContainer"); 13 | 14 | get runtime(): GaugeChartRuntime { 15 | return this.env.model.getters.getChartRuntime(this.props.figure.id) as GaugeChartRuntime; 16 | } 17 | 18 | setup() { 19 | useEffect( 20 | () => drawGaugeChart(this.canvas.el as HTMLCanvasElement, this.runtime), 21 | () => { 22 | const canvas = this.canvas.el as HTMLCanvasElement; 23 | const rect = canvas.getBoundingClientRect(); 24 | return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio]; 25 | } 26 | ); 27 | } 28 | } 29 | 30 | GaugeChartComponent.props = { 31 | figure: Object, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/figures/chart/gauge/gauge_chart_component.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/figures/chart/scorecard/chart_scorecard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/figures/figure_chart/figure_chart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/components/figures/figure_image/figure_image.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { Figure, SpreadsheetChildEnv, UID } from "../../../types"; 3 | 4 | interface Props { 5 | figure: Figure; 6 | onFigureDeleted: () => void; 7 | } 8 | 9 | export class ImageFigure extends Component { 10 | static template = "o-spreadsheet-ImageFigure"; 11 | static props = { 12 | figure: Object, 13 | onFigureDeleted: Function, 14 | }; 15 | static components = {}; 16 | 17 | // --------------------------------------------------------------------------- 18 | // Getters 19 | // --------------------------------------------------------------------------- 20 | 21 | get figureId(): UID { 22 | return this.props.figure.id; 23 | } 24 | 25 | get getImagePath(): string { 26 | return this.env.model.getters.getImagePath(this.figureId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/figures/figure_image/figure_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/filters/filter_icon/filter_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /src/components/filters/filter_icons_overlay/filter_icons_overlay.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { CellPosition, SpreadsheetChildEnv } from "../../../types"; 3 | import { GridCellIcon } from "../../grid_cell_icon/grid_cell_icon"; 4 | import { FilterIcon } from "../filter_icon/filter_icon"; 5 | 6 | export class FilterIconsOverlay extends Component<{}, SpreadsheetChildEnv> { 7 | static template = "o-spreadsheet-FilterIconsOverlay"; 8 | static props = {}; 9 | static components = { 10 | GridCellIcon, 11 | FilterIcon, 12 | }; 13 | 14 | getFilterHeadersPositions(): CellPosition[] { 15 | const sheetId = this.env.model.getters.getActiveSheetId(); 16 | return this.env.model.getters.getFilterHeaders(sheetId); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/filters/filter_icons_overlay/filter_icons_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/filters/filter_menu_item/filter_menu_value_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
9 |
10 |
11 | 12 |
13 |
14 |
15 | (Blanks) 16 | 17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/components/focus_store.ts: -------------------------------------------------------------------------------- 1 | // The name is misleading and can be confused with the DOM focus. 2 | export class FocusStore { 3 | mutators = ["focus", "unfocus"] as const; 4 | public focusedElement: object | null = null; 5 | 6 | focus(element: object) { 7 | this.focusedElement = element; 8 | } 9 | 10 | unfocus(element: object) { 11 | if (this.focusedElement && this.focusedElement === element) { 12 | this.focusedElement = null; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/grid/hovered_cell_store.ts: -------------------------------------------------------------------------------- 1 | import { SpreadsheetStore } from "../../stores"; 2 | import { Command, Position } from "../../types"; 3 | 4 | export class HoveredCellStore extends SpreadsheetStore { 5 | mutators = ["clear", "hover"] as const; 6 | col: number | undefined; 7 | row: number | undefined; 8 | 9 | handle(cmd: Command) { 10 | switch (cmd.type) { 11 | case "ACTIVATE_SHEET": 12 | this.clear(); 13 | } 14 | } 15 | 16 | hover(position: Position) { 17 | if (position.col === this.col && position.row === this.row) { 18 | return "noStateChange"; 19 | } 20 | this.col = position.col; 21 | this.row = position.row; 22 | return; 23 | } 24 | 25 | clear() { 26 | if (this.col === undefined && this.row === undefined) { 27 | return "noStateChange"; 28 | } 29 | this.col = undefined; 30 | this.row = undefined; 31 | return; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/grid_add_rows_footer/grid_add_rows_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
7 | 13 | 23 | more rows at the bottom 24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /src/components/grid_cell_icon/grid_cell_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
7 | 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/components/grid_overlay/grid_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
11 | 12 | 13 | 14 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/components/grid_popover/grid_popover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/header_group/header_group.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
9 |
12 |
15 | 16 |
17 |
18 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/helpers/autofocus_hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "@odoo/owl"; 2 | 3 | export function useAutofocus({ refName }: { refName: string }) { 4 | const ref = useRef(refName); 5 | useEffect( 6 | (el) => { 7 | el?.focus(); 8 | }, 9 | () => [ref.el] 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/helpers/highlight_hook.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, useEffect } from "@odoo/owl"; 2 | import { deepEquals } from "../../helpers"; 3 | import { useLocalStore, useStoreProvider } from "../../store_engine"; 4 | import { HighlightProvider, HighlightStore } from "../../stores/highlight_store"; 5 | import { Ref } from "../../types"; 6 | import { useHoveredElement } from "./listener_hook"; 7 | 8 | export function useHighlightsOnHover(ref: Ref, highlightProvider: HighlightProvider) { 9 | const hoverState = useHoveredElement(ref); 10 | useHighlights({ 11 | get highlights() { 12 | return hoverState.hovered ? highlightProvider.highlights : []; 13 | }, 14 | }); 15 | } 16 | 17 | export function useHighlights(highlightProvider: HighlightProvider) { 18 | const stores = useStoreProvider(); 19 | const store = useLocalStore(HighlightStore); 20 | onMounted(() => { 21 | store.register(highlightProvider); 22 | }); 23 | let currentHighlights = highlightProvider.highlights; 24 | useEffect( 25 | (highlights) => { 26 | if (!deepEquals(highlights, currentHighlights)) { 27 | currentHighlights = highlights; 28 | stores.trigger("store-updated"); 29 | } 30 | }, 31 | () => [highlightProvider.highlights] 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/helpers/html_content_helpers.ts: -------------------------------------------------------------------------------- 1 | import { HtmlContent } from "../composer/composer/composer"; 2 | 3 | export function getHtmlContentFromPattern( 4 | pattern: string, 5 | value: string, 6 | highlightColor: string, 7 | className: string 8 | ): HtmlContent[] { 9 | const pendingHtmlContent: HtmlContent[] = []; 10 | pattern = pattern.toLowerCase(); 11 | 12 | for (const patternChar of pattern) { 13 | const index = value.toLocaleLowerCase().indexOf(patternChar); 14 | if (index === -1) { 15 | continue; 16 | } 17 | pendingHtmlContent.push( 18 | { value: value.slice(0, index), color: "" }, 19 | { value: value[index], color: highlightColor, class: className } 20 | ); 21 | value = value.slice(index + 1); 22 | } 23 | 24 | pendingHtmlContent.push({ value }); 25 | const htmlContent = pendingHtmlContent.filter((content) => content.value); 26 | 27 | return htmlContent; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./css"; 2 | -------------------------------------------------------------------------------- /src/components/helpers/listener_hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "@odoo/owl"; 2 | import { Ref } from "../../types"; 3 | 4 | /** 5 | * Manages an event listener on a ref. Useful for hooks that want to manage 6 | * event listeners, especially more than one. Prefer using t-on directly in 7 | * components. If your hook only needs a single event listener, consider simply 8 | * returning it from the hook and letting the user attach it with t-on. 9 | * 10 | * Adapted from Odoo Community - See https://github.com/odoo/odoo/blob/saas-16.2/addons/web/static/src/core/utils/hooks.js 11 | */ 12 | export function useRefListener( 13 | ref: Ref, 14 | ...listener: Parameters 15 | ) { 16 | useEffect( 17 | (el: HTMLElement | null) => { 18 | el?.addEventListener(...listener); 19 | return () => el?.removeEventListener(...listener); 20 | }, 21 | () => [ref.el] 22 | ); 23 | } 24 | 25 | export function useHoveredElement(ref: Ref) { 26 | const state = useState({ hovered: false }); 27 | useRefListener(ref, "mouseenter", () => (state.hovered = true)); 28 | useRefListener(ref, "mouseleave", () => (state.hovered = false)); 29 | return state; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/helpers/selection_helpers.ts: -------------------------------------------------------------------------------- 1 | import { SelectionStreamProcessor } from "../../selection_stream/selection_stream_processor"; 2 | import { isCtrlKey } from "./dom_helpers"; 3 | 4 | const arrowMap = { 5 | ArrowDown: "down", 6 | ArrowLeft: "left", 7 | ArrowRight: "right", 8 | ArrowUp: "up", 9 | }; 10 | 11 | export function updateSelectionWithArrowKeys( 12 | ev: KeyboardEvent, 13 | selection: SelectionStreamProcessor 14 | ) { 15 | const direction = arrowMap[ev.key]; 16 | if (ev.shiftKey) { 17 | selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1); 18 | } else { 19 | selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/helpers/wheel_hook.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_CELL_HEIGHT } from "../../constants"; 2 | import { isMacOS } from "./dom_helpers"; 3 | 4 | export function useWheelHandler(handler: (deltaX: number, deltaY: number) => void) { 5 | function normalize(val: number, deltaMode: number): number { 6 | return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT); 7 | } 8 | const onMouseWheel = (ev: WheelEvent) => { 9 | const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode); 10 | const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode); 11 | handler(deltaX, deltaY); 12 | }; 13 | return onMouseWheel; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/highlight/border/border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/highlight/corner/corner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/highlight/highlight/highlight.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 11 | 12 | 13 | 20 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/icon_picker/icon_picker.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { ACTION_COLOR, BADGE_SELECTED_COLOR, ComponentsImportance } from "../../constants"; 3 | import { SpreadsheetChildEnv } from "../../types/env"; 4 | import { css } from "../helpers/css"; 5 | import { ICONS, ICON_SETS } from "../icons/icons"; 6 | 7 | interface Props { 8 | onIconPicked: (icon: string) => void; 9 | } 10 | 11 | css/* scss */ ` 12 | .o-icon-picker { 13 | position: absolute; 14 | z-index: ${ComponentsImportance.IconPicker}; 15 | box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15); 16 | background-color: white; 17 | padding: 2px 1px; 18 | } 19 | .o-cf-icon-line { 20 | display: flex; 21 | padding: 0 6px; 22 | } 23 | .o-icon-picker-item { 24 | cursor: pointer; 25 | &:hover { 26 | background-color: ${BADGE_SELECTED_COLOR}; 27 | outline: ${ACTION_COLOR} solid 1px; 28 | } 29 | } 30 | `; 31 | 32 | export class IconPicker extends Component { 33 | static template = "o-spreadsheet-IconPicker"; 34 | static props = { 35 | onIconPicked: Function, 36 | }; 37 | icons = ICONS; 38 | iconSets = ICON_SETS; 39 | 40 | onIconClick(icon: string) { 41 | if (icon) { 42 | this.props.onIconPicked(icon); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/icon_picker/icon_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
9 | 10 |
11 |
14 | 15 |
16 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Spreadsheet } from "./spreadsheet/spreadsheet"; 2 | -------------------------------------------------------------------------------- /src/components/link/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./link_display/link_display"; 2 | export * from "./link_editor/link_editor"; 3 | -------------------------------------------------------------------------------- /src/components/paint_format_button/paint_format_button.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { Store, useStore } from "../../store_engine"; 3 | import { SpreadsheetChildEnv } from "../../types"; 4 | import { PaintFormatStore } from "./paint_format_store"; 5 | 6 | interface Props { 7 | class?: string; 8 | } 9 | 10 | export class PaintFormatButton extends Component { 11 | static template = "o-spreadsheet-PaintFormatButton"; 12 | static props = { 13 | class: { type: String, optional: true }, 14 | }; 15 | 16 | private paintFormatStore!: Store; 17 | 18 | setup() { 19 | this.paintFormatStore = useStore(PaintFormatStore); 20 | } 21 | 22 | get isActive() { 23 | return this.paintFormatStore.isActive; 24 | } 25 | 26 | onDblClick() { 27 | this.paintFormatStore.activate({ persistent: true }); 28 | } 29 | 30 | togglePaintFormat() { 31 | if (this.isActive) { 32 | this.paintFormatStore.cancel(); 33 | } else { 34 | this.paintFormatStore.activate({ persistent: false }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/paint_format_button/paint_format_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/popover/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell_popover_store"; 2 | export * from "./popover"; 3 | export * from "./popover_builders"; 4 | -------------------------------------------------------------------------------- /src/components/popover/popover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
10 | 11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/components/popover/popover_builders.ts: -------------------------------------------------------------------------------- 1 | import { cellPopoverRegistry } from "../../registries/cell_popovers_registry"; 2 | import { ErrorToolTipPopoverBuilder } from "../error_tooltip/error_tooltip"; 3 | import { FilterMenuPopoverBuilder } from "../filters/filter_menu/filter_menu"; 4 | import { LinkCellPopoverBuilder, LinkEditorPopoverBuilder } from "../link"; 5 | 6 | cellPopoverRegistry 7 | .add("ErrorToolTip", ErrorToolTipPopoverBuilder) 8 | .add("LinkCell", LinkCellPopoverBuilder) 9 | .add("LinkEditor", LinkEditorPopoverBuilder) 10 | .add("FilterMenu", FilterMenuPopoverBuilder); 11 | -------------------------------------------------------------------------------- /src/components/scrollbar.ts: -------------------------------------------------------------------------------- 1 | import { Pixel } from "../types"; 2 | 3 | export type ScrollDirection = "horizontal" | "vertical"; 4 | 5 | export class ScrollBar { 6 | private direction: ScrollDirection; 7 | el: HTMLElement; 8 | constructor(el: HTMLElement | null, direction: ScrollDirection) { 9 | this.el = el!; 10 | this.direction = direction; 11 | } 12 | 13 | get scroll(): Pixel { 14 | return this.direction === "horizontal" ? this.el.scrollLeft : this.el.scrollTop; 15 | } 16 | 17 | set scroll(value: Pixel) { 18 | if (this.direction === "horizontal") { 19 | this.el.scrollLeft = value; 20 | } else { 21 | this.el.scrollTop = value; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/scrollbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./scrollbar_horizontal"; 2 | export * from "./scrollbar_vertical"; 3 | -------------------------------------------------------------------------------- /src/components/scrollbar/scrollbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/bar_chart/bar_chart_config_panel.ts: -------------------------------------------------------------------------------- 1 | import { BarChartDefinition } from "../../../../types/chart"; 2 | import { GenericChartConfigPanel } from "../building_blocks/generic_side_panel/config_panel"; 3 | 4 | export class BarConfigPanel extends GenericChartConfigPanel { 5 | static template = "o-spreadsheet-BarConfigPanel"; 6 | 7 | get stackedLabel(): string { 8 | const definition = this.props.definition as BarChartDefinition; 9 | return definition.horizontal 10 | ? this.chartTerms.StackedBarChart 11 | : this.chartTerms.StackedColumnChart; 12 | } 13 | 14 | onUpdateStacked(stacked: boolean) { 15 | this.props.updateChart(this.props.figureId, { 16 | stacked, 17 | }); 18 | } 19 | 20 | onUpdateAggregated(aggregated: boolean) { 21 | this.props.updateChart(this.props.figureId, { 22 | aggregated, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/bar_chart/bar_chart_config_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 11 |
12 | 17 | 24 | 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Axis title 4 |
5 | 10 |
11 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/data_series/data_series.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { _t } from "../../../../../translation"; 3 | import { Color, CustomizedDataSet, SpreadsheetChildEnv } from "../../../../../types"; 4 | import { SelectionInput } from "../../../../selection_input/selection_input"; 5 | import { Section } from "../../../components/section/section"; 6 | 7 | interface Props { 8 | ranges: CustomizedDataSet[]; 9 | hasSingleRange?: boolean; 10 | onSelectionChanged: (ranges: string[]) => void; 11 | onSelectionConfirmed: () => void; 12 | } 13 | 14 | export class ChartDataSeries extends Component { 15 | static template = "o-spreadsheet.ChartDataSeries"; 16 | static components = { SelectionInput, Section }; 17 | static props = { 18 | ranges: Array, 19 | hasSingleRange: { type: Boolean, optional: true }, 20 | onSelectionChanged: Function, 21 | onSelectionConfirmed: Function, 22 | }; 23 | 24 | get ranges(): string[] { 25 | return this.props.ranges.map((r) => r.dataRange); 26 | } 27 | 28 | get colors(): (Color | undefined)[] { 29 | return this.props.ranges.map((r) => r.backgroundColor); 30 | } 31 | 32 | get title() { 33 | return this.props.hasSingleRange ? _t("Data range") : _t("Data series"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/data_series/data_series.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/error_section/error_section.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { SpreadsheetChildEnv } from "../../../../../types"; 3 | import { ValidationMessages } from "../../../../validation_messages/validation_messages"; 4 | import { Section } from "../../../components/section/section"; 5 | 6 | interface Props { 7 | messages: string[]; 8 | } 9 | 10 | export class ChartErrorSection extends Component { 11 | static template = "o-spreadsheet.ChartErrorSection"; 12 | static components = { Section, ValidationMessages }; 13 | static props = { messages: { type: Array, element: String } }; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/error_section/error_section.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/general_design/general_design_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chart title 4 | 5 | General 6 | 7 |
8 | Background color 9 | 13 |
14 | 24 | 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/generic_side_panel/config_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 16 | 17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/label_range/label_range.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { _t } from "../../../../../translation"; 3 | import { SpreadsheetChildEnv } from "../../../../../types"; 4 | import { SelectionInput } from "../../../../selection_input/selection_input"; 5 | import { Checkbox } from "../../../components/checkbox/checkbox"; 6 | import { Section } from "../../../components/section/section"; 7 | 8 | interface Props { 9 | title?: string; 10 | range: string; 11 | isInvalid: boolean; 12 | onSelectionChanged: (range: string) => void; 13 | onSelectionConfirmed: () => void; 14 | options?: Array<{ 15 | name: string; 16 | label: string; 17 | value: boolean; 18 | onChange: (value: boolean) => void; 19 | }>; 20 | } 21 | 22 | export class ChartLabelRange extends Component { 23 | static template = "o-spreadsheet.ChartLabelRange"; 24 | static components = { SelectionInput, Checkbox, Section }; 25 | static props = { 26 | title: { type: String, optional: true }, 27 | range: String, 28 | isInvalid: Boolean, 29 | onSelectionChanged: Function, 30 | onSelectionConfirmed: Function, 31 | options: { type: Array, optional: true }, 32 | }; 33 | 34 | static defaultProps: Partial = { 35 | title: _t("Categories / Labels"), 36 | options: [], 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/building_blocks/label_range/label_range.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 12 | 13 | 20 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/combo_chart/combo_chart_config_panel.ts: -------------------------------------------------------------------------------- 1 | import { GenericChartConfigPanel } from "../building_blocks/generic_side_panel/config_panel"; 2 | 3 | export class ComboChartConfigPanel extends GenericChartConfigPanel { 4 | static template = "o-spreadsheet-ComboChartConfigPanel"; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/combo_chart/combo_chart_config_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 16 | 17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/gauge_chart_panel/gauge_chart_config_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 10 | 11 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/line_chart/line_chart_config_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 12 | 18 |
19 | 24 | 31 | 32 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/pie_chart/pie_chart_design_panel.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { DispatchResult, SpreadsheetChildEnv, UID } from "../../../../types"; 3 | import { PieChartDefinition } from "../../../../types/chart"; 4 | import { Checkbox } from "../../components/checkbox/checkbox"; 5 | import { Section } from "../../components/section/section"; 6 | import { GeneralDesignEditor } from "../building_blocks/general_design/general_design_editor"; 7 | 8 | interface Props { 9 | figureId: UID; 10 | definition: PieChartDefinition; 11 | canUpdateChart: (figureID: UID, definition: Partial) => DispatchResult; 12 | updateChart: (figureId: UID, definition: Partial) => DispatchResult; 13 | } 14 | 15 | export class PieChartDesignPanel extends Component { 16 | static template = "o-spreadsheet-PieChartDesignPanel"; 17 | static components = { 18 | GeneralDesignEditor, 19 | Section, 20 | Checkbox, 21 | }; 22 | static props = { 23 | figureId: String, 24 | definition: Object, 25 | updateChart: Function, 26 | canUpdateChart: { type: Function, optional: true }, 27 | }; 28 | 29 | updateLegendPosition(ev) { 30 | this.props.updateChart(this.props.figureId, { 31 | legendPosition: ev.target.value, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/pie_chart/pie_chart_design_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |
9 | Legend position 10 | 20 |
21 |
22 | Values 23 | 29 |
30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/components/side_panel/chart/scatter_chart/scatter_chart_config_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 16 | 17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/components/side_panel/components/badge_selection/badge_selection.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { ACTION_COLOR, BADGE_SELECTED_COLOR, GRAY_900 } from "../../../../constants"; 3 | import { SpreadsheetChildEnv } from "../../../../types"; 4 | import { css } from "../../../helpers/css"; 5 | 6 | interface Choice { 7 | value: string; 8 | label: string; 9 | } 10 | 11 | interface Props { 12 | choices: Choice[]; 13 | onChange: (value: string) => void; 14 | selectedValue: string; 15 | } 16 | 17 | css/* scss */ ` 18 | .o-badge-selection { 19 | gap: 1px; 20 | button.o-button { 21 | border-radius: 0; 22 | &.selected { 23 | color: ${GRAY_900}; 24 | border-color: ${ACTION_COLOR}; 25 | background: ${BADGE_SELECTED_COLOR}; 26 | font-weight: 600; 27 | } 28 | 29 | &:first-child { 30 | border-radius: 4px 0 0 4px; 31 | } 32 | &:last-child { 33 | border-radius: 0 4px 4px 0; 34 | } 35 | } 36 | } 37 | `; 38 | 39 | export class BadgeSelection extends Component { 40 | static template = "o-spreadsheet.BadgeSelection"; 41 | static props = { 42 | choices: Array, 43 | onChange: Function, 44 | selectedValue: String, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/side_panel/components/badge_selection/badge_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/components/side_panel/components/checkbox/checkbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/side_panel/components/cog_wheel_menu/cog_wheel_menu.ts: -------------------------------------------------------------------------------- 1 | import { Component, useRef, useState } from "@odoo/owl"; 2 | import { ActionSpec, createActions } from "../../../../actions/action"; 3 | import { MenuMouseEvent } from "../../../../types"; 4 | import { SpreadsheetChildEnv } from "../../../../types/env"; 5 | import { Menu, MenuState } from "../../../menu/menu"; 6 | 7 | interface Props { 8 | items: ActionSpec[]; 9 | } 10 | 11 | export class CogWheelMenu extends Component { 12 | static template = "o-spreadsheet-CogWheelMenu"; 13 | static components = { Menu }; 14 | static props = { 15 | items: Array, 16 | }; 17 | 18 | private buttonRef = useRef("button"); 19 | private menuState: MenuState = useState({ isOpen: false, position: null, menuItems: [] }); 20 | 21 | private menuId = this.env.model.uuidGenerator.uuidv4(); 22 | 23 | toggleMenu(ev: MenuMouseEvent) { 24 | if (ev.closedMenuId === this.menuId) { 25 | return; 26 | } 27 | 28 | const { x, y } = this.buttonRef.el!.getBoundingClientRect(); 29 | this.menuState.isOpen = !this.menuState.isOpen; 30 | this.menuState.position = { x, y }; 31 | this.menuState.menuItems = createActions(this.props.items); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/side_panel/components/cog_wheel_menu/cog_wheel_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/side_panel/components/collapsible/side_panel_collapsible.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/components/side_panel/components/radio_selection/radio_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
8 | 9 | 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/components/side_panel/components/round_color_picker/round_color_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/side_panel/components/section/section.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { SpreadsheetChildEnv } from "../../../../types"; 3 | 4 | interface Props { 5 | class?: string; 6 | } 7 | 8 | export class Section extends Component { 9 | static template = "o_spreadsheet.Section"; 10 | static props = { 11 | class: { type: String, optional: true }, 12 | slots: Object, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/side_panel/components/section/section.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/components/side_panel/conditional_formatting/cf_editor/data_bar_rule_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
Color
5 | 10 |
Range of values
11 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/components/side_panel/conditional_formatting/cf_preview_list/cf_preview_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
8 | 14 |
15 |
16 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/side_panel/conditional_formatting/conditional_formatting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/data_validation_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 11 | 12 |
13 | 16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_criterion_form.ts: -------------------------------------------------------------------------------- 1 | import { Component, onMounted } from "@odoo/owl"; 2 | import { useStore } from "../../../../store_engine"; 3 | import { DataValidationCriterion, SpreadsheetChildEnv } from "../../../../types"; 4 | import { ComposerFocusStore } from "../../../composer/composer_focus_store"; 5 | 6 | interface Props { 7 | criterion: T; 8 | onCriterionChanged: (criterion: DataValidationCriterion) => void; 9 | } 10 | 11 | export abstract class DataValidationCriterionForm< 12 | T extends DataValidationCriterion = DataValidationCriterion 13 | > extends Component, SpreadsheetChildEnv> { 14 | static props = { 15 | criterion: Object, 16 | onCriterionChanged: Function, 17 | }; 18 | setup() { 19 | const composerFocusStore = useStore(ComposerFocusStore); 20 | onMounted(() => { 21 | composerFocusStore.activeComposer.stopEdition(); 22 | }); 23 | } 24 | 25 | updateCriterion(criterion: Partial) { 26 | const filteredCriterion = { 27 | ...this.props.criterion, 28 | ...criterion, 29 | }; 30 | this.props.onCriterionChanged(filteredCriterion); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_date_criterion/dv_date_criterion.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_double_input_criterion/dv_double_input_criterion.ts: -------------------------------------------------------------------------------- 1 | import { DataValidationCriterionForm } from "../dv_criterion_form"; 2 | import { DataValidationInput } from "../dv_input/dv_input"; 3 | 4 | export class DataValidationDoubleInputCriterionForm extends DataValidationCriterionForm { 5 | static template = "o-spreadsheet-DataValidationDoubleInput"; 6 | static components = { DataValidationInput }; 7 | 8 | onFirstValueChanged(value: string) { 9 | const values = this.props.criterion.values; 10 | this.updateCriterion({ 11 | values: [value, values[1]], 12 | }); 13 | } 14 | 15 | onSecondValueChanged(value: string) { 16 | const values = this.props.criterion.values; 17 | this.updateCriterion({ 18 | values: [values[0], value], 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_double_input_criterion/dv_double_input_criterion.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 |
and
10 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_input/dv_input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 18 | 22 | 23 | 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_single_input_criterion/dv_single_input_criterion.ts: -------------------------------------------------------------------------------- 1 | import { deepCopy } from "../../../../../helpers"; 2 | import { DataValidationCriterionForm } from "../dv_criterion_form"; 3 | import { DataValidationInput } from "../dv_input/dv_input"; 4 | 5 | export class DataValidationSingleInputCriterionForm extends DataValidationCriterionForm { 6 | static template = "o-spreadsheet-DataValidationSingleInput"; 7 | static components = { DataValidationInput }; 8 | 9 | onValueChanged(value: string) { 10 | const criterion = deepCopy(this.props.criterion); 11 | criterion.values[0] = value; 12 | this.updateCriterion(criterion); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_single_input_criterion/dv_single_input_criterion.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_value_in_range_criterion/dv_value_in_range_criterion.ts: -------------------------------------------------------------------------------- 1 | import { onWillStart, onWillUpdateProps } from "@odoo/owl"; 2 | import { IsValueInRangeCriterion } from "../../../../../types"; 3 | import { SelectionInput } from "../../../../selection_input/selection_input"; 4 | import { DataValidationCriterionForm } from "../dv_criterion_form"; 5 | 6 | export class DataValidationValueInRangeCriterionForm extends DataValidationCriterionForm { 7 | static template = "o-spreadsheet-DataValidationValueInRangeCriterionForm"; 8 | static components = { SelectionInput }; 9 | 10 | setup() { 11 | super.setup(); 12 | const setupDefault = (props: this["props"]) => { 13 | if (props.criterion.displayStyle === undefined) { 14 | this.updateCriterion({ displayStyle: "arrow" }); 15 | } 16 | }; 17 | onWillUpdateProps(setupDefault); 18 | onWillStart(() => setupDefault(this.props)); 19 | } 20 | 21 | onRangeChanged(rangeXc: string) { 22 | this.updateCriterion({ values: [rangeXc] }); 23 | } 24 | 25 | onChangedDisplayStyle(ev: Event) { 26 | const displayStyle = (ev.target as HTMLInputElement).value as "arrow" | "plainText"; 27 | this.updateCriterion({ displayStyle }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_criterion_form/dv_value_in_range_criterion/dv_value_in_range_criterion.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 |
Display style
11 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/components/side_panel/data_validation/dv_preview/dv_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 | 13 |
14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/side_panel/more_formats/more_formats.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/editable_name/editable_name.ts: -------------------------------------------------------------------------------- 1 | /** @odoo-module */ 2 | 3 | import { Component, useState } from "@odoo/owl"; 4 | import { SpreadsheetChildEnv } from "../../../../types"; 5 | 6 | interface Props { 7 | name: string; 8 | displayName: string; 9 | onChanged: (name: string) => void; 10 | } 11 | 12 | export class EditableName extends Component { 13 | static template = "o-spreadsheet-EditableName"; 14 | static props = { 15 | name: String, 16 | displayName: String, 17 | onChanged: Function, 18 | }; 19 | private state!: { isEditing: boolean; name: string }; 20 | 21 | setup() { 22 | this.state = useState({ 23 | isEditing: false, 24 | name: "", 25 | }); 26 | } 27 | 28 | rename() { 29 | this.state.isEditing = true; 30 | this.state.name = this.props.name; 31 | } 32 | 33 | save() { 34 | this.props.onChanged(this.state.name.trim()); 35 | this.state.isEditing = false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/editable_name/editable_name.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_defer_update/pivot_defer_update.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { _t } from "../../../../translation"; 3 | import { SpreadsheetChildEnv } from "../../../../types"; 4 | import { css } from "../../../helpers/css"; 5 | import { Checkbox } from "../../components/checkbox/checkbox"; 6 | import { Section } from "../../components/section/section"; 7 | 8 | css/* scss */ ` 9 | .pivot-defer-update { 10 | min-height: 35px; 11 | } 12 | `; 13 | 14 | interface Props { 15 | deferUpdate: boolean; 16 | isDirty: boolean; 17 | toggleDeferUpdate: (value: boolean) => void; 18 | discard: () => void; 19 | apply: () => void; 20 | } 21 | 22 | export class PivotDeferUpdate extends Component { 23 | static template = "o-spreadsheet-PivotDeferUpdate"; 24 | static props = { 25 | deferUpdate: Boolean, 26 | isDirty: Boolean, 27 | toggleDeferUpdate: Function, 28 | discard: Function, 29 | apply: Function, 30 | }; 31 | static components = { 32 | Section, 33 | Checkbox, 34 | }; 35 | 36 | get deferUpdatesLabel() { 37 | return _t("Defer updates"); 38 | } 39 | 40 | get deferUpdatesTooltip() { 41 | return _t("Changing the pivot definition requires to reload the data. It may take some time."); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_defer_update/pivot_defer_update.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
5 | 11 |
12 | 17 | 21 | Update 22 | 23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_layout_configurator/add_dimension_button/add_dimension_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_layout_configurator/pivot_dimension/pivot_dimension.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
6 |
7 |
8 | 9 | 10 | 11 | 17 | 18 |
19 |
20 | 21 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_layout_configurator/pivot_dimension_granularity/pivot_dimension_granularity.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { ALL_PERIODS } from "../../../../../helpers/pivot/pivot_helpers"; 3 | import { SpreadsheetChildEnv } from "../../../../../types"; 4 | import { PivotDimension } from "../../../../../types/pivot"; 5 | 6 | interface Props { 7 | dimension: PivotDimension; 8 | onUpdated: (dimension: PivotDimension, ev: InputEvent) => void; 9 | availableGranularities: Set; 10 | allGranularities: string[]; 11 | } 12 | 13 | export class PivotDimensionGranularity extends Component { 14 | static template = "o-spreadsheet-PivotDimensionGranularity"; 15 | static props = { 16 | dimension: Object, 17 | onUpdated: Function, 18 | availableGranularities: Set, 19 | allGranularities: Array, 20 | }; 21 | periods = ALL_PERIODS; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_layout_configurator/pivot_dimension_granularity/pivot_dimension_granularity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
Granularity
7 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_layout_configurator/pivot_dimension_order/pivot_dimension_order.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { SpreadsheetChildEnv } from "../../../../../types"; 3 | import { PivotDimension } from "../../../../../types/pivot"; 4 | 5 | interface Props { 6 | dimension: PivotDimension; 7 | onUpdated: (dimension: PivotDimension, ev: InputEvent) => void; 8 | } 9 | 10 | export class PivotDimensionOrder extends Component { 11 | static template = "o-spreadsheet-PivotDimensionOrder"; 12 | static props = { 13 | dimension: Object, 14 | onUpdated: Function, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_layout_configurator/pivot_dimension_order/pivot_dimension_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
Order by
6 | 18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { pivotSidePanelRegistry } from "../../../../helpers/pivot/pivot_side_panel_registry"; 3 | import { SpreadsheetChildEnv, UID } from "../../../../types"; 4 | import { Section } from "../../components/section/section"; 5 | import { PivotLayoutConfigurator } from "../pivot_layout_configurator/pivot_layout_configurator"; 6 | 7 | interface Props { 8 | pivotId: UID; 9 | onCloseSidePanel: () => void; 10 | } 11 | 12 | export class PivotSidePanel extends Component { 13 | static template = "o-spreadsheet-PivotSidePanel"; 14 | static props = { 15 | pivotId: String, 16 | onCloseSidePanel: Function, 17 | }; 18 | static components = { 19 | PivotLayoutConfigurator, 20 | Section, 21 | }; 22 | 23 | get sidePanelEditor() { 24 | const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId); 25 | if (!pivot) { 26 | throw new Error("pivotId does not correspond to a pivot."); 27 | } 28 | return pivotSidePanelRegistry.get(pivot.type).editor; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_side_panel/pivot_side_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/side_panel/pivot/pivot_title_section/pivot_title_section.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | Name 7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/components/side_panel/select_menu/select_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/spreadsheet/spreadsheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
8 | 9 | 10 | 11 | 12 | 13 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 | -------------------------------------------------------------------------------- /src/components/tables/table_dropdown_button/table_dropdown_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 10 |
11 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/components/tables/table_resizer/table_resizer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/tables/table_style_picker/table_style_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | 14 | 15 |
16 |
19 | 20 |
21 |
22 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/components/tables/table_style_preview/table_style_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
10 |
11 | 12 |
13 |
18 | 19 |
20 |
21 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/text_input/text_input.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/components/validation_messages/validation_messages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 |
9 | 17 |
18 |
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/formulas/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The formulas module provides all functionality related to manipulating 3 | * formulas: 4 | * 5 | * - tokenization (transforming a string into a list of tokens) 6 | * - parsing (same, but into an AST (Abstract Syntax Tree)) 7 | * - compiler (getting an executable function representing a formula) 8 | */ 9 | 10 | export { compile } from "./compiler"; 11 | export * from "./helpers"; 12 | export { parse } from "./parser"; 13 | export { rangeTokenize } from "./range_tokenizer"; 14 | export { Token, tokenize } from "./tokenizer"; 15 | -------------------------------------------------------------------------------- /src/functions/helper_assert.ts: -------------------------------------------------------------------------------- 1 | import { Arg, FunctionResultNumber, FunctionResultObject, Matrix, isMatrix } from "../types"; 2 | import { EvaluationError } from "../types/errors"; 3 | import { assert } from "./helpers"; 4 | 5 | export function assertSingleColOrRow(errorStr: string, arg: Matrix) { 6 | assert(() => arg.length === 1 || arg[0].length === 1, errorStr); 7 | } 8 | 9 | export function assertSameDimensions(errorStr: string, ...args: Arg[]) { 10 | if (args.every(isMatrix)) { 11 | const cols = args[0].length; 12 | const rows = args[0][0].length; 13 | for (const arg of args) { 14 | assert(() => arg.length === cols && arg[0].length === rows, errorStr); 15 | } 16 | return; 17 | } 18 | if (args.some((arg) => Array.isArray(arg) && (arg.length !== 1 || arg[0].length !== 1))) { 19 | throw new EvaluationError(errorStr); 20 | } 21 | } 22 | 23 | export function assertPositive(errorStr: string, arg: number) { 24 | assert(() => arg > 0, errorStr); 25 | } 26 | 27 | export function assertSquareMatrix(errorStr: string, arg: Matrix) { 28 | assert(() => arg.length === arg[0].length, errorStr); 29 | } 30 | 31 | export function isNumberMatrix( 32 | arg: Matrix 33 | ): arg is Matrix { 34 | return arg.every((row) => row.every((data) => typeof data.value === "number")); 35 | } 36 | -------------------------------------------------------------------------------- /src/functions/helper_logical.ts: -------------------------------------------------------------------------------- 1 | import { Arg } from "../types"; 2 | import { conditionalVisitBoolean } from "./helpers"; 3 | 4 | export function boolAnd(args: Arg[]) { 5 | let foundBoolean = false; 6 | let acc = true; 7 | conditionalVisitBoolean(args, (arg) => { 8 | foundBoolean = true; 9 | acc = acc && arg; 10 | return acc; 11 | }); 12 | return { 13 | foundBoolean, 14 | result: acc, 15 | }; 16 | } 17 | 18 | export function boolOr(args: Arg[]) { 19 | let foundBoolean = false; 20 | let acc = false; 21 | conditionalVisitBoolean(args, (arg) => { 22 | foundBoolean = true; 23 | acc = acc || arg; 24 | return !acc; 25 | }); 26 | return { 27 | foundBoolean, 28 | result: acc, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/functions/helper_math.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Locale } from "../types"; 2 | import { isDataNonEmpty, reduceAny, reduceNumbers } from "./helpers"; 3 | 4 | export function sum(values: Arg[], locale: Locale): number { 5 | return reduceNumbers(values, (acc, a) => acc + a, 0, locale); 6 | } 7 | 8 | export function countUnique(args: Arg[]): number { 9 | return reduceAny(args, (acc, a) => (isDataNonEmpty(a) ? acc.add(a?.value) : acc), new Set()).size; 10 | } 11 | -------------------------------------------------------------------------------- /src/functions/module_custom.ts: -------------------------------------------------------------------------------- 1 | import { formatLargeNumber } from "../helpers"; 2 | import { _t } from "../translation"; 3 | import { 4 | AddFunctionDescription, 5 | FunctionResultNumber, 6 | FunctionResultObject, 7 | Maybe, 8 | } from "../types"; 9 | import { arg } from "./arguments"; 10 | import { toNumber } from "./helpers"; 11 | 12 | // ----------------------------------------------------------------------------- 13 | // FORMAT.LARGE.NUMBER 14 | // ----------------------------------------------------------------------------- 15 | 16 | export const FORMAT_LARGE_NUMBER = { 17 | description: _t("Apply a large number format"), 18 | args: [ 19 | arg("value (number)", _t("The number.")), 20 | arg( 21 | "unit (string, optional)", 22 | _t("The formatting unit. Use 'k', 'm', or 'b' to force the unit") 23 | ), 24 | ], 25 | compute: function ( 26 | value: Maybe, 27 | unite: Maybe 28 | ): FunctionResultNumber { 29 | return { 30 | value: toNumber(value, this.locale), 31 | format: formatLargeNumber(value, unite, this.locale), 32 | }; 33 | }, 34 | } satisfies AddFunctionDescription; 35 | -------------------------------------------------------------------------------- /src/functions/module_engineering.ts: -------------------------------------------------------------------------------- 1 | import { _t } from "../translation"; 2 | import { AddFunctionDescription, FunctionResultObject, Maybe } from "../types"; 3 | import { arg } from "./arguments"; 4 | import { toNumber } from "./helpers"; 5 | 6 | const DEFAULT_DELTA_ARG = 0; 7 | 8 | // ----------------------------------------------------------------------------- 9 | // DELTA 10 | // ----------------------------------------------------------------------------- 11 | export const DELTA = { 12 | description: _t("Compare two numeric values, returning 1 if they're equal."), 13 | args: [ 14 | arg("number1 (number)", _t("The first number to compare.")), 15 | arg(`number2 (number, default=${DEFAULT_DELTA_ARG})`, _t("The second number to compare.")), 16 | ], 17 | compute: function ( 18 | number1: Maybe, 19 | number2: Maybe = { value: DEFAULT_DELTA_ARG } 20 | ): number { 21 | const _number1 = toNumber(number1, this.locale); 22 | const _number2 = toNumber(number2, this.locale); 23 | return _number1 === _number2 ? 1 : 0; 24 | }, 25 | isExported: true, 26 | } satisfies AddFunctionDescription; 27 | -------------------------------------------------------------------------------- /src/functions/module_web.ts: -------------------------------------------------------------------------------- 1 | import { markdownLink } from "../helpers"; 2 | import { _t } from "../translation"; 3 | import { AddFunctionDescription, FunctionResultObject, Maybe } from "../types"; 4 | import { arg } from "./arguments"; 5 | import { toString } from "./helpers"; 6 | 7 | // ----------------------------------------------------------------------------- 8 | // HYPERLINK 9 | // ----------------------------------------------------------------------------- 10 | export const HYPERLINK = { 11 | description: _t("Creates a hyperlink in a cell."), 12 | args: [ 13 | arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")), 14 | arg( 15 | "link_label (string, optional)", 16 | _t("The text to display in the cell, enclosed in quotation marks.") 17 | ), 18 | ], 19 | compute: function ( 20 | url: Maybe, 21 | linkLabel: Maybe 22 | ): string { 23 | const processedUrl = toString(url).trim(); 24 | const processedLabel = toString(linkLabel) || processedUrl; 25 | if (processedUrl === "") return processedLabel; 26 | return markdownLink(processedLabel, processedUrl); 27 | }, 28 | isExported: true, 29 | } satisfies AddFunctionDescription; 30 | -------------------------------------------------------------------------------- /src/helpers/cells/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../links"; 2 | export * from "./cell_evaluation"; 3 | -------------------------------------------------------------------------------- /src/helpers/edge_scrolling.ts: -------------------------------------------------------------------------------- 1 | export const MAX_DELAY = 140; 2 | export const MIN_DELAY = 20; 3 | const ACCELERATION = 0.035; 4 | 5 | /** 6 | * Decreasing exponential function used to determine the "speed" of edge-scrolling 7 | * as the timeout delay. 8 | * 9 | * Returns a timeout delay in milliseconds. 10 | */ 11 | export function scrollDelay(value: number): number { 12 | // decreasing exponential from MAX_DELAY to MIN_DELAY 13 | return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1)); 14 | } 15 | -------------------------------------------------------------------------------- /src/helpers/figures/charts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./abstract_chart"; 2 | export * from "./bar_chart"; 3 | export * from "./chart_common"; 4 | export * from "./chart_factory"; 5 | export * from "./chart_ui_common"; 6 | export * from "./gauge_chart"; 7 | export * from "./line_chart"; 8 | export * from "./pie_chart"; 9 | export * from "./scorecard_chart"; 10 | export * from "./waterfall_chart"; 11 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./color"; 2 | export * from "./coordinates"; 3 | export * from "./data_normalization"; 4 | export * from "./data_validation_helpers"; 5 | export * from "./dates"; 6 | export * from "./edge_scrolling"; 7 | export * from "./format/format"; 8 | export * from "./misc"; 9 | export * from "./numbers"; 10 | export * from "./range"; 11 | export * from "./recompute_zones"; 12 | export * from "./references"; 13 | export * from "./search"; 14 | export * from "./sheet"; 15 | export * from "./text_helper"; 16 | export * from "./uuid"; 17 | export * from "./zones"; 18 | -------------------------------------------------------------------------------- /src/helpers/inverse_commands.ts: -------------------------------------------------------------------------------- 1 | import { inverseCommandRegistry } from "../registries/inverse_command_registry"; 2 | import { CoreCommand } from "../types"; 3 | 4 | export function inverseCommand(cmd: CoreCommand): CoreCommand[] { 5 | return inverseCommandRegistry.get(cmd.type)(cmd); 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/pivot/pivot_highlight.ts: -------------------------------------------------------------------------------- 1 | import { HIGHLIGHT_COLOR } from "../../constants"; 2 | import { CellPosition, Getters, Highlight, UID } from "../../types"; 3 | import { mergeContiguousZones, positionToZone } from "../zones"; 4 | 5 | export function getPivotHighlights(getters: Getters, pivotId: UID): Highlight[] { 6 | const sheetId = getters.getActiveSheetId(); 7 | const pivotCellPositions = getVisiblePivotCellPositions(getters, pivotId); 8 | const mergedZones = mergeContiguousZones(pivotCellPositions.map(positionToZone)); 9 | return mergedZones.map((zone) => ({ sheetId, zone, noFill: true, color: HIGHLIGHT_COLOR })); 10 | } 11 | 12 | function getVisiblePivotCellPositions(getters: Getters, pivotId: UID) { 13 | const positions: CellPosition[] = []; 14 | const sheetId = getters.getActiveSheetId(); 15 | for (const col of getters.getSheetViewVisibleCols()) { 16 | for (const row of getters.getSheetViewVisibleRows()) { 17 | const position = { sheetId, col, row }; 18 | const cellPivotId = getters.getPivotIdFromPosition(position); 19 | if (pivotId === cellPivotId) { 20 | positions.push(position); 21 | } 22 | } 23 | } 24 | return positions; 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/pivot/pivot_positional_formula_registry.ts: -------------------------------------------------------------------------------- 1 | import { Registry } from "../../registries/registry"; 2 | 3 | /** 4 | * Registry to enable or disable the support of positional arguments 5 | * (with a leading #) in pivot functions 6 | * e.g. =PIVOT.VALUE(1,"probability","#stage",1) 7 | */ 8 | export const supportedPivotPositionalFormulaRegistry = new Registry(); 9 | 10 | supportedPivotPositionalFormulaRegistry.add("SPREADSHEET", false); 11 | -------------------------------------------------------------------------------- /src/helpers/pivot/pivot_presence_tracker.ts: -------------------------------------------------------------------------------- 1 | import { CellValue } from "../.."; 2 | import { toString } from "../../functions/helpers"; 3 | import { PivotDomain } from "../../types"; 4 | 5 | export class PivotPresenceTracker { 6 | private trackedValues: Set = new Set(); 7 | 8 | private domainToArray(domain: PivotDomain): (string | CellValue)[] { 9 | return domain.flatMap((node) => [node.field, toString(node.value)]); 10 | } 11 | 12 | isValuePresent(measure: string, domain: PivotDomain) { 13 | const key = JSON.stringify({ measure, domain: this.domainToArray(domain) }); 14 | return this.trackedValues.has(key); 15 | } 16 | 17 | isHeaderPresent(domain: PivotDomain) { 18 | const key = JSON.stringify({ domain: this.domainToArray(domain) }); 19 | return this.trackedValues.has(key); 20 | } 21 | 22 | trackValue(measure: string, domain: PivotDomain) { 23 | const key = JSON.stringify({ measure, domain: this.domainToArray(domain) }); 24 | this.trackedValues.add(key); 25 | } 26 | 27 | trackHeader(domain: PivotDomain) { 28 | const key = JSON.stringify({ domain: this.domainToArray(domain) }); 29 | this.trackedValues.add(key); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/helpers/pivot/pivot_side_panel_registry.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@odoo/owl"; 2 | import { PivotSpreadsheetSidePanel } from "../../components/side_panel/pivot/pivot_side_panel/pivot_spreadsheet_side_panel/pivot_spreadsheet_side_panel"; 3 | import { Registry } from "../../registries/registry"; 4 | 5 | export interface PivotRegistryItem { 6 | editor: new (...args: any) => Component; 7 | } 8 | 9 | export const pivotSidePanelRegistry = new Registry(); 10 | 11 | pivotSidePanelRegistry.add("SPREADSHEET", { 12 | editor: PivotSpreadsheetSidePanel, 13 | }); 14 | -------------------------------------------------------------------------------- /src/helpers/pivot/spreadsheet_pivot/runtime_definition_spreadsheet_pivot.ts: -------------------------------------------------------------------------------- 1 | import { Getters, Range } from "../../../types"; 2 | import { PivotFields, SpreadsheetPivotCoreDefinition } from "../../../types/pivot"; 3 | import { PivotRuntimeDefinition } from "../pivot_runtime_definition"; 4 | 5 | export class SpreadsheetPivotRuntimeDefinition extends PivotRuntimeDefinition { 6 | readonly range?: Range; 7 | 8 | constructor(definition: SpreadsheetPivotCoreDefinition, fields: PivotFields, getters: Getters) { 9 | super(definition, fields); 10 | if (definition.dataSet) { 11 | const { sheetId, zone } = definition.dataSet; 12 | this.range = getters.getRangeFromZone(sheetId, zone); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/rectangle.ts: -------------------------------------------------------------------------------- 1 | import { Rect, Zone } from "../types"; 2 | import { intersection, union } from "./zones"; 3 | 4 | /** 5 | * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap 6 | */ 7 | export function rectIntersection(rect1: Rect, rect2: Rect): Rect | undefined { 8 | return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2))); 9 | } 10 | 11 | /** Compute the union of the rectangles, ie. the smallest rectangle that contain them all */ 12 | export function rectUnion(...rects: Rect[]): Rect { 13 | return zoneToRect(union(...rects.map(rectToZone)))!; 14 | } 15 | 16 | function rectToZone(rect: Rect): Zone { 17 | return { 18 | left: rect.x, 19 | top: rect.y, 20 | right: rect.x + rect.width, 21 | bottom: rect.y + rect.height, 22 | }; 23 | } 24 | 25 | function zoneToRect(zone: Zone | undefined): Rect | undefined { 26 | if (!zone) return undefined; 27 | return { 28 | x: zone.left, 29 | y: zone.top, 30 | width: zone.right - zone.left, 31 | height: zone.bottom - zone.top, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/rendering.ts: -------------------------------------------------------------------------------- 1 | import { GridRenderingContext, Highlight, Rect } from "../types"; 2 | import { toHex } from "./color"; 3 | 4 | import { HIGHLIGHT_COLOR } from "../constants"; 5 | import { setColorAlpha } from "./color"; 6 | 7 | export function drawHighlight( 8 | renderingContext: GridRenderingContext, 9 | highlight: Highlight, 10 | rect: Rect 11 | ) { 12 | const { x, y, width, height } = rect; 13 | if (width < 0 || height < 0) { 14 | return; 15 | } 16 | const color = highlight.color || HIGHLIGHT_COLOR; 17 | 18 | const { ctx } = renderingContext; 19 | ctx.save(); 20 | if (!highlight.noBorder) { 21 | if (highlight.dashed) { 22 | ctx.setLineDash([5, 3]); 23 | } 24 | ctx.strokeStyle = color; 25 | if (highlight.thinLine) { 26 | ctx.lineWidth = 1; 27 | ctx.strokeRect(x, y, width, height); 28 | } else { 29 | ctx.lineWidth = 2; 30 | /** + 0.5 offset to have sharp lines. See comment in {@link RendererPlugin#drawBorder} for more details */ 31 | ctx.strokeRect(x + 0.5, y + 0.5, width, height); 32 | } 33 | } 34 | if (!highlight.noFill) { 35 | ctx.fillStyle = setColorAlpha(toHex(color), highlight.fillAlpha ?? 0.12); 36 | ctx.fillRect(x, y, width, height); 37 | } 38 | ctx.restore(); 39 | } 40 | -------------------------------------------------------------------------------- /src/helpers/state_manager_helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create an empty structure according to the type of the node key: 3 | * string: object 4 | * number: array 5 | */ 6 | export function createEmptyStructure(node: string | number | any) { 7 | if (typeof node === "string") { 8 | return {}; 9 | } else if (typeof node === "number") { 10 | return []; 11 | } 12 | throw new Error(`Cannot create new node`); 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/ui/cut_interactive.ts: -------------------------------------------------------------------------------- 1 | import { _t } from "../../translation"; 2 | import { CommandResult, SpreadsheetChildEnv } from "../../types"; 3 | 4 | export function interactiveCut(env: SpreadsheetChildEnv) { 5 | const result = env.model.dispatch("CUT"); 6 | 7 | if (!result.isSuccessful) { 8 | if (result.isCancelledBecause(CommandResult.WrongCutSelection)) { 9 | env.raiseError(_t("This operation is not allowed with multiple selections.")); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/ui/freeze_interactive.ts: -------------------------------------------------------------------------------- 1 | import { MergeErrorMessage } from "../../components/translations_terms"; 2 | import { CommandResult, Dimension, HeaderIndex, SpreadsheetChildEnv } from "../../types"; 3 | 4 | export function interactiveFreezeColumnsRows( 5 | env: SpreadsheetChildEnv, 6 | dimension: Dimension, 7 | base: HeaderIndex 8 | ) { 9 | const sheetId = env.model.getters.getActiveSheetId(); 10 | const cmd = dimension === "COL" ? "FREEZE_COLUMNS" : "FREEZE_ROWS"; 11 | const result = env.model.dispatch(cmd, { sheetId, quantity: base }); 12 | 13 | if (result.isCancelledBecause(CommandResult.MergeOverlap)) { 14 | env.raiseError(MergeErrorMessage); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/ui/merge_interactive.ts: -------------------------------------------------------------------------------- 1 | import { _t } from "../../translation"; 2 | import { CommandResult, SpreadsheetChildEnv, UID, Zone } from "../../types"; 3 | 4 | export const AddMergeInteractiveContent = { 5 | MergeIsDestructive: _t( 6 | "Merging these cells will only preserve the top-leftmost value. Merge anyway?" 7 | ), 8 | MergeInFilter: _t("You can't merge cells inside of an existing filter."), 9 | }; 10 | 11 | export function interactiveAddMerge(env: SpreadsheetChildEnv, sheetId: UID, target: Zone[]) { 12 | const result = env.model.dispatch("ADD_MERGE", { sheetId, target }); 13 | if (result.isCancelledBecause(CommandResult.MergeInTable)) { 14 | env.raiseError(AddMergeInteractiveContent.MergeInFilter); 15 | } else if (result.isCancelledBecause(CommandResult.MergeIsDestructive)) { 16 | env.askConfirmation(AddMergeInteractiveContent.MergeIsDestructive, () => { 17 | env.model.dispatch("ADD_MERGE", { sheetId, target, force: true }); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers/ui/sheet_interactive.ts: -------------------------------------------------------------------------------- 1 | import { FORBIDDEN_SHEETNAME_CHARS } from "../../constants"; 2 | import { _t } from "../../translation"; 3 | import { CommandResult, SpreadsheetChildEnv, UID } from "../../types"; 4 | 5 | export function interactiveRenameSheet( 6 | env: SpreadsheetChildEnv, 7 | sheetId: UID, 8 | name: string, 9 | errorCallback: () => void 10 | ) { 11 | const result = env.model.dispatch("RENAME_SHEET", { sheetId, name }); 12 | if (result.reasons.includes(CommandResult.MissingSheetName)) { 13 | env.raiseError(_t("The sheet name cannot be empty."), errorCallback); 14 | } else if (result.reasons.includes(CommandResult.DuplicatedSheetName)) { 15 | env.raiseError( 16 | _t("A sheet with the name %s already exists. Please select another name.", name), 17 | errorCallback 18 | ); 19 | } else if (result.reasons.includes(CommandResult.ForbiddenCharactersInSheetName)) { 20 | env.raiseError( 21 | _t( 22 | "Some used characters are not allowed in a sheet name (Forbidden characters are %s).", 23 | FORBIDDEN_SHEETNAME_CHARS.join(" ") 24 | ), 25 | errorCallback 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/ui/split_to_columns_interactive.ts: -------------------------------------------------------------------------------- 1 | import { _t } from "../../translation"; 2 | import { CommandResult, SpreadsheetChildEnv } from "../../types"; 3 | import { DispatchResult } from "./../../types/commands"; 4 | 5 | export const SplitToColumnsInteractiveContent = { 6 | SplitIsDestructive: _t("This will overwrite data in the subsequent columns. Split anyway?"), 7 | }; 8 | 9 | export function interactiveSplitToColumns( 10 | env: SpreadsheetChildEnv, 11 | separator: string, 12 | addNewColumns: boolean 13 | ): DispatchResult { 14 | let result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", { separator, addNewColumns }); 15 | if (result.isCancelledBecause(CommandResult.SplitWillOverwriteContent)) { 16 | env.askConfirmation(SplitToColumnsInteractiveContent.SplitIsDestructive, () => { 17 | result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", { 18 | separator, 19 | addNewColumns, 20 | force: true, 21 | }); 22 | }); 23 | } 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/ui/toggle_group_interactive.ts: -------------------------------------------------------------------------------- 1 | import { _t } from "../../translation"; 2 | import { CommandResult, Dimension, HeaderIndex, SpreadsheetChildEnv, UID } from "../../types"; 3 | 4 | export const ToggleGroupInteractiveContent = { 5 | CannotHideAllRows: _t("Cannot hide all the rows of a sheet."), 6 | CannotHideAllColumns: _t("Cannot hide all the columns of a sheet."), 7 | }; 8 | 9 | export function interactiveToggleGroup( 10 | env: SpreadsheetChildEnv, 11 | sheetId: UID, 12 | dimension: Dimension, 13 | start: HeaderIndex, 14 | end: HeaderIndex 15 | ) { 16 | const group = env.model.getters.getHeaderGroup(sheetId, dimension, start, end); 17 | if (!group) { 18 | return; 19 | } 20 | const command = group.isFolded ? "UNFOLD_HEADER_GROUP" : "FOLD_HEADER_GROUP"; 21 | const result = env.model.dispatch(command, { 22 | sheetId, 23 | dimension, 24 | start: group.start, 25 | end: group.end, 26 | }); 27 | if (!result.isSuccessful) { 28 | if (result.isCancelledBecause(CommandResult.NotEnoughElements)) { 29 | const errorMessage = 30 | dimension === "ROW" 31 | ? ToggleGroupInteractiveContent.CannotHideAllRows 32 | : ToggleGroupInteractiveContent.CannotHideAllColumns; 33 | env.raiseError(errorMessage); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/history/operation.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from "../helpers"; 2 | import { Lazy, Transformation, UID } from "../types"; 3 | 4 | /** 5 | * An Operation can be executed to change a data structure from state A 6 | * to state B. 7 | * It should hold the necessary data used to perform this transition. 8 | * It should be possible to revert the changes made by this operation. 9 | * 10 | * In the context of o-spreadsheet, the data from an operation would 11 | * be a revision (the commands are used to execute it, the `changes` are used 12 | * to revert it). 13 | */ 14 | export class Operation { 15 | constructor(readonly id: UID, readonly data: T) {} 16 | 17 | transformed(transformation: Transformation): Operation { 18 | return new LazyOperation( 19 | this.id, 20 | lazy(() => transformation(this.data)) 21 | ); 22 | } 23 | } 24 | 25 | class LazyOperation implements Operation { 26 | constructor(readonly id: UID, private readonly lazyData: Lazy) {} 27 | 28 | get data(): T { 29 | return this.lazyData(); 30 | } 31 | 32 | transformed(transformation: Transformation): Operation { 33 | return new LazyOperation(this.id, this.lazyData.map(transformation)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/history/repeat_commands/repeat_revision.ts: -------------------------------------------------------------------------------- 1 | import { Revision } from "../../collaborative/revisions"; 2 | import { 3 | repeatCommandTransformRegistry, 4 | repeatCoreCommand, 5 | repeatLocalCommand, 6 | repeatLocalCommandTransformRegistry, 7 | } from "../../registries/repeat_commands_registry"; 8 | import { CoreCommand, Getters } from "../../types"; 9 | import { Command, isCoreCommand } from "../../types/commands"; 10 | 11 | export function canRepeatRevision(revision: Revision | undefined): boolean { 12 | if (!revision || !revision.rootCommand || typeof revision.rootCommand !== "object") { 13 | return false; 14 | } 15 | 16 | if (isCoreCommand(revision.rootCommand)) { 17 | return repeatCommandTransformRegistry.contains(revision.rootCommand.type); 18 | } 19 | 20 | return repeatLocalCommandTransformRegistry.contains(revision.rootCommand.type); 21 | } 22 | 23 | export function repeatRevision( 24 | revision: Revision, 25 | getters: Getters 26 | ): CoreCommand[] | Command | undefined { 27 | if (!revision.rootCommand || typeof revision.rootCommand !== "object") { 28 | return undefined; 29 | } 30 | 31 | if (isCoreCommand(revision.rootCommand)) { 32 | return repeatCoreCommand(getters, revision.rootCommand); 33 | } 34 | 35 | return repeatLocalCommand(getters, revision.rootCommand, revision.commands); 36 | } 37 | -------------------------------------------------------------------------------- /src/plugins/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./borders"; 2 | export * from "./cell"; 3 | export * from "./chart"; 4 | export * from "./conditional_format"; 5 | export * from "./data_validation"; 6 | export * from "./figures"; 7 | export * from "./header_size"; 8 | export * from "./header_visibility"; 9 | export * from "./image"; 10 | export * from "./merge"; 11 | export * from "./range"; 12 | export * from "./sheet"; 13 | export * from "./tables"; 14 | -------------------------------------------------------------------------------- /src/plugins/ui_core_views/cell_evaluation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./evaluation_plugin"; 2 | -------------------------------------------------------------------------------- /src/plugins/ui_core_views/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell_evaluation"; 2 | export * from "./custom_colors"; 3 | export * from "./evaluation_chart"; 4 | export * from "./evaluation_conditional_format"; 5 | export * from "./evaluation_data_validation"; 6 | -------------------------------------------------------------------------------- /src/plugins/ui_feature/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./autofill"; 2 | export * from "./automatic_sum"; 3 | export * from "./collaborative"; 4 | export * from "./data_cleanup"; 5 | export * from "./format"; 6 | export * from "./header_visibility_ui"; 7 | export * from "./insert_pivot"; 8 | export * from "./sort"; 9 | export * from "./ui_options"; 10 | export * from "./ui_sheet"; 11 | -------------------------------------------------------------------------------- /src/plugins/ui_feature/pivot_presence_plugin.ts: -------------------------------------------------------------------------------- 1 | import { PivotPresenceTracker } from "../../helpers/pivot/pivot_presence_tracker"; 2 | import { Command, UID } from "../../types"; 3 | import { UIPlugin } from "../ui_plugin"; 4 | 5 | export class PivotPresencePlugin extends UIPlugin { 6 | static getters = ["getPivotPresenceTracker"] as const; 7 | 8 | private trackPresencePivotId?: UID; 9 | private tracker?: PivotPresenceTracker; 10 | 11 | handle(cmd: Command) { 12 | switch (cmd.type) { 13 | case "PIVOT_START_PRESENCE_TRACKING": 14 | this.tracker = new PivotPresenceTracker(); 15 | this.trackPresencePivotId = cmd.pivotId; 16 | break; 17 | case "PIVOT_STOP_PRESENCE_TRACKING": 18 | this.trackPresencePivotId = undefined; 19 | break; 20 | } 21 | } 22 | 23 | getPivotPresenceTracker(pivotId: UID) { 24 | if (this.trackPresencePivotId !== pivotId) { 25 | return undefined; 26 | } 27 | if (!this.tracker) { 28 | throw new Error("Tracker not initialized"); 29 | } 30 | return this.tracker; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/plugins/ui_feature/ui_options.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../types/index"; 2 | import { UIPlugin } from "../ui_plugin"; 3 | 4 | export class UIOptionsPlugin extends UIPlugin { 5 | static getters = ["shouldShowFormulas"] as const; 6 | private showFormulas: boolean = false; 7 | 8 | // --------------------------------------------------------------------------- 9 | // Command Handling 10 | // --------------------------------------------------------------------------- 11 | 12 | handle(cmd: Command) { 13 | switch (cmd.type) { 14 | case "SET_FORMULA_VISIBILITY": 15 | this.showFormulas = cmd.show; 16 | break; 17 | } 18 | } 19 | 20 | // --------------------------------------------------------------------------- 21 | // Getters 22 | // --------------------------------------------------------------------------- 23 | 24 | shouldShowFormulas(): boolean { 25 | return this.showFormulas; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/plugins/ui_stateful/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./clipboard"; 2 | export * from "./filter_evaluation"; 3 | export * from "./selection"; 4 | export * from "./sheetview"; 5 | -------------------------------------------------------------------------------- /src/registries/auto_completes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auto_complete_registry"; 2 | export * from "./data_validation_auto_complete"; 3 | export * from "./function_auto_complete"; 4 | export * from "./pivot_auto_complete"; 5 | export * from "./sheet_name_auto_complete"; 6 | -------------------------------------------------------------------------------- /src/registries/auto_completes/sheet_name_auto_complete.ts: -------------------------------------------------------------------------------- 1 | import { getCanonicalSymbolName } from "../../helpers"; 2 | import { autoCompleteProviders } from "./auto_complete_registry"; 3 | 4 | autoCompleteProviders.add("sheet_names", { 5 | sequence: 150, 6 | autoSelectFirstProposal: true, 7 | getProposals(tokenAtCursor) { 8 | if ( 9 | tokenAtCursor.type === "SYMBOL" || 10 | (tokenAtCursor.type === "UNKNOWN" && tokenAtCursor.value.startsWith("'")) 11 | ) { 12 | return this.getters.getSheetIds().map((sheetId) => { 13 | const sheetName = getCanonicalSymbolName(this.getters.getSheetName(sheetId)); 14 | return { 15 | text: sheetName, 16 | fuzzySearchKey: sheetName.startsWith("'") ? sheetName : "'" + sheetName, // typing a single quote is a way to avoid matching function names 17 | }; 18 | }); 19 | } 20 | return []; 21 | }, 22 | selectProposal(tokenAtCursor, value) { 23 | const start = tokenAtCursor.start; 24 | const end = tokenAtCursor.end; 25 | this.composer.changeComposerCursorSelection(start, end); 26 | this.composer.replaceComposerCursorSelection(value + "!"); 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/registries/cell_clickable_registry.ts: -------------------------------------------------------------------------------- 1 | import { openLink } from "../helpers/links"; 2 | import { CellPosition, Getters, SpreadsheetChildEnv } from "../types"; 3 | import { Registry } from "./registry"; 4 | 5 | export interface CellClickableItem { 6 | condition: (position: CellPosition, getters: Getters) => boolean; 7 | execute: (position: CellPosition, env: SpreadsheetChildEnv) => void; 8 | sequence: number; 9 | } 10 | 11 | export const clickableCellRegistry = new Registry(); 12 | 13 | clickableCellRegistry.add("link", { 14 | condition: (position: CellPosition, getters: Getters) => { 15 | return !!getters.getEvaluatedCell(position).link; 16 | }, 17 | execute: (position: CellPosition, env: SpreadsheetChildEnv) => 18 | openLink(env.model.getters.getEvaluatedCell(position).link!, env), 19 | sequence: 5, 20 | }); 21 | -------------------------------------------------------------------------------- /src/registries/cell_popovers_registry.ts: -------------------------------------------------------------------------------- 1 | import { PopoverBuilders } from "../types/cell_popovers"; 2 | import { Registry } from "./registry"; 3 | 4 | export const cellPopoverRegistry = new Registry(); 5 | -------------------------------------------------------------------------------- /src/registries/currencies_registry.ts: -------------------------------------------------------------------------------- 1 | import { Currency } from "../types"; 2 | import { Registry } from "./registry"; 3 | 4 | /** 5 | * Registry intended to support usual currencies. It is mainly used to create 6 | * currency formats that can be selected or modified when customizing formats. 7 | */ 8 | export const currenciesRegistry = new Registry(); 9 | -------------------------------------------------------------------------------- /src/registries/evaluation_registry.ts: -------------------------------------------------------------------------------- 1 | import { pivotRegistry } from "../helpers/pivot/pivot_registry"; 2 | import { Getters } from "../types"; 3 | import { Registry } from "./registry"; 4 | 5 | /** 6 | * This registry is used to register functions that should be called after each iteration of the evaluation. 7 | * This is use currently to mark the each pivot to be re-evaluated. We have to do this after each iteration 8 | * to avoid to reload the data of the pivot at each function call (PIVOT.VALUE and PIVOT.HEADER). After each 9 | * evaluation iteration, the pivot has to be re-evaluated during the next iteration. 10 | */ 11 | export const onIterationEndEvaluationRegistry = new Registry<(getters: Getters) => void>(); 12 | 13 | onIterationEndEvaluationRegistry.add("pivots", (getters: Getters) => { 14 | for (const pivotId of getters.getPivotIds()) { 15 | const pivot = getters.getPivot(pivotId); 16 | pivotRegistry.get(pivot.type).onIterationEndEvaluation(pivot); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/registries/icons_on_cell_registry.ts: -------------------------------------------------------------------------------- 1 | import { CellPosition, Getters } from "../types"; 2 | import { ImageSrc } from "../types/image"; 3 | import { Registry } from "./registry"; 4 | 5 | type ImageSrcCallback = (getters: Getters, position: CellPosition) => ImageSrc | undefined; 6 | 7 | /** 8 | * Registry to draw icons on cells 9 | */ 10 | export const iconsOnCellRegistry = new Registry(); 11 | -------------------------------------------------------------------------------- /src/registries/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auto_completes/index"; 2 | export * from "./autofill_modifiers"; 3 | export * from "./autofill_rules"; 4 | export * from "./cell_popovers_registry"; 5 | export * from "./chart_types"; 6 | export * from "./currencies_registry"; 7 | export * from "./figure_registry"; 8 | export * from "./inverse_command_registry"; 9 | export * from "./menus/index"; 10 | export * from "./ot_registry"; 11 | export * from "./side_panel_registry_entries"; 12 | export * from "./topbar_component_registry"; 13 | -------------------------------------------------------------------------------- /src/registries/menus/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell_menu_registry"; 2 | export * from "./col_menu_registry"; 3 | export * from "./link_menu_registry"; 4 | export * from "./number_format_menu_registry"; 5 | export * from "./row_menu_registry"; 6 | export * from "./sheet_menu_registry"; 7 | export * from "./topbar_menu_registry"; 8 | -------------------------------------------------------------------------------- /src/registries/menus/link_menu_registry.ts: -------------------------------------------------------------------------------- 1 | import * as ACTION_SHEET from "../../actions/sheet_actions"; 2 | import { MenuItemRegistry } from "../menu_items_registry"; 3 | 4 | //------------------------------------------------------------------------------ 5 | // Link Menu Registry 6 | //------------------------------------------------------------------------------ 7 | 8 | export const linkMenuRegistry = new MenuItemRegistry(); 9 | 10 | linkMenuRegistry.add("sheet", { 11 | ...ACTION_SHEET.linkSheet, 12 | sequence: 10, 13 | }); 14 | -------------------------------------------------------------------------------- /src/registries/menus/sheet_menu_registry.ts: -------------------------------------------------------------------------------- 1 | import * as ACTION_SHEET from "../../actions/sheet_actions"; 2 | import { MenuItemRegistry } from "../menu_items_registry"; 3 | 4 | export function getSheetMenuRegistry(args: { 5 | renameSheetCallback: () => void; 6 | openSheetColorPickerCallback: () => void; 7 | }): MenuItemRegistry { 8 | const sheetMenuRegistry = new MenuItemRegistry(); 9 | 10 | sheetMenuRegistry 11 | .add("delete", { 12 | ...ACTION_SHEET.deleteSheet, 13 | sequence: 10, 14 | }) 15 | .add("hide_sheet", { 16 | ...ACTION_SHEET.hideSheet, 17 | sequence: 20, 18 | }) 19 | .add("duplicate", { 20 | ...ACTION_SHEET.duplicateSheet, 21 | sequence: 30, 22 | separator: true, 23 | }) 24 | .add("rename", { 25 | ...ACTION_SHEET.renameSheet(args), 26 | sequence: 40, 27 | }) 28 | .add("change_color", { 29 | ...ACTION_SHEET.changeSheetColor(args), 30 | sequence: 50, 31 | separator: true, 32 | }) 33 | .add("move_right", { 34 | ...ACTION_SHEET.sheetMoveRight, 35 | sequence: 60, 36 | }) 37 | .add("move_left", { 38 | ...ACTION_SHEET.sheetMoveLeft, 39 | sequence: 70, 40 | }); 41 | 42 | return sheetMenuRegistry; 43 | } 44 | -------------------------------------------------------------------------------- /src/registries/menus/table_style_menu_registry.ts: -------------------------------------------------------------------------------- 1 | import { Action, createActions } from "../../actions/action"; 2 | import { _t } from "../../translation"; 3 | import { SpreadsheetChildEnv } from "./../../types/env"; 4 | 5 | export function createTableStyleContextMenuActions( 6 | env: SpreadsheetChildEnv, 7 | styleId: string 8 | ): Action[] { 9 | if (!env.model.getters.isTableStyleEditable(styleId)) { 10 | return []; 11 | } 12 | return createActions([ 13 | { 14 | id: "editTableStyle", 15 | name: _t("Edit table style"), 16 | execute: (env) => env.openSidePanel("TableStyleEditorPanel", { styleId }), 17 | icon: "o-spreadsheet-Icon.EDIT", 18 | }, 19 | { 20 | id: "deleteTableStyle", 21 | name: _t("Delete table style"), 22 | execute: (env) => env.model.dispatch("REMOVE_TABLE_STYLE", { tableStyleId: styleId }), 23 | icon: "o-spreadsheet-Icon.TRASH", 24 | }, 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /src/registries/side_panel_registry.ts: -------------------------------------------------------------------------------- 1 | import { SidePanelState } from "../components/side_panel/side_panel/side_panel_store"; 2 | import { Getters, SpreadsheetChildEnv } from "../types"; 3 | import { Registry } from "./registry"; 4 | 5 | //------------------------------------------------------------------------------ 6 | // Side Panel Registry 7 | //------------------------------------------------------------------------------ 8 | 9 | export interface SidePanelContent { 10 | title: string | ((env: SpreadsheetChildEnv, props: object) => string); 11 | Body: any; 12 | Footer?: any; 13 | /** 14 | * A callback used to validate the props or generate new props 15 | * based on the current state of the spreadsheet model, using the getters. 16 | */ 17 | computeState?: (getters: Getters, initialProps: object) => SidePanelState; 18 | } 19 | 20 | export const sidePanelRegistry = new Registry(); 21 | -------------------------------------------------------------------------------- /src/registries/topbar_component_registry.ts: -------------------------------------------------------------------------------- 1 | import { UuidGenerator } from "../helpers"; 2 | import { UID } from "../types"; 3 | import { SpreadsheetChildEnv } from "../types/env"; 4 | import { Registry } from "./registry"; 5 | 6 | //------------------------------------------------------------------------------ 7 | // Topbar Component Registry 8 | //------------------------------------------------------------------------------ 9 | export interface TopbarComponent { 10 | id: UID; 11 | component: any; 12 | isVisible?: (env: SpreadsheetChildEnv) => boolean; 13 | sequence: number; 14 | } 15 | 16 | class TopBarComponentRegistry extends Registry { 17 | mapping: { [key: string]: Function } = {}; 18 | uuidGenerator = new UuidGenerator(); 19 | 20 | add(name: string, value: Omit) { 21 | const component: TopbarComponent = { ...value, id: this.uuidGenerator.uuidv4() }; 22 | return super.add(name, component); 23 | } 24 | 25 | getAllOrdered(): TopbarComponent[] { 26 | return this.getAll().sort((a, b) => a.sequence - b.sequence); 27 | } 28 | } 29 | 30 | export const topbarComponentRegistry = new TopBarComponentRegistry(); 31 | -------------------------------------------------------------------------------- /src/store_engine/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dependency_container"; 2 | export * from "./store"; 3 | export * from "./store_hooks"; 4 | -------------------------------------------------------------------------------- /src/stores/DOM_focus_store.ts: -------------------------------------------------------------------------------- 1 | export class DOMFocusableElementStore { 2 | mutators = ["setFocusableElement", "focus"] as const; 3 | private focusableElement: HTMLElement | undefined = undefined; 4 | 5 | setFocusableElement(element: HTMLElement) { 6 | this.focusableElement = element; 7 | return "noStateChange"; 8 | } 9 | 10 | focus() { 11 | if (this.focusableElement === document.activeElement) { 12 | return "noStateChange"; 13 | } 14 | this.focusableElement?.focus(); 15 | return; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/stores/array_formula_highlight.ts: -------------------------------------------------------------------------------- 1 | import { Get } from "../store_engine"; 2 | import { Highlight, Zone } from "../types"; 3 | import { CellErrorType } from "../types/errors"; 4 | import { HighlightStore } from "./highlight_store"; 5 | import { SpreadsheetStore } from "./spreadsheet_store"; 6 | 7 | export class ArrayFormulaHighlight extends SpreadsheetStore { 8 | protected highlightStore = this.get(HighlightStore); 9 | 10 | constructor(get: Get) { 11 | super(get); 12 | this.highlightStore.register(this); 13 | } 14 | 15 | get highlights(): Highlight[] { 16 | let zone: Zone | undefined; 17 | const position = this.model.getters.getActivePosition(); 18 | const cell = this.getters.getEvaluatedCell(position); 19 | const spreader = this.model.getters.getArrayFormulaSpreadingOn(position); 20 | zone = spreader 21 | ? this.model.getters.getSpreadZone(spreader, { ignoreSpillError: true }) 22 | : this.model.getters.getSpreadZone(position, { ignoreSpillError: true }); 23 | if (!zone) { 24 | return []; 25 | } 26 | return [ 27 | { 28 | sheetId: position.sheetId, 29 | zone, 30 | dashed: cell.value === CellErrorType.SpilledBlocked, 31 | color: "#17A2B8", 32 | noFill: true, 33 | thinLine: true, 34 | }, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./model_store"; 2 | export * from "./spreadsheet_store"; 3 | -------------------------------------------------------------------------------- /src/stores/model_store.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../model"; 2 | import { createAbstractStore } from "../store_engine"; 3 | 4 | export const ModelStore = createAbstractStore("Model"); 5 | -------------------------------------------------------------------------------- /src/stores/notification_store.ts: -------------------------------------------------------------------------------- 1 | import { InformationNotification } from "../types"; 2 | 3 | export interface NotificationStoreMethods { 4 | notifyUser: (notification: InformationNotification) => void; 5 | raiseError: (text: string, callback?: () => void) => void; 6 | askConfirmation: (content: string, confirm: () => void, cancel?: () => void) => void; 7 | } 8 | 9 | export class NotificationStore { 10 | mutators = [ 11 | "notifyUser", 12 | "raiseError", 13 | "askConfirmation", 14 | "updateNotificationCallbacks", 15 | ] as const; 16 | notifyUser: NotificationStoreMethods["notifyUser"] = (notification) => 17 | window.alert(notification.text); 18 | askConfirmation: NotificationStoreMethods["askConfirmation"] = (content, confirm, cancel) => { 19 | if (window.confirm(content)) { 20 | confirm(); 21 | } else { 22 | cancel?.(); 23 | } 24 | }; 25 | raiseError: NotificationStoreMethods["raiseError"] = (text, callback) => { 26 | window.alert(text); 27 | callback?.(); 28 | }; 29 | 30 | updateNotificationCallbacks(methods: Partial) { 31 | this.notifyUser = methods.notifyUser || this.notifyUser; 32 | this.raiseError = methods.raiseError || this.raiseError; 33 | this.askConfirmation = methods.askConfirmation || this.askConfirmation; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/stores/renderer_store.ts: -------------------------------------------------------------------------------- 1 | import { GridRenderingContext, LayerName } from "../types"; 2 | 3 | export interface Renderer { 4 | drawLayer(ctx: GridRenderingContext, layer: LayerName): void; 5 | renderingLayers: Readonly; 6 | } 7 | 8 | export class RendererStore { 9 | mutators = ["register", "unRegister", "drawLayer"] as const; 10 | private renderers: Partial> = {}; 11 | 12 | register(renderer: Renderer) { 13 | if (!renderer.renderingLayers.length) { 14 | return; 15 | } 16 | for (const layer of renderer.renderingLayers) { 17 | if (!this.renderers[layer]) { 18 | this.renderers[layer] = []; 19 | } 20 | this.renderers[layer]!.push(renderer); 21 | } 22 | } 23 | 24 | unRegister(renderer: Renderer) { 25 | for (const layer of Object.keys(this.renderers)) { 26 | this.renderers[layer] = this.renderers[layer].filter((r: Renderer) => r !== renderer); 27 | } 28 | } 29 | 30 | drawLayer(context: GridRenderingContext, layer: LayerName) { 31 | const renderers = this.renderers[layer]; 32 | if (renderers) { 33 | for (const renderer of renderers) { 34 | context.ctx.save(); 35 | renderer.drawLayer(context, layer); 36 | context.ctx.restore(); 37 | } 38 | } 39 | return "noStateChange"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/stores/spreadsheet_store.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../model"; 2 | import { DisposableStore, Get } from "../store_engine"; 3 | import { Command, GridRenderingContext, LayerName } from "../types"; 4 | import { ModelStore } from "./model_store"; 5 | import { RendererStore } from "./renderer_store"; 6 | 7 | export class SpreadsheetStore extends DisposableStore { 8 | // cast the model store as Model to allow model.dispatch to return the DispatchResult 9 | protected model = this.get(ModelStore) as Model; 10 | protected getters = this.model.getters; 11 | private renderer = this.get(RendererStore); 12 | 13 | constructor(get: Get) { 14 | super(get); 15 | this.model.on("command-dispatched", this, this.handle); 16 | this.model.on("command-finalized", this, this.finalize); 17 | this.renderer.register(this); 18 | 19 | this.onDispose(() => { 20 | this.model.off("command-dispatched", this); 21 | this.model.off("command-finalized", this); 22 | this.renderer.unRegister(this); 23 | }); 24 | } 25 | 26 | get renderingLayers(): Readonly { 27 | return []; 28 | } 29 | 30 | protected handle(cmd: Command) {} 31 | protected finalize() {} 32 | 33 | drawLayer(ctx: GridRenderingContext, layer: LayerName) {} 34 | } 35 | -------------------------------------------------------------------------------- /src/types/chart/bar_chart.ts: -------------------------------------------------------------------------------- 1 | import { ChartConfiguration } from "chart.js"; 2 | import { Color } from "../misc"; 3 | import { ComboBarChartDefinition } from "./common_bar_combo"; 4 | 5 | export interface BarChartDefinition extends ComboBarChartDefinition { 6 | readonly type: "bar"; 7 | readonly stacked: boolean; 8 | readonly horizontal?: boolean; 9 | } 10 | 11 | export type BarChartRuntime = { 12 | chartJsConfig: ChartConfiguration; 13 | background: Color; 14 | }; 15 | -------------------------------------------------------------------------------- /src/types/chart/combo_chart.ts: -------------------------------------------------------------------------------- 1 | import { ChartConfiguration } from "chart.js"; 2 | import { Color } from "../misc"; 3 | import { CustomizedDataSet } from "./chart"; 4 | import { ComboBarChartDefinition } from "./common_bar_combo"; 5 | 6 | export interface ComboChartDefinition extends ComboBarChartDefinition { 7 | readonly dataSets: ComboChartDataSet[]; 8 | readonly type: "combo"; 9 | } 10 | 11 | export type ComboChartDataSet = CustomizedDataSet & { type?: "bar" | "line" }; 12 | 13 | export type ComboChartRuntime = { 14 | chartJsConfig: ChartConfiguration; 15 | background: Color; 16 | }; 17 | -------------------------------------------------------------------------------- /src/types/chart/common_bar_combo.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "../misc"; 2 | import { AxesDesign, CustomizedDataSet, TitleDesign } from "./chart"; 3 | import { LegendPosition } from "./common_chart"; 4 | 5 | export interface ComboBarChartDefinition { 6 | readonly dataSets: CustomizedDataSet[]; 7 | readonly dataSetsHaveTitle: boolean; 8 | readonly labelRange?: string; 9 | readonly title: TitleDesign; 10 | readonly background?: Color; 11 | readonly legendPosition: LegendPosition; 12 | readonly aggregated?: boolean; 13 | readonly axesDesign?: AxesDesign; 14 | readonly showValues?: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /src/types/chart/common_chart.ts: -------------------------------------------------------------------------------- 1 | export type VerticalAxisPosition = "left" | "right"; 2 | export type LegendPosition = "top" | "bottom" | "left" | "right" | "none"; 3 | -------------------------------------------------------------------------------- /src/types/chart/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bar_chart"; 2 | export * from "./chart"; 3 | export * from "./common_chart"; 4 | export * from "./gauge_chart"; 5 | export * from "./line_chart"; 6 | export * from "./pie_chart"; 7 | export * from "./scorecard_chart"; 8 | -------------------------------------------------------------------------------- /src/types/chart/line_chart.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from "chart.js"; 2 | import { Color } from "../misc"; 3 | import { AxesDesign, CustomizedDataSet, TitleDesign } from "./chart"; 4 | import { LegendPosition } from "./common_chart"; 5 | 6 | export interface LineChartDefinition { 7 | readonly type: "line"; 8 | readonly dataSets: CustomizedDataSet[]; 9 | readonly dataSetsHaveTitle: boolean; 10 | readonly labelRange?: string; 11 | readonly title: TitleDesign; 12 | readonly background?: Color; 13 | readonly legendPosition: LegendPosition; 14 | readonly labelsAsText: boolean; 15 | readonly stacked: boolean; 16 | readonly aggregated?: boolean; 17 | readonly cumulative: boolean; 18 | readonly axesDesign?: AxesDesign; 19 | readonly fillArea?: boolean; 20 | readonly showValues?: boolean; 21 | } 22 | 23 | export type LineChartRuntime = { 24 | chartJsConfig: ChartConfiguration; 25 | background: Color; 26 | }; 27 | -------------------------------------------------------------------------------- /src/types/chart/pie_chart.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from "chart.js"; 2 | import { Color } from "../misc"; 3 | import { AxesDesign, CustomizedDataSet, TitleDesign } from "./chart"; 4 | import { LegendPosition } from "./common_chart"; 5 | 6 | export interface PieChartDefinition { 7 | readonly type: "pie"; 8 | readonly dataSets: CustomizedDataSet[]; 9 | readonly dataSetsHaveTitle: boolean; 10 | readonly labelRange?: string; 11 | readonly title: TitleDesign; 12 | readonly background?: Color; 13 | readonly legendPosition: LegendPosition; 14 | readonly aggregated?: boolean; 15 | readonly axesDesign?: AxesDesign; 16 | readonly isDoughnut?: boolean; 17 | readonly showValues?: boolean; 18 | } 19 | 20 | export type PieChartRuntime = { 21 | chartJsConfig: ChartConfiguration; 22 | background: Color; 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/chart/pyramid_chart.ts: -------------------------------------------------------------------------------- 1 | import { ChartConfiguration } from "chart.js"; 2 | import { Color } from "../misc"; 3 | import { BarChartDefinition } from "./bar_chart"; 4 | 5 | export interface PyramidChartDefinition extends Omit { 6 | readonly type: "pyramid"; 7 | } 8 | 9 | export type PyramidChartRuntime = { 10 | chartJsConfig: ChartConfiguration; 11 | background: Color; 12 | }; 13 | -------------------------------------------------------------------------------- /src/types/chart/scatter_chart.ts: -------------------------------------------------------------------------------- 1 | import { LineChartDefinition, LineChartRuntime } from "./line_chart"; 2 | 3 | export interface ScatterChartDefinition 4 | extends Omit { 5 | readonly type: "scatter"; 6 | } 7 | 8 | export type ScatterChartRuntime = LineChartRuntime; 9 | -------------------------------------------------------------------------------- /src/types/chart/scorecard_chart.ts: -------------------------------------------------------------------------------- 1 | import { Color, Style } from "../misc"; 2 | import { TitleDesign } from "./chart"; 3 | 4 | export interface ScorecardChartDefinition { 5 | readonly type: "scorecard"; 6 | readonly title: TitleDesign; 7 | readonly keyValue?: string; 8 | readonly baseline?: string; 9 | readonly baselineMode: BaselineMode; 10 | readonly baselineDescr?: string; 11 | readonly background?: Color; 12 | readonly baselineColorUp: Color; 13 | readonly baselineColorDown: Color; 14 | readonly humanize?: boolean; 15 | } 16 | 17 | export type BaselineMode = "text" | "difference" | "percentage" | "progress"; 18 | export type BaselineArrowDirection = "neutral" | "up" | "down"; 19 | 20 | export interface ProgressBar { 21 | readonly value: number; 22 | readonly color: Color; 23 | } 24 | 25 | export interface ScorecardChartRuntime { 26 | readonly title: TitleDesign; 27 | readonly keyValue: string; 28 | readonly baselineDisplay: string; 29 | readonly baselineColor?: string; 30 | readonly baselineArrow: BaselineArrowDirection; 31 | readonly baselineDescr?: string; 32 | readonly background: Color; 33 | readonly fontColor: Color; 34 | readonly keyValueStyle?: Style; 35 | readonly baselineStyle?: Style; 36 | readonly progressBar?: ProgressBar; 37 | } 38 | -------------------------------------------------------------------------------- /src/types/chart/waterfall_chart.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from "chart.js"; 2 | import { Color } from "../misc"; 3 | import { AxesDesign, CustomizedDataSet, TitleDesign } from "./chart"; 4 | import { LegendPosition, VerticalAxisPosition } from "./common_chart"; 5 | 6 | export interface WaterfallChartDefinition { 7 | readonly type: "waterfall"; 8 | readonly dataSets: CustomizedDataSet[]; 9 | readonly dataSetsHaveTitle: boolean; 10 | readonly labelRange?: string; 11 | readonly title: TitleDesign; 12 | readonly background?: Color; 13 | readonly verticalAxisPosition: VerticalAxisPosition; 14 | readonly legendPosition: LegendPosition; 15 | readonly aggregated?: boolean; 16 | readonly showSubTotals: boolean; 17 | readonly showConnectorLines: boolean; 18 | readonly firstValueAsSubtotal?: boolean; 19 | readonly positiveValuesColor?: Color; 20 | readonly negativeValuesColor?: Color; 21 | readonly subTotalValuesColor?: Color; 22 | readonly axesDesign?: AxesDesign; 23 | readonly showValues?: boolean; 24 | } 25 | 26 | export type WaterfallChartRuntime = { 27 | chartJsConfig: ChartConfiguration; 28 | background: Color; 29 | }; 30 | -------------------------------------------------------------------------------- /src/types/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { SpreadsheetClipboardData } from "../plugins/ui_stateful"; 2 | import { HeaderIndex, UID, Zone } from "./misc"; 3 | 4 | export enum ClipboardMIMEType { 5 | PlainText = "text/plain", 6 | Html = "text/html", 7 | } 8 | 9 | export type OSClipboardContent = { [type in ClipboardMIMEType]?: string }; 10 | 11 | export type ParsedOSClipboardContent = { 12 | text?: string; 13 | data?: SpreadsheetClipboardData; 14 | }; 15 | 16 | export interface ClipboardOptions { 17 | isCutOperation: boolean; 18 | pasteOption?: ClipboardPasteOptions; 19 | selectTarget?: boolean; 20 | } 21 | export type ClipboardPasteOptions = "onlyFormat" | "asValue"; 22 | export type ClipboardOperation = "CUT" | "COPY"; 23 | 24 | export type ClipboardCellData = { 25 | sheetId: UID; 26 | zones: Zone[]; 27 | rowsIndexes: HeaderIndex[]; 28 | columnsIndexes: HeaderIndex[]; 29 | clippedZones: Zone[]; 30 | }; 31 | 32 | export type ClipboardFigureData = { 33 | sheetId: UID; 34 | figureId: UID; 35 | }; 36 | 37 | export type ClipboardData = ClipboardCellData | ClipboardFigureData; 38 | 39 | export type ClipboardPasteTarget = { 40 | sheetId: UID; 41 | zones: Zone[]; 42 | figureId?: UID; 43 | }; 44 | -------------------------------------------------------------------------------- /src/types/collaborative/revisions.ts: -------------------------------------------------------------------------------- 1 | import { CoreCommand, UID } from ".."; 2 | import { ClientId } from "./session"; 3 | 4 | export interface RevisionData { 5 | readonly id: UID; 6 | readonly clientId: ClientId; 7 | readonly commands: readonly CoreCommand[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/currency.ts: -------------------------------------------------------------------------------- 1 | export interface Currency { 2 | name: string; 3 | code: string; 4 | symbol: string; 5 | decimalPlaces: number; 6 | position: "before" | "after"; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/env.ts: -------------------------------------------------------------------------------- 1 | import { Model } from ".."; 2 | import { ClipboardInterface } from "../helpers/clipboard/navigator_clipboard_wrapper"; 3 | import { Get } from "../store_engine"; 4 | import { NotificationStoreMethods } from "../stores/notification_store"; 5 | import { Currency } from "./currency"; 6 | import { ImageProviderInterface } from "./files"; 7 | import { Locale } from "./locale"; 8 | 9 | export interface EditTextOptions { 10 | error?: string; 11 | placeholder?: string; 12 | } 13 | 14 | export type NotificationType = "danger" | "info" | "success" | "warning"; 15 | 16 | export interface InformationNotification { 17 | text: string; 18 | type: NotificationType; 19 | sticky: boolean; 20 | } 21 | 22 | export interface SpreadsheetChildEnv extends NotificationStoreMethods { 23 | model: Model; 24 | imageProvider?: ImageProviderInterface; 25 | isDashboard: () => boolean; 26 | openSidePanel: (panel: string, panelProps?: any) => void; 27 | toggleSidePanel: (panel: string, panelProps?: any) => void; 28 | clipboard: ClipboardInterface; 29 | startCellEdition: (content?: string) => void; 30 | loadCurrencies?: () => Promise; 31 | loadLocales: () => Promise; 32 | getStore: Get; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/event_stream/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./selection_events"; 2 | -------------------------------------------------------------------------------- /src/types/event_stream/selection_events.ts: -------------------------------------------------------------------------------- 1 | import { AnchorZone } from ".."; 2 | 3 | export type SelectionEventOptions = { 4 | scrollIntoView?: boolean; 5 | unbounded?: boolean; 6 | }; 7 | 8 | export interface SelectionEvent { 9 | anchor: AnchorZone; 10 | previousAnchor: AnchorZone; 11 | mode: "newAnchor" | "overrideSelection" | "updateAnchor"; 12 | options: SelectionEventOptions; 13 | } 14 | -------------------------------------------------------------------------------- /src/types/figure.ts: -------------------------------------------------------------------------------- 1 | import { Pixel, UID } from "."; 2 | 3 | export interface Figure { 4 | id: UID; 5 | x: Pixel; 6 | y: Pixel; 7 | width: Pixel; 8 | height: Pixel; 9 | tag: string; 10 | } 11 | 12 | export interface FigureSize { 13 | width: Pixel; 14 | height: Pixel; 15 | } 16 | 17 | export interface ExcelFigureSize { 18 | cx: number; 19 | cy: number; 20 | } 21 | 22 | export type ResizeDirection = -1 | 0 | 1; 23 | -------------------------------------------------------------------------------- /src/types/files.ts: -------------------------------------------------------------------------------- 1 | import { FigureSize } from "./figure"; 2 | import { Image } from "./image"; 3 | 4 | type FilePath = string; 5 | 6 | /** 7 | * FileStore manage the transfer of file with the server. 8 | */ 9 | export interface FileStore { 10 | /** 11 | * Upload a file to a server and returns its path. 12 | */ 13 | upload(file: File): Promise; 14 | 15 | /** 16 | * Delete a file from the server 17 | */ 18 | delete(filePath: FilePath): Promise; 19 | } 20 | 21 | /** 22 | * ImageProvider can request the user to input an image file before sending it to a server. 23 | */ 24 | export interface ImageProviderInterface { 25 | /** 26 | * RequestImage ask the user to input an image file. Then send it to a server trough an FileStore. Finally it return the path and the size of the image in the server. 27 | */ 28 | requestImage(): Promise; 29 | getImageOriginalSize(path: string): Promise; 30 | } 31 | -------------------------------------------------------------------------------- /src/types/find_and_replace.ts: -------------------------------------------------------------------------------- 1 | import { Range } from "./range"; 2 | 3 | export interface SearchOptions { 4 | matchCase: boolean; 5 | exactMatch: boolean; 6 | searchFormulas: boolean; 7 | searchScope: "allSheets" | "activeSheet" | "specificRange"; 8 | specificRange?: Range; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/format.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from "./locale"; 2 | import { Alias } from "./misc"; 3 | 4 | export type Format = string & Alias; 5 | 6 | export type FormattedValue = string & Alias; 7 | 8 | export interface LocaleFormat { 9 | locale: Locale; 10 | format?: Format; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/image.ts: -------------------------------------------------------------------------------- 1 | import { FigureSize } from "./figure"; 2 | import { XLSXFigureSize } from "./xlsx"; 3 | 4 | /** 5 | * Image source given to 6 | */ 7 | export type ImageSrc = string; 8 | 9 | export interface Image { 10 | path: string; 11 | size: FigureSize; 12 | mimetype?: string; 13 | } 14 | 15 | export interface ExcelImage { 16 | imageSrc: string; 17 | size: XLSXFigureSize; 18 | mimetype?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * State 3 | * 4 | * This file defines the basic types involved in maintaining the running state 5 | * of a o-spreadsheet. 6 | * 7 | * The most important exported values are: 8 | * - interface GridState: the internal type of the state managed by the model 9 | */ 10 | 11 | import { Chart } from "chart.js"; 12 | 13 | export * from "./autofill"; 14 | export * from "./cells"; 15 | export * from "./chart/chart"; 16 | export * from "./clipboard"; 17 | export * from "./collaborative/revisions"; 18 | export * from "./collaborative/session"; 19 | export * from "./commands"; 20 | export * from "./conditional_formatting"; 21 | export * from "./currency"; 22 | export * from "./data_validation"; 23 | export * from "./env"; 24 | export * from "./figure"; 25 | export * from "./format"; 26 | export * from "./functions"; 27 | export * from "./getters"; 28 | export * from "./history"; 29 | export * from "./locale"; 30 | export * from "./misc"; 31 | export * from "./pivot"; 32 | export * from "./pivot_runtime"; 33 | export * from "./range"; 34 | export * from "./rendering"; 35 | export * from "./table"; 36 | export * from "./workbook_data"; 37 | 38 | declare global { 39 | interface Window { 40 | Chart: typeof Chart; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/types/locale.ts: -------------------------------------------------------------------------------- 1 | import { Alias } from "./misc"; 2 | 3 | export type LocaleCode = string & Alias; 4 | 5 | export interface Locale { 6 | name: string; 7 | code: LocaleCode; 8 | thousandsSeparator?: string; 9 | decimalSeparator: string; 10 | weekStart: number; //1 = Monday, 7 = Sunday 11 | dateFormat: string; 12 | timeFormat: string; 13 | formulaArgSeparator: string; 14 | } 15 | 16 | export const DEFAULT_LOCALES: Locale[] = [ 17 | { 18 | name: "English (US)", 19 | code: "en_US", 20 | thousandsSeparator: ",", 21 | decimalSeparator: ".", 22 | weekStart: 7, // Sunday 23 | dateFormat: "m/d/yyyy", 24 | timeFormat: "hh:mm:ss a", 25 | formulaArgSeparator: ",", 26 | }, 27 | { 28 | name: "French", 29 | code: "fr_FR", 30 | thousandsSeparator: " ", 31 | decimalSeparator: ",", 32 | weekStart: 1, // Monday 33 | dateFormat: "dd/mm/yyyy", 34 | timeFormat: "hh:mm:ss", 35 | formulaArgSeparator: ";", 36 | }, 37 | ]; 38 | export const DEFAULT_LOCALE: Locale = DEFAULT_LOCALES[0]; 39 | -------------------------------------------------------------------------------- /src/types/range.ts: -------------------------------------------------------------------------------- 1 | import { Cloneable, UID, UnboundedZone, Zone } from "./misc"; 2 | 3 | export interface RangePart { 4 | readonly colFixed: boolean; 5 | readonly rowFixed: boolean; 6 | } 7 | 8 | export interface Range extends Cloneable { 9 | readonly zone: Readonly; 10 | readonly parts: readonly RangePart[]; 11 | readonly invalidXc?: string; 12 | /** true if the user provided the range with the sheet name */ 13 | readonly prefixSheet: boolean; 14 | /** the name of any sheet that is invalid */ 15 | readonly invalidSheetName?: string; 16 | /** the sheet on which the range is defined */ 17 | readonly sheetId: UID; 18 | readonly rangeData: RangeData; 19 | } 20 | 21 | export interface RangeData { 22 | _zone: Zone | UnboundedZone; 23 | _sheetId: UID; 24 | } 25 | -------------------------------------------------------------------------------- /src/types/validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandResult } from "./commands"; 2 | import { Validation } from "./misc"; 3 | 4 | export interface Validator { 5 | /** 6 | * Combine multiple validation functions into a single function 7 | * returning the list of result of every validation. 8 | */ 9 | batchValidations(...validations: Validation[]): Validation; 10 | 11 | /** 12 | * Combine multiple validation functions. Every validation is executed one after 13 | * the other. As soon as one validation fails, it stops and the cancelled reason 14 | * is returned. 15 | */ 16 | chainValidations(...validations: Validation[]): Validation; 17 | 18 | checkValidations(command: T, ...validations: Validation[]): CommandResult | CommandResult[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/xlsx/conversion/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cf_conversion"; 2 | export * from "./color_conversion"; 3 | export * from "./conversion_maps"; 4 | export * from "./figure_conversion"; 5 | export * from "./sheet_conversion"; 6 | export * from "./style_conversion"; 7 | export * from "./table_conversion"; 8 | -------------------------------------------------------------------------------- /src/xlsx/extraction/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./misc_extractor"; 2 | export * from "./sheet_extractor"; 3 | export * from "./style_extractor"; 4 | -------------------------------------------------------------------------------- /src/xlsx/helpers/colors.ts: -------------------------------------------------------------------------------- 1 | import { toHex } from "../../helpers"; 2 | import { Color } from "../../types"; 3 | import { XlsxHexColor } from "../../types/xlsx"; 4 | 5 | /** 6 | * Convert a JS color hexadecimal to an excel compatible color. 7 | * 8 | * In Excel the color don't start with a '#' and the format is AARRGGBB instead of RRGGBBAA 9 | */ 10 | export function toXlsxHexColor(color: Color): XlsxHexColor { 11 | color = toHex(color).replace("#", ""); 12 | // alpha channel goes first 13 | if (color.length === 8) { 14 | return color.slice(6) + color.slice(0, 6); 15 | } 16 | return color; 17 | } 18 | -------------------------------------------------------------------------------- /tests/__mocks__/dom_helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return true if the event was triggered from 3 | * a child element. 4 | */ 5 | export function isChildEvent(parent: HTMLElement, ev: Event): boolean { 6 | return !!ev.target && parent!.contains(ev.target as Node); 7 | } 8 | 9 | export function gridOverlayPosition() { 10 | const spreadsheetElement = document.querySelector(".o-grid-overlay"); 11 | if (spreadsheetElement) { 12 | const { top, left } = spreadsheetElement.getBoundingClientRect(); 13 | return { top, left }; 14 | } else { 15 | return { top: 0, left: 0 }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/__mocks__/mock_file_store.ts: -------------------------------------------------------------------------------- 1 | import { FileStore as FileStoreInterface } from "../../src/types/files"; 2 | 3 | export class FileStore implements FileStoreInterface { 4 | private fileId = 0; 5 | async upload(_file: File): Promise { 6 | return `file/${this.fileId++}`; 7 | } 8 | 9 | async delete() {} 10 | } 11 | -------------------------------------------------------------------------------- /tests/__mocks__/mock_image_provider.ts: -------------------------------------------------------------------------------- 1 | import { FigureSize } from "../../src/types"; 2 | import { FileStore, ImageProviderInterface } from "../../src/types/files"; 3 | import { Image } from "../../src/types/image"; 4 | 5 | export class ImageProvider implements ImageProviderInterface { 6 | private path = "https://sorrygooglesheet.com/icon-picture"; 7 | private size = { 8 | width: 1443, 9 | height: 2168, 10 | }; 11 | private mimetype = "image/jpeg"; 12 | 13 | constructor(_fileStore: FileStore) {} 14 | 15 | async requestImage(): Promise { 16 | return { path: this.path, size: this.size, mimetype: this.mimetype }; 17 | } 18 | 19 | async getImageOriginalSize(path: string): Promise { 20 | return this.size; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/__mocks__/mock_misc_helpers.ts: -------------------------------------------------------------------------------- 1 | import { DebouncedFunction } from "../../src/types"; 2 | 3 | /** Mocked debounce that doesn't actually do any debouncing, but just calls the function directly */ 4 | export function debounce void>(func: T): DebouncedFunction { 5 | const debounced = function (this: any): void { 6 | func.apply(this, Array.from(arguments)); 7 | }; 8 | debounced.isDebouncePending = () => false; 9 | debounced.stopDebounce = () => {}; 10 | return debounced as DebouncedFunction; 11 | } 12 | -------------------------------------------------------------------------------- /tests/__snapshots__/cog_wheel_menu.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CogWheelMenu Can render a cog wheel menu 1`] = ` 4 |
5 |
8 | 11 | 12 |
13 |
14 | `; 15 | -------------------------------------------------------------------------------- /tests/__xlsx__/read_demo_xlsx.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import JsZip from "jszip"; 3 | import { ImportedFiles } from "../../src/types/xlsx"; 4 | 5 | const PATH = "./tests/__xlsx__/xlsx_demo_data.xlsx"; 6 | 7 | export async function getTextXlsxFiles(): Promise { 8 | const file = readFileSync(PATH); 9 | const jsZip = new JsZip(); 10 | const zip = await jsZip.loadAsync(file); 11 | const files = Object.keys(zip.files); 12 | const contents = await Promise.all( 13 | files.map((file) => 14 | file.includes("media/image") ? { imageSrc: "relative path" } : zip.files[file].async("text") 15 | ) 16 | ); 17 | const inputFiles = {}; 18 | for (let i = 0; i < contents.length; i++) { 19 | inputFiles[files[i]] = contents[i]; 20 | } 21 | 22 | return inputFiles; 23 | } 24 | -------------------------------------------------------------------------------- /tests/__xlsx__/xlsx_demo_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odoo/o-spreadsheet/bef1e2bd5da8a72feada65405a2fb2cf782d4ab3/tests/__xlsx__/xlsx_demo_data.xlsx -------------------------------------------------------------------------------- /tests/action_button.test.ts: -------------------------------------------------------------------------------- 1 | import { Component, xml } from "@odoo/owl"; 2 | import { ActionSpec } from "../src/actions/action"; 3 | import { ActionButton } from "../src/components/action_button/action_button"; 4 | import { SpreadsheetChildEnv } from "../src/types"; 5 | import { mountComponent, nextTick } from "./test_helpers/helpers"; 6 | 7 | interface ParentProps { 8 | getAction: () => ActionSpec; 9 | } 10 | 11 | class Parent extends Component { 12 | static components = { ActionButton }; 13 | static props = { getAction: Function }; 14 | static template = xml/*xml*/ ` 15 | 16 | `; 17 | } 18 | 19 | test("ActionButton is updated when its props are updated", async () => { 20 | let action = { isActive: () => true, name: "TestAction" }; 21 | const { parent, fixture } = await mountComponent(Parent, { props: { getAction: () => action } }); 22 | const actionButton = fixture.querySelector(".o-menu-item-button")!; 23 | expect(actionButton.classList).toContain("active"); 24 | 25 | action = { isActive: () => false, name: "TestAction" }; 26 | parent.render(true); 27 | await nextTick(); 28 | expect(actionButton.classList).not.toContain("active"); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/collaborative/ot/ot.test.ts: -------------------------------------------------------------------------------- 1 | import { transform } from "../../../src/collaborative/ot/ot"; 2 | import { DeleteFigureCommand, UpdateChartCommand, UpdateFigureCommand } from "../../../src/types"; 3 | import { LineChartDefinition } from "../../../src/types/chart/line_chart"; 4 | 5 | describe("OT with DELETE_FIGURE", () => { 6 | const deleteFigure: DeleteFigureCommand = { 7 | type: "DELETE_FIGURE", 8 | sheetId: "42", 9 | id: "42", 10 | }; 11 | const updateChart: Omit = { 12 | type: "UPDATE_CHART", 13 | sheetId: "42", 14 | definition: {} as LineChartDefinition, 15 | }; 16 | const updateFigure: Omit = { 17 | type: "UPDATE_FIGURE", 18 | sheetId: "42", 19 | }; 20 | 21 | describe.each([updateChart, updateFigure])("UPDATE_CHART & UPDATE_FIGURE", (cmd) => { 22 | test("Same ID", () => { 23 | expect(transform({ ...cmd, id: "42" }, deleteFigure)).toBeUndefined(); 24 | }); 25 | 26 | test("distinct ID", () => { 27 | expect(transform({ ...cmd, id: "otherId" }, deleteFigure)).toEqual({ ...cmd, id: "otherId" }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/components/__snapshots__/text_input.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TextInput Can render a text input 1`] = ` 4 |
5 |
8 |
11 | 15 |
16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /tests/composer/__snapshots__/composer_integration_component.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Grid composer grid composer basic style Grid composer snapshot 1`] = ` 4 |
8 |
11 |
14 | 15 |
22 | A 23 |
24 |
25 | 26 |
27 |
28 | `; 29 | -------------------------------------------------------------------------------- /tests/composer/__snapshots__/content_editable_helpers.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ContentEditableHelper setText only applies a diff to the current content Adding some text 1`] = ` 4 |
5 |

6 | 7 | hello 8 | 9 |

10 |
11 | `; 12 | 13 | exports[`ContentEditableHelper setText only applies a diff to the current content Adding some text 2`] = ` 14 |
15 |

16 | 17 | hello 18 | 19 | 20 | test 21 | 22 |

23 |
24 | `; 25 | 26 | exports[`ContentEditableHelper setText only applies a diff to the current content Removing some text 1`] = ` 27 |
28 |

29 | 30 | hello 31 | 32 | 33 | test 34 | 35 |

36 |
37 | `; 38 | 39 | exports[`ContentEditableHelper setText only applies a diff to the current content Removing some text 2`] = ` 40 |
41 |

42 | 43 | hello 44 | 45 |

46 |
47 | `; 48 | -------------------------------------------------------------------------------- /tests/functions/module_engineering.test.ts: -------------------------------------------------------------------------------- 1 | import { evaluateCell } from "../test_helpers/helpers"; 2 | 3 | describe("DELTA formula", () => { 4 | test("take 1 or 2 arguments", () => { 5 | expect(evaluateCell("A1", { A1: "=DELTA()" })).toBe("#BAD_EXPR"); 6 | expect(evaluateCell("A1", { A1: "=DELTA(0)" })).toBe(1); 7 | expect(evaluateCell("A1", { A1: "=DELTA(0,0)" })).toBe(1); 8 | expect(evaluateCell("A1", { A1: "=DELTA(1,2,3)" })).toBe("#BAD_EXPR"); 9 | }); 10 | 11 | test.each([ 12 | ["0", "1", 0], 13 | ["1", "1", 1], 14 | ["-1", "-1", 1], 15 | ])("delta value", (value1, value2, result) => { 16 | expect(evaluateCell("A1", { A1: "=DELTA(A2, A3)", A2: value1, A3: value2 })).toBe(result); 17 | }); 18 | 19 | test("default value for arg 2 is 0", () => { 20 | expect(evaluateCell("A1", { A1: "=DELTA(0)" })).toBe(1); 21 | }); 22 | 23 | test("empty cell are considered as 0", () => { 24 | expect(evaluateCell("A1", { A1: "=DELTA(A2)" })).toBe(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/helpers/positions_map.test.ts: -------------------------------------------------------------------------------- 1 | import { PositionMap } from "../../src/plugins/ui_core_views/cell_evaluation/position_map"; 2 | 3 | describe("PositionMap", () => { 4 | test("set an element", () => { 5 | const map = new PositionMap(); 6 | const A1 = { sheetId: "1", row: 0, col: 0 }; 7 | map.set(A1, 1); 8 | expect(map.get(A1)).toBe(1); 9 | expect(map.has(A1)).toBe(true); 10 | }); 11 | 12 | test("remove an element", () => { 13 | const map = new PositionMap(); 14 | const A1 = { sheetId: "1", row: 0, col: 0 }; 15 | map.set(A1, 1); 16 | map.delete(A1); 17 | expect(map.get(A1)).toBeUndefined(); 18 | expect(map.has(A1)).toBe(false); 19 | }); 20 | 21 | test("empty map has no element", () => { 22 | const map = new PositionMap(); 23 | expect(map.has({ sheetId: "1", row: 0, col: 0 })).toBe(false); 24 | }); 25 | 26 | test("iterate over empty map keys", () => { 27 | const map = new PositionMap(); 28 | expect([...map.keys()]).toEqual([]); 29 | }); 30 | 31 | test("iterate over keys", () => { 32 | const map = new PositionMap(); 33 | const A1 = { sheetId: "1", row: 0, col: 0 }; 34 | const A2 = { sheetId: "1", row: 0, col: 1 }; 35 | map.set(A1, 1); 36 | map.set(A2, 2); 37 | expect([...map.keys()]).toEqual([A1, A2]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/helpers/sheet.test.ts: -------------------------------------------------------------------------------- 1 | import { isSheetNameEqual } from "../../src/helpers"; 2 | 3 | test("sheet equality", () => { 4 | expect(isSheetNameEqual("Sheet1", "Sheet1")).toBeTruthy(); 5 | expect(isSheetNameEqual("Sheet1", "SHEET1")).toBeTruthy(); 6 | expect(isSheetNameEqual("Sheet1", "sheet1")).toBeTruthy(); 7 | expect(isSheetNameEqual("Sheet1", "'sheet1'")).toBeTruthy(); 8 | expect(isSheetNameEqual("Sheet1", "'SHEET1'")).toBeTruthy(); 9 | expect(isSheetNameEqual("Sheet1", "ShEeT1")).toBeTruthy(); 10 | expect(isSheetNameEqual("Sheet1", "Sheet1 ")).toBeTruthy(); 11 | expect(isSheetNameEqual("Sheet1", " Sheet1")).toBeTruthy(); 12 | }); 13 | 14 | test("sheet inequality", () => { 15 | expect(isSheetNameEqual("Sheet1", "Sheet")).toBeFalsy(); 16 | expect(isSheetNameEqual("Sheet1", '"Sheet1"')).toBeFalsy(); 17 | expect(isSheetNameEqual("Sheet1", "Sheet 1")).toBeFalsy(); 18 | expect(isSheetNameEqual("Sheet1", "Sheet1!")).toBeFalsy(); 19 | expect(isSheetNameEqual("Sheet1", "Sheet2")).toBeFalsy(); 20 | expect(isSheetNameEqual("Sheet1", "")).toBeFalsy(); 21 | expect(isSheetNameEqual("Sheet1", undefined)).toBeFalsy(); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/link/__snapshots__/link_display_component.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`link display component simple snapshot 1`] = ` 4 | "" 6 | `; 7 | -------------------------------------------------------------------------------- /tests/popover/__snapshots__/error_tooltip_component.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Grid integration can display error tooltip 1`] = ` 4 |
8 |
11 |
14 | Error 15 |
16 |
19 | The divisor must be different from zero. 20 |
21 | 22 |
23 |
24 | `; 25 | -------------------------------------------------------------------------------- /tests/readme.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | o-spreadsheet is a well tested library with a plethora of tests, using the Jest testing framework. 4 | 5 | ## Running tests 6 | 7 | ```bash 8 | # install dependencies 9 | npm install 10 | # run the test suite 11 | npm run test 12 | ``` 13 | 14 | ## Writing tests guidelines 15 | 16 | - Tests should be located in `tests/` folder. 17 | - Test files should be suffixed by `.test.ts` (eg. `my_feature.test.ts`) 18 | - Test file name should be suffixed by `_component` or `_plugin` where this is relevant. 19 | - this means that a feature should split into separate files testing the components and the plugins 20 | - Test files for the same feature should be grouped inside a single folder 21 | 22 | Example: 23 | 24 | ``` 25 | tests/ 26 | ├─ find_and_replace/ 27 | │ ├─ find_and_replace_plugin.test.ts 28 | │ ├─ find_and_replace_component.test.ts 29 | ├─ readme.md 30 | ``` 31 | 32 | ## Owl Templates 33 | 34 | In an effort to run the tests faster, we pre-compile the owl templates before running the tests. This is done by running the `compileOwlTemplates` script in the `package.json` file. This script compiles all the owl templates in the `src` folder into the `tools/owl_templates/_compiled/owl_compiled_templates.cjs` file. 35 | -------------------------------------------------------------------------------- /tests/setup/jest_global_setup.ts: -------------------------------------------------------------------------------- 1 | import { writeTemplatesToFile } from "../../tools/owl_templates/compile_templates.cjs"; 2 | 3 | module.exports = function () { 4 | writeTemplatesToFile(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/setup/jest_global_teardown.ts: -------------------------------------------------------------------------------- 1 | import { deleteCompiledTemplatesFile } from "../../tools/owl_templates/compile_templates.cjs"; 2 | 3 | module.exports = function () { 4 | deleteCompiledTemplatesFile(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/setup/polyfill.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | Object.groupBy ??= function (values, keyFinder) { 3 | // using reduce to aggregate values 4 | //@ts-ignore 5 | return values.reduce((a, b) => { 6 | // depending upon the type of keyFinder 7 | // if it is function, pass the value to it 8 | // if it is a property, access the property 9 | //@ts-ignore 10 | const key = typeof keyFinder === "function" ? keyFinder(b) : b[keyFinder]; 11 | 12 | // aggregate values based on the keys 13 | if (!a[key]) { 14 | a[key] = [b]; 15 | } else { 16 | a[key] = [...a[key], b]; 17 | } 18 | 19 | return a; 20 | }, {}); 21 | }; 22 | -------------------------------------------------------------------------------- /tests/setup/resize_observer.mock.ts: -------------------------------------------------------------------------------- 1 | class MockResizeObserver { 2 | public cb: Function; 3 | constructor(cb: Function) { 4 | this.cb = cb; 5 | } 6 | observe() { 7 | window.resizers.add(this); 8 | Promise.resolve().then(() => this.cb()); 9 | } 10 | 11 | unobserve() { 12 | window.resizers.remove(this); 13 | } 14 | 15 | disconnect() { 16 | window.resizers.remove(this); 17 | } 18 | } 19 | window.ResizeObserver = MockResizeObserver; 20 | 21 | export class Resizers { 22 | private resizers: Set = new Set(); 23 | 24 | add(resizeObserver: MockResizeObserver) { 25 | this.resizers.add(resizeObserver); 26 | } 27 | 28 | remove(resizeObserver: MockResizeObserver) { 29 | this.resizers.delete(resizeObserver); 30 | } 31 | 32 | removeAll() { 33 | this.resizers = new Set(); 34 | } 35 | 36 | resize() { 37 | this.resizers.forEach((r) => r.cb()); 38 | } 39 | } 40 | 41 | window.resizers = new Resizers(); 42 | -------------------------------------------------------------------------------- /tests/side_panels/building_blocks/__snapshots__/data_series.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Data Series Can render a data series component 1`] = ` 4 |
5 |
8 |
11 | Data series 12 |
13 | 14 |
17 |
20 |
23 | 30 | 31 |
32 | 33 |
34 | 35 | 36 |
39 | 44 | 45 | 46 |
47 |
48 | 49 |
50 |
51 | `; 52 | -------------------------------------------------------------------------------- /tests/side_panels/building_blocks/__snapshots__/round_color_picker.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Panel color picker Can render a panel color picker component 1`] = ` 4 |
5 |
8 |
12 | 13 |
14 |
15 | `; 16 | -------------------------------------------------------------------------------- /tests/side_panels/building_blocks/error_section.test.ts: -------------------------------------------------------------------------------- 1 | import { ChartErrorSection } from "../../../src/components/side_panel/chart/building_blocks/error_section/error_section"; 2 | import { mountComponent } from "../../test_helpers/helpers"; 3 | 4 | let fixture: HTMLElement; 5 | 6 | async function mountChartErrorSection(props: ChartErrorSection["props"]) { 7 | ({ fixture } = await mountComponent(ChartErrorSection, { props })); 8 | } 9 | 10 | describe("Chart error section", () => { 11 | test("Can render a chart error section component", async () => { 12 | await mountChartErrorSection({ 13 | messages: ["error_1", "error_2"], 14 | }); 15 | expect(fixture).toMatchSnapshot(); 16 | }); 17 | 18 | test("Error section does not have a title", async () => { 19 | await mountChartErrorSection({ 20 | messages: ["error_1", "error_2"], 21 | }); 22 | expect(fixture.querySelector(".o-section-title")).toBeNull(); 23 | }); 24 | 25 | test("Messages are in error", async () => { 26 | await mountChartErrorSection({ 27 | messages: ["error_1", "error_2"], 28 | }); 29 | expect(fixture.querySelectorAll(".o-validation-error")).toHaveLength(2); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/side_panels/building_blocks/label_range.test.ts: -------------------------------------------------------------------------------- 1 | import { ChartLabelRange } from "../../../src/components/side_panel/chart/building_blocks/label_range/label_range"; 2 | import { mountComponent } from "../../test_helpers/helpers"; 3 | 4 | let fixture: HTMLElement; 5 | 6 | async function mountLabelRange(props: ChartLabelRange["props"]) { 7 | ({ fixture } = await mountComponent(ChartLabelRange, { props })); 8 | } 9 | 10 | describe("Label range", () => { 11 | test("Can render a label range component", async () => { 12 | await mountLabelRange({ 13 | range: "A1:B1", 14 | isInvalid: false, 15 | onSelectionChanged: () => {}, 16 | onSelectionConfirmed: () => {}, 17 | }); 18 | expect(fixture).toMatchSnapshot(); 19 | }); 20 | 21 | test("Can add options to the label range component", async () => { 22 | await mountLabelRange({ 23 | range: "A1:B1", 24 | isInvalid: false, 25 | onSelectionChanged: () => {}, 26 | onSelectionConfirmed: () => {}, 27 | options: [ 28 | { 29 | name: "my_option", 30 | label: "My option", 31 | value: true, 32 | onChange: () => {}, 33 | }, 34 | ], 35 | }); 36 | expect(fixture).toMatchSnapshot(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/side_panels/components/__snapshots__/checkbox.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Checkbox Can render a checkbox 1`] = ` 4 |
5 | 14 |
15 | `; 16 | -------------------------------------------------------------------------------- /tests/side_panels/components/__snapshots__/section.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Section Can render a section with a title 1`] = ` 4 |
5 |
8 |
11 |
14 | My title 15 |
16 | 17 |
20 | My content 21 |
22 | 23 |
24 |
25 |
26 | `; 27 | 28 | exports[`Section Can render a section without a title 1`] = ` 29 |
30 |
33 |
36 | 37 |
40 | My content 41 |
42 | 43 |
44 |
45 |
46 | `; 47 | -------------------------------------------------------------------------------- /tests/test_helpers/chart_helpers.ts: -------------------------------------------------------------------------------- 1 | import { Model, SpreadsheetChildEnv, UID } from "../../src"; 2 | import { simulateClick } from "./dom_helper"; 3 | import { nextTick } from "./helpers"; 4 | 5 | export function isChartAxisStacked(model: Model, chartId: UID, axis: "x" | "y"): boolean { 6 | return getChartConfiguration(model, chartId).options?.scales?.[axis]?.stacked; 7 | } 8 | 9 | export function getChartConfiguration(model: Model, chartId: UID) { 10 | const runtime = model.getters.getChartRuntime(chartId) as any; 11 | return runtime.chartJsConfig; 12 | } 13 | 14 | export async function openChartConfigSidePanel(model: Model, env: SpreadsheetChildEnv, id: UID) { 15 | model.dispatch("SELECT_FIGURE", { id }); 16 | env.openSidePanel("ChartPanel"); 17 | await nextTick(); 18 | } 19 | 20 | export async function openChartDesignSidePanel( 21 | model: Model, 22 | env: SpreadsheetChildEnv, 23 | fixture: HTMLElement, 24 | id: UID 25 | ) { 26 | if (!fixture.querySelector(".o-chart")) { 27 | await openChartConfigSidePanel(model, env, id); 28 | } 29 | await simulateClick(".o-panel-element.inactive"); 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./commands_helpers"; 2 | export * from "./dom_helper"; 3 | export * from "./getters_helpers"; 4 | export * from "./mock_helpers"; 5 | -------------------------------------------------------------------------------- /tests/test_helpers/stores.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../../src"; 2 | import { DependencyContainer, StoreConstructor, StoreParams } from "../../src/store_engine"; 3 | import { ModelStore } from "../../src/stores"; 4 | import { NotificationStore } from "../../src/stores/notification_store"; 5 | import { makeTestNotificationStore } from "./helpers"; 6 | 7 | export function makeStore(Store: T, ...args: StoreParams) { 8 | return makeStoreWithModel(new Model(), Store, ...args); 9 | } 10 | 11 | export function makeStoreWithModel( 12 | model: Model, 13 | Store: T, 14 | ...args: StoreParams 15 | ) { 16 | const container = new DependencyContainer(); 17 | container.inject(ModelStore, model); 18 | container.inject(NotificationStore, makeTestNotificationStore()); 19 | return { 20 | store: container.instantiate(Store, ...args) as InstanceType, 21 | container, 22 | model: container.get(ModelStore), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /tools/bundle_xlsx/unzip_xlsx_demo.cjs: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process"); 2 | 3 | let unzipXlsxCommand = "sh ./tools/bundle_xlsx/unzip_xlsx_demo.sh"; 4 | if (process.platform === "win32") { 5 | exec("where wsl", (error) => { 6 | if (error !== null) { 7 | console.error(`This script needs wsl to be installed to run on windows`); 8 | process.exit(1); 9 | } 10 | }); 11 | 12 | unzipXlsxCommand = "wsl " + unzipXlsxCommand; 13 | } 14 | 15 | exec(unzipXlsxCommand, (error, stdout, stderr) => { 16 | console.log(stdout); 17 | console.log(stderr); 18 | if (error !== null) { 19 | console.log(`exec error: ${error}`); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /tools/bundle_xlsx/unzip_xlsx_demo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -rf ./tests/__xlsx__/xlsx_demo_data/* 3 | unzip -o -d ./tests/__xlsx__/xlsx_demo_data ./tests/__xlsx__/xlsx_demo_data.xlsx 4 | 5 | if dpkg-query -W -f'${Status}' "libxml2-utils" 2>/dev/null | grep -q "ok installed"; 6 | then 7 | find ./tests/__xlsx__/xlsx_demo_data -regex ".*\(xml\|xml.rels\)$" -type f -exec xmllint --output '{}' --format '{}' \; 8 | else 9 | echo "install libxml2-utils if you want to have prettified xmls"; 10 | fi 11 | -------------------------------------------------------------------------------- /tools/bundle_xlsx/zip_xlsx_demo.cjs: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process"); 2 | 3 | let zipXlsxCommand = "sh ./tools/bundle_xlsx/zip_xlsx_demo.sh"; 4 | if (process.platform === "win32") { 5 | exec("where wsl", (error) => { 6 | if (error !== null) { 7 | console.error(`This script needs wsl to be installed to run on windows`); 8 | process.exit(1); 9 | } 10 | }); 11 | 12 | zipXlsxCommand = "wsl " + zipXlsxCommand; 13 | } 14 | 15 | exec(zipXlsxCommand, (error, stdout, stderr) => { 16 | console.log(stdout); 17 | console.log(stderr); 18 | if (error !== null) { 19 | console.log(`exec error: ${error}`); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /tools/bundle_xlsx/zip_xlsx_demo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ./tests/__xlsx__/xlsx_demo_data 3 | find . -type f | xargs zip ../xlsx_demo_data.xlsx 4 | 5 | -------------------------------------------------------------------------------- /tools/bundle_xml/main.cjs: -------------------------------------------------------------------------------- 1 | const { bundle } = require("../bundle.cjs"); 2 | const xmlBundle = require("./bundle_xml_templates.cjs"); 3 | const parseArgs = require("minimist"); 4 | 5 | const DEFAULT_DIR = "dist"; 6 | 7 | const argv = parseArgs(process.argv.slice(2)); 8 | 9 | xmlBundle.writeOwlTemplateBundleToFile(argv.outDir || DEFAULT_DIR, bundle.xmlBanner()); 10 | -------------------------------------------------------------------------------- /tools/bundle_xml/watch_xml_templates.cjs: -------------------------------------------------------------------------------- 1 | const watch = require("node-watch"); 2 | const bundle = require("./bundle_xml_templates.cjs"); 3 | 4 | const watcher = watch("./src", { filter: /\.xml$/, recursive: true }, (ev, name) => { 5 | console.log(`\nFile ${name}: ${ev}`); 6 | bundle.writeOwlTemplateBundleToFile("build"); 7 | }); 8 | 9 | watcher.on("ready", () => console.log("Watching .xml files...")); 10 | watcher.on("error", (err) => console.error(`Error watching .xml files ${err}`)); 11 | 12 | process.on("SIGINT", () => watcher.close()); 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES6", 4 | "target": "ESNext", 5 | "preserveConstEnums": true, 6 | "noImplicitThis": true, 7 | "moduleResolution": "node", 8 | "removeComments": false, 9 | "alwaysStrict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": false, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "strictPropertyInitialization": true, 15 | "strictNullChecks": true, 16 | "esModuleInterop": true, 17 | "allowJs": true, 18 | "sourceMap": true, 19 | "strictBindCallApply": true 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src", "tests"], 7 | "files": ["global.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "build/js", 5 | "preserveWatchOutput": true 6 | }, 7 | "include": ["src"] 8 | } 9 | --------------------------------------------------------------------------------