card/lib/card/view/classy.rb
class Card
class View
# API to change css classes in other places
module Classy
# Add additional css classes to a css class
#
# Example
# class_up "card-slot", "card-dark text-muted"
#
# If a view later adds the css "card-slot" to a html tag with
#
# classy("card-slot")
#
# then all additional css classes will be added.
#
# The scope when these additional classes apply can be restricted
# @param klass [String, Symbol] the css class to be enriched with additional classes
# @param classier [String, Array<String>] additional css classes
# @param scope [Symbol]
# :view only in the same view
# :subviews the same and all subviews; not in nests or where its nested
# :format all views, sub and parent views; not in nests or where its nested
# :nests the same as :format but also in nests
# :single_use the same as :nests but is removed after the first use
# :global always everywhere
def class_up klass, classier, scope=:subviews
storage_voo(scope).add_extra_classes klass.to_s, classier, scope
end
def class_down klass, classier
remove_extra_classes klass, classier, :private
end
def with_class_up klass, classier, scope=:subviews
class_up klass, classier, scope
yield
ensure
class_down klass, classier
end
# don't use in the given block the additional class that
# was added to `klass`
def without_upped_class klass
tmp_class = class_list.delete klass
result = yield tmp_class
class_list[klass] = tmp_class
result
end
def classy *classes
classes = Array.wrap(classes).flatten
[classes, extra_classes(classes)].flatten.compact.join " "
end
def add_extra_classes key, classier, scope
list = class_list class_list_type(scope)
list[key] = [list[key], classier].flatten.compact.join " "
end
# remove classes everywhere where they are visible for the given scope
def remove_extra_classes klass, classier, type
# TODO: scope handling
# Method is not used and maybe no longer necessary with the scope feature
# for class_up.
# It's no longer sufficient to remove only public classes for ancestors.
# Needs an approach similar to extra_classes with the "space" argument
next_ancestor&.remove_extra_classes klass, classier, :public
cl = class_list type
return unless cl[klass]
if cl[klass] == classier
cl.delete klass
else
cl[klass].gsub!(/#{classier}\s?/, "")
end
end
def extra_classes klass
klass = klass.first if klass.is_a?(Array)
deep_extra_classes klass.to_s, :self
end
# recurse through voos and formats to find all extra classes
# @param space [:self, :self_format, :ancestor_format]
def deep_extra_classes klass, space
[self_extra_classes(klass, space),
ancestor_extra_classes(klass, space)].flatten.compact
end
private
def ancestor_extra_classes klass, space
if parent
parent_space = space == :self ? :self_format : :ancestor_format
parent.deep_extra_classes(klass, parent_space)
else
next_format_ancestor&.deep_extra_classes(klass, :ancestor_format)
end
end
def storage_voo scope
# When we climb up the voo tree and cross a nest boundary then we can jump only
# to the root voo of the parent format. Hence we have to add classes to the root
# if we want them to be found by nests.
case scope
when :view, :subviews then self
when :format, :nests, :single_use then root
when :global then deep_root
else
raise ArgumentError, "invalid class_up scope: #{scope}"
end
end
def self_extra_classes klass, space
classes = ok_types(space).map { |ot| class_list(ot)[klass] }
return classes unless class_list(:single_use)&.key? klass
[classes, class_list(:single_use).delete(klass)]
end
def ok_types space
case space
when :ancestor_format then [:public]
when :self_format then %i[public format_private]
when :self then %i[public format_private private]
end
end
def class_list type=:private
unless type.in? %i[private format_private public single_use]
raise ArgumentError, "#{type} not a valid class list"
end
@class_list ||= {}
@class_list[type] ||= {}
end
CLASS_LIST_TYPE = { view: :private,
format: :format_private,
subviews: :format_private,
nests: :public,
global: :public,
single_use: :single_use }.freeze
# Translates scopes to the privacy types used to manage the class lists.
# A #classy calls looks in the following class_lists:
# private - only in the same voo
# format_private - the same voo and all parent voos in the same format
# public - in all voos in all parent formats
def class_list_type scope
CLASS_LIST_TYPE[scope] || raise(ArgumentError, "invalid class_up scope: #{scope}")
end
end
end
end