').append(err);
26 | $rootScope.showFlash($err.find('title').text(), $err.find('p').text());
27 | }
28 | };
29 | }]);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## Purpose
4 |
5 | Carty is an integrated tool to create, edit, review and test Sling mappings for Adobe CQ/AEM. [Read more](http://cognifide.github.io/Carty/).
6 |
7 | ## Features
8 |
9 | * mapping generator - enter domain and the content path and Carty will take of everything else,
10 | * mapping editor - edit, move, create and delete sling:Mapping entries,
11 | * tester - check what will be the result of the map or resolve operation,
12 | * highlighter - Carty explains which entries have been applied to a specific part of the tested URL,
13 | * configuration - you may choose any path to create mappings, it doesn't have to be the mapping root currently set in the Resource Resolver
14 |
15 | ## Prerequisites
16 |
17 | * AEM 6.2 or AEM 6.3
18 | * Maven 3.x
19 |
20 | ## Installation
21 |
22 | mvn clean install crx:install
23 |
24 | Optionally, you may set Maven properties: `instance.url`, `instance.login` and `instance.password`.
25 |
26 | ## Usage
27 |
28 | Find Carty under the `/miscadmin` or /etc/carty.html.
29 |
30 | ## Screenshot
31 |
32 |

33 |
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/mapper/AppliedMappingEntry.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty.mapper;
2 |
3 | import com.cognifide.cq.carty.Mapping;
4 |
5 | public class AppliedMappingEntry {
6 |
7 | private final Mapping mapping;
8 |
9 | private final String matchingInternalRedirect;
10 |
11 | private final String url;
12 |
13 | private final AppliedMappingEntry parent;
14 |
15 | public AppliedMappingEntry(Mapping mapping, String matchingInternalRedirect, String url,
16 | AppliedMappingEntry parent) {
17 | this.mapping = mapping;
18 | this.matchingInternalRedirect = matchingInternalRedirect;
19 | this.url = url;
20 | this.parent = parent;
21 | }
22 |
23 | public AppliedMappingEntry(Mapping mapping, String url, AppliedMappingEntry parent) {
24 | this(mapping, null, url, parent);
25 | }
26 |
27 | public Mapping getMapping() {
28 | return mapping;
29 | }
30 |
31 | public String getMatchingInternalRedirect() {
32 | return matchingInternalRedirect;
33 | }
34 |
35 | public String getUrl() {
36 | return url;
37 | }
38 |
39 | public AppliedMappingEntry getParent() {
40 | return parent;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/angular.ui/css/angular-ui-tree.min.css:
--------------------------------------------------------------------------------
1 | .angular-ui-tree-empty{border:1px dashed #bbb;min-height:100px;background-color:#e5e5e5;background-image:-webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff),-webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff);background-image:-moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff),-moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff);background-image:linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff),linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff);background-size:60px 60px;background-position:0 0,30px 30px}.angular-ui-tree-nodes{position:relative;margin:0px;padding:0px;list-style:none}.angular-ui-tree-nodes .angular-ui-tree-nodes{padding-left:20px}.angular-ui-tree-node,.angular-ui-tree-placeholder{position:relative;margin:0px;padding:0px;min-height:20px;line-height:20px}.angular-ui-tree-hidden{display:none}.angular-ui-tree-placeholder{margin:5px 0;padding:0;min-height:30px}.angular-ui-tree-handle{cursor:pointer;text-decoration:none;font-weight:bold;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;min-height:20px;line-height:20px}.angular-ui-tree-drag{position:absolute;pointer-events:none;z-index:999;opacity:.8}
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/angular.ui/css/xeditable.css:
--------------------------------------------------------------------------------
1 | /*!
2 | angular-xeditable - 0.1.8
3 | Edit-in-place for angular.js
4 | Build date: 2014-01-10
5 | */
6 |
7 | .editable-wrap{display:inline-block;white-space:nowrap;margin:0}.editable-wrap .editable-controls,.editable-wrap .editable-error{margin-bottom:0}.editable-wrap .editable-controls>input,.editable-wrap .editable-controls>select,.editable-wrap .editable-controls>textarea{margin-bottom:0}.editable-wrap .editable-input{display:inline-block}.editable-buttons{display:inline-block;vertical-align:top}.editable-buttons button{margin-left:5px}.editable-input.editable-has-buttons{width:auto}.editable-bstime .editable-input input[type=text]{width:46px}.editable-bstime .well-small{margin-bottom:0;padding:10px}.editable-range output{display:inline-block;min-width:30px;vertical-align:top;text-align:center}.editable-color input[type=color]{width:50px}.editable-checkbox label span,.editable-checklist label span,.editable-radiolist label span{margin-left:7px;margin-right:10px}.editable-hide{display:none!important}.editable-click,a.editable-click{text-decoration:none;color:#428bca;border-bottom:dashed 1px #428bca}.editable-click:hover,a.editable-click:hover{text-decoration:none;color:#2a6496;border-bottom-color:#2a6496}.editable-empty,.editable-empty:hover,.editable-empty:focus,a.editable-empty,a.editable-empty:hover,a.editable-empty:focus{font-style:italic;color:#D14;text-decoration:none}
--------------------------------------------------------------------------------
/src/main/assembly/cq.xml:
--------------------------------------------------------------------------------
1 |
5 | cq
6 |
7 | zip
8 |
9 | false
10 |
11 |
12 | src/main/cq/jcr_root
13 | /jcr_root
14 |
15 | **/.vlt
16 | /libs/**
17 | /WEB-INF/**
18 |
19 |
20 |
21 | src/main/vault/common
22 | /META-INF/vault
23 | false
24 |
25 |
26 | src/main/vault/profile
27 | /META-INF/vault
28 | true
29 |
30 |
31 | ${project.build.directory}
32 | /jcr_root/apps/carty/install
33 |
34 | *.jar
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/etc/carty/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
10 |
13 |
14 | <_domainName_
15 | jcr:primaryType="sling:Mapping"
16 | sling:internalRedirect="[_contentRoot_]">
17 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/carty/css/tree.less:
--------------------------------------------------------------------------------
1 | #carty-app {
2 | .angular-ui-tree-handle {
3 | background: #f8faff;
4 | border: 1px solid #dae2ea;
5 | padding: 10px 10px;
6 | button {
7 | height: auto;
8 | }
9 | }
10 |
11 | .angular-ui-tree-handle:hover,
12 | .angular-ui-tree-handle.highlighted {
13 | color: #438eb9;
14 | background: #f4f6f7;
15 | border-color: #dce2e8;
16 | }
17 |
18 | .angular-ui-tree-placeholder {
19 | background: #f0f9ff;
20 | border: 2px dashed #bed2db;
21 | -webkit-box-sizing: border-box;
22 | -moz-box-sizing: border-box;
23 | box-sizing: border-box;
24 | }
25 |
26 | .mapping-tree {
27 | button {
28 | visibility: visible;
29 | }
30 | }
31 | }
32 |
33 | .mapping-tree {
34 | button {
35 | visibility: hidden;
36 | }
37 |
38 | .mapping-entry {
39 | margin-left: 2em;
40 | font-weight: normal;
41 |
42 | p {
43 | margin: 0;
44 | font-weight: bold;
45 | }
46 |
47 | .short.mapping-name {
48 | display: block;
49 | width: 10em;
50 | float: left;
51 | }
52 |
53 | input {
54 | margin-left: 0.5em;
55 | padding: 0 0.25em 0 0.25em;
56 | height: auto;
57 | width: 15em;
58 | }
59 |
60 | .mapping-definition {
61 | overflow: hidden;
62 | width: 100%;
63 | }
64 |
65 | .short-mapping-definition {
66 | font-weight: normal;
67 | }
68 |
69 | dl {
70 | dt {
71 | float: left;
72 | width: 8em;
73 | text-align: right;
74 | font-weight: bold;
75 | margin-right: 0.5em;
76 | }
77 |
78 | dd {
79 | margin-left: 8.5em;
80 | }
81 | }
82 |
83 | dl:first-of-type {
84 | float: left;
85 | width: 28.7em;
86 | }
87 | }
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/resolver/AppliedResolutionEntry.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty.resolver;
2 |
3 | import org.apache.commons.lang.StringUtils;
4 | import org.apache.sling.api.resource.Resource;
5 | import org.apache.sling.api.resource.ResourceResolver;
6 | import org.apache.sling.api.resource.ResourceUtil;
7 |
8 | import com.cognifide.cq.carty.Mapping;
9 |
10 | public class AppliedResolutionEntry {
11 | private final Mapping mapping;
12 |
13 | private final int from;
14 |
15 | private final int to;
16 |
17 | private transient final String[] groups;
18 |
19 | private transient final String[] searchList;
20 |
21 | public AppliedResolutionEntry(Mapping mapping, int from, int to, String[] groups) {
22 | this.mapping = mapping;
23 | this.from = from;
24 | this.to = to;
25 | this.groups = groups;
26 |
27 | searchList = new String[groups.length];
28 | for (int i = 0; i < searchList.length; i++) {
29 | searchList[i] = String.format("$%d", i + 1);
30 | }
31 | }
32 |
33 | public Mapping getMapping() {
34 | return mapping;
35 | }
36 |
37 | public int getFrom() {
38 | return from;
39 | }
40 |
41 | public int getTo() {
42 | return to;
43 | }
44 |
45 | public Resource getResource(String relPath, ResourceResolver resolver) {
46 | Resource res = null;
47 |
48 | for (final String r : mapping.getInternalRedirect()) {
49 | final String path = fillPattern(r) + relPath;
50 | res = resolver.resolve(path);
51 | if (!ResourceUtil.isNonExistingResource(res)) {
52 | return res;
53 | }
54 | }
55 | return res;
56 | }
57 |
58 | private String fillPattern(String subject) {
59 | return StringUtils.replaceEach(subject, searchList, groups);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/carty/js/resolveCtrl.js:
--------------------------------------------------------------------------------
1 | /*global angular: false */
2 |
3 | angular.module('cartyApp')
4 | .controller('ResolveCtrl', ['$scope', '$http', '$rootScope', 'settings', 'localStorageService',
5 | function($scope, $http, $rootScope, settings, localStorageService) {
6 |
7 | $scope.form = localStorageService.get('carty-form') || {};
8 |
9 | function setMatchingPaths(data) {
10 | $rootScope.matchingPaths = _(data.mappings).map(function(v) {
11 | return v.mapping.path;
12 | });
13 | }
14 |
15 | $scope.resolve = function() {
16 | $scope.resolveResult = null;
17 | $scope.mapResult = null;
18 | localStorageService.set('carty-form', $scope.form);
19 |
20 | $http.get(settings.apiPath + '.resolve.json', {
21 | params: {
22 | 'url': $scope.form.path,
23 | 'mappingsRoot': settings.mappingsRoot
24 | }
25 | }).success(function(data) {
26 | $scope.resolveResult = data;
27 | setMatchingPaths(data);
28 | }).error($rootScope.httpError);
29 | };
30 |
31 | $scope.map = function() {
32 | $scope.resolveResult = null;
33 | $scope.mapResult = null;
34 | localStorageService.set('carty-form', $scope.form);
35 |
36 | $http.get(settings.apiPath + '.map.json', {
37 | params: {
38 | 'path': $scope.form.path,
39 | 'host': $scope.form.host,
40 | 'mappingsRoot': settings.mappingsRoot
41 | }
42 | }).success(function(data) {
43 | $scope.mapResult = data;
44 | setMatchingPaths(data);
45 | }).error($rootScope.httpError);
46 | };
47 |
48 | $scope.highlightMapping = function(path) {
49 | $rootScope.$emit('highlightMapping', path);
50 | };
51 |
52 | $scope.clearMappingHighlight = function() {
53 | $rootScope.$emit('clearMappingHighlight');
54 | };
55 | }]);
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/CartyStringUtils.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty;
2 |
3 | import java.net.URI;
4 | import java.net.URISyntaxException;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | public final class CartyStringUtils {
9 | private CartyStringUtils() {
10 | }
11 |
12 | public static String[] multiSubstring(final String string, final int... indices) {
13 | final List
closedIndices = new ArrayList<>();
14 | closedIndices.add(0);
15 | for (final int i : indices) {
16 | closedIndices.add(i);
17 | }
18 | closedIndices.add(string.length());
19 |
20 | final String[] result = new String[closedIndices.size() - 1];
21 | for (int i = 0; i < result.length; i++) {
22 | final int from = closedIndices.get(i);
23 | final int to = closedIndices.get(i + 1);
24 | result[i] = string.substring(from, to);
25 | }
26 | return result;
27 | }
28 |
29 | public static String urlToMappingForm(String url) throws URISyntaxException {
30 | final URI uri = new URI(url);
31 | if (uri.getScheme() == null) {
32 | return url;
33 | }
34 |
35 | final StringBuilder builder = new StringBuilder();
36 | builder.append(uri.getScheme());
37 | builder.append('/');
38 | builder.append(uri.getHost());
39 | final int port = getPort(uri);
40 | if (port != -1) {
41 | builder.append('.').append(port);
42 | }
43 | builder.append(uri.getPath());
44 | return builder.toString();
45 | }
46 |
47 | private static int getPort(URI uri) {
48 | int port = uri.getPort();
49 | if (port == -1) {
50 | if ("http".equals(uri.getScheme())) {
51 | port = 80;
52 | } else if ("https".equals(uri.getScheme())) {
53 | port = 443;
54 | }
55 | }
56 | return port;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/libs/foundation/global.jsp:
--------------------------------------------------------------------------------
1 | <%--
2 | Copyright 1997-2008 Day Management AG
3 | Barfuesserplatz 6, 4001 Basel, Switzerland
4 | All Rights Reserved.
5 |
6 | This software is the confidential and proprietary information of
7 | Day Management AG, ("Confidential Information"). You shall not
8 | disclose such Confidential Information and shall use it only in
9 | accordance with the terms of the license agreement you entered into
10 | with Day.
11 |
12 | ==============================================================================
13 |
14 | Global WCM script.
15 |
16 | This script can be used by any other script in order to get the default
17 | tag libs, sling objects and CQ objects defined.
18 |
19 | the following page context attributes are initialized via the
20 | tag:
21 |
22 | @param slingRequest SlingHttpServletRequest
23 | @param slingResponse SlingHttpServletResponse
24 | @param resource the current resource
25 | @param currentNode the current node
26 | @param log default logger
27 | @param sling sling script helper
28 |
29 | @param componentContext component context of this request
30 | @param editContext edit context of this request
31 | @param properties properties of the addressed resource (aka "localstruct")
32 | @param pageManager page manager
33 | @param currentPage containing page addressed by the request (aka "actpage")
34 | @param resourcePage containing page of the addressed resource (aka "myPage")
35 | @param pageProperties properties of the containing page
36 | @param component current CQ5 component
37 | @param designer designer
38 | @param currentDesign design of the addressed resource (aka "actdesign")
39 | @param resourceDesign design of the addressed resource (aka "myDesign")
40 | @param currentStyle style of the addressed resource (aka "actstyle")
41 |
42 | ==============================================================================
43 |
44 | --%><%@page session="false" import="javax.jcr.*,
45 | org.apache.sling.api.resource.Resource,
46 | com.day.cq.wcm.commons.WCMUtils,
47 | com.day.cq.wcm.api.Page,
48 | com.day.cq.wcm.api.NameConstants,
49 | com.day.cq.wcm.api.PageManager,
50 | com.day.cq.wcm.api.designer.Designer,
51 | com.day.cq.wcm.api.designer.Design,
52 | com.day.cq.wcm.api.designer.Style,
53 | org.apache.sling.api.resource.ValueMap,
54 | com.day.cq.wcm.api.components.ComponentContext,
55 | com.day.cq.wcm.api.components.EditContext"
56 | %><%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0" %><%
57 | %><%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0" %><%
58 | %><%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%
59 | %><%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><%
60 | %><%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %><%
61 | %><%
62 |
63 | // add more initialization code here
64 |
65 | %>
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/resolver/ResolveServlet.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty.resolver;
2 |
3 | import java.io.IOException;
4 | import java.net.URISyntaxException;
5 | import java.rmi.ServerException;
6 |
7 | import org.apache.felix.scr.annotations.sling.SlingServlet;
8 | import org.apache.sling.api.SlingHttpServletRequest;
9 | import org.apache.sling.api.SlingHttpServletResponse;
10 | import org.apache.sling.api.resource.Resource;
11 | import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
12 |
13 | import com.google.gson.Gson;
14 | import com.google.gson.GsonBuilder;
15 | import com.google.gson.JsonArray;
16 | import com.google.gson.JsonElement;
17 | import com.google.gson.JsonObject;
18 |
19 | import static com.cognifide.cq.carty.CartyStringUtils.multiSubstring;
20 |
21 | @SlingServlet(methods = "GET", resourceTypes = "carty/components/cartyApi", selectors = "resolve", extensions = "json")
22 | public class ResolveServlet extends SlingSafeMethodsServlet {
23 |
24 | private static final long serialVersionUID = -4571406107865187279L;
25 |
26 | private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
27 |
28 | public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
29 | final JsonElement json;
30 | try {
31 | json = resolve(request);
32 | } catch (URISyntaxException e) {
33 | throw new ServerException("Can't parse path", e);
34 | }
35 |
36 | response.setContentType("application/json");
37 | response.getWriter().print(GSON.toJson(json));
38 | }
39 |
40 | private JsonElement resolve(SlingHttpServletRequest request) throws URISyntaxException {
41 | final String url = request.getParameter("url");
42 | final String mappingsRoot = request.getParameter("mappingsRoot");
43 | final CartyResolver cartyResolver = new CartyResolver(mappingsRoot, request.getResourceResolver());
44 | final ResolverResult result = cartyResolver.resolve(url);
45 |
46 | final JsonObject json = new JsonObject();
47 | final Resource resource = result.getResource();
48 | if (resource != null) {
49 | json.addProperty("resourcePath", resource.getPath());
50 | json.addProperty("resourceType", resource.getResourceType());
51 | json.addProperty("resourceSuperType", resource.getResourceSuperType());
52 | json.addProperty("class", resource.getClass().getName());
53 | }
54 |
55 | final JsonArray mappings = new JsonArray();
56 | final String parsedUrl = result.getParsedUrl();
57 | for (AppliedResolutionEntry m : result.getAppliedMappings()) {
58 | final JsonObject n = new JsonObject();
59 | n.add("mapping", GSON.toJsonTree(m.getMapping()));
60 |
61 | final String[] path = multiSubstring(parsedUrl, m.getFrom(), m.getTo());
62 | n.add("path", GSON.toJsonTree(path));
63 | mappings.add(n);
64 | }
65 | json.add("mappings", mappings);
66 |
67 | return json;
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/vault/common/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
17 |
18 |
19 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/resolver/CartyResolver.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty.resolver;
2 |
3 | import java.net.URI;
4 | import java.net.URISyntaxException;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.regex.Matcher;
8 |
9 | import org.apache.commons.lang.ArrayUtils;
10 | import org.apache.sling.api.resource.Resource;
11 | import org.apache.sling.api.resource.ResourceResolver;
12 |
13 | import com.cognifide.cq.carty.CartyStringUtils;
14 | import com.cognifide.cq.carty.Mapping;
15 |
16 | public class CartyResolver {
17 |
18 | private final String mappingsRoot;
19 |
20 | private final ResourceResolver resolver;
21 |
22 | public CartyResolver(final String mappingsRoot, final ResourceResolver resolver) {
23 | this.mappingsRoot = mappingsRoot;
24 | this.resolver = resolver;
25 | }
26 |
27 | public ResolverResult resolve(String url) throws URISyntaxException {
28 | final Resource mapping = resolver.getResource(mappingsRoot);
29 | final String uriToParse = CartyStringUtils.urlToMappingForm(url);
30 |
31 | final List applied = new ArrayList();
32 |
33 | if (uriToParse != null) {
34 | applied.addAll(resolve(uriToParse, 0, mapping));
35 | }
36 |
37 | if (applied.size() == 1) {
38 | applied.clear();
39 | }
40 |
41 | final AppliedResolutionEntry lastMapping = getLastValidEntry(applied);
42 | final Resource resource;
43 | if (lastMapping == null) {
44 | resource = resolver.resolve(new URI(url).getPath());
45 | } else {
46 | final String relPath = uriToParse.substring(lastMapping.getTo());
47 | resource = lastMapping.getResource(relPath, resolver);
48 | }
49 | return new ResolverResult(applied, uriToParse, resource);
50 | }
51 |
52 | private AppliedResolutionEntry getLastValidEntry(List list) {
53 | for (int i = list.size() - 1; i >= 0; i--) {
54 | AppliedResolutionEntry entry = list.get(i);
55 | if (ArrayUtils.isNotEmpty(entry.getMapping().getInternalRedirect())) {
56 | return entry;
57 | }
58 | }
59 | return null;
60 | }
61 |
62 | private List resolve(String uri, int from, Resource parentResource) {
63 | final List result = new ArrayList();
64 | for (final Resource child : parentResource.getChildren()) {
65 | final Mapping mapping = new Mapping(child);
66 | final Matcher matcher = mapping.matcher(uri).region(from, uri.length());
67 | if (!matcher.lookingAt()) {
68 | continue;
69 | }
70 |
71 | final List list = resolve(uri, matcher.end(), child);
72 | final String[] groups = new String[matcher.groupCount()];
73 | for (int i = 0; i < groups.length; i++) {
74 | groups[i] = matcher.group(i);
75 | }
76 |
77 | result.add(new AppliedResolutionEntry(mapping, matcher.start(), matcher.end(), groups));
78 | result.addAll(list);
79 | break;
80 | }
81 | return result;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/components/page/page.jsp:
--------------------------------------------------------------------------------
1 | <%@include file="/libs/foundation/global.jsp" %>
2 |
3 |
4 |
5 |
6 |
7 |
8 | Carty
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
{{flash.title}}
46 |
48 |
49 |
50 | {{flash.msg}}
51 |
52 |
53 |

54 |
55 |
56 | Resolve & map
57 | <%@include file="includes/resolve.jsp" %>
58 |
59 |
60 |
61 | Mappings
62 | Review and edit mappings
63 | <%@include file="includes/tree.jsp" %>
64 |
65 |
66 |
67 | Add new domain
68 | Create a set of mappings necessary to handle a domain.
69 | <%@include file="includes/template.jsp" %>
70 |
71 |
72 |
73 | Settings
74 | Change mappings settings
75 | <%@include file="includes/settings.jsp" %>
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/Mapping.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty;
2 |
3 | import java.util.regex.Matcher;
4 | import java.util.regex.Pattern;
5 |
6 | import org.apache.commons.lang.ArrayUtils;
7 | import org.apache.commons.lang.StringUtils;
8 | import org.apache.sling.api.resource.Resource;
9 | import org.apache.sling.api.resource.ValueMap;
10 |
11 | public class Mapping {
12 |
13 | private static final String[] SUPPORTED_SCHEME = new String[] { "http", "https" };
14 |
15 | private final String path;
16 |
17 | private final String name;
18 |
19 | private final String match;
20 |
21 | private final String[] internalRedirect;
22 |
23 | private final String redirect;
24 |
25 | private final boolean schemeMapping;
26 |
27 | private final boolean domainMapping;
28 |
29 | private final int domainPort;
30 |
31 | private transient final Resource resource;
32 |
33 | public Mapping(final Resource resource) {
34 | final ValueMap map = resource.adaptTo(ValueMap.class);
35 | this.path = resource.getPath();
36 | this.name = resource.getName();
37 | this.match = map.get("sling:match", String.class);
38 | this.internalRedirect = map.get("sling:internalRedirect", String[].class);
39 | this.redirect = map.get("sling:redirect", String.class);
40 | this.schemeMapping = ArrayUtils.contains(SUPPORTED_SCHEME, resource.getName());
41 |
42 | final String parentName = resource.getParent().getName();
43 | if (ArrayUtils.contains(SUPPORTED_SCHEME, parentName)) {
44 | this.domainMapping = true;
45 | if ("http".equals(parentName)) {
46 | domainPort = 80;
47 | } else if ("https".equals(parentName)) {
48 | domainPort = 443;
49 | } else {
50 | domainPort = -1;
51 | }
52 | } else {
53 | domainMapping = false;
54 | domainPort = -1;
55 | }
56 | this.resource = resource;
57 | }
58 |
59 | public Matcher matcher(String uri) {
60 | final StringBuilder regexp = new StringBuilder("/?");
61 | if (StringUtils.isBlank(getMatch())) {
62 | regexp.append(Pattern.quote(name));
63 | if (domainPort != -1 && !name.matches(".+\\.\\d+$")) {
64 | regexp.append("\\." + domainPort);
65 | }
66 | } else {
67 | regexp.append(match);
68 | }
69 | final Pattern pattern = Pattern.compile(regexp.toString());
70 | return pattern.matcher(uri);
71 | }
72 |
73 | public String getPath() {
74 | return path;
75 | }
76 |
77 | public String getName() {
78 | return name;
79 | }
80 |
81 | public String getMatch() {
82 | return match;
83 | }
84 |
85 | public String[] getInternalRedirect() {
86 | return internalRedirect;
87 | }
88 |
89 | public String getRedirect() {
90 | return redirect;
91 | }
92 |
93 | public boolean isSchemeMapping() {
94 | return schemeMapping;
95 | }
96 |
97 | public boolean isDomainMapping() {
98 | return domainMapping;
99 | }
100 |
101 | public Resource getResource() {
102 | return resource;
103 | }
104 |
105 | public String getUrlSegment() {
106 | return domainMapping ? getDomainNameWithPort() : getMatchOrDomain();
107 | }
108 |
109 | public String getMatchOrDomain() {
110 | return StringUtils.defaultIfEmpty(match, name);
111 | }
112 |
113 | public String getDomainNameWithPort() {
114 | final String domainName = getMatchOrDomain();
115 | if (domainName.matches(".+\\.\\d+$") || domainPort == -1) {
116 | return domainName;
117 | } else {
118 | return domainName + "." + domainPort;
119 | }
120 | }
121 |
122 | public boolean isFinal() {
123 | return getMatchOrDomain().endsWith("$");
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/java/com/cognifide/cq/carty/template/CreateMappingsServlet.java:
--------------------------------------------------------------------------------
1 | package com.cognifide.cq.carty.template;
2 |
3 | import java.io.IOException;
4 | import java.util.LinkedHashMap;
5 | import java.util.Map;
6 | import java.util.Map.Entry;
7 |
8 | import org.apache.commons.lang.text.StrSubstitutor;
9 | import org.apache.felix.scr.annotations.sling.SlingServlet;
10 | import org.apache.sling.api.SlingHttpServletRequest;
11 | import org.apache.sling.api.SlingHttpServletResponse;
12 | import org.apache.sling.api.resource.PersistenceException;
13 | import org.apache.sling.api.resource.Resource;
14 | import org.apache.sling.api.resource.ResourceResolver;
15 | import org.apache.sling.api.resource.ValueMap;
16 | import org.apache.sling.api.servlets.SlingAllMethodsServlet;
17 |
18 | import com.google.gson.Gson;
19 | import com.google.gson.GsonBuilder;
20 | import com.google.gson.JsonElement;
21 | import com.google.gson.JsonObject;
22 |
23 | @SlingServlet(methods = "POST", resourceTypes = "carty/components/cartyApi", selectors = "createMappings", extensions = "json")
24 | public class CreateMappingsServlet extends SlingAllMethodsServlet {
25 |
26 | private static final long serialVersionUID = 1L;
27 |
28 | private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
29 |
30 | public void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
31 | final JsonObject payload = GSON.fromJson(request.getReader(), JsonObject.class);
32 | final String mappingsRoot = payload.get("mappingsRoot").getAsString();
33 | final String templateName = payload.get("template").getAsString();
34 | final Map data = new LinkedHashMap();
35 | for (Entry e : payload.get("data").getAsJsonObject().entrySet()) {
36 | data.put(e.getKey(), e.getValue().getAsString());
37 | }
38 | final Resource template = request.getResource().getChild("../jcr:content/templates/" + templateName);
39 | createMappings(template, mappingsRoot, data);
40 | response.setStatus(200);
41 | }
42 |
43 | private void createMappings(Resource templateRoot, String mappingsRootPath, Map data)
44 | throws PersistenceException {
45 | final ResourceResolver resolver = templateRoot.getResourceResolver();
46 | final Resource mappingsRoot = resolver.getResource(mappingsRootPath);
47 | for (final Resource r : templateRoot.getChildren()) {
48 | copy(r, mappingsRoot, data);
49 | }
50 | resolver.commit();
51 | }
52 |
53 | private static void copy(Resource src, Resource parent, Map values)
54 | throws PersistenceException {
55 | final ResourceResolver resolver = src.getResourceResolver();
56 | final String name = fillPlaceholders(src.getName(), values);
57 |
58 | final Map props = new LinkedHashMap();
59 | for (Entry e : src.adaptTo(ValueMap.class).entrySet()) {
60 | final String key = fillPlaceholders(e.getKey(), values);
61 | final Object value = fillPlaceholders(e.getValue(), values);
62 | props.put(key, value);
63 | }
64 |
65 | Resource dst = parent.getChild(name);
66 | if (dst == null) {
67 | dst = resolver.create(parent, name, props);
68 | }
69 | for (final Resource child : src.getChildren()) {
70 | copy(child, dst, values);
71 | }
72 | }
73 |
74 | private static Object fillPlaceholders(Object target, Map values) {
75 | if (target instanceof String) {
76 | return fillPlaceholders((String) target, values);
77 | } else if (target instanceof String[]) {
78 | final String[] src = (String[]) target;
79 | final String[] dst = new String[src.length];
80 | int i = 0;
81 | for (final String text : src) {
82 | dst[i++] = fillPlaceholders(text, values);
83 | }
84 | return dst;
85 | } else {
86 | return target;
87 | }
88 | }
89 |
90 | private static String fillPlaceholders(String text, Map values) {
91 | return StrSubstitutor.replace(text, values, "_", "_");
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/carty/css/app.less:
--------------------------------------------------------------------------------
1 | html {
2 | text-rendering: optimizeLegibility;
3 | font-size: 16px;
4 | }
5 |
6 | #carty-app {
7 |
8 | div[role=main] {
9 | overflow: hidden;
10 | }
11 |
12 | .coral-Icon--sizeS {
13 | font-size: 1.30rem;
14 | }
15 |
16 | nav coral-Icon {
17 | font-size: 1rem;
18 | position: relative;
19 | padding-right: 0.45rem;
20 | opacity: 1;
21 | color: #cecece;
22 | }
23 |
24 | header {
25 | height: 46px;
26 | }
27 |
28 | header.top > nav.crumbs a {
29 | font-weight: normal;
30 | font-size: 1.125rem;
31 | line-height: 2.75rem;
32 | height: 2.75rem;
33 | }
34 |
35 | header.top div.logo {
36 | width: 25px;
37 | display: inline-block;
38 | padding: 0.625rem 0.375rem 0.625rem 0.9375rem;
39 | vertical-align: bottom;
40 | text-align: center;
41 | }
42 |
43 | header .logo, header nav, header nav a, header nav coral-icon {
44 | display: inline-block;
45 | }
46 |
47 | header.top > nav.crumbs coral-icon {
48 | font-size: 1rem;
49 | color: #787878;
50 | vertical-align: bottom;
51 | margin-bottom: 15px;
52 | }
53 |
54 | nav.crumbs a:before, nav.crumbs a:hover:before {
55 | font-size: 1rem;
56 | position: relative;
57 | padding-right: 0.625rem;
58 | opacity: 1;
59 | color: #cecece;
60 | }
61 |
62 | header nav a {
63 | text-decoration: none;
64 | padding: 0 0.25rem 0 0;
65 | }
66 |
67 | coral-alert {
68 | width: 100%;
69 | }
70 |
71 | coral-alert-header .alert-close-button {
72 | width: 20px;
73 | height: 20px;
74 | min-width: 20px;
75 | min-height: 20px;
76 | line-height: 20px;
77 | font-size: 15px;
78 |
79 | coral-button-label {
80 | font-size: 15px;
81 | }
82 | }
83 |
84 | .logo .coral-Heading {
85 | width: 1.625rem;
86 | height: 1.625rem;
87 | color: #ffffff;
88 | }
89 |
90 | .flexible-parent {
91 | display: flex;
92 | }
93 |
94 | .flexible {
95 | flex: 1;
96 | }
97 |
98 | .alert-close-button {
99 | width: 20px;
100 | }
101 |
102 | .coral-Shell-header {
103 | padding: 0px;
104 | }
105 |
106 | header.top > nav.crumbs a {
107 | font-size: 18px;
108 | color: #e6e6e6;
109 | }
110 |
111 | i.coral-Icon {
112 | font-size: 13px;
113 | pointer-events:none;
114 | }
115 |
116 | .coral--light {
117 | background-color: #f5f5f5;
118 | color: #4b4b4b;
119 | }
120 |
121 | .coral-Textfield {
122 | background-color: #fafafa;
123 | }
124 |
125 | .coral-Checkbox {
126 | display: inline;
127 | }
128 |
129 | .coral-Button.coral-Button--square.coral-Button--quiet {
130 | min-width: 22px;
131 | min-height: 22px;
132 | width: 22px;
133 | height: 22px;
134 | margin: 0px;
135 | padding: 0px;
136 | }
137 |
138 | .coral-Button.coral-Button--square.coral-Button--quiet.tree-expand-collapse {
139 | float: left;
140 | }
141 |
142 | .content-container {
143 | top: 0;
144 | padding-top: 2em;
145 |
146 | .content-container-inner {
147 | width: 960px;
148 | margin: 0 auto;
149 | }
150 | }
151 |
152 | section.resolve-map {
153 | margin-right: 128px;
154 | }
155 |
156 | #logo {
157 | float: right;
158 | }
159 |
160 | section {
161 | margin: 2em 0;
162 |
163 | h2 {
164 | margin-bottom: .5em;
165 | }
166 | }
167 |
168 | .section:last-of-type {
169 | overflow: hidden;
170 | }
171 |
172 | form {
173 | .form-row {
174 | line-height: 3em;
175 | margin-bottom: 1em;
176 |
177 | .form-left-cell {
178 | width: 10em;
179 | padding-right: 1em;
180 | margin: 0em 1em 0em 0em;
181 | float: left;
182 | height: 4em;
183 | }
184 |
185 | h4 {
186 | .form-left-cell();
187 | font-weight: bold;
188 | text-align: right;
189 | }
190 |
191 | input[type=text] {
192 | width: 75%;
193 | }
194 |
195 | .ng-invalid {
196 | border: solid 1px #FF4132 !important;
197 | color: #CC544B
198 | }
199 | }
200 | }
201 | }
202 |
203 | .angular-ui-tree-drag {
204 | font-size: 13px;
205 | min-height: 20px;
206 | line-height: 20px;
207 | }
208 |
209 | .angular-ui-tree-drag button {
210 | display: none;
211 | }
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/angularjs/angular-sanitize.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | AngularJS v1.2.23
3 | (c) 2010-2014 Google, Inc. http://angularjs.org
4 | License: MIT
5 | */
6 | (function(q,g,r){'use strict';function F(a){var d=[];t(d,g.noop).chars(a);return d.join("")}function m(a){var d={};a=a.split(",");var c;for(c=0;c=c;e--)d.end&&d.end(f[e]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,l,f=[],n=a,h;for(f.last=function(){return f[f.length-1]};a;){h="";l=!0;if(f.last()&&y[f.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(I,"$1").replace(J,"$1");d.chars&&d.chars(s(b));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&d.comment(a.substring(4,
8 | b)),a=a.substring(b+3),l=!1);else if(z.test(a)){if(b=a.match(z))a=a.replace(b[0],""),l=!1}else if(K.test(a)){if(b=a.match(A))a=a.substring(b[0].length),b[0].replace(A,e),l=!1}else L.test(a)&&((b=a.match(B))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(B,c)),l=!1):(h+="<",a=a.substring(1)));l&&(b=a.indexOf("<"),h+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(s(h)))}if(a==n)throw M("badparse",a);n=a}e()}function s(a){if(!a)return"";var d=N.exec(a);a=d[1];var c=d[3];if(d=d[2])p.innerHTML=
9 | d.replace(//g,">")}function t(a,d){var c=!1,e=g.bind(a,a.push);return{start:function(a,l,f){a=g.lowercase(a);!c&&y[a]&&(c=a);c||!0!==D[a]||(e("<"),e(a),g.forEach(l,function(c,f){var k=
10 | g.lowercase(f),l="img"===a&&"src"===k||"background"===k;!0!==Q[k]||!0===E[k]&&!d(c,l)||(e(" "),e(f),e('="'),e(C(c)),e('"'))}),e(f?"/>":">"))},end:function(a){a=g.lowercase(a);c||!0!==D[a]||(e(""),e(a),e(">"));a==c&&(c=!1)},chars:function(a){c||e(C(a))}}}var M=g.$$minErr("$sanitize"),B=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,A=/^<\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^,
11 | K=/^<\//,I=/\x3c!--(.*?)--\x3e/g,z=/]*?)>/i,J=/"]/,c=/^mailto:/;return function(e,b){function l(a){a&&k.push(F(a))}function f(a,c){k.push("');l(c);k.push("")}
14 | if(!e)return e;for(var n,h=e,k=[],m,p;n=h.match(d);)m=n[0],n[2]==n[3]&&(m="mailto:"+m),p=n.index,l(h.substr(0,p)),f(m,n[0].replace(c,"")),h=h.substring(p+n[0].length);l(h);return a(k.join(""))}}])})(window,window.angular);
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/components/page/includes/resolve.jsp:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
Resource info
30 |
31 |
32 |
33 | | Path |
34 | {{resolveResult.resourcePath}} |
35 |
36 |
37 |
38 | | Resource type |
39 | {{resolveResult.resourceType}} |
40 |
41 |
42 |
43 | | Resource super type |
44 | {{resolveResult.resourceSuperType}} |
45 |
46 |
47 |
48 | | Class |
49 | {{resolveResult.class}} |
50 |
51 |
52 |
53 |
54 | There are no matching mappings
55 |
56 |
57 |
58 |
Matching mappings
59 |
60 |
61 |
62 | | name |
63 | match |
64 | path |
65 |
66 |
67 |
68 | | {{m.mapping.name}} |
69 |
70 | {{m.mapping.match}}
71 | {{m.mapping.name}}
72 | |
73 |
74 |
75 | {{m.path[0]}}{{m.path[1]}}{{m.path[2]}}
76 |
77 | |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
Results
85 |
86 |
87 |
88 | | Mapped URL |
89 | {{mapResult.url}} |
90 |
91 |
92 |
93 |
94 | There are no matching mappings
95 |
96 |
97 |
98 |
Mappings used to generate URL
99 |
100 |
101 |
102 | | mapping name |
103 | matching path part |
104 | mapped url part |
105 |
106 |
107 |
108 | | {{m.mapping.name}} |
109 |
110 |
111 | {{m.matchingPath[0]}}{{m.matchingPath[1]}}
112 |
113 |
114 | -
115 |
116 | |
117 |
118 |
119 | {{m.url[0]}}{{m.url[1]}}{{m.url[2]}}
120 |
121 | |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/angularjs/angular-local-storage.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * An Angular module that gives you access to the browsers local storage
3 | * @version v0.1.5 - 2014-11-04
4 | * @link https://github.com/grevory/angular-local-storage
5 | * @author grevory
6 | * @license MIT License, http://www.opensource.org/licenses/MIT
7 | */!function(a,b){"use strict";function c(a){return/^-?\d+\.?\d*$/.test(a.replace(/["']/g,""))}var d=b.isDefined,e=b.isUndefined,f=b.isNumber,g=b.isObject,h=b.isArray,i=b.extend,j=b.toJson,k=b.fromJson,l=b.module("LocalStorageModule",[]);l.provider("localStorageService",function(){this.prefix="ls",this.storageType="localStorage",this.cookie={expiry:30,path:"/"},this.notify={setItem:!0,removeItem:!1},this.setPrefix=function(a){return this.prefix=a,this},this.setStorageType=function(a){return this.storageType=a,this},this.setStorageCookie=function(a,b){return this.cookie={expiry:a,path:b},this},this.setStorageCookieDomain=function(a){return this.cookie.domain=a,this},this.setNotify=function(a,b){return this.notify={setItem:a,removeItem:b},this},this.$get=["$rootScope","$window","$document","$parse",function(a,b,l,m){var n,o=this,p=o.prefix,q=o.cookie,r=o.notify,s=o.storageType;l?l[0]&&(l=l[0]):l=document,"."!==p.substr(-1)&&(p=p?p+".":"");var t=function(a){return p+a},u=function(){try{var c=s in b&&null!==b[s],d=t("__"+Math.round(1e7*Math.random()));return c&&(n=b[s],n.setItem(d,""),n.removeItem(d)),c}catch(e){return s="cookie",a.$broadcast("LocalStorageModule.notification.error",e.message),!1}}(),v=function(b,c){if(e(c)?c=null:(g(c)||h(c)||f(+c||c))&&(c=j(c)),!u||"cookie"===o.storageType)return u||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),r.setItem&&a.$broadcast("LocalStorageModule.notification.setitem",{key:b,newvalue:c,storageType:"cookie"}),B(b,c);try{(g(c)||h(c))&&(c=j(c)),n&&n.setItem(t(b),c),r.setItem&&a.$broadcast("LocalStorageModule.notification.setitem",{key:b,newvalue:c,storageType:o.storageType})}catch(d){return a.$broadcast("LocalStorageModule.notification.error",d.message),B(b,c)}return!0},w=function(b){if(!u||"cookie"===o.storageType)return u||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),C(b);var d=n?n.getItem(t(b)):null;return d&&"null"!==d?"{"===d.charAt(0)||"["===d.charAt(0)||c(d)?k(d):d:null},x=function(b){if(!u||"cookie"===o.storageType)return u||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),r.removeItem&&a.$broadcast("LocalStorageModule.notification.removeitem",{key:b,storageType:"cookie"}),D(b);try{n.removeItem(t(b)),r.removeItem&&a.$broadcast("LocalStorageModule.notification.removeitem",{key:b,storageType:o.storageType})}catch(c){return a.$broadcast("LocalStorageModule.notification.error",c.message),D(b)}return!0},y=function(){if(!u)return a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),!1;var b=p.length,c=[];for(var d in n)if(d.substr(0,b)===p)try{c.push(d.substr(b))}catch(e){return a.$broadcast("LocalStorageModule.notification.error",e.Description),[]}return c},z=function(b){b=b||"";var c=p.slice(0,-1),d=new RegExp(c+"."+b);if(!u||"cookie"===o.storageType)return u||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),E();var e=p.length;for(var f in n)if(d.test(f))try{x(f.substr(e))}catch(g){return a.$broadcast("LocalStorageModule.notification.error",g.message),E()}return!0},A=function(){try{return b.navigator.cookieEnabled||"cookie"in l&&(l.cookie.length>0||(l.cookie="test").indexOf.call(l.cookie,"test")>-1)}catch(c){return a.$broadcast("LocalStorageModule.notification.error",c.message),!1}}(),B=function(b,c){if(e(c))return!1;if((h(c)||g(c))&&(c=j(c)),!A)return a.$broadcast("LocalStorageModule.notification.error","COOKIES_NOT_SUPPORTED"),!1;try{var d="",f=new Date,i="";if(null===c?(f.setTime(f.getTime()+-864e5),d="; expires="+f.toGMTString(),c=""):0!==q.expiry&&(f.setTime(f.getTime()+24*q.expiry*60*60*1e3),d="; expires="+f.toGMTString()),b){var k="; path="+q.path;q.domain&&(i="; domain="+q.domain),l.cookie=t(b)+"="+encodeURIComponent(c)+d+k+i}}catch(m){return a.$broadcast("LocalStorageModule.notification.error",m.message),!1}return!0},C=function(b){if(!A)return a.$broadcast("LocalStorageModule.notification.error","COOKIES_NOT_SUPPORTED"),!1;for(var c=l.cookie&&l.cookie.split(";")||[],d=0;d mappings = map(path, new AppliedMappingEntry(root, "", null));
24 | if (mappings.isEmpty()) {
25 | return new MapperResult(path, mappings);
26 | }
27 |
28 | final List filtered;
29 | if (StringUtils.isBlank(urlPrefix)) {
30 | filtered = mappings;
31 | } else {
32 | filtered = filter(mappings, urlPrefix);
33 | }
34 | Collections.sort(filtered, new Comparator() {
35 | @Override
36 | public int compare(AppliedMappingEntry o1, AppliedMappingEntry o2) {
37 | return o2.getMatchingInternalRedirect().length() - o1.getMatchingInternalRedirect().length();
38 | }
39 | });
40 |
41 | final AppliedMappingEntry appliedMapping = filtered.get(0);
42 | final List ancestors = getAncestors(appliedMapping);
43 | Collections.reverse(ancestors);
44 | return new MapperResult(appliedMapping.getUrl(), ancestors);
45 | }
46 |
47 | private List getAncestors(AppliedMappingEntry appliedMapping) {
48 | final List ancestors = new ArrayList();
49 | AppliedMappingEntry currentMapping = appliedMapping;
50 | do {
51 | ancestors.add(currentMapping);
52 | currentMapping = currentMapping.getParent();
53 | } while (currentMapping.getMapping() != root);
54 | return ancestors;
55 | }
56 |
57 | private List filter(List mappings, String urlPrefix) {
58 | final List filtered = new ArrayList<>();
59 | for (final AppliedMappingEntry e : mappings) {
60 | if (e.getUrl().startsWith(urlPrefix)) {
61 | filtered.add(e);
62 | }
63 | }
64 | if (filtered.isEmpty()) {
65 | filtered.addAll(mappings);
66 | }
67 | return filtered;
68 | }
69 |
70 | public List map(String path, AppliedMappingEntry parent) {
71 | final List list = new ArrayList();
72 | for (final Resource resource : parent.getMapping().getResource().getChildren()) {
73 | final Mapping mapping = new Mapping(resource);
74 | final String url = getUrl(mapping, parent.getUrl());
75 | if (isValidMapping(mapping)) {
76 | list.addAll(findMatchingRedirects(path, mapping, url, parent));
77 | }
78 | if (!url.endsWith("$")) {
79 | final AppliedMappingEntry currentMapping = new AppliedMappingEntry(mapping, url, parent);
80 | list.addAll(map(path, currentMapping));
81 | }
82 | }
83 | return list;
84 | }
85 |
86 | private String getUrl(Mapping mapping, String urlPrefix) {
87 | final String urlSuffix = mapping.getUrlSegment();
88 | if (StringUtils.isBlank(urlPrefix)) {
89 | return urlSuffix;
90 | } else {
91 | return String.format("%s/%s", urlPrefix, urlSuffix);
92 | }
93 | }
94 |
95 | private List findMatchingRedirects(final String path, final Mapping mapping,
96 | final String url, final AppliedMappingEntry parent) {
97 | final List list = new ArrayList();
98 | final boolean isFinal = url.endsWith("$");
99 |
100 | for (final String internalRedirect : mapping.getInternalRedirect()) {
101 | String fullUrl = null;
102 | if (path.equals(internalRedirect)) {
103 | fullUrl = url;
104 | } else if (!isFinal && path.startsWith(internalRedirect)) {
105 | fullUrl = url + path.substring(internalRedirect.length());
106 | }
107 | if (fullUrl != null) {
108 | list.add(new AppliedMappingEntry(mapping, internalRedirect, fullUrl, parent));
109 | }
110 | }
111 | return list;
112 | }
113 |
114 | private static boolean isValidMapping(Mapping mapping) {
115 | if (StringUtils.isNotBlank(mapping.getRedirect())) {
116 | return false;
117 | }
118 | if (mapping.getMatch() != null && isRegExp(mapping.getMatch())) {
119 | return false;
120 | }
121 | if (mapping.getInternalRedirect() == null) {
122 | return false;
123 | }
124 | return true;
125 | }
126 |
127 | private static boolean isRegExp(final String string) {
128 | for (int i = 0; i < string.length(); i++) {
129 | final char c = string.charAt(i);
130 | if (c == '\\') {
131 | i++; // just skip
132 | } else if ("+*?|()[]".indexOf(c) >= 0) {
133 | return true; // assume an unescaped pattern character
134 | }
135 | }
136 | return false;
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/carty/js/treeCtrl.js:
--------------------------------------------------------------------------------
1 | /*global angular: false */
2 |
3 | angular.module('cartyApp').controller('TreeCtrl', ['$scope', '$http', '$log', '$timeout', '$rootScope', 'settings',
4 | function($scope, $http, $log, $timeout, $rootScope, settings) {
5 |
6 | var properties = ['match', 'internalRedirect', 'redirect', 'status'];
7 |
8 | function transform(data, parent, name) {
9 | var m = {};
10 | _.each(data, function(v, k) {
11 | if (k.substr(0, 6) === 'sling:') {
12 | m[k.substr(6)] = v;
13 | }
14 | });
15 | m.name = name;
16 | m.parent = parent;
17 | m.path = parent.path + '/' + name;
18 | m.isMapping = data['jcr:primaryType'] === 'sling:Mapping';
19 | m.items = [];
20 |
21 | _.each(data, function(v, k) {
22 | if (_.isObject(v) && !_.isArray(v)) {
23 | m.items.push(transform(v, m, k));
24 | }
25 | });
26 |
27 | return m;
28 | }
29 |
30 | function loadMappings() {
31 | var root = settings.mappingsRoot;
32 |
33 | $http.get(root + ".-1.json").success(function(data) {
34 | var i = root.lastIndexOf('/'),
35 | parent = root.substring(0, i),
36 | name = root.substring(i + 1);
37 | $scope.mappings = transform(data, {'path' : parent}, name).items;
38 | $scope.errorMessage = null;
39 | }).error(function(err, status) {
40 | $scope.mappings = [];
41 | $rootScope.httpError.apply(this, arguments);
42 | });
43 | }
44 |
45 | function slingPost(path, data) {
46 | return $http({
47 | method: 'POST',
48 | url: path,
49 | data: data,
50 | headers: {'Content-Type': 'application/x-www-form-urlencoded'},
51 | transformRequest: function(data) {
52 | var str = [], p, q;
53 | _(data).each(function(v, k) {
54 | if (typeof v === 'object') {
55 | str.push(k + '@TypeHint=' + 'String[]');
56 | _(v).each(function(v1, k1) {
57 | str.push(k + "=" + encodeURIComponent(v1));
58 | });
59 | } else {
60 | str.push(k + "=" + encodeURIComponent(v));
61 | }
62 | });
63 | return str.join('&');
64 | }
65 | }).error($rootScope.httpError);
66 | }
67 |
68 | function reorder(path, newIndex) {
69 | var data = {};
70 | data[':order'] = newIndex;
71 | return slingPost(path, data);
72 | }
73 |
74 | function move(from, newParent) {
75 | var data = {};
76 | data[from.name + '@MoveFrom'] = from.path;
77 | return slingPost(newParent.path, data);
78 | }
79 |
80 | function swap(array, i, j) {
81 | var tmp = array[i];
82 | array[i] = array[j];
83 | array[j] = tmp;
84 | }
85 |
86 | $scope.saveMapping = function(mapping) {
87 | var data = {};
88 | _(properties).each(function(p) {
89 | if (mapping[p]) {
90 | data['sling:' + p] = mapping[p];
91 | } else {
92 | data['sling:' + p + '@Delete'] = 'true';
93 | }
94 | });
95 | slingPost(mapping.path, data).success(loadMappings);
96 | };
97 |
98 | $scope.newSubItem = function(mapping) {
99 | slingPost(mapping.path + '/*', {
100 | ':nameHint' : 'new mapping',
101 | 'jcr:primaryType' : 'sling:Mapping'
102 | }).success(loadMappings);
103 | };
104 |
105 | $scope.removeMapping = function(mapping) {
106 | slingPost(mapping.path, {':operation' : 'delete'}).success(loadMappings);
107 | };
108 |
109 | $scope.expanded = {};
110 |
111 | $scope.toggleMapping = function(path) {
112 | $scope.expanded[path] = !$scope.expanded[path];
113 | };
114 |
115 | $scope.checkNewName = function(mapping, newName) {
116 | if (_(mapping.parent.items).find(function(v) {
117 | if (v.name === newName && mapping !== v) {
118 | return true;
119 | }
120 | })) {
121 | return 'Node with this name already exists';
122 | }
123 | };
124 |
125 | $scope.rename = function(mapping) {
126 | var index = mapping.parent.items.indexOf(mapping);
127 | move(mapping, mapping.parent).success(function() {
128 | var newPath = mapping.parent.path + '/' + mapping.name;
129 | $scope.expanded[newPath] = $scope.expanded[mapping.path];
130 | reorder(newPath, index)['finally'](loadMappings);
131 | });
132 | };
133 |
134 | $scope.redirectUp = function(mapping, i, form) {
135 | swap(mapping.internalRedirect, i, i-1);
136 | $scope.saveMapping(mapping);
137 | };
138 |
139 | $scope.redirectDown = function(mapping, i, form) {
140 | swap(mapping.internalRedirect, i, i+1);
141 | $scope.saveMapping(mapping);
142 | };
143 |
144 | $scope.deleteRedirect = function(mapping, i) {
145 | mapping.internalRedirect.splice(i, 1);
146 | if (_.isEmpty(mapping.internalRedirect)) {
147 | mapping.internalRedirect.push("");
148 | }
149 | $scope.saveMapping(mapping);
150 | };
151 |
152 | $scope.addRedirect = function(mapping, redirect) {
153 | mapping.internalRedirect = mapping.internalRedirect || [];
154 | mapping.internalRedirect.push("/");
155 | $scope.saveMapping(mapping);
156 | };
157 |
158 | $scope.dragEnabled = true;
159 |
160 | $scope.disableDrag = function() {
161 | $scope.dragEnabled = false;
162 | };
163 |
164 | $scope.enableDrag = function() {
165 | $scope.dragEnabled = true;
166 | };
167 |
168 | $scope.treeCallbacks = {
169 | dropped : function(event) {
170 | var source = event.source,
171 | dest = event.dest,
172 | mapping = source.nodeScope.$modelValue,
173 | parent = dest.nodesScope.$parent.$modelValue,
174 | newIndex = dest.index;
175 | if (parent === mapping.parent) {
176 | reorder(parent.path + '/' + mapping.name, newIndex)['finally'](loadMappings);
177 | } else {
178 | move(mapping, parent).success(function() {
179 | var newPath = parent.path + '/' + mapping.name;
180 | $scope.expanded[newPath] = $scope.expanded[mapping.path];
181 | reorder(newPath, newIndex)['finally'](loadMappings);
182 | });
183 | }
184 | }
185 | };
186 |
187 | $rootScope.$on('reloadTree', function() {
188 | loadMappings();
189 | });
190 |
191 | $rootScope.$on('highlightMapping', function(event, path) {
192 | $scope.highlighted = path;
193 | });
194 |
195 | $rootScope.$on('clearMappingHighlight', function() {
196 | $scope.highlighted = null;
197 | });
198 |
199 | $scope.isMatchingPath = function(path) {
200 | return $rootScope.matchingPaths.indexOf(path) > -1;
201 | };
202 |
203 | loadMappings();
204 | }]);
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/components/page/includes/tree.jsp:
--------------------------------------------------------------------------------
1 |
2 |
153 |
154 |
159 |
160 |
161 |
162 |
163 |
164 |
166 |
167 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | org.sonatype.oss
8 | oss-parent
9 | 7
10 |
11 |
12 | 4.0.0
13 | com.cognifide.cq
14 | carty
15 | 1.0-SNAPSHOT
16 | bundle
17 | Carty
18 | Wunderman Thompson Technology mapping tool
19 | https://github.com/wttech/Carty
20 |
21 |
22 | UTF-8
23 | http://localhost:4502
24 | admin
25 | admin
26 |
27 |
28 |
29 | scm:git:ssh://git@github.com:wttech/Carty.git
30 | scm:git:ssh://git@github.com:wttech/Carty.git
31 | https://github.com/wttech/Carty
32 |
33 |
34 |
35 | Wunderman Thompson Technology
36 | http://www.cognifide.com
37 |
38 |
39 |
40 |
41 | The Apache Software License, Version 2.0
42 | http://www.apache.org/licenses/LICENSE-2.0.txt
43 | repo
44 |
45 |
46 |
47 |
48 |
49 |
50 | maven-compiler-plugin
51 | 3.2
52 |
53 | 1.7
54 | 1.7
55 |
56 |
57 |
58 | org.apache.felix
59 | maven-scr-plugin
60 | 1.20.0
61 |
62 |
63 | generate-scr-scrdescriptor
64 |
65 | scr
66 |
67 |
68 |
69 |
70 |
71 | org.apache.felix
72 | maven-bundle-plugin
73 | 2.5.3
74 | true
75 |
76 |
77 | cognifide
78 | ${project.artifactId}
79 | ${project.name}
80 | ${project.organization.name}
81 | *;scope=compile|runtime
82 |
83 |
84 |
85 |
86 | maven-assembly-plugin
87 | 2.4.1
88 |
89 | ${project.artifactId}-${project.version}
90 | false
91 |
92 | src/main/assembly/cq.xml
93 |
94 |
95 |
96 |
97 | package
98 |
99 | single
100 |
101 |
102 |
103 |
104 |
105 | com.cognifide.maven.plugins
106 | maven-crx-plugin
107 | 1.0.3
108 |
109 | ${instance.url}
110 | ${instance.username}
111 | ${instance.password}
112 |
113 |
114 |
115 |
116 | com.googlecode.jslint4java
117 | jslint4java-maven-plugin
118 | 2.0.2
119 |
120 |
121 |
122 | process-resources
123 |
124 | lint
125 |
126 |
127 | true
128 |
129 |
130 | _,$
131 | false
132 | true
133 | true
134 | true
135 | true
136 | 10000
137 | true
138 | ${lint.dev}
139 | 120
140 | true
141 |
142 |
143 | ${basedir}/src/main/cq/jcr_root/apps/carty/clientlibs/carty/js
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
155 |
156 | org.eclipse.m2e
157 | lifecycle-mapping
158 | 1.0.0
159 |
160 |
161 |
162 |
163 |
164 | org.apache.felix
165 | maven-scr-plugin
166 | [1.9.8,)
167 |
168 | scr
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | org.apache.sling
187 | org.apache.sling.api
188 | 2.4.0
189 | provided
190 |
191 |
192 | org.apache.sling
193 | org.apache.sling.jcr.api
194 | 2.1.0
195 | provided
196 |
197 |
198 |
199 |
200 | org.osgi
201 | org.osgi.core
202 | 4.1.0
203 | provided
204 |
205 |
206 | org.osgi
207 | org.osgi.compendium
208 | 4.1.0
209 | provided
210 |
211 |
212 |
213 |
214 | javax.servlet
215 | servlet-api
216 | 2.5
217 | provided
218 |
219 |
220 |
221 |
222 | org.apache.felix
223 | org.apache.felix.scr.annotations
224 | 1.9.8
225 | provided
226 |
227 |
228 |
229 |
230 | org.slf4j
231 | slf4j-api
232 | 1.6.1
233 | provided
234 |
235 |
236 |
237 |
238 | commons-lang
239 | commons-lang
240 | 2.5
241 | provided
242 |
243 |
244 | com.google.code.gson
245 | gson
246 | 2.3.1
247 |
248 |
249 |
250 |
--------------------------------------------------------------------------------
/src/main/cq/jcr_root/apps/carty/clientlibs/angular.ui/js/xeditable.js:
--------------------------------------------------------------------------------
1 | /*!
2 | angular-xeditable - 0.1.8
3 | Edit-in-place for angular.js
4 | Build date: 2014-01-10
5 | */
6 | /**
7 | * Angular-xeditable module
8 | *
9 | */
10 | angular.module('xeditable', [])
11 |
12 |
13 | /**
14 | * Default options.
15 | *
16 | * @namespace editable-options
17 | */
18 | //todo: maybe better have editableDefaults, not options...
19 | .value('editableOptions', {
20 | /**
21 | * Theme. Possible values `bs3`, `bs2`, `default`.
22 | *
23 | * @var {string} theme
24 | * @memberOf editable-options
25 | */
26 | theme: 'default',
27 | /**
28 | * Whether to show buttons for single editalbe element.
29 | * Possible values `right` (default), `no`.
30 | *
31 | * @var {string} buttons
32 | * @memberOf editable-options
33 | */
34 | buttons: 'right',
35 | /**
36 | * Default value for `blur` attribute of single editable element.
37 | * Can be `cancel|submit|ignore`.
38 | *
39 | * @var {string} blurElem
40 | * @memberOf editable-options
41 | */
42 | blurElem: 'cancel',
43 | /**
44 | * Default value for `blur` attribute of editable form.
45 | * Can be `cancel|submit|ignore`.
46 | *
47 | * @var {string} blurForm
48 | * @memberOf editable-options
49 | */
50 | blurForm: 'ignore',
51 | /**
52 | * How input elements get activated. Possible values: `focus|select|none`.
53 | *
54 | * @var {string} activate
55 | * @memberOf editable-options
56 | */
57 | activate: 'focus'
58 |
59 | });
60 | /*
61 | Angular-ui bootstrap datepicker
62 | http://angular-ui.github.io/bootstrap/#/datepicker
63 | */
64 | angular.module('xeditable').directive('editableBsdate', ['editableDirectiveFactory',
65 | function(editableDirectiveFactory) {
66 | return editableDirectiveFactory({
67 | directiveName: 'editableBsdate',
68 | inputTpl: ''
69 | });
70 | }]);
71 | /*
72 | Angular-ui bootstrap editable timepicker
73 | http://angular-ui.github.io/bootstrap/#/timepicker
74 | */
75 | angular.module('xeditable').directive('editableBstime', ['editableDirectiveFactory',
76 | function(editableDirectiveFactory) {
77 | return editableDirectiveFactory({
78 | directiveName: 'editableBstime',
79 | inputTpl: '',
80 | render: function() {
81 | this.parent.render.call(this);
82 |
83 | // timepicker can't update model when ng-model set directly to it
84 | // see: https://github.com/angular-ui/bootstrap/issues/1141
85 | // so we wrap it into DIV
86 | var div = angular.element('');
87 |
88 | // move ng-model to wrapping div
89 | div.attr('ng-model', this.inputEl.attr('ng-model'));
90 | this.inputEl.removeAttr('ng-model');
91 |
92 | // move ng-change to wrapping div
93 | if(this.attrs.eNgChange) {
94 | div.attr('ng-change', this.inputEl.attr('ng-change'));
95 | this.inputEl.removeAttr('ng-change');
96 | }
97 |
98 | // wrap
99 | this.inputEl.wrap(div);
100 | }
101 | });
102 | }]);
103 | //checkbox
104 | angular.module('xeditable').directive('editableCheckbox', ['editableDirectiveFactory',
105 | function(editableDirectiveFactory) {
106 | return editableDirectiveFactory({
107 | directiveName: 'editableCheckbox',
108 | inputTpl: '',
109 | render: function() {
110 | this.parent.render.call(this);
111 | if(this.attrs.eTitle) {
112 | this.inputEl.wrap('');
113 | this.inputEl.after(angular.element('').text(this.attrs.eTitle));
114 | }
115 | },
116 | autosubmit: function() {
117 | var self = this;
118 | self.inputEl.bind('change', function() {
119 | setTimeout(function() {
120 | self.scope.$apply(function() {
121 | self.scope.$form.$submit();
122 | });
123 | }, 500);
124 | });
125 | }
126 | });
127 | }]);
128 | // checklist
129 | angular.module('xeditable').directive('editableChecklist', [
130 | 'editableDirectiveFactory',
131 | 'editableNgOptionsParser',
132 | function(editableDirectiveFactory, editableNgOptionsParser) {
133 | return editableDirectiveFactory({
134 | directiveName: 'editableChecklist',
135 | inputTpl: '',
136 | useCopy: true,
137 | render: function() {
138 | this.parent.render.call(this);
139 | var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
140 | var html = '';
143 |
144 | this.inputEl.removeAttr('ng-model');
145 | this.inputEl.removeAttr('ng-options');
146 | this.inputEl.html(html);
147 | }
148 | });
149 | }]);
150 | /*
151 | Input types: text|email|tel|number|url|search|color|date|datetime|time|month|week
152 | */
153 |
154 | (function() {
155 |
156 | var types = 'text|email|tel|number|url|search|color|date|datetime|time|month|week'.split('|');
157 |
158 | //todo: datalist
159 |
160 | // generate directives
161 | angular.forEach(types, function(type) {
162 | var directiveName = 'editable'+type.charAt(0).toUpperCase() + type.slice(1);
163 | angular.module('xeditable').directive(directiveName, ['editableDirectiveFactory',
164 | function(editableDirectiveFactory) {
165 | return editableDirectiveFactory({
166 | directiveName: directiveName,
167 | inputTpl: ''
168 | });
169 | }]);
170 | });
171 |
172 | //`range` is bit specific
173 | angular.module('xeditable').directive('editableRange', ['editableDirectiveFactory',
174 | function(editableDirectiveFactory) {
175 | return editableDirectiveFactory({
176 | directiveName: 'editableRange',
177 | inputTpl: '',
178 | render: function() {
179 | this.parent.render.call(this);
180 | this.inputEl.after('');
181 | }
182 | });
183 | }]);
184 |
185 | }());
186 |
187 |
188 | // radiolist
189 | angular.module('xeditable').directive('editableRadiolist', [
190 | 'editableDirectiveFactory',
191 | 'editableNgOptionsParser',
192 | function(editableDirectiveFactory, editableNgOptionsParser) {
193 | return editableDirectiveFactory({
194 | directiveName: 'editableRadiolist',
195 | inputTpl: '',
196 | render: function() {
197 | this.parent.render.call(this);
198 | var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
199 | var html = '';
202 |
203 | this.inputEl.removeAttr('ng-model');
204 | this.inputEl.removeAttr('ng-options');
205 | this.inputEl.html(html);
206 | },
207 | autosubmit: function() {
208 | var self = this;
209 | self.inputEl.bind('change', function() {
210 | setTimeout(function() {
211 | self.scope.$apply(function() {
212 | self.scope.$form.$submit();
213 | });
214 | }, 500);
215 | });
216 | }
217 | });
218 | }]);
219 | //select
220 | angular.module('xeditable').directive('editableSelect', ['editableDirectiveFactory',
221 | function(editableDirectiveFactory) {
222 | return editableDirectiveFactory({
223 | directiveName: 'editableSelect',
224 | inputTpl: '',
225 | autosubmit: function() {
226 | var self = this;
227 | self.inputEl.bind('change', function() {
228 | self.scope.$apply(function() {
229 | self.scope.$form.$submit();
230 | });
231 | });
232 | }
233 | });
234 | }]);
235 | //textarea
236 | angular.module('xeditable').directive('editableTextarea', ['editableDirectiveFactory',
237 | function(editableDirectiveFactory) {
238 | return editableDirectiveFactory({
239 | directiveName: 'editableTextarea',
240 | inputTpl: '',
241 | addListeners: function() {
242 | var self = this;
243 | self.parent.addListeners.call(self);
244 | // submit textarea by ctrl+enter even with buttons
245 | if (self.single && self.buttons !== 'no') {
246 | self.autosubmit();
247 | }
248 | },
249 | autosubmit: function() {
250 | var self = this;
251 | self.inputEl.bind('keydown', function(e) {
252 | if ((e.ctrlKey || e.metaKey) && (e.keyCode === 13)) {
253 | self.scope.$apply(function() {
254 | self.scope.$form.$submit();
255 | });
256 | }
257 | });
258 | }
259 | });
260 | }]);
261 |
262 | /**
263 | * EditableController class.
264 | * Attached to element with `editable-xxx` directive.
265 | *
266 | * @namespace editable-element
267 | */
268 | /*
269 | TODO: this file should be refactored to work more clear without closures!
270 | */
271 | angular.module('xeditable').factory('editableController',
272 | ['$q', 'editableUtils',
273 | function($q, editableUtils) {
274 |
275 | //EditableController function
276 | EditableController.$inject = ['$scope', '$attrs', '$element', '$parse', 'editableThemes', 'editableOptions', '$rootScope', '$compile', '$q'];
277 | function EditableController($scope, $attrs, $element, $parse, editableThemes, editableOptions, $rootScope, $compile, $q) {
278 | var valueGetter;
279 |
280 | //if control is disabled - it does not participate in waiting process
281 | var inWaiting;
282 |
283 | var self = this;
284 |
285 | self.scope = $scope;
286 | self.elem = $element;
287 | self.attrs = $attrs;
288 | self.inputEl = null;
289 | self.editorEl = null;
290 | self.single = true;
291 | self.error = '';
292 | self.theme = editableThemes[editableOptions.theme] || editableThemes['default'];
293 | self.parent = {};
294 |
295 | //to be overwritten by directive
296 | self.inputTpl = '';
297 | self.directiveName = '';
298 |
299 | // with majority of controls copy is not needed, but..
300 | // copy MUST NOT be used for `select-multiple` with objects as items
301 | // copy MUST be used for `checklist`
302 | self.useCopy = false;
303 |
304 | //runtime (defaults)
305 | self.single = null;
306 |
307 | /**
308 | * Attributes defined with `e-*` prefix automatically transfered from original element to
309 | * control.
310 | * For example, if you set `
311 | * then input will appear as ``.
312 | * See [demo](#text-customize).
313 | *
314 | * @var {any|attribute} e-*
315 | * @memberOf editable-element
316 | */
317 |
318 | /**
319 | * Whether to show ok/cancel buttons. Values: `right|no`.
320 | * If set to `no` control automatically submitted when value changed.
321 | * If control is part of form buttons will never be shown.
322 | *
323 | * @var {string|attribute} buttons
324 | * @memberOf editable-element
325 | */
326 | self.buttons = 'right';
327 | /**
328 | * Action when control losses focus. Values: `cancel|submit|ignore`.
329 | * Has sense only for single editable element.
330 | * Otherwise, if control is part of form - you should set `blur` of form, not of individual element.
331 | *
332 | * @var {string|attribute} blur
333 | * @memberOf editable-element
334 | */
335 | // no real `blur` property as it is transfered to editable form
336 |
337 | //init
338 | self.init = function(single) {
339 | self.single = single;
340 |
341 | self.name = $attrs.eName || $attrs[self.directiveName];
342 | /*
343 | if(!$attrs[directiveName] && !$attrs.eNgModel && ($attrs.eValue === undefined)) {
344 | throw 'You should provide value for `'+directiveName+'` or `e-value` in editable element!';
345 | }
346 | */
347 | if($attrs[self.directiveName]) {
348 | valueGetter = $parse($attrs[self.directiveName]);
349 | } else {
350 | throw 'You should provide value for `'+self.directiveName+'` in editable element!';
351 | }
352 |
353 | // settings for single and non-single
354 | if (!self.single) {
355 | // hide buttons for non-single
356 | self.buttons = 'no';
357 | } else {
358 | self.buttons = self.attrs.buttons || editableOptions.buttons;
359 | }
360 |
361 | //if name defined --> watch changes and update $data in form
362 | if($attrs.eName) {
363 | self.scope.$watch('$data', function(newVal){
364 | self.scope.$form.$data[$attrs.eName] = newVal;
365 | });
366 | }
367 |
368 | /**
369 | * Called when control is shown.
370 | * See [demo](#select-remote).
371 | *
372 | * @var {method|attribute} onshow
373 | * @memberOf editable-element
374 | */
375 | if($attrs.onshow) {
376 | self.onshow = function() {
377 | return self.catchError($parse($attrs.onshow)($scope));
378 | };
379 | }
380 |
381 | /**
382 | * Called when control is hidden after both save or cancel.
383 | *
384 | * @var {method|attribute} onhide
385 | * @memberOf editable-element
386 | */
387 | if($attrs.onhide) {
388 | self.onhide = function() {
389 | return $parse($attrs.onhide)($scope);
390 | };
391 | }
392 |
393 | /**
394 | * Called when control is cancelled.
395 | *
396 | * @var {method|attribute} oncancel
397 | * @memberOf editable-element
398 | */
399 | if($attrs.oncancel) {
400 | self.oncancel = function() {
401 | return $parse($attrs.oncancel)($scope);
402 | };
403 | }
404 |
405 | /**
406 | * Called during submit before value is saved to model.
407 | * See [demo](#onbeforesave).
408 | *
409 | * @var {method|attribute} onbeforesave
410 | * @memberOf editable-element
411 | */
412 | if ($attrs.onbeforesave) {
413 | self.onbeforesave = function() {
414 | return self.catchError($parse($attrs.onbeforesave)($scope));
415 | };
416 | }
417 |
418 | /**
419 | * Called during submit after value is saved to model.
420 | * See [demo](#onaftersave).
421 | *
422 | * @var {method|attribute} onaftersave
423 | * @memberOf editable-element
424 | */
425 | if ($attrs.onaftersave) {
426 | self.onaftersave = function() {
427 | return self.catchError($parse($attrs.onaftersave)($scope));
428 | };
429 | }
430 |
431 | // watch change of model to update editable element
432 | // now only add/remove `editable-empty` class.
433 | // Initially this method called with newVal = undefined, oldVal = undefined
434 | // so no need initially call handleEmpty() explicitly
435 | $scope.$parent.$watch($attrs[self.directiveName], function(newVal, oldVal) {
436 | self.handleEmpty();
437 | });
438 | };
439 |
440 | self.render = function() {
441 | var theme = self.theme;
442 |
443 | //build input
444 | self.inputEl = angular.element(self.inputTpl);
445 |
446 | //build controls
447 | self.controlsEl = angular.element(theme.controlsTpl);
448 | self.controlsEl.append(self.inputEl);
449 |
450 | //build buttons
451 | if(self.buttons !== 'no') {
452 | self.buttonsEl = angular.element(theme.buttonsTpl);
453 | self.submitEl = angular.element(theme.submitTpl);
454 | self.cancelEl = angular.element(theme.cancelTpl);
455 | self.buttonsEl.append(self.submitEl).append(self.cancelEl);
456 | self.controlsEl.append(self.buttonsEl);
457 |
458 | self.inputEl.addClass('editable-has-buttons');
459 | }
460 |
461 | //build error
462 | self.errorEl = angular.element(theme.errorTpl);
463 | self.controlsEl.append(self.errorEl);
464 |
465 | //build editor
466 | self.editorEl = angular.element(self.single ? theme.formTpl : theme.noformTpl);
467 | self.editorEl.append(self.controlsEl);
468 |
469 | // transfer `e-*|data-e-*|x-e-*` attributes
470 | for(var k in $attrs.$attr) {
471 | if(k.length <= 1) {
472 | continue;
473 | }
474 | var transferAttr = false;
475 | var nextLetter = k.substring(1, 2);
476 |
477 | // if starts with `e` + uppercase letter
478 | if(k.substring(0, 1) === 'e' && nextLetter === nextLetter.toUpperCase()) {
479 | transferAttr = k.substring(1); // cut `e`
480 | } else {
481 | continue;
482 | }
483 |
484 | // exclude `form` and `ng-submit`,
485 | if(transferAttr === 'Form' || transferAttr === 'NgSubmit') {
486 | continue;
487 | }
488 |
489 | // convert back to lowercase style
490 | transferAttr = transferAttr.substring(0, 1).toLowerCase() + editableUtils.camelToDash(transferAttr.substring(1));
491 |
492 | // workaround for attributes without value (e.g. `multiple = "multiple"`)
493 | var attrValue = ($attrs[k] === '') ? transferAttr : $attrs[k];
494 |
495 | // set attributes to input
496 | self.inputEl.attr(transferAttr, attrValue);
497 | }
498 |
499 | self.inputEl.addClass('editable-input');
500 | self.inputEl.attr('ng-model', '$data');
501 |
502 | // add directiveName class to editor, e.g. `editable-text`
503 | self.editorEl.addClass(editableUtils.camelToDash(self.directiveName));
504 |
505 | if(self.single) {
506 | self.editorEl.attr('editable-form', '$form');
507 | // transfer `blur` to form
508 | self.editorEl.attr('blur', self.attrs.blur || (self.buttons === 'no' ? 'cancel' : editableOptions.blurElem));
509 | }
510 |
511 | //apply `postrender` method of theme
512 | if(angular.isFunction(theme.postrender)) {
513 | theme.postrender.call(self);
514 | }
515 |
516 | };
517 |
518 | // with majority of controls copy is not needed, but..
519 | // copy MUST NOT be used for `select-multiple` with objects as items
520 | // copy MUST be used for `checklist`
521 | self.setLocalValue = function() {
522 | self.scope.$data = self.useCopy ?
523 | angular.copy(valueGetter($scope.$parent)) :
524 | valueGetter($scope.$parent);
525 | };
526 |
527 | //show
528 | self.show = function() {
529 | // set value of scope.$data
530 | self.setLocalValue();
531 |
532 | /*
533 | Originally render() was inside init() method, but some directives polluting editorEl,
534 | so it is broken on second openning.
535 | Cloning is not a solution as jqLite can not clone with event handler's.
536 | */
537 | self.render();
538 |
539 | // insert into DOM
540 | $element.after(self.editorEl);
541 |
542 | // compile (needed to attach ng-* events from markup)
543 | $compile(self.editorEl)($scope);
544 |
545 | // attach listeners (`escape`, autosubmit, etc)
546 | self.addListeners();
547 |
548 | // hide element
549 | $element.addClass('editable-hide');
550 |
551 | // onshow
552 | return self.onshow();
553 | };
554 |
555 | //hide
556 | self.hide = function() {
557 | self.editorEl.remove();
558 | $element.removeClass('editable-hide');
559 |
560 | // onhide
561 | return self.onhide();
562 | };
563 |
564 | // cancel
565 | self.cancel = function() {
566 | // oncancel
567 | self.oncancel();
568 | // don't call hide() here as it called in form's code
569 | };
570 |
571 | /*
572 | Called after show to attach listeners
573 | */
574 | self.addListeners = function() {
575 | // bind keyup for `escape`
576 | self.inputEl.bind('keyup', function(e) {
577 | if(!self.single) {
578 | return;
579 | }
580 |
581 | // todo: move this to editable-form!
582 | switch(e.keyCode) {
583 | // hide on `escape` press
584 | case 27:
585 | self.scope.$apply(function() {
586 | self.scope.$form.$cancel();
587 | });
588 | break;
589 | }
590 | });
591 |
592 | // autosubmit when `no buttons`
593 | if (self.single && self.buttons === 'no') {
594 | self.autosubmit();
595 | }
596 |
597 | // click - mark element as clicked to exclude in document click handler
598 | self.editorEl.bind('click', function(e) {
599 | // ignore right/middle button click
600 | if (e.which !== 1) {
601 | return;
602 | }
603 |
604 | if (self.scope.$form.$visible) {
605 | self.scope.$form._clicked = true;
606 | }
607 | });
608 | };
609 |
610 | // setWaiting
611 | self.setWaiting = function(value) {
612 | if (value) {
613 | // participate in waiting only if not disabled
614 | inWaiting = !self.inputEl.attr('disabled') &&
615 | !self.inputEl.attr('ng-disabled') &&
616 | !self.inputEl.attr('ng-enabled');
617 | if (inWaiting) {
618 | self.inputEl.attr('disabled', 'disabled');
619 | if(self.buttonsEl) {
620 | self.buttonsEl.find('button').attr('disabled', 'disabled');
621 | }
622 | }
623 | } else {
624 | if (inWaiting) {
625 | self.inputEl.removeAttr('disabled');
626 | if (self.buttonsEl) {
627 | self.buttonsEl.find('button').removeAttr('disabled');
628 | }
629 | }
630 | }
631 | };
632 |
633 | self.activate = function() {
634 | setTimeout(function() {
635 | var el = self.inputEl[0];
636 | if (editableOptions.activate === 'focus' && el.focus) {
637 | el.focus();
638 | }
639 | if (editableOptions.activate === 'select' && el.select) {
640 | el.select();
641 | }
642 | }, 0);
643 | };
644 |
645 | self.setError = function(msg) {
646 | if(!angular.isObject(msg)) {
647 | $scope.$error = msg;
648 | self.error = msg;
649 | }
650 | };
651 |
652 | /*
653 | Checks that result is string or promise returned string and shows it as error message
654 | Applied to onshow, onbeforesave, onaftersave
655 | */
656 | self.catchError = function(result, noPromise) {
657 | if (angular.isObject(result) && noPromise !== true) {
658 | $q.when(result).then(
659 | //success and fail handlers are equal
660 | angular.bind(this, function(r) {
661 | this.catchError(r, true);
662 | }),
663 | angular.bind(this, function(r) {
664 | this.catchError(r, true);
665 | })
666 | );
667 | //check $http error
668 | } else if (noPromise && angular.isObject(result) && result.status &&
669 | (result.status !== 200) && result.data && angular.isString(result.data)) {
670 | this.setError(result.data);
671 | //set result to string: to let form know that there was error
672 | result = result.data;
673 | } else if (angular.isString(result)) {
674 | this.setError(result);
675 | }
676 | return result;
677 | };
678 |
679 | self.save = function() {
680 | valueGetter.assign($scope.$parent, angular.copy(self.scope.$data));
681 |
682 | // no need to call handleEmpty here as we are watching change of model value
683 | // self.handleEmpty();
684 | };
685 |
686 | /*
687 | attach/detach `editable-empty` class to element
688 | */
689 | self.handleEmpty = function() {
690 | var val = valueGetter($scope.$parent);
691 | var isEmpty = val === null || val === undefined || val === "" || (angular.isArray(val) && val.length === 0);
692 | $element.toggleClass('editable-empty', isEmpty);
693 | };
694 |
695 | /*
696 | Called when `buttons = "no"` to submit automatically
697 | */
698 | self.autosubmit = angular.noop;
699 |
700 | self.onshow = angular.noop;
701 | self.onhide = angular.noop;
702 | self.oncancel = angular.noop;
703 | self.onbeforesave = angular.noop;
704 | self.onaftersave = angular.noop;
705 | }
706 |
707 | return EditableController;
708 | }]);
709 |
710 | /*
711 | editableFactory is used to generate editable directives (see `/directives` folder)
712 | Inside it does several things:
713 | - detect form for editable element. Form may be one of three types:
714 | 1. autogenerated form (for single editable elements)
715 | 2. wrapper form (element wrapped by