Skip to content

Commit

Permalink
Merge pull request #2582 from sascha-karnatz/update_javascript/page_s…
Browse files Browse the repository at this point in the history
…elect

Page Select Component
  • Loading branch information
tvdeyen authored Sep 27, 2023
2 parents b432945 + 2435410 commit b85bd1b
Show file tree
Hide file tree
Showing 18 changed files with 436 additions and 145 deletions.
1 change: 0 additions & 1 deletion app/assets/javascripts/alchemy/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,4 @@
//= require alchemy/alchemy.list_filter
//= require alchemy/alchemy.uploader
//= require alchemy/alchemy.preview_window
//= require alchemy/page_select
//= require alchemy/node_select
46 changes: 0 additions & 46 deletions app/assets/javascripts/alchemy/page_select.js

This file was deleted.

42 changes: 42 additions & 0 deletions app/components/alchemy/admin/page_select.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Alchemy
module Admin
class PageSelect < ViewComponent::Base
delegate :alchemy, to: :helpers

def initialize(page = nil, url: nil, allow_clear: false, placeholder: Alchemy.t(:search_page), query_params: nil)
@page = page
@url = url
@allow_clear = allow_clear
@placeholder = placeholder
@query_params = query_params
end

def call
content_tag("alchemy-page-select", content, attributes)
end

private

def attributes
options = {
placeholder: @placeholder,
url: @url || alchemy.api_pages_path
}

options = options.merge({"allow-clear": @allow_clear}) if @allow_clear
options = options.merge({"query-params": @query_params.to_json}) if @query_params

if @page
selection = {
id: @page.id,
name: @page.name,
url_path: @page.url_path
}
options = options.merge({selection: selection.to_json})
end

options
end
end
end
end
1 change: 1 addition & 0 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ $.fx.speeds._default = 400
import "alchemy_admin/components/char_counter"
import "alchemy_admin/components/datepicker"
import "alchemy_admin/components/overlay"
import "alchemy_admin/components/page_select"
import "alchemy_admin/components/spinner"
import "alchemy_admin/components/tinymce"
import "alchemy_admin/components/tooltip"
Expand Down
42 changes: 21 additions & 21 deletions app/javascript/alchemy_admin/components/alchemy_html_element.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toCamelCase } from "alchemy_admin/utils/string_conversions"

export class AlchemyHTMLElement extends HTMLElement {
static properties = {}

Expand Down Expand Up @@ -25,16 +27,16 @@ export class AlchemyHTMLElement extends HTMLElement {
* @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Components#reference
*/
connectedCallback() {
// parse the properties object and register property variables
Object.keys(this.constructor.properties).forEach((propertyName) => {
// parse the properties object and register property with the default values
Object.keys(this.constructor.properties).forEach((name) => {
// if the options was given via the constructor, they should be prefer (e.g. new <WebComponentName>({title: "Foo"}))
if (this.options[propertyName]) {
this[propertyName] = this.options[propertyName]
} else {
this._updateProperty(propertyName, this.getAttribute(propertyName))
}
this[name] =
this.options[name] ?? this.constructor.properties[name].default
})

// then process the attributes
this.getAttributeNames().forEach((name) => this._updateFromAttribute(name))

// render the component
this._updateComponent()
this.connected()
Expand All @@ -54,8 +56,8 @@ export class AlchemyHTMLElement extends HTMLElement {
* triggered by the browser, if one of the observed attributes is changing
* @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Components#reference
*/
attributeChangedCallback(name, oldValue, newValue) {
this._updateProperty(name, newValue)
attributeChangedCallback(name) {
this._updateFromAttribute(name)
this._updateComponent()
}

Expand Down Expand Up @@ -96,23 +98,21 @@ export class AlchemyHTMLElement extends HTMLElement {
}

/**
* update the property value
* if the value is undefined the default value is used
* update the value from the given attribute
*
* @param {string} propertyName
* @param {string} value
* @param {string} name
* @private
*/
_updateProperty(propertyName, value) {
const property = this.constructor.properties[propertyName]
_updateFromAttribute(name) {
const attributeValue = this.getAttribute(name)
const propertyName = toCamelCase(name)
const isBooleanValue =
attributeValue.length === 0 || attributeValue === "true"

const value = isBooleanValue ? true : attributeValue

if (this[propertyName] !== value) {
this[propertyName] = value
if (
typeof property.default !== "undefined" &&
this[propertyName] === null
) {
this[propertyName] = property.default
}
this.changeComponent = true
}
}
Expand Down
120 changes: 120 additions & 0 deletions app/javascript/alchemy_admin/components/page_select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { AlchemyHTMLElement } from "./alchemy_html_element"

class PageSelect extends AlchemyHTMLElement {
static properties = {
allowClear: { default: false },
selection: { default: undefined },
placeholder: { default: "" },
queryParams: { default: "{}" },
url: { default: "" }
}

connected() {
this.input.classList.add("alchemy_selectbox")

const dispatchCustomEvent = (name, detail = {}) => {
this.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }))
}

$(this.input)
.select2(this.select2Config)
.on("select2-open", (event) => {
// add focus to the search input. Select2 is handling the focus on the first opening,
// but it does not work the second time. One process in select2 is "stealing" the focus
// if the command is not delayed. It is an intermediate solution until we are going to
// move away from Select2
setTimeout(() => {
document.querySelector("#select2-drop .select2-input").focus()
}, 100)
})
.on("change", (event) => {
if (event.added) {
dispatchCustomEvent("Alchemy.PageSelect.PageAdded", event.added)
} else {
dispatchCustomEvent("Alchemy.PageSelect.PageRemoved")
}
})
}

get input() {
return this.getElementsByTagName("input")[0]
}

get select2Config() {
return {
placeholder: this.placeholder,
allowClear: this.allowClear,
initSelection: (_$el, callback) => {
if (this.selection) {
callback(JSON.parse(this.selection))
}
},
ajax: this.ajaxConfig,
formatSelection: this._renderResult,
formatResult: this._renderListEntry
}
}

/**
* Ajax configuration for Select2
* @returns {object}
*/
get ajaxConfig() {
const data = (term, page) => {
return {
q: { name_cont: term, ...JSON.parse(this.queryParams) },
page: page
}
}

const results = (data) => {
const meta = data.meta
return {
results: data.pages,
more: meta.page * meta.per_page < meta.total_count
}
}

return {
url: this.url,
datatype: "json",
quietMillis: 300,
data,
results
}
}

/**
* result which is visible if a page was selected
* @param {object} page
* @returns {string}
* @private
*/
_renderResult(page) {
return page.text || page.name
}

/**
* html template for each list entry
* @param {object} page
* @returns {string}
* @private
*/
_renderListEntry(page) {
return `
<div class="page-select--page">
<div class="page-select--top">
<i class="icon far fa-file fa-lg"></i>
<span class="page-select--page-name">${page.name}</span>
<span class="page-select--page-urlname">${page.url_path}</span>
</div>
<div class="page-select--bottom">
<span class="page-select--site-name">${page.site.name}</span>
<span class="page-select--language-code">${page.language.name}</span>
</div>
</div>
`
}
}

customElements.define("alchemy-page-select", PageSelect)
10 changes: 10 additions & 0 deletions app/javascript/alchemy_admin/utils/string_conversions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* convert dashes and underscore strings into camelCase strings
* @param {string} str
* @returns {string}
*/
export function toCamelCase(str) {
return str
.split(/-|_/)
.reduce((a, b) => a + b.charAt(0).toUpperCase() + b.slice(1))
}
39 changes: 19 additions & 20 deletions app/views/alchemy/admin/nodes/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= alchemy_form_for([:admin, node]) do |f| %>
<%= alchemy_form_for([:admin, node], id: "node_form") do |f| %>
<% if node.new_record? && node.root? %>
<%= f.input :menu_type,
collection: Alchemy::Language.current.available_menu_names.map { |n| [I18n.t(n, scope: [:alchemy, :menu_names]), n] },
Expand All @@ -13,7 +13,9 @@
value: node.page && node.read_attribute(:name).blank? ? nil : node.name,
placeholder: node.page ? node.page.name : nil
} %>
<%= f.input :page_id, label: Alchemy::Page.model_name.human, input_html: { class: 'alchemy_selectbox' } %>
<%= render Alchemy::Admin::PageSelect.new(node.page, allow_clear: true) do %>
<%= f.input :page_id, label: Alchemy::Page.model_name.human %>
<% end %>
<%= f.input :url, input_html: { disabled: node.page }, hint: Alchemy.t(:node_url_hint) %>
<%= f.input :title %>
<%= f.input :nofollow %>
Expand All @@ -26,23 +28,20 @@
<% end %>

<script>
$('#node_page_id').alchemyPageSelect({
placeholder: "<%= Alchemy.t(:search_page) %>",
url: "<%= alchemy.api_pages_path %>",
<% if node.page %>
initialSelection: {
id: <%= node.page_id %>,
text: "<%= node.page.name %>",
url_path: "<%= node.page.url_path %>"
}
<% end %>
}).on('change', function(e) {
if (e.val === '') {
$('#node_name').removeAttr('placeholder')
$('#node_url').val('').prop('disabled', false)
} else {
$('#node_name').attr('placeholder', e.added.name)
$('#node_url').val(e.added.url_path).prop('disabled', true)
}
const nodeName = document.getElementById("node_name")
const nodeUrl = document.getElementById("node_url")
const form = document.getElementById("node_form")

form.addEventListener("Alchemy.PageSelect.PageAdded", (event) => {
const page = event.detail
nodeName.setAttribute("placeholder", page.name)
nodeUrl.value = page.url_path
nodeUrl.setAttribute("disabled", "disabled")
})

form.addEventListener("Alchemy.PageSelect.PageRemoved", (event) => {
nodeName.removeAttribute("placeholder")
nodeUrl.value = ""
nodeUrl.removeAttribute("disabled")
})
</script>
19 changes: 3 additions & 16 deletions app/views/alchemy/admin/pages/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<%= alchemy_form_for [:admin, @page], class: 'edit_page' do |f| %>
<% unless @page.language_root? || @page.layoutpage %>
<%= f.input :parent_id, required: true, input_html: { class: 'alchemy_selectbox' } %>
<%= render Alchemy::Admin::PageSelect.new(@page.parent) do %>
<%= f.input :parent_id, required: true %>
<% end %>
<% end %>

<div class="input check_boxes">
Expand Down Expand Up @@ -52,18 +54,3 @@

<%= f.submit Alchemy.t(:save) %>
<% end %>

<script>
$('#page_parent_id').alchemyPageSelect({
placeholder: "<%= Alchemy.t(:search_page) %>",
url: "<%= alchemy.api_pages_path %>",
allowClear: false,
<% if @page.parent %>
initialSelection: {
id: <%= @page.parent.id %>,
text: "<%= @page.parent.name %>",
url_path: "<%= @page.parent.url_path %>"
}
<% end %>
})
</script>
Loading

0 comments on commit b85bd1b

Please sign in to comment.