lib/ckeditor5/rails/presets/toolbar_builder.rb



# frozen_string_literal: true

module CKEditor5::Rails::Presets
  # Builder class for configuring CKEditor5 toolbar items.
  #
  # @example Basic toolbar configuration
  #   toolbar = ToolbarBuilder.new([:bold, :italic])
  #   toolbar.append(:link)
  #   toolbar.prepend(:heading)
  class ToolbarBuilder
    attr_reader :items

    # Initialize a new toolbar builder with given items.
    #
    # @param items [Array<Symbol>] Initial toolbar items
    # @example Create new toolbar
    #   ToolbarBuilder.new([:bold, :italic, :|, :link])
    def initialize(items)
      @items = items
    end

    # Returns toolbar line break symbol
    #
    # @return [Symbol] Line break symbol (-)
    # @example Add line break to toolbar
    #   toolbar do
    #     append :bold, break_line, :italic
    #   end
    def break_line
      :-
    end

    # Returns toolbar separator symbol
    #
    # @return [Symbol] Separator symbol (|)
    # @example Add separator to toolbar
    #   toolbar do
    #     append :bold, separator, :italic
    #   end
    def separator
      :|
    end

    # Remove items from the editor toolbar.
    #
    # @param removed_items [Array<Symbol>] Toolbar items to be removed
    # @example Remove items from toolbar
    #   toolbar do
    #     remove :underline, :heading
    #   end
    def remove(*removed_items)
      items.delete_if do |existing_item|
        removed_items.any? { |item_to_remove| item_matches?(existing_item, item_to_remove) }
      end
    end

    # Prepend items to the editor toolbar.
    #
    # @param prepended_items [Array<Symbol>] Toolbar items to be prepended
    # @param before [Symbol, nil] Optional item before which to insert new items
    # @example Prepend items to toolbar
    #   toolbar do
    #     prepend :selectAll, :|, :selectAll, :selectAll
    #   end
    # @example Insert items before specific item
    #   toolbar do
    #     prepend :selectAll, before: :bold
    #   end
    # @raise [ArgumentError] When the specified 'before' item is not found
    def prepend(*prepended_items, before: nil)
      if before
        index = find_item_index(before)
        raise ArgumentError, "Item '#{before}' not found in array" unless index

        items.insert(index, *prepended_items)
      else
        items.insert(0, *prepended_items)
      end
    end

    # Append items to the editor toolbar.
    #
    # @param appended_items [Array<Symbol>] Toolbar items to be appended
    # @param after [Symbol, nil] Optional item after which to insert new items
    # @example Append items to toolbar
    #   toolbar do
    #     append :selectAll, :|, :selectAll, :selectAll
    #   end
    # @example Insert items after specific item
    #   toolbar do
    #     append :selectAll, after: :bold
    #   end
    # @raise [ArgumentError] When the specified 'after' item is not found
    def append(*appended_items, after: nil)
      if after
        index = find_item_index(after)
        raise ArgumentError, "Item '#{after}' not found in array" unless index

        items.insert(index + 1, *appended_items)
      else
        items.push(*appended_items)
      end
    end

    # Find group by name in toolbar items
    #
    # @param name [Symbol] Group name to find
    # @return [ToolbarGroupItem, nil] Found group or nil
    def find_group(name)
      items.find { |item| item.is_a?(ToolbarGroupItem) && item.name == name }
    end

    # Remove group by name from toolbar items
    #
    # @param name [Symbol] Group name to remove
    def remove_group(name)
      items.delete_if { |item| item.is_a?(ToolbarGroupItem) && item.name == name }
    end

    # Create and add new group to toolbar
    #
    # @param name [Symbol] Group name
    # @param options [Hash] Group options (label:, icons:)
    # @param block [Proc] Configuration block
    # @return [ToolbarGroupItem] Created group
    def group(name, **options, &block)
      group = ToolbarGroupItem.new(name, [], **options)
      group.instance_eval(&block) if block_given?
      items << group
      group
    end

    private

    # Find index of an item or group by name
    #
    # @param item [Symbol] Item or group name to find
    # @return [Integer, nil] Index of the found item or nil
    def find_item_index(item)
      items.find_index { |existing_item| item_matches?(existing_item, item) }
    end

    # Checks if the existing item matches the given item or group name
    #
    # @param existing_item [Symbol, ToolbarGroupItem] Item to check
    # @param item [Symbol] Item or group name to match against
    # @return [Boolean] true if items match, false otherwise
    # @example Check if items match
    #   item_matches?(:bold, :bold)           # => true
    #   item_matches?(group(:text), :text)    # => true
    def item_matches?(existing_item, item)
      if existing_item.is_a?(ToolbarGroupItem)
        existing_item.name == item
      else
        existing_item == item
      end
    end
  end

  # Builder class for configuring CKEditor5 toolbar groups.
  # Allows creating named groups of toolbar items with optional labels and icons.
  #
  # @example Creating a text formatting group
  #   group = ToolbarGroupItem.new(:text_formatting, [:bold, :italic], label: 'Text')
  #   group.append(:underline)
  class ToolbarGroupItem < ToolbarBuilder
    attr_reader :name, :label, :icon

    # Initialize a new toolbar group item.
    #
    # @param name [Symbol] Name of the toolbar group
    # @param items [Array<Symbol>, ToolbarBuilder] Items to be included in the group
    # @param label [String, nil] Optional label for the group
    # @param icon [String, nil] Optional icon for the group
    # @example Create a new toolbar group
    #   ToolbarGroupItem.new(:text, [:bold, :italic], label: 'Text formatting')
    def initialize(name, items = [], label: nil, icon: nil)
      super(items)
      @name = name
      @label = label
      @icon = icon
    end
  end
end