mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-08 20:32:56 +03:00
Spreadsheet: Implement infinit-scroll for columns
This naturally also implements multi-char columns, and also integrates it into the js runtime (such columns can be named in ranges too).
This commit is contained in:
parent
f6ae4edbd2
commit
474453244b
Notes:
sideshowbarker
2024-07-19 01:09:34 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/474453244b5 Pull-request: https://github.com/SerenityOS/serenity/pull/4177 Issue: https://github.com/SerenityOS/serenity/issues/4167 Issue: https://github.com/SerenityOS/serenity/issues/4168 Issue: https://github.com/SerenityOS/serenity/issues/4170 Issue: https://github.com/SerenityOS/serenity/issues/4171 Reviewed-by: https://github.com/awesomekling
@ -83,6 +83,8 @@ void SheetGlobalObject::initialize()
|
||||
GlobalObject::initialize();
|
||||
define_native_function("parse_cell_name", parse_cell_name, 1);
|
||||
define_native_function("current_cell_position", current_cell_position, 0);
|
||||
define_native_function("column_arithmetic", column_arithmetic, 2);
|
||||
define_native_function("column_index", column_index, 1);
|
||||
}
|
||||
|
||||
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::parse_cell_name)
|
||||
@ -137,6 +139,82 @@ JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::current_cell_position)
|
||||
return object;
|
||||
}
|
||||
|
||||
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_index)
|
||||
{
|
||||
if (vm.argument_count() != 2) {
|
||||
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to column_index()");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto column_name = vm.argument(0);
|
||||
if (!column_name.is_string()) {
|
||||
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "String");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto& column_name_str = column_name.as_string().string();
|
||||
|
||||
auto* this_object = vm.this_value(global_object).to_object(global_object);
|
||||
if (!this_object)
|
||||
return JS::js_null();
|
||||
|
||||
if (StringView("SheetGlobalObject") != this_object->class_name()) {
|
||||
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
|
||||
auto& sheet = sheet_object->m_sheet;
|
||||
auto column_index = sheet.column_index(column_name_str);
|
||||
if (!column_index.has_value()) {
|
||||
vm.throw_exception(global_object, JS::TypeError::create(global_object, String::formatted("'{}' is not a valid column", column_name_str)));
|
||||
return {};
|
||||
}
|
||||
|
||||
return JS::Value((i32)column_index.value());
|
||||
}
|
||||
|
||||
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_arithmetic)
|
||||
{
|
||||
if (vm.argument_count() != 2) {
|
||||
vm.throw_exception<JS::TypeError>(global_object, "Expected exactly two arguments to column_arithmetic()");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto column_name = vm.argument(0);
|
||||
if (!column_name.is_string()) {
|
||||
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "String");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto& column_name_str = column_name.as_string().string();
|
||||
|
||||
auto offset = vm.argument(1).to_number(global_object);
|
||||
if (!offset.is_number())
|
||||
return {};
|
||||
|
||||
auto offset_number = offset.as_i32();
|
||||
|
||||
auto* this_object = vm.this_value(global_object).to_object(global_object);
|
||||
if (!this_object)
|
||||
return JS::js_null();
|
||||
|
||||
if (StringView("SheetGlobalObject") != this_object->class_name()) {
|
||||
vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
|
||||
auto& sheet = sheet_object->m_sheet;
|
||||
auto new_column = sheet.column_arithmetic(column_name_str, offset_number);
|
||||
if (!new_column.has_value()) {
|
||||
vm.throw_exception(global_object, JS::TypeError::create(global_object, String::formatted("'{}' is not a valid column", column_name_str)));
|
||||
return {};
|
||||
}
|
||||
|
||||
return JS::js_string(vm, new_column.release_value());
|
||||
}
|
||||
|
||||
WorkbookObject::WorkbookObject(Workbook& workbook)
|
||||
: JS::Object(*JS::Object::create_empty(workbook.global_object()))
|
||||
, m_workbook(workbook)
|
||||
|
@ -46,6 +46,8 @@ public:
|
||||
|
||||
JS_DECLARE_NATIVE_FUNCTION(parse_cell_name);
|
||||
JS_DECLARE_NATIVE_FUNCTION(current_cell_position);
|
||||
JS_DECLARE_NATIVE_FUNCTION(column_index);
|
||||
JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
|
||||
|
||||
private:
|
||||
Sheet& m_sheet;
|
||||
|
@ -99,28 +99,57 @@ size_t Sheet::add_row()
|
||||
return m_rows++;
|
||||
}
|
||||
|
||||
static String convert_to_string(size_t value, unsigned base = 26, StringView map = {})
|
||||
{
|
||||
if (map.is_null())
|
||||
map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
ASSERT(base >= 2 && base <= map.length());
|
||||
|
||||
// The '8 bits per byte' assumption may need to go?
|
||||
Array<char, round_up_to_power_of_two(sizeof(size_t) * 8 + 1, 2)> buffer;
|
||||
size_t i = 0;
|
||||
do {
|
||||
buffer[i++] = map[value % base];
|
||||
value /= base;
|
||||
} while (value > 0);
|
||||
|
||||
// NOTE: Weird as this may seem, the thing that comes after 'A' is 'AA', which as a number would be '00'
|
||||
// to make this work, only the most significant digit has to be in a range of (1..25) as opposed to (0..25),
|
||||
// but only if it's not the only digit in the string.
|
||||
if (i > 1)
|
||||
--buffer[i - 1];
|
||||
|
||||
for (size_t j = 0; j < i / 2; ++j)
|
||||
swap(buffer[j], buffer[i - j - 1]);
|
||||
|
||||
return String { ReadonlyBytes(buffer.data(), i) };
|
||||
}
|
||||
|
||||
static size_t convert_from_string(StringView str, unsigned base = 26, StringView map = {})
|
||||
{
|
||||
if (map.is_null())
|
||||
map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
ASSERT(base >= 2 && base <= map.length());
|
||||
|
||||
size_t value = 0;
|
||||
for (size_t i = str.length(); i > 0; --i) {
|
||||
auto digit_value = map.find_first_of(str[i - 1]).value_or(0);
|
||||
// NOTE: Refer to the note in `convert_to_string()'.
|
||||
if (i == str.length() && str.length() > 1)
|
||||
++digit_value;
|
||||
value = value * base + digit_value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
String Sheet::add_column()
|
||||
{
|
||||
if (m_current_column_name_length == 0) {
|
||||
m_current_column_name_length = 1;
|
||||
m_columns.append("A");
|
||||
return "A";
|
||||
}
|
||||
|
||||
if (m_current_column_name_length == 1) {
|
||||
auto last_char = m_columns.last()[0];
|
||||
if (last_char == 'Z') {
|
||||
m_current_column_name_length = 2;
|
||||
m_columns.append("AA");
|
||||
return "AA";
|
||||
}
|
||||
|
||||
last_char++;
|
||||
m_columns.append({ &last_char, 1 });
|
||||
return m_columns.last();
|
||||
}
|
||||
|
||||
TODO();
|
||||
auto next_column = convert_to_string(m_columns.size());
|
||||
m_columns.append(next_column);
|
||||
return next_column;
|
||||
}
|
||||
|
||||
void Sheet::update()
|
||||
@ -207,6 +236,31 @@ Optional<Position> Sheet::parse_cell_name(const StringView& name)
|
||||
return Position { col, row.to_uint().value() };
|
||||
}
|
||||
|
||||
Optional<size_t> Sheet::column_index(const StringView& column_name) const
|
||||
{
|
||||
auto index = convert_from_string(column_name);
|
||||
if (m_columns.size() <= index || m_columns[index] != column_name)
|
||||
return {};
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
Optional<String> Sheet::column_arithmetic(const StringView& column_name, int offset)
|
||||
{
|
||||
auto maybe_index = column_index(column_name);
|
||||
if (!maybe_index.has_value())
|
||||
return {};
|
||||
|
||||
auto index = maybe_index.value() + offset;
|
||||
if (m_columns.size() > index)
|
||||
return m_columns[index];
|
||||
|
||||
for (size_t i = m_columns.size(); i <= index; ++i)
|
||||
add_column();
|
||||
|
||||
return m_columns.last();
|
||||
}
|
||||
|
||||
Cell* Sheet::from_url(const URL& url)
|
||||
{
|
||||
auto maybe_position = position_from_url(url);
|
||||
@ -335,6 +389,11 @@ RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook)
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
if (sheet->m_columns.size() < default_column_count && sheet->columns_are_standard()) {
|
||||
for (size_t i = sheet->m_columns.size(); i < default_column_count; ++i)
|
||||
sheet->add_column();
|
||||
}
|
||||
|
||||
auto cells = object.get("cells").as_object();
|
||||
auto json = sheet->interpreter().global_object().get("JSON");
|
||||
auto& parse_function = json.as_object().get("parse").as_function();
|
||||
@ -425,15 +484,28 @@ Position Sheet::written_data_bounds() const
|
||||
{
|
||||
Position bound;
|
||||
for (auto& entry : m_cells) {
|
||||
if (entry.key.row > bound.row)
|
||||
if (entry.key.row >= bound.row)
|
||||
bound.row = entry.key.row;
|
||||
if (entry.key.column > bound.column)
|
||||
if (entry.key.column >= bound.column)
|
||||
bound.column = entry.key.column;
|
||||
}
|
||||
|
||||
return bound;
|
||||
}
|
||||
|
||||
/// The sheet is allowed to have nonstandard column names
|
||||
/// this checks whether all existing columns are 'standard'
|
||||
/// (i.e. as generated by 'convert_to_string()'
|
||||
bool Sheet::columns_are_standard() const
|
||||
{
|
||||
for (size_t i = 0; i < m_columns.size(); ++i) {
|
||||
if (m_columns[i] != convert_to_string(i))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
JsonObject Sheet::to_json() const
|
||||
{
|
||||
JsonObject object;
|
||||
@ -448,12 +520,13 @@ JsonObject Sheet::to_json() const
|
||||
|
||||
auto bottom_right = written_data_bounds();
|
||||
|
||||
if (!columns_are_standard()) {
|
||||
auto columns = JsonArray();
|
||||
for (auto& column : m_columns)
|
||||
columns.append(column);
|
||||
object.set("columns", move(columns));
|
||||
|
||||
object.set("rows", bottom_right.row);
|
||||
}
|
||||
object.set("rows", bottom_right.row + 1);
|
||||
|
||||
JsonObject cells;
|
||||
for (auto& it : m_cells) {
|
||||
@ -520,12 +593,21 @@ Vector<Vector<String>> Sheet::to_xsv() const
|
||||
auto bottom_right = written_data_bounds();
|
||||
|
||||
// First row = headers.
|
||||
size_t column_count = m_columns.size();
|
||||
if (columns_are_standard()) {
|
||||
column_count = convert_from_string(bottom_right.column) + 1;
|
||||
Vector<String> cols;
|
||||
for (size_t i = 0; i < column_count; ++i)
|
||||
cols.append(m_columns[i]);
|
||||
data.append(move(cols));
|
||||
} else {
|
||||
data.append(m_columns);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i <= bottom_right.row; ++i) {
|
||||
Vector<String> row;
|
||||
row.resize(m_columns.size());
|
||||
for (size_t j = 0; j < m_columns.size(); ++j) {
|
||||
row.resize(column_count);
|
||||
for (size_t j = 0; j < column_count; ++j) {
|
||||
auto cell = at({ m_columns[j], i });
|
||||
if (cell)
|
||||
row[j] = cell->typed_display();
|
||||
@ -546,6 +628,10 @@ RefPtr<Sheet> Sheet::from_xsv(const Reader::XSV& xsv, Workbook& workbook)
|
||||
sheet->m_columns = cols;
|
||||
for (size_t i = 0; i < max(rows, Sheet::default_row_count); ++i)
|
||||
sheet->add_row();
|
||||
if (sheet->columns_are_standard()) {
|
||||
for (size_t i = sheet->m_columns.size(); i < Sheet::default_column_count; ++i)
|
||||
sheet->add_column();
|
||||
}
|
||||
|
||||
for (auto row : xsv) {
|
||||
for (size_t i = 0; i < cols.size(); ++i) {
|
||||
|
@ -52,6 +52,8 @@ public:
|
||||
~Sheet();
|
||||
|
||||
static Optional<Position> parse_cell_name(const StringView&);
|
||||
Optional<size_t> column_index(const StringView& column_name) const;
|
||||
Optional<String> column_arithmetic(const StringView& column_name, int offset);
|
||||
|
||||
Cell* from_url(const URL&);
|
||||
const Cell* from_url(const URL& url) const { return const_cast<Sheet*>(this)->from_url(url); }
|
||||
@ -130,6 +132,8 @@ public:
|
||||
/// Gives the bottom-right corner of the smallest bounding box containing all the written data.
|
||||
Position written_data_bounds() const;
|
||||
|
||||
bool columns_are_standard() const;
|
||||
|
||||
private:
|
||||
explicit Sheet(Workbook&);
|
||||
explicit Sheet(const StringView& name, Workbook&);
|
||||
@ -145,8 +149,6 @@ private:
|
||||
|
||||
Cell* m_current_cell_being_evaluated { nullptr };
|
||||
|
||||
size_t m_current_column_name_length { 0 };
|
||||
|
||||
HashTable<Cell*> m_visited_cells_in_update;
|
||||
};
|
||||
|
||||
|
@ -62,10 +62,15 @@ void InfinitelyScrollableTableView::did_scroll()
|
||||
{
|
||||
TableView::did_scroll();
|
||||
auto& vscrollbar = vertical_scrollbar();
|
||||
auto& hscrollbar = horizontal_scrollbar();
|
||||
if (vscrollbar.is_visible() && vscrollbar.value() == vscrollbar.max()) {
|
||||
if (on_reaching_vertical_end)
|
||||
on_reaching_vertical_end();
|
||||
}
|
||||
if (hscrollbar.is_visible() && hscrollbar.value() == hscrollbar.max()) {
|
||||
if (on_reaching_horizontal_end)
|
||||
on_reaching_horizontal_end();
|
||||
}
|
||||
}
|
||||
|
||||
void SpreadsheetView::update_with_model()
|
||||
@ -92,6 +97,15 @@ SpreadsheetView::SpreadsheetView(Sheet& sheet)
|
||||
};
|
||||
update_with_model();
|
||||
};
|
||||
m_table_view->on_reaching_horizontal_end = [&]() {
|
||||
for (size_t i = 0; i < 10; ++i) {
|
||||
m_sheet->add_column();
|
||||
auto last_column_index = m_sheet->column_count() - 1;
|
||||
m_table_view->set_column_width(last_column_index, 50);
|
||||
m_table_view->set_column_header_alignment(last_column_index, Gfx::TextAlignment::Center);
|
||||
}
|
||||
update_with_model();
|
||||
};
|
||||
|
||||
set_focus_proxy(m_table_view);
|
||||
|
||||
|
@ -83,6 +83,7 @@ class InfinitelyScrollableTableView : public GUI::TableView {
|
||||
C_OBJECT(InfinitelyScrollableTableView)
|
||||
public:
|
||||
Function<void()> on_reaching_vertical_end;
|
||||
Function<void()> on_reaching_horizontal_end;
|
||||
|
||||
private:
|
||||
virtual void did_scroll() override;
|
||||
|
@ -27,20 +27,20 @@ class Position {
|
||||
|
||||
left(how_many) {
|
||||
how_many = how_many ?? 1;
|
||||
const column = Math.min(
|
||||
"Z".charCodeAt(0),
|
||||
Math.max("A".charCodeAt(0), this.column.charCodeAt(0) - how_many)
|
||||
return new Position(
|
||||
this.sheet.column_arithmetic(this.column, -how_many),
|
||||
this.row,
|
||||
this.sheet
|
||||
);
|
||||
return new Position(String.fromCharCode(column), this.row, this.sheet);
|
||||
}
|
||||
|
||||
right(how_many) {
|
||||
how_many = how_many ?? 1;
|
||||
const column = Math.min(
|
||||
"Z".charCodeAt(0),
|
||||
Math.max("A".charCodeAt(0), this.column.charCodeAt(0) + how_many)
|
||||
return new Position(
|
||||
this.sheet.column_arithmetic(this.column, how_many),
|
||||
this.row,
|
||||
this.sheet
|
||||
);
|
||||
return new Position(String.fromCharCode(column), this.row, this.sheet);
|
||||
}
|
||||
|
||||
with_column(value) {
|
||||
@ -78,22 +78,21 @@ function range(start, end, columnStep, rowStep) {
|
||||
end = parse_cell_name(end) ?? start;
|
||||
}
|
||||
|
||||
if (end.column.length > 1 || start.column.length > 1)
|
||||
throw new TypeError("Only single-letter column names are allowed (TODO)");
|
||||
|
||||
const cells = [];
|
||||
|
||||
for (
|
||||
let col = Math.min(start.column.charCodeAt(0), end.column.charCodeAt(0));
|
||||
col <= Math.max(start.column.charCodeAt(0), end.column.charCodeAt(0));
|
||||
col += columnStep
|
||||
) {
|
||||
const start_column_index = column_index(start.column);
|
||||
const end_column_index = column_index(end.column);
|
||||
const start_column = start_column_index > end_column_index ? end.column : start.column;
|
||||
const distance = Math.abs(start_column_index - end_column_index);
|
||||
|
||||
for (let col = 0; col <= distance; col += columnStep) {
|
||||
const column = column_arithmetic(start_column, col);
|
||||
for (
|
||||
let row = Math.min(start.row, end.row);
|
||||
row <= Math.max(start.row, end.row);
|
||||
row += rowStep
|
||||
) {
|
||||
cells.push(String.fromCharCode(col) + row);
|
||||
cells.push(column + row);
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,6 +344,7 @@ range.__documentation = JSON.stringify({
|
||||
examples: {
|
||||
'range("A1", "C4")': "Generate a range A1:C4",
|
||||
'range("A1", "C4", 2)': "Generate a range A1:C4, skipping every other column",
|
||||
'range("AA1", "AC4", 2)': "Generate a range AA1:AC4, skipping every other column",
|
||||
},
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user