diff --git a/core/src/main/java/org/primefaces/extensions/component/clockpicker/ClockPicker.java b/core/src/main/java/org/primefaces/extensions/component/clockpicker/ClockPicker.java new file mode 100644 index 000000000..0f524bbbe --- /dev/null +++ b/core/src/main/java/org/primefaces/extensions/component/clockpicker/ClockPicker.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2011-2024 PrimeFaces Extensions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.primefaces.extensions.component.clockpicker; + +import java.util.Locale; + +import javax.faces.application.ResourceDependency; +import javax.faces.component.html.HtmlInputText; +import javax.faces.context.FacesContext; + +import org.primefaces.component.api.Widget; +import org.primefaces.util.Constants; +import org.primefaces.util.LocaleUtils; + +@ResourceDependency(library = "primefaces", name = "components.css") +@ResourceDependency(library = "primefaces", name = "jquery/jquery.js") +@ResourceDependency(library = "primefaces", name = "jquery/jquery-plugins.js") +@ResourceDependency(library = "primefaces", name = "core.js") +@ResourceDependency(library = "primefaces-extensions", name = "clockpicker/0-clockpicker.css") +@ResourceDependency(library = "primefaces-extensions", name = "clockpicker/0-clockpicker.js") +@ResourceDependency(library = "primefaces-extensions", name = "clockpicker/1-clockpicker-widget.js") +public class ClockPicker extends HtmlInputText implements Widget { + public static final String CONTAINER_CLASS = "pe-clockpicker ui-widget ui-corner-all input-group clockpicker"; + + public static final String COMPONENT_TYPE = "org.primefaces.extensions.component.ClockPicker"; + public static final String COMPONENT_FAMILY = "org.primefaces.extensions.component"; + private static final String DEFAULT_RENDERER = "org.primefaces.extensions.component.ClockPickerRenderer"; + + private Locale appropriateLocale; + + protected enum PropertyKeys { + + //@formatter:off + widgetVar, + placement, + align, + donetext, + autoclose, + locale, + vibrate; + + private String toString; + + PropertyKeys(final String toString) { + this.toString = toString; + } + + PropertyKeys() { + } + + @Override + public String toString() { + return toString != null ? toString : super.toString(); + } + } + + public ClockPicker() { + setRendererType(DEFAULT_RENDERER); + } + + + private boolean isSelfRequest(final FacesContext fc) { + return this.getClientId(fc).equals( + fc.getExternalContext().getRequestParameterMap().get(Constants.RequestParams.PARTIAL_SOURCE_PARAM)); + } + + @Override + public String getFamily() { + return COMPONENT_FAMILY; + } + + public String getWidgetVar() { + return (String) getStateHelper().eval(PropertyKeys.widgetVar, null); + } + + public void setWidgetVar(final String widgetVar) { + getStateHelper().put(PropertyKeys.widgetVar, widgetVar); + } + + public String getPlacement() { + return (String) getStateHelper().eval(PropertyKeys.placement, "bottom"); + } + + public void setPlacement(final String placement) { + getStateHelper().put(PropertyKeys.placement, placement); + } + + public String getAlign() { + return (String) getStateHelper().eval(PropertyKeys.align, "left"); + } + + public void setAlign(final String align) { + getStateHelper().put(PropertyKeys.align, align); + } + + public String getDonetext() { + return (String) getStateHelper().eval(PropertyKeys.donetext, "Done"); + } + + public void setDonetext(final String donetext) { + getStateHelper().put(PropertyKeys.donetext, donetext); + } + + public Boolean getAutoclose() { + return (Boolean) getStateHelper().eval(PropertyKeys.autoclose, false); + } + + public void setAutoclose(final Boolean autoclose) { + getStateHelper().put(PropertyKeys.autoclose, autoclose); + } + + public Boolean getVibrate() { + return (Boolean) getStateHelper().eval(PropertyKeys.vibrate, true); + } + + public void setVibrate(final Boolean vibrate) { + getStateHelper().put(PropertyKeys.vibrate, vibrate); + } + + public Object getLocale() { + return getStateHelper().eval(PropertyKeys.locale, null); + } + + public void setLocale(final Object locale) { + getStateHelper().put(PropertyKeys.locale, locale); + } + + public Locale calculateLocale() { + if (appropriateLocale == null) { + final FacesContext fc = FacesContext.getCurrentInstance(); + appropriateLocale = LocaleUtils.resolveLocale(fc, getLocale(), getClientId(fc)); + } + return appropriateLocale; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/primefaces/extensions/component/clockpicker/ClockPickerRenderer.java b/core/src/main/java/org/primefaces/extensions/component/clockpicker/ClockPickerRenderer.java new file mode 100644 index 000000000..97e9164f7 --- /dev/null +++ b/core/src/main/java/org/primefaces/extensions/component/clockpicker/ClockPickerRenderer.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2011-2024 PrimeFaces Extensions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.primefaces.extensions.component.clockpicker; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.el.ValueExpression; +import javax.faces.application.FacesMessage; +import javax.faces.component.UIComponent; +import javax.faces.context.FacesContext; +import javax.faces.context.ResponseWriter; +import javax.faces.convert.Converter; +import javax.faces.convert.ConverterException; + +import org.primefaces.renderkit.CoreRenderer; +import org.primefaces.util.LangUtils; +import org.primefaces.util.MessageFactory; +import org.primefaces.util.WidgetBuilder; + +public class ClockPickerRenderer extends CoreRenderer { + + @Override + public void decode(FacesContext context, UIComponent component) { + + final ClockPicker clockPicker = (ClockPicker) component; + + if (clockPicker.isDisabled() || clockPicker.isReadonly()) { + return; + } + final String param = clockPicker.getClientId(context) + "_input"; + final String submittedValue = context.getExternalContext().getRequestParameterMap().get(param); + + if (null != submittedValue) { + clockPicker.setSubmittedValue(submittedValue); + } + decodeBehaviors(context, component); + } + + @Override + public void encodeEnd(FacesContext context, UIComponent component) throws IOException { + final ClockPicker clockPicker = (ClockPicker) component; + + final String value = getValueAsString(context, clockPicker); + encodeMarkup(context, clockPicker, value); + encodeScript(context, clockPicker, value); + } + + private void encodeMarkup(FacesContext context, ClockPicker clockPicker, final String value) throws IOException { + ResponseWriter writer = context.getResponseWriter(); + String clientId = clockPicker.getClientId(); + String inputId = clientId + "_input"; + + writer.startElement("div", clockPicker); + writer.writeAttribute("class", ClockPicker.CONTAINER_CLASS, null); + + writer.startElement("input", null); + writer.writeAttribute("id", inputId, null); + writer.writeAttribute("name", inputId, null); + writer.writeAttribute("type", "text", null); + // writer.writeAttribute("class", "form_control", null); + writer.writeAttribute("class", "ui-inputfield ui-widget ui-state-default ui-corner-all", null); + writer.writeAttribute("size", 5, null); + writer.writeAttribute("maxlength", 5, null); + + if (LangUtils.isNotBlank(value)) { + writer.writeAttribute("value", value, null); + } + + writer.endElement("input"); + + writer.startElement("span", null); + writer.writeAttribute("class", "input-group-addon", null); + writer.startElement("span", null); + writer.writeAttribute("class", "glyphicon glyphicon-time", null); + writer.endElement("span"); + writer.endElement("span"); + writer.endElement("div"); + } + + private void encodeScript(final FacesContext context, final ClockPicker clockPicker, final String value) throws IOException { + final WidgetBuilder wb = getWidgetBuilder(context); + final String clientId = clockPicker.getClientId(context); + + wb.init("ExtClockPicker", clockPicker.resolveWidgetVar(), clientId); + encodeClientBehaviors(context, clockPicker); + + wb.attr("placement", clockPicker.getPlacement()); + wb.attr("align", clockPicker.getAlign()); + wb.attr("donetext", clockPicker.getDonetext()); + wb.attr("autoclose", clockPicker.getAutoclose()); + wb.attr("vibrate", clockPicker.getVibrate()); + + wb.finish(); + } + + protected static String getValueAsString(final FacesContext context, final ClockPicker clockPicker) { + final Object submittedValue = clockPicker.getSubmittedValue(); + if (submittedValue != null) { + return submittedValue.toString(); + } + + final Object value = clockPicker.getValue(); + if (value == null) { + return null; + } + else { + if (clockPicker.getConverter() != null) { + // convert via registered converter + return clockPicker.getConverter().getAsString(context, clockPicker, value); + } + else { + // use built-in converter + SimpleDateFormat timeFormat; + timeFormat = new SimpleDateFormat("HH:mm", clockPicker.calculateLocale()); + + return timeFormat.format(value); + } + } + } + + @Override + public Object getConvertedValue(FacesContext context, UIComponent component, + Object value) throws ConverterException { + final ClockPicker clockPicker = (ClockPicker) component; + String submittedValue = (String) value; + SimpleDateFormat format = null; + + if (isValueBlank(submittedValue)) { + return null; + } + + // Delegate to user supplied converter if defined + try { + Converter converter = clockPicker.getConverter(); + if (converter != null) { + return converter.getAsObject(context, clockPicker, submittedValue); + } + } + catch (ConverterException e) { + // clockPicker.setConversionFailed(true); + + throw e; + } + + // Delegate to global defined converter (e.g. joda or java8) + try { + ValueExpression ve = clockPicker.getValueExpression("value"); + if (ve != null) { + Class type = ve.getType(context.getELContext()); + if (type != null && type != Object.class && type != Date.class) { + Converter converter = context.getApplication().createConverter(type); + if (converter != null) { + return converter.getAsObject(context, clockPicker, submittedValue); + } + } + } + } + catch (ConverterException e) { + // clockPicker.setConversionFailed(true); + + throw e; + } + + // Use built-in converter + format = new SimpleDateFormat("HH:mm", clockPicker.calculateLocale()); + format.setLenient(false); + // format.setTimeZone(clockPicker.calculateTimeZone()); + try { + return format.parse(submittedValue); + } + catch (ParseException e) { + // clockPicker.setConversionFailed(true); + + String message = null; + Object[] params = new Object[3]; + params[0] = submittedValue; + params[1] = format.format(new Date()); + params[2] = MessageFactory.getLabel(context, clockPicker); + + message = MessageFactory.getMessage("javax.faces.converter.DateTimeConverter.DATE", FacesMessage.SEVERITY_ERROR, params); + + throw new ConverterException(message); + } + } + +} \ No newline at end of file diff --git a/core/src/main/resources/META-INF/faces-config.xml b/core/src/main/resources/META-INF/faces-config.xml index ae7e005e9..4ab35bb03 100755 --- a/core/src/main/resources/META-INF/faces-config.xml +++ b/core/src/main/resources/META-INF/faces-config.xml @@ -234,6 +234,10 @@ org.primefaces.extensions.component.SunEditor org.primefaces.extensions.component.suneditor.SunEditor + + org.primefaces.extensions.component.ClockPicker + org.primefaces.extensions.component.clockpicker.ClockPicker + org.primefaces.extensions.converter.JsonConverter @@ -464,6 +468,11 @@ org.primefaces.extensions.component.SunEditorRenderer org.primefaces.extensions.component.suneditor.SunEditorRenderer + + org.primefaces.extensions.component + org.primefaces.extensions.component.ClockPickerRenderer + org.primefaces.extensions.component.clockpicker.ClockPickerRenderer + org.primefaces.extensions.behavior.JavascriptBehaviorRenderer diff --git a/core/src/main/resources/META-INF/primefaces-extensions.taglib.xml b/core/src/main/resources/META-INF/primefaces-extensions.taglib.xml index 250cc9c0a..af16f281c 100755 --- a/core/src/main/resources/META-INF/primefaces-extensions.taglib.xml +++ b/core/src/main/resources/META-INF/primefaces-extensions.taglib.xml @@ -10414,5 +10414,68 @@ See showcase for an example. Default null.]]> java.lang.String + + + clockpicker + + org.primefaces.extensions.component.ClockPicker + org.primefaces.extensions.component.ClockPickerRenderer + + + + id + false + java.lang.String + + + + rendered + false + java.lang.Boolean + + + + + value + false + java.lang.Object + + + + widgetVar + false + java.lang.String + + + + placement + false + java.lang.String + + + + align + false + java.lang.String + + + + donetext + false + java.lang.String + + + + autoclose + false + java.lang.Boolean + + + + vibrate + false + java.lang.Boolean + + diff --git a/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/0-clockpicker.css b/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/0-clockpicker.css new file mode 100644 index 000000000..330254218 --- /dev/null +++ b/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/0-clockpicker.css @@ -0,0 +1,174 @@ +/*! + * ClockPicker v{package.version} for Bootstrap (http://weareoutman.github.io/clockpicker/) + * Copyright 2014 Wang Shenwei. + * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) + */ + +.clockpicker .input-group-addon { + cursor: pointer; +} +.clockpicker-moving { + cursor: move; +} +.clockpicker-align-left.popover > .arrow { + left: 25px; +} +.clockpicker-align-top.popover > .arrow { + top: 17px; +} +.clockpicker-align-right.popover > .arrow { + left: auto; + right: 25px; +} +.clockpicker-align-bottom.popover > .arrow { + top: auto; + bottom: 6px; +} + +/**Custom CSS**/ +.clockpicker-popover { + width: 224px; +} + +.clockpicker-popover .popover-title { + background-color: #fff; + color: #999; + font-size: 24px; + font-weight: bold; + line-height: 30px; + text-align: center; +} +.clockpicker-popover .popover-title span { + cursor: pointer; +} +.clockpicker-popover .popover-content { + background-color: #f8f8f8; + padding: 12px; +} +.popover-content:last-child { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} +.clockpicker-plate { + background-color: #fff; + border: 1px solid #ccc; + border-radius: 50%; + width: 200px; + height: 200px; + overflow: visible; + position: relative; + /* Disable text selection highlighting. Thanks to Hermanya */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.clockpicker-canvas, +.clockpicker-dial { + width: 200px; + height: 200px; + position: absolute; + left: -1px; + top: -1px; +} +.clockpicker-minutes { + visibility: hidden; +} +.clockpicker-tick { + border-radius: 50%; + color: #666; + line-height: 26px; + text-align: center; + width: 26px; + height: 26px; + position: absolute; + cursor: pointer; +} +.clockpicker-tick.active, +.clockpicker-tick:hover { + background-color: rgb(192, 229, 247); + background-color: rgba(0, 149, 221, .25); +} +.clockpicker-button { + background-image: none; + background-color: #fff; + border-width: 1px 0 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + margin: 0; + padding: 10px 0; +} +.clockpicker-button:hover { + background-image: none; + background-color: #ebebeb; +} +.clockpicker-button:focus { + outline: none!important; +} +.clockpicker-dial { + -webkit-transition: -webkit-transform 350ms, opacity 350ms; + -moz-transition: -moz-transform 350ms, opacity 350ms; + -ms-transition: -ms-transform 350ms, opacity 350ms; + -o-transition: -o-transform 350ms, opacity 350ms; + transition: transform 350ms, opacity 350ms; +} +.clockpicker-dial-out { + opacity: 0; +} +.clockpicker-hours.clockpicker-dial-out { + -webkit-transform: scale(1.2, 1.2); + -moz-transform: scale(1.2, 1.2); + -ms-transform: scale(1.2, 1.2); + -o-transform: scale(1.2, 1.2); + transform: scale(1.2, 1.2); +} +.clockpicker-minutes.clockpicker-dial-out { + -webkit-transform: scale(.8, .8); + -moz-transform: scale(.8, .8); + -ms-transform: scale(.8, .8); + -o-transform: scale(.8, .8); + transform: scale(.8, .8); +} +.clockpicker-canvas { + -webkit-transition: opacity 175ms; + -moz-transition: opacity 175ms; + -ms-transition: opacity 175ms; + -o-transition: opacity 175ms; + transition: opacity 175ms; +} +.clockpicker-canvas-out { + opacity: 0.25; +} +.clockpicker-canvas-bearing, +.clockpicker-canvas-fg { + stroke: none; + fill: rgb(0, 149, 221); +} +.clockpicker-canvas-bg { + stroke: none; + fill: rgb(192, 229, 247); +} +.clockpicker-canvas-bg-trans { + fill: rgba(0, 149, 221, .25); +} +.clockpicker-canvas line { + stroke: rgb(0, 149, 221); + stroke-width: 1; + stroke-linecap: round; + /*shape-rendering: crispEdges;*/ +} +.clockpicker-button.am-button { + margin: 1px; + padding: 5px; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 4px; + +} +.clockpicker-button.pm-button { + margin: 1px 1px 1px 136px; + padding: 5px; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 4px; +} \ No newline at end of file diff --git a/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/0-clockpicker.js b/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/0-clockpicker.js new file mode 100644 index 000000000..831e5f018 --- /dev/null +++ b/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/0-clockpicker.js @@ -0,0 +1,729 @@ +/*! + * ClockPicker v{package.version} (http://weareoutman.github.io/clockpicker/) + * Copyright 2014 Wang Shenwei. + * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) + */ + +;(function(){ + var $ = window.jQuery, + $win = $(window), + $doc = $(document), + $body; + + // Can I use inline svg ? + var svgNS = 'http://www.w3.org/2000/svg', + svgSupported = 'SVGAngle' in window && (function(){ + var supported, + el = document.createElement('div'); + el.innerHTML = ''; + supported = (el.firstChild && el.firstChild.namespaceURI) == svgNS; + el.innerHTML = ''; + return supported; + })(); + + // Can I use transition ? + var transitionSupported = (function(){ + var style = document.createElement('div').style; + return 'transition' in style || + 'WebkitTransition' in style || + 'MozTransition' in style || + 'msTransition' in style || + 'OTransition' in style; + })(); + + // Listen touch events in touch screen device, instead of mouse events in desktop. + var touchSupported = 'ontouchstart' in window, + mousedownEvent = 'mousedown' + ( touchSupported ? ' touchstart' : ''), + mousemoveEvent = 'mousemove.clockpicker' + ( touchSupported ? ' touchmove.clockpicker' : ''), + mouseupEvent = 'mouseup.clockpicker' + ( touchSupported ? ' touchend.clockpicker' : ''); + + // Vibrate the device if supported + var vibrate = navigator.vibrate ? 'vibrate' : navigator.webkitVibrate ? 'webkitVibrate' : null; + + function createSvgElement(name) { + return document.createElementNS(svgNS, name); + } + + function leadingZero(num) { + return (num < 10 ? '0' : '') + num; + } + + // Get a unique id + var idCounter = 0; + function uniqueId(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + } + + // Clock size + var dialRadius = 100, + outerRadius = 80, + // innerRadius = 80 on 12 hour clock + innerRadius = 54, + tickRadius = 13, + diameter = dialRadius * 2, + duration = transitionSupported ? 350 : 1; + + // Popover template + var tpl = [ + '
', + '
', + '
', + '', + ' : ', + '', + '', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '', + '', + '
', + '
' + ].join(''); + + // ClockPicker + function ClockPicker(element, options) { + var popover = $(tpl), + plate = popover.find('.clockpicker-plate'), + hoursView = popover.find('.clockpicker-hours'), + minutesView = popover.find('.clockpicker-minutes'), + amPmBlock = popover.find('.clockpicker-am-pm-block'), + isInput = element.prop('tagName') === 'INPUT', + input = isInput ? element : element.find('input'), + addon = element.find('.input-group-addon'), + self = this, + timer; + + this.id = uniqueId('cp'); + this.element = element; + this.options = options; + this.isAppended = false; + this.isShown = false; + this.currentView = 'hours'; + this.isInput = isInput; + this.input = input; + this.addon = addon; + this.popover = popover; + this.plate = plate; + this.hoursView = hoursView; + this.minutesView = minutesView; + this.amPmBlock = amPmBlock; + this.spanHours = popover.find('.clockpicker-span-hours'); + this.spanMinutes = popover.find('.clockpicker-span-minutes'); + this.spanAmPm = popover.find('.clockpicker-span-am-pm'); + this.amOrPm = "PM"; + + // Setup for for 12 hour clock if option is selected + if (options.twelvehour) { + + var amPmButtonsTemplate = ['
', + '', + '', + '
'].join(''); + + var amPmButtons = $(amPmButtonsTemplate); + //amPmButtons.appendTo(plate); + + ////Not working b/c they are not shown when this runs + //$('clockpicker-am-button') + // .on("click", function() { + // self.amOrPm = "AM"; + // $('.clockpicker-span-am-pm').empty().append('AM'); + // }); + // + //$('clockpicker-pm-button') + // .on("click", function() { + // self.amOrPm = "PM"; + // $('.clockpicker-span-am-pm').empty().append('PM'); + // }); + + $('') + .on("click", function() { + self.amOrPm = "AM"; + $('.clockpicker-span-am-pm').empty().append('AM'); + }).appendTo(this.amPmBlock); + + + $('') + .on("click", function() { + self.amOrPm = 'PM'; + $('.clockpicker-span-am-pm').empty().append('PM'); + }).appendTo(this.amPmBlock); + + } + + if (! options.autoclose) { + // If autoclose is not setted, append a button + $('') + .click($.proxy(this.done, this)) + .appendTo(popover); + } + + // Placement and arrow align - make sure they make sense. + if ((options.placement === 'top' || options.placement === 'bottom') && (options.align === 'top' || options.align === 'bottom')) options.align = 'left'; + if ((options.placement === 'left' || options.placement === 'right') && (options.align === 'left' || options.align === 'right')) options.align = 'top'; + + popover.addClass(options.placement); + popover.addClass('clockpicker-align-' + options.align); + + this.spanHours.click($.proxy(this.toggleView, this, 'hours')); + this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes')); + + // Show or toggle + input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this)); + addon.on('click.clockpicker', $.proxy(this.toggle, this)); + + // Build ticks + var tickTpl = $('
'), + i, tick, radian, radius; + + // Hours view + if (options.twelvehour) { + for (i = 1; i < 13; i += 1) { + tick = tickTpl.clone(); + radian = i / 6 * Math.PI; + radius = outerRadius; + tick.css('font-size', '120%'); + tick.css({ + left: dialRadius + Math.sin(radian) * radius - tickRadius, + top: dialRadius - Math.cos(radian) * radius - tickRadius + }); + tick.html(i === 0 ? '00' : i); + hoursView.append(tick); + tick.on(mousedownEvent, mousedown); + } + } else { + for (i = 0; i < 24; i += 1) { + tick = tickTpl.clone(); + radian = i / 6 * Math.PI; + var inner = i > 0 && i < 13; + radius = inner ? innerRadius : outerRadius; + tick.css({ + left: dialRadius + Math.sin(radian) * radius - tickRadius, + top: dialRadius - Math.cos(radian) * radius - tickRadius + }); + if (inner) { + tick.css('font-size', '120%'); + } + tick.html(i === 0 ? '00' : i); + hoursView.append(tick); + tick.on(mousedownEvent, mousedown); + } + } + + // Minutes view + for (i = 0; i < 60; i += 5) { + tick = tickTpl.clone(); + radian = i / 30 * Math.PI; + tick.css({ + left: dialRadius + Math.sin(radian) * outerRadius - tickRadius, + top: dialRadius - Math.cos(radian) * outerRadius - tickRadius + }); + tick.css('font-size', '120%'); + tick.html(leadingZero(i)); + minutesView.append(tick); + tick.on(mousedownEvent, mousedown); + } + + // Clicking on minutes view space + plate.on(mousedownEvent, function(e){ + if ($(e.target).closest('.clockpicker-tick').length === 0) { + mousedown(e, true); + } + }); + + // Mousedown or touchstart + function mousedown(e, space) { + var offset = plate.offset(), + isTouch = /^touch/.test(e.type), + x0 = offset.left + dialRadius, + y0 = offset.top + dialRadius, + dx = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0, + dy = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0, + z = Math.sqrt(dx * dx + dy * dy), + moved = false; + + // When clicking on minutes view space, check the mouse position + if (space && (z < outerRadius - tickRadius || z > outerRadius + tickRadius)) { + return; + } + e.preventDefault(); + + // Set cursor style of body after 200ms + var movingTimer = setTimeout(function(){ + $body.addClass('clockpicker-moving'); + }, 200); + + // Place the canvas to top + if (svgSupported) { + plate.append(self.canvas); + } + + // Clock + self.setHand(dx, dy, ! space, true); + + // Mousemove on document + $doc.off(mousemoveEvent).on(mousemoveEvent, function(e){ + e.preventDefault(); + var isTouch = /^touch/.test(e.type), + x = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0, + y = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0; + if (! moved && x === dx && y === dy) { + // Clicking in chrome on windows will trigger a mousemove event + return; + } + moved = true; + self.setHand(x, y, false, true); + }); + + // Mouseup on document + $doc.off(mouseupEvent).on(mouseupEvent, function(e){ + $doc.off(mouseupEvent); + e.preventDefault(); + var isTouch = /^touch/.test(e.type), + x = (isTouch ? e.originalEvent.changedTouches[0] : e).pageX - x0, + y = (isTouch ? e.originalEvent.changedTouches[0] : e).pageY - y0; + if ((space || moved) && x === dx && y === dy) { + self.setHand(x, y); + } + if (self.currentView === 'hours') { + self.toggleView('minutes', duration / 2); + } else { + if (options.autoclose) { + self.minutesView.addClass('clockpicker-dial-out'); + setTimeout(function(){ + self.done(); + }, duration / 2); + } + } + plate.prepend(canvas); + + // Reset cursor style of body + clearTimeout(movingTimer); + $body.removeClass('clockpicker-moving'); + + // Unbind mousemove event + $doc.off(mousemoveEvent); + }); + } + + if (svgSupported) { + // Draw clock hands and others + var canvas = popover.find('.clockpicker-canvas'), + svg = createSvgElement('svg'); + svg.setAttribute('class', 'clockpicker-svg'); + svg.setAttribute('width', diameter); + svg.setAttribute('height', diameter); + var g = createSvgElement('g'); + g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')'); + var bearing = createSvgElement('circle'); + bearing.setAttribute('class', 'clockpicker-canvas-bearing'); + bearing.setAttribute('cx', 0); + bearing.setAttribute('cy', 0); + bearing.setAttribute('r', 2); + var hand = createSvgElement('line'); + hand.setAttribute('x1', 0); + hand.setAttribute('y1', 0); + var bg = createSvgElement('circle'); + bg.setAttribute('class', 'clockpicker-canvas-bg'); + bg.setAttribute('r', tickRadius); + var fg = createSvgElement('circle'); + fg.setAttribute('class', 'clockpicker-canvas-fg'); + fg.setAttribute('r', 3.5); + g.appendChild(hand); + g.appendChild(bg); + g.appendChild(fg); + g.appendChild(bearing); + svg.appendChild(g); + canvas.append(svg); + + this.hand = hand; + this.bg = bg; + this.fg = fg; + this.bearing = bearing; + this.g = g; + this.canvas = canvas; + } + + raiseCallback(this.options.init); + } + + function raiseCallback(callbackFunction) { + if (callbackFunction && typeof callbackFunction === "function") { + callbackFunction(); + } + } + + // Default options + ClockPicker.DEFAULTS = { + 'default': '', // default time, 'now' or '13:14' e.g. + fromnow: 0, // set default time to * milliseconds from now (using with default = 'now') + placement: 'bottom', // clock popover placement + align: 'left', // popover arrow align + donetext: '完成', // done button text + autoclose: false, // auto close when minute is selected + twelvehour: false, // change to 12 hour AM/PM clock from 24 hour + vibrate: true // vibrate the device when dragging clock hand + }; + + // Show or hide popover + ClockPicker.prototype.toggle = function(){ + this[this.isShown ? 'hide' : 'show'](); + }; + + // Set popover position + ClockPicker.prototype.locate = function(){ + var element = this.element, + popover = this.popover, + offset = element.offset(), + width = element.outerWidth(), + height = element.outerHeight(), + placement = this.options.placement, + align = this.options.align, + styles = {}, + self = this; + + popover.show(); + + // Place the popover + switch (placement) { + case 'bottom': + styles.top = offset.top + height; + break; + case 'right': + styles.left = offset.left + width; + break; + case 'top': + styles.top = offset.top - popover.outerHeight(); + break; + case 'left': + styles.left = offset.left - popover.outerWidth(); + break; + } + + // Align the popover arrow + switch (align) { + case 'left': + styles.left = offset.left; + break; + case 'right': + styles.left = offset.left + width - popover.outerWidth(); + break; + case 'top': + styles.top = offset.top; + break; + case 'bottom': + styles.top = offset.top + height - popover.outerHeight(); + break; + } + + popover.css(styles); + }; + + // Show popover + ClockPicker.prototype.show = function(e){ + // Not show again + if (this.isShown) { + return; + } + + raiseCallback(this.options.beforeShow); + + var self = this; + + // Initialize + if (! this.isAppended) { + // Append popover to body + $body = $(document.body).append(this.popover); + + // Reset position when resize + $win.on('resize.clockpicker' + this.id, function(){ + if (self.isShown) { + self.locate(); + } + }); + + this.isAppended = true; + } + + // Get the time + var value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':'); + if (value[0] === 'now') { + var now = new Date(+ new Date() + this.options.fromnow); + value = [ + now.getHours(), + now.getMinutes() + ]; + } + this.hours = + value[0] || 0; + this.minutes = + value[1] || 0; + this.spanHours.html(leadingZero(this.hours)); + this.spanMinutes.html(leadingZero(this.minutes)); + + // Toggle to hours view + this.toggleView('hours'); + + // Set position + this.locate(); + + this.isShown = true; + + // Hide when clicking or tabbing on any element except the clock, input and addon + $doc.on('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id, function(e){ + var target = $(e.target); + if (target.closest(self.popover).length === 0 && + target.closest(self.addon).length === 0 && + target.closest(self.input).length === 0) { + self.hide(); + } + }); + + // Hide when ESC is pressed + $doc.on('keyup.clockpicker.' + this.id, function(e){ + if (e.keyCode === 27) { + self.hide(); + } + }); + + raiseCallback(this.options.afterShow); + }; + + // Hide popover + ClockPicker.prototype.hide = function(){ + raiseCallback(this.options.beforeHide); + + this.isShown = false; + + // Unbinding events on document + $doc.off('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id); + $doc.off('keyup.clockpicker.' + this.id); + + this.popover.hide(); + + raiseCallback(this.options.afterHide); + }; + + // Toggle to hours or minutes view + ClockPicker.prototype.toggleView = function(view, delay){ + var raiseAfterHourSelect = false; + if (view === 'minutes' && $(this.hoursView).css("visibility") === "visible") { + raiseCallback(this.options.beforeHourSelect); + raiseAfterHourSelect = true; + } + var isHours = view === 'hours', + nextView = isHours ? this.hoursView : this.minutesView, + hideView = isHours ? this.minutesView : this.hoursView; + + this.currentView = view; + + this.spanHours.toggleClass('text-primary', isHours); + this.spanMinutes.toggleClass('text-primary', ! isHours); + + // Let's make transitions + hideView.addClass('clockpicker-dial-out'); + nextView.css('visibility', 'visible').removeClass('clockpicker-dial-out'); + + // Reset clock hand + this.resetClock(delay); + + // After transitions ended + clearTimeout(this.toggleViewTimer); + this.toggleViewTimer = setTimeout(function(){ + hideView.css('visibility', 'hidden'); + }, duration); + + if (raiseAfterHourSelect) { + raiseCallback(this.options.afterHourSelect); + } + }; + + // Reset clock hand + ClockPicker.prototype.resetClock = function(delay){ + var view = this.currentView, + value = this[view], + isHours = view === 'hours', + unit = Math.PI / (isHours ? 6 : 30), + radian = value * unit, + radius = isHours && value > 0 && value < 13 ? innerRadius : outerRadius, + x = Math.sin(radian) * radius, + y = - Math.cos(radian) * radius, + self = this; + if (svgSupported && delay) { + self.canvas.addClass('clockpicker-canvas-out'); + setTimeout(function(){ + self.canvas.removeClass('clockpicker-canvas-out'); + self.setHand(x, y); + }, delay); + } else { + this.setHand(x, y); + } + }; + + // Set clock hand to (x, y) + ClockPicker.prototype.setHand = function(x, y, roundBy5, dragging){ + var radian = Math.atan2(x, - y), + isHours = this.currentView === 'hours', + unit = Math.PI / (isHours || roundBy5 ? 6 : 30), + z = Math.sqrt(x * x + y * y), + options = this.options, + inner = isHours && z < (outerRadius + innerRadius) / 2, + radius = inner ? innerRadius : outerRadius, + value; + + if (options.twelvehour) { + radius = outerRadius; + } + + // Radian should in range [0, 2PI] + if (radian < 0) { + radian = Math.PI * 2 + radian; + } + + // Get the round value + value = Math.round(radian / unit); + + // Get the round radian + radian = value * unit; + + // Correct the hours or minutes + if (options.twelvehour) { + if (isHours) { + if (value === 0) { + value = 12; + } + } else { + if (roundBy5) { + value *= 5; + } + if (value === 60) { + value = 0; + } + } + } else { + if (isHours) { + if (value === 12) { + value = 0; + } + value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; + } else { + if (roundBy5) { + value *= 5; + } + if (value === 60) { + value = 0; + } + } + } + + // Once hours or minutes changed, vibrate the device + if (this[this.currentView] !== value) { + if (vibrate && this.options.vibrate) { + // Do not vibrate too frequently + if (! this.vibrateTimer) { + navigator[vibrate](10); + this.vibrateTimer = setTimeout($.proxy(function(){ + this.vibrateTimer = null; + }, this), 100); + } + } + } + + this[this.currentView] = value; + this[isHours ? 'spanHours' : 'spanMinutes'].html(leadingZero(value)); + + // If svg is not supported, just add an active class to the tick + if (! svgSupported) { + this[isHours ? 'hoursView' : 'minutesView'].find('.clockpicker-tick').each(function(){ + var tick = $(this); + tick.toggleClass('active', value === + tick.html()); + }); + return; + } + + // Place clock hand at the top when dragging + if (dragging || (! isHours && value % 5)) { + this.g.insertBefore(this.hand, this.bearing); + this.g.insertBefore(this.bg, this.fg); + this.bg.setAttribute('class', 'clockpicker-canvas-bg clockpicker-canvas-bg-trans'); + } else { + // Or place it at the bottom + this.g.insertBefore(this.hand, this.bg); + this.g.insertBefore(this.fg, this.bg); + this.bg.setAttribute('class', 'clockpicker-canvas-bg'); + } + + // Set clock hand and others' position + var cx = Math.sin(radian) * radius, + cy = - Math.cos(radian) * radius; + this.hand.setAttribute('x2', cx); + this.hand.setAttribute('y2', cy); + this.bg.setAttribute('cx', cx); + this.bg.setAttribute('cy', cy); + this.fg.setAttribute('cx', cx); + this.fg.setAttribute('cy', cy); + }; + + // Hours and minutes are selected + ClockPicker.prototype.done = function() { + raiseCallback(this.options.beforeDone); + this.hide(); + var last = this.input.prop('value'), + value = leadingZero(this.hours) + ':' + leadingZero(this.minutes); + if (this.options.twelvehour) { + value = value + this.amOrPm; + } + + this.input.prop('value', value); + if (value !== last) { + this.input.triggerHandler('change'); + if (! this.isInput) { + this.element.trigger('change'); + } + } + + if (this.options.autoclose) { + this.input.trigger('blur'); + } + + raiseCallback(this.options.afterDone); + }; + + // Remove clockpicker from input + ClockPicker.prototype.remove = function() { + this.element.removeData('clockpicker'); + this.input.off('focus.clockpicker click.clockpicker'); + this.addon.off('click.clockpicker'); + if (this.isShown) { + this.hide(); + } + if (this.isAppended) { + $win.off('resize.clockpicker' + this.id); + this.popover.remove(); + } + }; + + // Extends $.fn.clockpicker + $.fn.clockpicker = function(option){ + var args = Array.prototype.slice.call(arguments, 1); + return this.each(function(){ + var $this = $(this), + data = $this.data('clockpicker'); + if (! data) { + var options = $.extend({}, ClockPicker.DEFAULTS, $this.data(), typeof option == 'object' && option); + $this.data('clockpicker', new ClockPicker($this, options)); + } else { + // Manual operatsions. show, hide, remove, e.g. + if (typeof data[option] === 'function') { + data[option].apply(data, args); + } + } + }); + }; +}()); \ No newline at end of file diff --git a/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/1-clockpicker-widget.js b/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/1-clockpicker-widget.js new file mode 100644 index 000000000..ff96c5508 --- /dev/null +++ b/core/src/main/resources/META-INF/resources/primefaces-extensions/clockpicker/1-clockpicker-widget.js @@ -0,0 +1,25 @@ +/** + * PrimeFaces Extensions ClockPicker Widget. + * + */ +PrimeFaces.widget.ExtClockPicker = PrimeFaces.widget.BaseWidget.extend({ + /** + * Initializes the widget. + * + * @param {object} + * cfg The widget configuration. + */ + init : function(cfg) { + this._super(cfg); + this.id = PrimeFaces.escapeClientId(cfg.id); + this.cfg = cfg; + this.jqEl = this.jqId + '_input'; + this.jq = $(this.jqEl); + + this.jq.clockpicker(this.cfg); + // pfs metadata + $(this.jqId + '_input').data(PrimeFaces.CLIENT_ID_DATA, this.id); + this.originalValue = this.jq.val(); + + } +}); \ No newline at end of file diff --git a/showcase/src/main/java/org/primefaces/extensions/showcase/controller/ClockPickerController.java b/showcase/src/main/java/org/primefaces/extensions/showcase/controller/ClockPickerController.java new file mode 100644 index 000000000..db4e4d718 --- /dev/null +++ b/showcase/src/main/java/org/primefaces/extensions/showcase/controller/ClockPickerController.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2011-2024 PrimeFaces Extensions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.primefaces.extensions.showcase.controller; + +import java.io.Serializable; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; + +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.faces.view.ViewScoped; +import javax.inject.Named; + +/** + * ClockPickerController + * + * @author @jxmai / last modified by $Author$ + * @version $Revision$ + */ +@Named +@ViewScoped +public class ClockPickerController implements Serializable { + + private static final long serialVersionUID = 897540091000342926L; + + private Date time; + + public ClockPickerController() { + final Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.AM_PM, Calendar.AM); + calendar.set(Calendar.HOUR, 8); + calendar.set(Calendar.MINUTE, 15); + time = calendar.getTime(); + } + + public void showTime() { + LocalTime localTime = time.toInstant().atZone(ZoneId.systemDefault()).toLocalTime(); + int hour = localTime.getHour(); + int min = localTime.getMinute(); + + String message = String.format("Selected hour: %d, Selected min: %d", hour, min); + addMessage(FacesMessage.SEVERITY_INFO, "Info Message", message); + } + + private void addMessage(FacesMessage.Severity severity, String summary, String detail) { + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(severity, summary, detail)); + } + + public Date getTime() { + return time; + } + + public void setTime(Date time) { + this.time = time; + } + +} diff --git a/showcase/src/main/webapp/sections/clockpicker/basicUsage.xhtml b/showcase/src/main/webapp/sections/clockpicker/basicUsage.xhtml new file mode 100644 index 000000000..e9076d2d3 --- /dev/null +++ b/showcase/src/main/webapp/sections/clockpicker/basicUsage.xhtml @@ -0,0 +1,37 @@ + + + + + + + + clockpicker + + + + + + + + +${showcase:getFileContent('/sections/clockpicker/example-basicUsage.xhtml')} + + +${showcase:getFileContent('/org/primefaces/extensions/showcase/controller/ClockPickerController.java')} + + + + + + + + + + + + + diff --git a/showcase/src/main/webapp/sections/clockpicker/example-basicUsage.xhtml b/showcase/src/main/webapp/sections/clockpicker/example-basicUsage.xhtml new file mode 100644 index 000000000..5f0fc7005 --- /dev/null +++ b/showcase/src/main/webapp/sections/clockpicker/example-basicUsage.xhtml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/showcase/src/main/webapp/sections/clockpicker/useCasesChoice.xhtml b/showcase/src/main/webapp/sections/clockpicker/useCasesChoice.xhtml new file mode 100644 index 000000000..221dd20be --- /dev/null +++ b/showcase/src/main/webapp/sections/clockpicker/useCasesChoice.xhtml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/showcase/src/main/webapp/templates/showcaseLayout.xhtml b/showcase/src/main/webapp/templates/showcaseLayout.xhtml index 0d6bc3a53..ef8daf08f 100644 --- a/showcase/src/main/webapp/templates/showcaseLayout.xhtml +++ b/showcase/src/main/webapp/templates/showcaseLayout.xhtml @@ -60,6 +60,9 @@ + diff --git a/showcase/src/main/webapp/views/clockpicker.xhtml b/showcase/src/main/webapp/views/clockpicker.xhtml new file mode 100644 index 000000000..154c628e6 --- /dev/null +++ b/showcase/src/main/webapp/views/clockpicker.xhtml @@ -0,0 +1,24 @@ + + + + + + + + Based on clockpicker by weareoutman. + + + + + + + + + + + + +